Ver código fonte

Merge pull request #77 from j-p-higgins/multiple-input-files

Multiple input files
Jonathan Higgins 6 meses atrás
pai
commit
9facac3167

+ 2 - 6
Global/Global.gd

@@ -1,8 +1,4 @@
 extends Node
 
-var infile = "no_file"
-var infile_stereo = false
-var outfile = "no_file"
-var trim_infile = false
-var infile_start = 0
-var infile_stop = 1
+var outfile = "no_file" #bad name for the output directory
+var cdpoutput = "no_file" #output from running thread used for recycling output files

+ 19 - 32
scenes/Nodes/audioplayer.gd

@@ -21,12 +21,11 @@ func _ready():
 	file_dialog.connect("file_selected", Callable(self, "_on_file_selected"))
 	audio_player.connect("finished", Callable(self, "_on_audio_finished"))
 	
-	if get_meta("loadenable") == true:
-		$RecycleButton.hide()
-		$LoadButton.show()
-	else:
+	if get_meta("loadenable") == false:
+		$PlayButton.position.x = $LoadButton.position.x
+		$PlayButton.size.x = $Panel.size.x
 		$LoadButton.hide()
-		$RecycleButton.show()
+		$RecycleButton.hide()
 	
 	$WavError.hide()
 	
@@ -34,6 +33,9 @@ func _ready():
 	voice_preview_generator = preload("res://addons/audio_preview/voice_preview_generator.tscn").instantiate()
 	add_child(voice_preview_generator)
 	voice_preview_generator.texture_ready.connect(_on_texture_ready)
+	
+	#setup meta to say the player is empty and no trim points have been set
+	set_meta("trimfile", false)
 
 #func _on_files_dropped(files):
 	#if files[0].get_extension() == "wav" or files[0].get_extension() == "WAV":
@@ -56,13 +58,8 @@ func _on_load_button_button_down() -> void:
 
 func _on_file_selected(path: String):
 	audio_player.stream = AudioStreamWAV.load_from_file(path)
-	Global.infile_stereo = audio_player.stream.stereo
-	#if audio_player.stream.stereo == true:
-		##audio_player.stream = null
-		##$WavError.show()
 	voice_preview_generator.generate_preview(audio_player.stream)
-	Global.infile = path
-	print("Infile set: " + Global.infile)
+	set_meta("inputfile", path)
 	reset_playback()
 	
 func reset_playback():
@@ -71,7 +68,7 @@ func reset_playback():
 	$PlayButton.text = "Play"
 	$Timer.stop()
 	if get_meta("loadenable") == true:
-		Global.trim_infile = false
+		set_meta("timefile", false)
 	
 	
 func play_outfile(path: String):
@@ -86,16 +83,11 @@ func play_outfile(path: String):
 	reset_playback()
 
 	
-func recycle_outfile(path: String):
-	audio_player.stream = AudioStreamWAV.load_from_file(path)
-	Global.infile_stereo = audio_player.stream.stereo
-	#if audio_player.stream.stereo == true:
-		##audio_player.stream = null
-		##$WavError.show()
-	voice_preview_generator.generate_preview(audio_player.stream)
-	Global.infile = path
-	print("Infile set: " + Global.infile)
-	reset_playback()
+func recycle_outfile():
+	print("recycle pressed")
+	print(Global.cdpoutput)
+	if Global.cdpoutput != "no_file":
+		_on_file_selected(Global.cdpoutput)
 
 
 func _on_play_button_button_down() -> void:
@@ -187,17 +179,12 @@ func _on_button_button_down() -> void:
 func _on_button_button_up() -> void:
 	rect_focus = false
 	if get_meta("loadenable") == true:
-		print("got meta")
 		if $LoopRegion.size.x > 0:
-			Global.trim_infile = true
+			set_meta("trimfile", true)
 			var length = $AudioStreamPlayer.stream.get_length()
 			var pixel_to_time = length / 399
-			Global.infile_start = pixel_to_time * $LoopRegion.position.x
-			Global.infile_stop = Global.infile_start + (pixel_to_time * $LoopRegion.size.x)
-			print(Global.trim_infile)
-			print(Global.infile_start)
-			print(Global.infile_stop)
+			var start = pixel_to_time * $LoopRegion.position.x
+			var end = start + (pixel_to_time * $LoopRegion.size.x)
+			set_meta("trimpoints", [start, end])
 		else:
-			Global.trim_infile = false
-			print(Global.trim_infile)
-	
+			set_meta("trimfile", false)

+ 7 - 6
scenes/Nodes/audioplayer.tscn

@@ -36,24 +36,25 @@ use_native_dialog = true
 [node name="LoadButton" type="Button" parent="."]
 layout_mode = 0
 offset_top = 104.0
-offset_right = 196.0
+offset_right = 128.0
 offset_bottom = 147.0
 text = "Load File"
 
 [node name="RecycleButton" type="Button" parent="." groups=["outputnode"]]
 layout_mode = 0
+offset_left = 272.0
 offset_top = 104.0
-offset_right = 196.0
+offset_right = 400.0
 offset_bottom = 147.0
 tooltip_text = "Copies your output file back to your input for further processing."
-text = "Recycle File"
+text = "Reuse Output"
 metadata/outputfunction = "recycle"
 
 [node name="PlayButton" type="Button" parent="."]
 layout_mode = 0
-offset_left = 204.0
+offset_left = 136.0
 offset_top = 104.0
-offset_right = 400.0
+offset_right = 264.0
 offset_bottom = 147.0
 text = "Play"
 
@@ -112,7 +113,7 @@ flat = true
 [node name="Timer" type="Timer" parent="."]
 
 [connection signal="button_down" from="LoadButton" to="." method="_on_load_button_button_down"]
-[connection signal="button_down" from="RecycleButton" to="." method="_on_recycle_button_button_down"]
+[connection signal="button_down" from="RecycleButton" to="." method="recycle_outfile"]
 [connection signal="button_down" from="PlayButton" to="." method="_on_play_button_button_down"]
 [connection signal="button_down" from="WavError/CloseButton" to="." method="_on_close_button_button_down"]
 [connection signal="button_down" from="Button" to="." method="_on_button_button_down"]

+ 0 - 1
scenes/Nodes/nodes.tscn

@@ -42,7 +42,6 @@ slot/1/right_icon = null
 slot/1/draw_stylebox = true
 script = ExtResource("3_uv17x")
 metadata/command = "inputfile"
-metadata/utility = true
 
 [node name="Control" type="Control" parent="inputfile"]
 layout_mode = 2

+ 18 - 18
scenes/main/scripts/control.gd

@@ -6,11 +6,9 @@ var effect_position = Vector2(40,40) #tracks mouse position for node placement o
 var cdpprogs_location #stores the cdp programs location from user prefs for easy access
 var delete_intermediate_outputs # tracks state of delete intermediate outputs toggle
 @onready var console_output: RichTextLabel = $Console/ConsoleOutput
-var final_output_dir
 var undo_redo := UndoRedo.new() 
 var output_audio_player #tracks the node that is the current output player for linking
 var input_audio_player #tracks node that is the current input player for linking
-var outfile = "no file" #tracks dir of output file from cdp process
 var currentfile = "none" #tracks dir of currently loaded file for saving
 var changesmade = false #tracks if user has made changes to the currently loaded save file
 var savestate # tracks what the user is trying to do when savechangespopup is called
@@ -124,7 +122,6 @@ func new_patch():
 	graph_edit._register_node_movement() #link nodes for tracking position changes for changes tracking
 	
 	changesmade = false #so it stops trying to save unchanged empty files
-	Global.infile = "no_file" #resets input to stop processes running with old files
 	get_window().title = "SoundThread"
 	link_output()
 	
@@ -137,8 +134,8 @@ func link_output():
 			control.button_pressed = true
 		elif control.get_meta("outputfunction") == "runprocess": #link runprocess button
 			control.button_down.connect(_run_process)
-		elif control.get_meta("outputfunction") == "recycle": #link recycle button
-			control.button_down.connect(_recycle_outfile)
+		#elif control.get_meta("outputfunction") == "recycle": #link recycle button
+			#control.button_down.connect(_recycle_outfile)
 		elif control.get_meta("outputfunction") == "audioplayer": #link output audio player
 			output_audio_player = control
 		elif control.get_meta("outputfunction") == "filename":
@@ -150,10 +147,10 @@ func link_output():
 		elif control.get_meta("outputfunction") == "openfolder":
 			control.button_down.connect(_open_output_folder)
 
-	for control in get_tree().get_nodes_in_group("inputnode"):
-		if control.get_meta("inputfunction") == "audioplayer": #link input for recycle function
-			print("input player found")
-			input_audio_player = control
+	#for control in get_tree().get_nodes_in_group("inputnode"):
+		#if control.get_meta("inputfunction") == "audioplayer": #link input for recycle function
+			#print("input player found")
+			#input_audio_player = control
 
 func check_user_preferences():
 	var interface_settings = ConfigHandler.load_interface_settings()
@@ -249,13 +246,16 @@ func simulate_mouse_click():
 
 
 func _run_process() -> void:
-	if Global.infile == "no_file":
-		$NoInputPopup.popup_centered()
+	#check if any of the inputfile nodes don't have files loaded
+	for node in graph_edit.get_children():
+		if node.get_meta("command") == "inputfile" and node.get_node("AudioPlayer").has_meta("inputfile") == false:
+			$NoInputPopup.popup_centered()
+			return
+	#check if the reuse folder toggle is set and a folder has been previously chosen
+	if foldertoggle.button_pressed == true and lastoutputfolder != "none":
+		_on_file_dialog_dir_selected(lastoutputfolder)
 	else:
-		if foldertoggle.button_pressed == true and lastoutputfolder != "none":
-			_on_file_dialog_dir_selected(lastoutputfolder)
-		else:
-			$FileDialog.show()
+		$FileDialog.show()
 			
 
 func _on_file_dialog_dir_selected(dir: String) -> void:
@@ -465,9 +465,9 @@ func _on_help_button_index_pressed(index: int) -> void:
 		12:
 			OS.shell_open("https://github.com/j-p-higgins/SoundThread/issues")
 
-func _recycle_outfile():
-	if outfile != "no file":
-		input_audio_player.recycle_outfile(outfile)
+#func _recycle_outfile():
+	#if outfile != "no file":
+		#input_audio_player.recycle_outfile(outfile)
 
 
 

+ 12 - 4
scenes/main/scripts/graph_edit.gd

@@ -83,12 +83,20 @@ func _unhandled_key_input(event: InputEvent) -> void:
 
 func _on_graph_edit_delete_nodes_request(nodes: Array[StringName]) -> void:
 	control_script.undo_redo.create_action("Delete Nodes (Undo only)")
-
+	
+	#get the number of inputs in the patch
+	var number_of_inputs = 0
+	for allnodes in get_children():
+		if allnodes.get_meta("command") == "inputfile":
+			number_of_inputs += 1
+			
 	for node in selected_nodes.keys():
 		if selected_nodes[node]:
-			if node.get_meta("command") == "inputfile" or node.get_meta("command") == "outputfile":
-				print("can't delete input or output")
+			#check if node is the output or the last input node and do nothing
+			if (number_of_inputs <= 1 and node.get_meta("command") == "inputfile") or node.get_meta("command") == "outputfile":
+				pass
 			else:
+				number_of_inputs -= 1
 				# Store duplicate and state for undo
 				var node_data = node.duplicate()
 				var position = node.position_offset
@@ -140,7 +148,7 @@ func copy_selected_nodes():
 	for node in get_children():
 		# Check if the node is selected and not an 'inputfile' or 'outputfile'
 		if node is GraphNode and selected_nodes.get(node, false):
-			if node.get_meta("command") == "inputfile" or node.get_meta("command") == "outputfile":
+			if node.get_meta("command") == "outputfile":
 				continue  # Skip these nodes
 
 			var node_data = {

+ 287 - 197
scenes/main/scripts/run_thread.gd

@@ -11,6 +11,7 @@ var process_successful #tracks if the last run process was successful
 var process_info = {} #tracks the data of the currently running process
 var process_running := false #tracks if a process is currently running
 var process_cancelled = false #checks if the currently running process has been cancelled
+var final_output_dir
 
 # Called when the node enters the scene tree for the first time.
 func _ready() -> void:
@@ -57,26 +58,68 @@ func run_thread_with_branches():
 		log_console("[color=#9c2828][b]Error: Valid Thread not found[/b][/color]", true)
 		log_console("Threads must contain at least one processing node and a valid path from the Input File to the Output File.", true)
 		await get_tree().process_frame  # Let UI update
+		if progress_window.visible:
+			progress_window.hide()
+		if !console_window.visible:
+			console_window.popup_centered()
 		return
 	else:
 		log_console("[color=#638382][b]Valid Thread found[/b][/color]", true)
 		await get_tree().process_frame  # Let UI update
 		
 	# Step 1: Gather nodes from the GraphEdit
+	var inputcount = 0 # used for tracking the number of input nodes and trims on input files for progress bar
 	for child in graph_edit.get_children():
 		if child is GraphNode:
+			var includenode = true
 			var name = str(child.name)
 			all_nodes[name] = child
-			if not child.has_meta("utility"):
+			if child.has_meta("utility"):
+				includenode = false
+			else:
+				#check if node has inputs
+				if child.get_input_port_count() > 0:
+					#if it does scan through those inputs
+					for i in range(child.get_input_port_count()):
+						#check if it can find any valid connections
+						var connected = false
+						for conn in connections:
+							if conn["to_node"] == name and conn["to_port"] == i:
+								connected = true
+								break
+						#if no valid connections are found break the for loop to skip checking other inputs and set include to false
+						if connected == false:
+							log_console(name + " input is not connected, skipping node.", true)
+							includenode = false
+							break
+				#check if node has outputs
+				if child.get_output_port_count() > 0:
+					#if it does scan through those outputs
+					for i in range(child.get_output_port_count()):
+						#check if it can find any valid connections
+						var connected = false
+						for conn in connections:
+							if conn["from_node"] == name and conn["to_port"] == i:
+								connected = true
+								break
+						#if no valid connections are found break the for loop to skip checking other inputs and set include to false
+						if connected == false:
+							log_console(name + " output is not connected, skipping node.", true)
+							includenode = false
+							break
+								
+			if includenode == true:
 				graph[name] = []
 				reverse_graph[name] = []
 				indegree[name] = 0  # Start with zero incoming edges
+				if child.get_meta("command") == "inputfile":
+					inputcount -= 1
+					if child.get_node("AudioPlayer").get_meta("trimfile"):
+						inputcount += 1
 	#do calculations for progress bar
 	var progress_step
-	if Global.trim_infile == true:
-		progress_step = 100 / (graph.size() + 4)
-	else:
-		progress_step = 100 / (graph.size() + 3)
+	progress_step = 100 / (graph.size() + 3 + inputcount)
+
 	
 
 	# Step 2: Build graph relationships from connections
@@ -125,25 +168,7 @@ func run_thread_with_branches():
 	var output_files = {}
 	var process_count = 0
 
-	# Start with the original input file
-	var starting_infile = Global.infile
-	
-	
-	#If trim is enabled trim input audio
-	if Global.trim_infile == true:
-		if process_cancelled:
-			progress_label.text = "Thread Stopped"
-			log_console("[b]Thread Stopped[/b]", true)
-			return
-		else:
-			progress_label.text = "Trimming input audio"
-		await run_command(control_script.cdpprogs_location + "/sfedit", ["cut", "1", starting_infile, "%s_trimmed.wav" % Global.outfile, str(Global.infile_start), str(Global.infile_stop)])
-		starting_infile = Global.outfile + "_trimmed.wav"
-		# Mark trimmed file for cleanup if needed
-		if control_script.delete_intermediate_outputs:
-			intermediate_files.append(Global.outfile + "_trimmed.wav")
-		progress_bar.value += progress_step
-	var current_infile = starting_infile
+	var current_infile
 
 	# Iterate over the processing nodes in topological order
 	for node_name in sorted:
@@ -181,47 +206,53 @@ func run_thread_with_branches():
 
 		## If no input, use the original input file
 		else:
-			current_infile = starting_infile
-
-		# Build the command for the current node's audio processing
-		var slider_data = _get_slider_values_ordered(node)
+			#if no input i need to skip the node
+			pass
 		
-		if node.get_slot_type_right(0) == 1: #detect if process outputs pvoc data
-			if typeof(current_infile) == TYPE_ARRAY:
-				#check if infile is an array meaning that the last pvoc process was run in dual mono mode
-				# Process left and right seperately
-				var pvoc_stereo_files = []
+		if node.get_meta("command") == "inputfile":
+			#get the inputfile from the nodes meta
+			var loadedfile = node.get_node("AudioPlayer").get_meta("inputfile")
+			#get wether trim has been enabled
+			var trimfile = node.get_node("AudioPlayer").get_meta("trimfile")
+			
+			#if trim is enabled trim the file
+			if trimfile == true:
+				#get the start and end points
+				var start = node.get_node("AudioPlayer").get_meta("trimpoints")[0]
+				var end = node.get_node("AudioPlayer").get_meta("trimpoints")[1]
 				
-				for infile in current_infile:
-					var makeprocess = await make_process(node, process_count, infile, slider_data)
-					# run the command
-					await run_command(makeprocess[0], makeprocess[3])
-					await get_tree().process_frame
-					var output_file = makeprocess[1]
-					pvoc_stereo_files.append(output_file)
-					
-					# Mark file for cleanup if needed
-					if control_script.delete_intermediate_outputs:
-						for file in makeprocess[2]:
-							breakfiles.append(file)
-						intermediate_files.append(output_file)
-
-					process_count += 1
-					
-				output_files[node_name] = pvoc_stereo_files
+				if process_cancelled:
+					#exit out of process if cancelled
+					progress_label.text = "Thread Stopped"
+					log_console("[b]Thread Stopped[/b]", true)
+					return
+				else:
+					progress_label.text = "Trimming input audio"
+				await run_command(control_script.cdpprogs_location + "/sfedit", ["cut", "1", loadedfile, "%s_%d_input_trim.wav" % [Global.outfile, process_count], str(start), str(end)])
+				
+				output_files[node_name] =  "%s_%d_input_trim.wav" % [Global.outfile, process_count]
+				
+				# Mark trimmed file for cleanup if needed
+				if control_script.delete_intermediate_outputs:
+					intermediate_files.append("%s_%d_input_trim.wav" % [Global.outfile, process_count])
+				progress_bar.value += progress_step
 			else:
-				var input_stereo = await is_stereo(current_infile)
-				if input_stereo == true: 
-					#audio file is stereo and needs to be split for pvoc processing
-					var pvoc_stereo_files = []
-					##Split stereo to c1/c2
-					await run_command(control_script.cdpprogs_location + "/housekeep",["chans", "2", current_infile])
+				#if trim not enabled pass the loaded file
+				output_files[node_name] =  loadedfile
+				
+			process_count += 1
+		else:
+			# Build the command for the current node's audio processing
+			var slider_data = _get_slider_values_ordered(node)
 			
+			if node.get_slot_type_right(0) == 1: #detect if process outputs pvoc data
+				if typeof(current_infile) == TYPE_ARRAY:
+					#check if infile is an array meaning that the last pvoc process was run in dual mono mode
 					# Process left and right seperately
-					for channel in ["c1", "c2"]:
-						var dual_mono_file = current_infile.get_basename() + "_%s.wav" % channel
-						
-						var makeprocess = await make_process(node, process_count, dual_mono_file, slider_data)
+					var pvoc_stereo_files = []
+					
+					for infile in current_infile:
+						var makeprocess = await make_process(node, process_count, infile, slider_data)
 						# run the command
 						await run_command(makeprocess[0], makeprocess[3])
 						await get_tree().process_frame
@@ -233,98 +264,19 @@ func run_thread_with_branches():
 							for file in makeprocess[2]:
 								breakfiles.append(file)
 							intermediate_files.append(output_file)
-						
-						#Delete c1 and c2 because they can be in the wrong folder and if the same infile is used more than once
-						#with this stereo process CDP will throw errors in the console even though its fine
-						if is_windows:
-							dual_mono_file = dual_mono_file.replace("/", "\\")
-						await run_command(delete_cmd, [dual_mono_file])
+
 						process_count += 1
 						
-						# Store output file path for this node
 					output_files[node_name] = pvoc_stereo_files
-				else: 
-					#input file is mono run through process
-					var makeprocess = await make_process(node, process_count, current_infile, slider_data)
-					# run the command
-					await run_command(makeprocess[0], makeprocess[3])
-					await get_tree().process_frame
-					var output_file = makeprocess[1]
-
-					# Store output file path for this node
-					output_files[node_name] = output_file
-
-					# Mark file for cleanup if needed
-					if control_script.delete_intermediate_outputs:
-						for file in makeprocess[2]:
-							breakfiles.append(file)
-						intermediate_files.append(output_file)
-
-		# Increase the process step count
-			process_count += 1
-			
-		else: 
-			#Process outputs audio
-			#check if this is the last pvoc process in a stereo processing chain
-			if node.get_meta("command") == "pvoc_synth" and typeof(current_infile) == TYPE_ARRAY:
-			
-				#check if infile is an array meaning that the last pvoc process was run in dual mono mode
-				# Process left and right seperately
-				var pvoc_stereo_files = []
-				
-				for infile in current_infile:
-					var makeprocess = await make_process(node, process_count, infile, slider_data)
-					# run the command
-					await run_command(makeprocess[0], makeprocess[3])
-					await get_tree().process_frame
-					var output_file = makeprocess[1]
-					pvoc_stereo_files.append(output_file)
-					
-					# Mark file for cleanup if needed
-					if control_script.delete_intermediate_outputs:
-						for file in makeprocess[2]:
-							breakfiles.append(file)
-						intermediate_files.append(output_file)
-
-					process_count += 1
-					
-					
-				#interleave left and right
-				var output_file = Global.outfile.get_basename() + str(process_count) + "_interleaved.wav"
-				await run_command(control_script.cdpprogs_location + "/submix", ["interleave", pvoc_stereo_files[0], pvoc_stereo_files[1], output_file])
-				# Store output file path for this node
-				output_files[node_name] = output_file
-				
-				# Mark file for cleanup if needed
-				if control_script.delete_intermediate_outputs:
-					intermediate_files.append(output_file)
-
-			else:
-				#Detect if input file is mono or stereo
-				var input_stereo = await is_stereo(current_infile)
-				if input_stereo == true:
-					if node.get_meta("stereo_input") == true: #audio file is stereo and process is stereo, run file through process
-						var makeprocess = await make_process(node, process_count, current_infile, slider_data)
-						# run the command
-						await run_command(makeprocess[0], makeprocess[3])
-						await get_tree().process_frame
-						var output_file = makeprocess[1]
-						
-						# Store output file path for this node
-						output_files[node_name] = output_file
-
-						# Mark file for cleanup if needed
-						if control_script.delete_intermediate_outputs:
-							for file in makeprocess[2]:
-								breakfiles.append(file)
-							intermediate_files.append(output_file)
-
-					else: #audio file is stereo and process is mono, split stereo, process and recombine
+				else:
+					var input_stereo = await is_stereo(current_infile)
+					if input_stereo == true: 
+						#audio file is stereo and needs to be split for pvoc processing
+						var pvoc_stereo_files = []
 						##Split stereo to c1/c2
 						await run_command(control_script.cdpprogs_location + "/housekeep",["chans", "2", current_infile])
 				
 						# Process left and right seperately
-						var dual_mono_output = []
 						for channel in ["c1", "c2"]:
 							var dual_mono_file = current_infile.get_basename() + "_%s.wav" % channel
 							
@@ -333,7 +285,7 @@ func run_thread_with_branches():
 							await run_command(makeprocess[0], makeprocess[3])
 							await get_tree().process_frame
 							var output_file = makeprocess[1]
-							dual_mono_output.append(output_file)
+							pvoc_stereo_files.append(output_file)
 							
 							# Mark file for cleanup if needed
 							if control_script.delete_intermediate_outputs:
@@ -347,38 +299,145 @@ func run_thread_with_branches():
 								dual_mono_file = dual_mono_file.replace("/", "\\")
 							await run_command(delete_cmd, [dual_mono_file])
 							process_count += 1
-						
-						
-						var output_file = Global.outfile.get_basename() + str(process_count) + "_interleaved.wav"
-						await run_command(control_script.cdpprogs_location + "/submix", ["interleave", dual_mono_output[0], dual_mono_output[1], output_file])
-						
+							
+							# Store output file path for this node
+						output_files[node_name] = pvoc_stereo_files
+					else: 
+						#input file is mono run through process
+						var makeprocess = await make_process(node, process_count, current_infile, slider_data)
+						# run the command
+						await run_command(makeprocess[0], makeprocess[3])
+						await get_tree().process_frame
+						var output_file = makeprocess[1]
+
 						# Store output file path for this node
 						output_files[node_name] = output_file
 
 						# Mark file for cleanup if needed
 						if control_script.delete_intermediate_outputs:
+							for file in makeprocess[2]:
+								breakfiles.append(file)
 							intermediate_files.append(output_file)
 
-				else: #audio file is mono, run through the process
-					var makeprocess = await make_process(node, process_count, current_infile, slider_data)
-					# run the command
-					await run_command(makeprocess[0], makeprocess[3])
-					await get_tree().process_frame
-					var output_file = makeprocess[1]
+			# Increase the process step count
+				process_count += 1
+				
+			else: 
+				#Process outputs audio
+				#check if this is the last pvoc process in a stereo processing chain
+				if node.get_meta("command") == "pvoc_synth" and typeof(current_infile) == TYPE_ARRAY:
+				
+					#check if infile is an array meaning that the last pvoc process was run in dual mono mode
+					# Process left and right seperately
+					var pvoc_stereo_files = []
 					
+					for infile in current_infile:
+						var makeprocess = await make_process(node, process_count, infile, slider_data)
+						# run the command
+						await run_command(makeprocess[0], makeprocess[3])
+						await get_tree().process_frame
+						var output_file = makeprocess[1]
+						pvoc_stereo_files.append(output_file)
+						
+						# Mark file for cleanup if needed
+						if control_script.delete_intermediate_outputs:
+							for file in makeprocess[2]:
+								breakfiles.append(file)
+							intermediate_files.append(output_file)
 
+						process_count += 1
+						
+						
+					#interleave left and right
+					var output_file = Global.outfile.get_basename() + str(process_count) + "_interleaved.wav"
+					await run_command(control_script.cdpprogs_location + "/submix", ["interleave", pvoc_stereo_files[0], pvoc_stereo_files[1], output_file])
 					# Store output file path for this node
 					output_files[node_name] = output_file
-
+					
 					# Mark file for cleanup if needed
 					if control_script.delete_intermediate_outputs:
-						for file in makeprocess[2]:
-							breakfiles.append(file)
 						intermediate_files.append(output_file)
 
-			# Increase the process step count
-			process_count += 1
-		progress_bar.value += progress_step
+				else:
+					#Detect if input file is mono or stereo
+					var input_stereo = await is_stereo(current_infile)
+					if input_stereo == true:
+						if node.get_meta("stereo_input") == true: #audio file is stereo and process is stereo, run file through process
+							var makeprocess = await make_process(node, process_count, current_infile, slider_data)
+							# run the command
+							await run_command(makeprocess[0], makeprocess[3])
+							await get_tree().process_frame
+							var output_file = makeprocess[1]
+							
+							# Store output file path for this node
+							output_files[node_name] = output_file
+
+							# Mark file for cleanup if needed
+							if control_script.delete_intermediate_outputs:
+								for file in makeprocess[2]:
+									breakfiles.append(file)
+								intermediate_files.append(output_file)
+
+						else: #audio file is stereo and process is mono, split stereo, process and recombine
+							##Split stereo to c1/c2
+							await run_command(control_script.cdpprogs_location + "/housekeep",["chans", "2", current_infile])
+					
+							# Process left and right seperately
+							var dual_mono_output = []
+							for channel in ["c1", "c2"]:
+								var dual_mono_file = current_infile.get_basename() + "_%s.wav" % channel
+								
+								var makeprocess = await make_process(node, process_count, dual_mono_file, slider_data)
+								# run the command
+								await run_command(makeprocess[0], makeprocess[3])
+								await get_tree().process_frame
+								var output_file = makeprocess[1]
+								dual_mono_output.append(output_file)
+								
+								# Mark file for cleanup if needed
+								if control_script.delete_intermediate_outputs:
+									for file in makeprocess[2]:
+										breakfiles.append(file)
+									intermediate_files.append(output_file)
+								
+								#Delete c1 and c2 because they can be in the wrong folder and if the same infile is used more than once
+								#with this stereo process CDP will throw errors in the console even though its fine
+								if is_windows:
+									dual_mono_file = dual_mono_file.replace("/", "\\")
+								await run_command(delete_cmd, [dual_mono_file])
+								process_count += 1
+							
+							
+							var output_file = Global.outfile.get_basename() + str(process_count) + "_interleaved.wav"
+							await run_command(control_script.cdpprogs_location + "/submix", ["interleave", dual_mono_output[0], dual_mono_output[1], output_file])
+							
+							# Store output file path for this node
+							output_files[node_name] = output_file
+
+							# Mark file for cleanup if needed
+							if control_script.delete_intermediate_outputs:
+								intermediate_files.append(output_file)
+
+					else: #audio file is mono, run through the process
+						var makeprocess = await make_process(node, process_count, current_infile, slider_data)
+						# run the command
+						await run_command(makeprocess[0], makeprocess[3])
+						await get_tree().process_frame
+						var output_file = makeprocess[1]
+						
+
+						# Store output file path for this node
+						output_files[node_name] = output_file
+
+						# Mark file for cleanup if needed
+						if control_script.delete_intermediate_outputs:
+							for file in makeprocess[2]:
+								breakfiles.append(file)
+							intermediate_files.append(output_file)
+
+				# Increase the process step count
+				process_count += 1
+			progress_bar.value += progress_step
 	# FINAL OUTPUT STAGE
 
 	# Collect all nodes that are connected to the outputfile node
@@ -403,7 +462,7 @@ func run_thread_with_branches():
 	# If multiple outputs go to the outputfile node, merge them
 	if final_outputs.size() > 1:
 		var runmerge = await merge_many_files(process_count, final_outputs)
-		control_script.final_output_dir = runmerge[0]
+		final_output_dir = runmerge[0]
 		var converted_files = runmerge[1]
 		
 		if control_script.delete_intermediate_outputs:
@@ -414,7 +473,7 @@ func run_thread_with_branches():
 	# Only one output, no merge needed
 	elif final_outputs.size() == 1:
 		var single_output = final_outputs[0]
-		control_script.final_output_dir = single_output
+		final_output_dir = single_output
 		intermediate_files.erase(single_output)
 	progress_bar.value += progress_step
 	# CLEANUP: Delete intermediate files after processing and rename final output
@@ -442,16 +501,16 @@ func run_thread_with_branches():
 		await get_tree().process_frame
 		
 	var final_filename = "%s.wav" % Global.outfile
-	var final_output_dir_fixed_path = control_script.final_output_dir
+	var final_output_dir_fixed_path = final_output_dir
 	if is_windows:
 		final_output_dir_fixed_path = final_output_dir_fixed_path.replace("/", "\\")
 		await run_command(rename_cmd, [final_output_dir_fixed_path, final_filename.get_file()])
 	else:
 		await run_command(rename_cmd, [final_output_dir_fixed_path, "%s.wav" % Global.outfile])
-	control_script.final_output_dir = Global.outfile + ".wav"
+	final_output_dir = Global.outfile + ".wav"
 	
-	control_script.output_audio_player.play_outfile(control_script.final_output_dir)
-	control_script.outfile = control_script.final_output_dir
+	control_script.output_audio_player.play_outfile(final_output_dir)
+	Global.cdpoutput = final_output_dir
 	progress_bar.value = 100.0
 	var interface_settings = ConfigHandler.load_interface_settings() #checks if close console is enabled and closes console on a success
 	progress_window.hide()
@@ -471,16 +530,48 @@ func is_stereo(file: String) -> bool:
 	else:
 		log_console("[color=#9c2828]Error: Only mono and stereo files are supported[/color]", true)
 		return false
+		
+func get_samplerate(file: String) -> int:
+	var output = await run_command(control_script.cdpprogs_location + "/sfprops", ["-r", file])
+	output = int(output.strip_edges())
+	return output
 
 func merge_many_files(process_count: int, input_files: Array) -> Array:
 	var merge_output = "%s_merge_%d.wav" % [Global.outfile.get_basename(), process_count]
-	var converted_files := []  # Track any mono->stereo converted files
+	var converted_files := []  # Track any mono->stereo converted files or upsampled files
 	var inputs_to_merge := []  # Files to be used in the final merge
 
 	var mono_files := []
 	var stereo_files := []
+	var sample_rates := []
+	
+	#Get all sample rates
+	for f in input_files:
+		var samplerate = await get_samplerate(f)
+		sample_rates.append(samplerate)
+	
+	#Check if all sample rates are the same
+	if sample_rates.all(func(v): return v == sample_rates[0]):
+		pass
+	else:
+		log_console("Different sample rates found, upsampling files to match highest current sample rate before mixing.", true)
+		#if not find the highest sample rate
+		var highest_sample_rate = sample_rates.max()
+		var index = 0
+		#move through all input files and compare match their index to the sample_rate array
+		for f in input_files:
+			#check if sample rate of current file is less than the highest sample rate
+			if sample_rates[index] < highest_sample_rate:
+				#up sample it to the highest sample rate if so
+				var upsample_output = Global.outfile + "_" + str(process_count) + f.get_file().get_slice(".wav", 0) + "_" + str(highest_sample_rate) + ".wav"
+				await run_command(control_script.cdpprogs_location + "/housekeep", ["respec", "1", f, upsample_output, str(highest_sample_rate)])
+				#replace the file in the input_file index with the new upsampled file
+				input_files[index] = upsample_output
+				converted_files.append(upsample_output)
+				
+			index += 1
 
-	# STEP 1: Check each file's channel count
+	# Check each file's channel count
 	for f in input_files:
 		var stereo = await is_stereo(f)
 		if stereo == false:
@@ -488,11 +579,14 @@ func merge_many_files(process_count: int, input_files: Array) -> Array:
 		elif stereo == true:
 			stereo_files.append(f)
 
+			
 
 	# STEP 2: Convert mono to stereo if there is a mix
 	if mono_files.size() > 0 and stereo_files.size() > 0:
+		log_console("Mix of mono and stereo files found, interleaving mono files to stereo before mixing.", true)
 		for mono_file in mono_files:
-			var stereo_file = "%s_stereo.wav" % mono_file.get_basename()
+			var stereo_file = Global.outfile + "_" + str(process_count) + mono_file.get_file().get_slice(".wav", 0) + "_stereo.wav"
+			#var stereo_file = "%s_stereo.wav" % mono_file.get_basename()
 			await run_command(control_script.cdpprogs_location + "/submix", ["interleave", mono_file, mono_file, stereo_file])
 			if process_successful == false:
 				log_console("Failed to interleave mono file: %s" % mono_file, true)
@@ -506,6 +600,7 @@ func merge_many_files(process_count: int, input_files: Array) -> Array:
 		inputs_to_merge = input_files.duplicate()
 
 	# STEP 3: Merge all input files (converted or original)
+	log_console("Mixing files to combined input.", true)
 	var quoted_inputs := []
 	for f in inputs_to_merge:
 		quoted_inputs.append(f)
@@ -648,7 +743,7 @@ func path_exists_through_all_nodes() -> bool:
 	var all_nodes = {}
 	var graph = {}
 
-	var input_node_name = ""
+	var input_node_names = []
 	var output_node_name = ""
 
 	# Gather all relevant nodes
@@ -659,19 +754,13 @@ func path_exists_through_all_nodes() -> bool:
 
 			var command = child.get_meta("command")
 			if command == "inputfile":
-				input_node_name = name
+				input_node_names.append(name)
 			elif command == "outputfile":
 				output_node_name = name
 
 			# Skip utility nodes, include others
 			if command in ["inputfile", "outputfile"] or not child.has_meta("utility"):
 				graph[name] = []
-
-	# Ensure both input and output were found
-	if input_node_name == "" or output_node_name == "":
-		print("Input or output node not found!")
-		return false
-
 	# Add edges to graph from the connection list
 	var connection_list = graph_edit.get_connection_list()
 	for conn in connection_list:
@@ -680,28 +769,29 @@ func path_exists_through_all_nodes() -> bool:
 		if graph.has(from):
 			graph[from].append(to)
 
-	# BFS traversal to check path and depth
-	var visited = {}
-	var queue = [ { "node": input_node_name, "depth": 0 } ]
-	var has_intermediate = false
+	# BFS from each input node
+	for input_node_name in input_node_names:
+		var visited = {}
+		var queue = [{ "node": input_node_name, "depth": 0 }]
 
-	while queue.size() > 0:
-		var current = queue.pop_front()
-		var current_node = current["node"]
-		var depth = current["depth"]
+		while queue.size() > 0:
+			var current = queue.pop_front()
+			var current_node = current["node"]
+			var depth = current["depth"]
 
-		if current_node in visited:
-			continue
-		visited[current_node] = true
+			if current_node in visited:
+				continue
+			visited[current_node] = true
 
-		if current_node == output_node_name and depth >= 2:
-			has_intermediate = true
+			if current_node == output_node_name and depth >= 2:
+				return true  # Found a valid path from this input node
 
-		if graph.has(current_node):
-			for neighbor in graph[current_node]:
-				queue.append({ "node": neighbor, "depth": depth + 1 })
+			if graph.has(current_node):
+				for neighbor in graph[current_node]:
+					queue.append({ "node": neighbor, "depth": depth + 1 })
 
-	return has_intermediate
+	# If no path from any input node to output node was found
+	return false
 	
 func log_console(text: String, update: bool) -> void:
 	console_output.append_text(text + "\n \n")

+ 4 - 4
scenes/menu/explore_menu.gd

@@ -26,8 +26,8 @@ func fill_menu():
 		var item = node_data[key]
 		var title = item.get("title", "")
 		
-		#filter out input and output nodes
-		if title == "Input File" or title == "Output File":
+		#filter out output nodes
+		if title == "Output File":
 			continue
 		
 		var category = item.get("category", "")
@@ -114,8 +114,8 @@ func fill_search(filter: String):
 		var item = node_data[key]
 		var title = item.get("title", "")
 		
-		#filter out input and output nodes
-		if title == "Input File" or title == "Output File":
+		#filter out output node
+		if title == "Output File":
 			continue
 		
 		var category = item.get("category", "")

+ 4 - 2
scenes/menu/search_menu.gd

@@ -37,8 +37,8 @@ func display_items(filter: String):
 		var item = node_data[key]
 		var title = item.get("title", "")
 		
-		#filter out input and output nodes
-		if title == "Input File" or title == "Output File":
+		#filter out output node
+		if title == "Output File":
 			continue
 		
 		var category = item.get("category", "")
@@ -58,6 +58,8 @@ func display_items(filter: String):
 		btn.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS #and replace with ...
 		if category.to_lower() == "pvoc": #format node names correctly, only show the category for PVOC
 			btn.text = "%s %s: %s - %s" % [category.to_upper(), subcategory.to_pascal_case(), title, short_desc]
+		elif title.to_lower() == "input file":
+			btn.text = "%s - %s" % [title, short_desc]
 		else:
 			btn.text = "%s: %s - %s" % [subcategory.to_pascal_case(), title, short_desc]
 		btn.connect("pressed", Callable(self, "_on_item_selected").bind(key)) #pass key (process name) when button is pressed