| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949 |
- extends Node
- var control_script
- var progress_label
- var progress_bar
- var graph_edit
- var console_output
- var progress_window
- var console_window
- 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:
- pass
- func init(main_node: Node, progresswindow: Window, progresslabel: Label, progressbar: ProgressBar, graphedit: GraphEdit, consolewindow: Window, consoleoutput: RichTextLabel) -> void:
- control_script = main_node
- progress_window = progresswindow
- progress_label = progresslabel
- progress_bar = progressbar
- graph_edit = graphedit
- console_window = consolewindow
- console_output = consoleoutput
-
- func run_thread_with_branches():
- process_cancelled = false
- process_successful = true
- # Detect platform: Determine if the OS is Windows
- var is_windows := OS.get_name() == "Windows"
-
- # Choose appropriate commands based on OS
- var delete_cmd = "del" if is_windows else "rm"
- var rename_cmd = "ren" if is_windows else "mv"
- var path_sep := "/" # Always use forward slash for paths
- # Get all node connections in the GraphEdit
- var connections = graph_edit.get_connection_list()
- # Prepare data structures for graph traversal
- var graph = {} # forward adjacency list
- var reverse_graph = {} # reverse adjacency list (for input lookup)
- var indegree = {} # used for topological sort
- var all_nodes = {} # map of node name -> GraphNode reference
- log_console("Mapping thread.", true)
- await get_tree().process_frame # Let UI update
- #Step 0: check thread is valid
- var is_valid = path_exists_through_all_nodes()
- if is_valid == false:
- 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 or Synthesis node 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 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
- progress_step = 100 / (graph.size() + 3 + inputcount)
-
- # Step 2: Build graph relationships from connections
- if process_cancelled:
- progress_label.text = "Thread Stopped"
- log_console("[b]Thread Stopped[/b]", true)
- return
- else:
- progress_label.text = "Building Thread"
- for conn in connections:
- var from = str(conn["from_node"])
- var to = str(conn["to_node"])
- if graph.has(from) and graph.has(to):
- graph[from].append(to)
- reverse_graph[to].append(from)
- indegree[to] += 1 # Count incoming edges
- # Step 3: Topological sort to get execution order
- var sorted = [] # Sorted list of node names
- var queue = [] # Queue of nodes with 0 indegree
- for node in graph.keys():
- if indegree[node] == 0:
- queue.append(node)
- while not queue.is_empty():
- var current = queue.pop_front()
- sorted.append(current)
- for neighbor in graph[current]:
- indegree[neighbor] -= 1
- if indegree[neighbor] == 0:
- queue.append(neighbor)
- # If not all nodes were processed, there's a cycle
- if sorted.size() != graph.size():
- log_console("[color=#9c2828][b]Error: Thread not valid[/b][/color]", true)
- log_console("Threads cannot contain loops.", true)
- return
- progress_bar.value = progress_step
- # Step 4: Start processing audio
- var batch_lines = [] # Holds all batch file commands
- var intermediate_files = [] # Files to delete later
- var breakfiles = [] #breakfiles to delete later
- # Dictionary to keep track of each node's output file
- var output_files = {}
- var process_count = 0
- var current_infile
- # Iterate over the processing nodes in topological order
- for node_name in sorted:
- var node = all_nodes[node_name]
- if process_cancelled:
- progress_label.text = "Thread Stopped"
- log_console("[b]Thread Stopped[/b]", true)
- break
- else:
- progress_label.text = "Running process: " + node.get_title()
- # Find upstream nodes connected to the current node
- var inputs = reverse_graph[node_name]
- var input_files = []
- for input_node in inputs:
- input_files.append(output_files[input_node])
- # Merge inputs if this node has more than one input
- if input_files.size() > 1:
- # Prepare final merge output file name
- var runmerge = await merge_many_files(process_count, input_files)
- var merge_output = runmerge[0]
- var converted_files = runmerge[1]
- # Track the output and intermediate files
- current_infile = merge_output
-
- if control_script.delete_intermediate_outputs:
- intermediate_files.append(merge_output)
- for f in converted_files:
- intermediate_files.append(f)
- # If only one input, use that
- elif input_files.size() == 1:
- current_infile = input_files[0]
- else:
- #if no input i need to skip the node
- pass
-
- #check if node is some form of input node
- if node.get_input_port_count() == 0:
- 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]
-
- 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:
- #if trim not enabled pass the loaded file
- output_files[node_name] = loadedfile
-
- process_count += 1
- else: #not an audio file must be synthesis
- var slider_data = _get_slider_values_ordered(node)
- var makeprocess = await make_process(node, process_count, "none", 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)
-
- 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
- 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
-
- output_files[node_name] = pvoc_stereo_files
- 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
- 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]
- 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)
-
- #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
- ##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
- if process_cancelled:
- progress_label.text = "Thread Stopped"
- log_console("[b]Thread Stopped[/b]", true)
- return
- else:
- progress_label.text = "Finalising output"
- var output_inputs := []
- for conn in connections:
- var to_node = str(conn["to_node"])
- if all_nodes.has(to_node) and all_nodes[to_node].get_meta("command") == "outputfile":
- output_inputs.append(str(conn["from_node"]))
- # List to hold the final output files to be merged (if needed)
- var final_outputs := []
- for node_name in output_inputs:
- if output_files.has(node_name):
- final_outputs.append(output_files[node_name])
- # 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)
- final_output_dir = runmerge[0]
- var converted_files = runmerge[1]
-
- if control_script.delete_intermediate_outputs:
- for f in converted_files:
- intermediate_files.append(f)
- # Only one output, no merge needed
- elif final_outputs.size() == 1:
- var single_output = final_outputs[0]
- 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
- if process_cancelled:
- progress_label.text = "Thread Stopped"
- log_console("[b]Thread Stopped[/b]", true)
- return
- else:
- log_console("Cleaning up intermediate files.", true)
- progress_label.text = "Cleaning up"
- for file_path in intermediate_files:
- # Adjust file path format for Windows if needed
- var fixed_path = file_path
- if is_windows:
- fixed_path = fixed_path.replace("/", "\\")
- await run_command(delete_cmd, [fixed_path])
- await get_tree().process_frame
- #delete break files
- for file_path in breakfiles:
- # Adjust file path format for Windows if needed
- var fixed_path = file_path
- if is_windows:
- fixed_path = fixed_path.replace("/", "\\")
- await run_command(delete_cmd, [fixed_path])
- await get_tree().process_frame
-
- var final_filename = "%s.wav" % Global.outfile
- 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])
- final_output_dir = Global.outfile + ".wav"
-
- 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()
- if interface_settings.auto_close_console and process_successful == true:
- console_window.hide()
- func is_stereo(file: String) -> bool:
- if file != "none":
- var output = await run_command(control_script.cdpprogs_location + "/sfprops", ["-c", file])
- output = int(output.strip_edges()) #convert output from cmd to clean int
- if output == 1:
- return false
- elif output == 2:
- return true
- elif output == 1026: #ignore pvoc .ana files
- return false
- else:
- log_console("[color=#9c2828]Error: Only mono and stereo files are supported[/color]", true)
- return false
- return true
-
- 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 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
- # Check each file's channel count
- for f in input_files:
- var stereo = await is_stereo(f)
- if stereo == false:
- mono_files.append(f)
- 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 = 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)
- else:
- converted_files.append(stereo_file)
- inputs_to_merge.append(stereo_file)
- # Add existing stereo files
- inputs_to_merge += stereo_files
- else:
- # All mono or all stereo — use input_files directly
- 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)
- quoted_inputs.insert(0, "mergemany")
- quoted_inputs.append(merge_output)
- await run_command(control_script.cdpprogs_location + "/submix", quoted_inputs)
- if process_successful == false:
- log_console("Failed to to merge files to" + merge_output, true)
-
- return [merge_output, converted_files]
- func _get_slider_values_ordered(node: Node) -> Array:
- var results := []
- for child in node.get_children():
- if child is Range:
- var flag = child.get_meta("flag") if child.has_meta("flag") else ""
- var time = child.get_meta("time")
- var brk_data = []
- var min_slider = child.min_value
- var max_slider = child.max_value
- var exp = child.exp_edit
- if child.has_meta("brk_data"):
- brk_data = child.get_meta("brk_data")
- results.append(["slider", flag, child.value, time, brk_data, min_slider, max_slider, exp])
- elif child is CheckButton:
- var flag = child.get_meta("flag") if child.has_meta("flag") else ""
- results.append(["checkbutton", flag, child.button_pressed])
- elif child is OptionButton:
- var flag = child.get_meta("flag") if child.has_meta("flag") else ""
- var value = child.get_item_text(child.selected)
- results.append(["optionbutton", flag, value])
- #call this function recursively to find any nested sliders in scenes
- if child.get_child_count() > 0:
- var nested := _get_slider_values_ordered(child)
- results.append_array(nested)
- return results
- func make_process(node: Node, process_count: int, current_infile: String, slider_data: Array) -> Array:
- # Determine output extension: .wav or .ana based on the node's slot type
- var extension = ".wav" if node.get_slot_type_right(0) == 0 else ".ana"
- # Construct output filename for this step
- var output_file = "%s_%d%s" % [Global.outfile.get_basename(), process_count, extension]
- # Get the command name from metadata or default to node name
- var command_name = str(node.get_meta("command"))
- command_name = command_name.split("_", true, 1)
- var command = "%s/%s" %[control_script.cdpprogs_location, command_name[0]]
- var args = command_name[1].split("_", true, 1)
- if current_infile != "none":
- #check if input is none, e.g. synthesis nodes, otherwise append input file to arguments
- args.append(current_infile)
- args.append(output_file)
- # Start building the command line windows i dont think this is used anymore
- #var line = "%s/%s \"%s\" \"%s\" " % [control_script.cdpprogs_location, command_name, current_infile, output_file]
- #mac
-
- var cleanup = []
- # Append parameter values from the sliders, include flags if present
- var slider_count = 0
- for entry in slider_data:
- if entry[0] == "slider":
- var flag = entry[1]
- var value = entry[2]
- #if value == int(value):
- #value = int(value)
- var time = entry[3] #checks if slider is a time percentage slider
- var brk_data = entry[4]
- var min_slider = entry[5]
- var max_slider = entry[6]
- var exp = entry[7]
- if brk_data.size() > 0: #if breakpoint data is present on slider
- #Sort all points by time
- var sorted_brk_data = []
- sorted_brk_data = brk_data.duplicate()
- sorted_brk_data.sort_custom(sort_points)
-
- var calculated_brk = []
-
- #get length of input file in seconds
- var infile_length = 1 #set infile length to dummy value just incase it does get used where it shouldn't to avoid crashes
- if current_infile != "none":
- infile_length = await run_command(control_script.cdpprogs_location + "/sfprops", ["-d", current_infile])
- infile_length = float(infile_length.strip_edges())
-
- #scale values from automation window to the right length for file and correct slider values
- #if node has an output duration then breakpoint files should be x = outputduration y= slider value else x=input duration, y=value
- if node.has_meta("outputduration"):
- for i in range(sorted_brk_data.size()):
- var point = sorted_brk_data[i]
- var new_x = float(node.get_meta("outputduration")) * (point.x / 700) #output time
- if i == sorted_brk_data.size() - 1: #check if this is last automation point
- new_x = float(node.get_meta("outputduration")) + 0.1 # force last point's x to infile_length + 100ms to make sure the file is defo over
- var new_y
- #check if slider is exponential and scale automation
- if exp:
- new_y = remap_y_to_log_scale(point.y, 0.0, 255.0, min_slider, max_slider)
- else:
- new_y = remap(point.y, 255, 0, min_slider, max_slider) #slider value
- if time: #check if this is a time slider and convert to percentage of input file
- new_y = infile_length * (new_y / 100)
- calculated_brk.append(Vector2(new_x, new_y))
- else:
- for i in range(sorted_brk_data.size()):
- var point = sorted_brk_data[i]
- var new_x = infile_length * (point.x / 700) #time
- if i == sorted_brk_data.size() - 1: #check if this is last automation point
- new_x = infile_length + 0.1 # force last point's x to infile_length + 100ms to make sure the file is defo over
- var new_y
- #check if slider is exponential and scale automation
- if exp:
- new_y = remap_y_to_log_scale(point.y, 0.0, 255.0, min_slider, max_slider)
- else:
- new_y = remap(point.y, 255, 0, min_slider, max_slider) #slider value
- calculated_brk.append(Vector2(new_x, new_y))
-
- #make text file
- var brk_file_path = output_file.get_basename() + "_" + str(slider_count) + ".txt"
- write_breakfile(calculated_brk, brk_file_path)
-
- #add breakfile to cleanup before adding flag
- cleanup.append(brk_file_path)
-
- #append text file in place of value
- #include flag if this param has a flag
- if flag.begins_with("-"):
- brk_file_path = flag + brk_file_path
- args.append(brk_file_path)
-
-
- else: #no break file append slider value
- if time == true:
- var infile_length = await run_command(control_script.cdpprogs_location + "/sfprops", ["-d", current_infile])
- infile_length = float(infile_length.strip_edges())
- value = infile_length * (value / 100) #calculate percentage time of the input file
- #line += ("%s%.2f " % [flag, value]) if flag.begins_with("-") else ("%.2f " % value)
- args.append(("%s%.2f " % [flag, value]) if flag.begins_with("-") else str(value))
-
- elif entry[0] == "checkbutton":
- var flag = entry[1]
- var value = entry[2]
- #if button is pressed add the flag to the arguments list
- if value == true:
- args.append(flag)
-
- elif entry[0] == "optionbutton":
- var flag = entry[1]
- var value = entry[2]
- args.append(("%s%.2f " % [flag, value]) if flag.begins_with("-") else str(value))
-
- slider_count += 1
- return [command, output_file, cleanup, args]
- #return [line.strip_edges(), output_file, cleanup]
- func remap_y_to_log_scale(y: float, min_y: float, max_y: float, min_val: float, max_val: float) -> float:
- var t = clamp((y - min_y) / (max_y - min_y), 0.0, 1.0)
- # Since y goes top-down (0 = top, 255 = bottom), we invert t
- t = 1.0 - t
- var log_min = log(min_val) / log(10)
- var log_max = log(max_val) / log(10)
- var log_val = lerp(log_min, log_max, t)
- return pow(10.0, log_val)
- func sort_points(a, b):
- return a.x < b.x
-
- func write_breakfile(points: Array, path: String):
- var file = FileAccess.open(path, FileAccess.WRITE)
- if file:
- for point in points:
- var line = str(point.x) + " " + str(point.y) + "\n"
- file.store_string(line)
- file.close()
- else:
- print("Failed to open file for writing.")
- func _on_kill_process_button_down() -> void:
- if process_running and process_info.has("pid"):
- progress_window.hide()
- # Terminate the process by PID
- OS.kill(process_info["pid"])
- process_running = false
- print("Process cancelled.")
- process_cancelled = true
-
- func path_exists_through_all_nodes() -> bool:
- var all_nodes = {}
- var graph = {}
- var input_node_names = []
- var output_node_name = ""
- # Gather all relevant nodes
- for child in graph_edit.get_children():
- if child is GraphNode:
- var name = str(child.name)
- all_nodes[name] = child
- var command = child.get_meta("command")
- var input = child.get_meta("input")
- if input:
- 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] = []
- # Add edges to graph from the connection list
- var connection_list = graph_edit.get_connection_list()
- for conn in connection_list:
- var from = str(conn["from_node"])
- var to = str(conn["to_node"])
- if graph.has(from):
- graph[from].append(to)
- # 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"]
- if current_node in visited:
- continue
- visited[current_node] = 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 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")
- console_output.scroll_to_line(console_output.get_line_count() - 1)
- if update == true:
- await get_tree().process_frame # Optional: ensure UI updates
- func run_command(command: String, args: Array) -> String:
- var is_windows = OS.get_name() == "Windows"
- console_output.append_text(command + " " + " ".join(args) + "\n")
- console_output.scroll_to_line(console_output.get_line_count() - 1)
- await get_tree().process_frame
-
- if is_windows and (command == "del" or command == "ren"): #checks if the command is a windows system command and runs it through cmd.exe
- args.insert(0, command)
- args.insert(0, "/C")
- process_info = OS.execute_with_pipe("cmd.exe", args, false)
- else:
- process_info = OS.execute_with_pipe(command, args, false)
- # Check if the process was successfully started
- if !process_info.has("pid"):
- print("Failed to start process.")
- return ""
-
- process_running = true
-
- # Start monitoring the process output and status
- return await monitor_process(process_info["pid"], process_info["stdio"], process_info["stderr"])
- func monitor_process(pid: int, stdout: FileAccess, stderr: FileAccess) -> String:
- var output := ""
-
- while OS.is_process_running(pid):
- await get_tree().process_frame
-
- while stdout.get_position() < stdout.get_length():
- var line = stdout.get_line()
- output += line
- console_output.append_text(line + "\n")
- console_output.scroll_to_line(console_output.get_line_count() - 1)
- while stderr.get_position() < stderr.get_length():
- var line = stderr.get_line()
- output += line
- console_output.append_text(line + "\n")
- console_output.scroll_to_line(console_output.get_line_count() - 1)
-
- var exit_code = OS.get_process_exit_code(pid)
- if exit_code == 0:
- if output.contains("ERROR:"): #checks if CDP reported an error but passed exit code 0 anyway
- console_output.append_text("[color=#9c2828][b]Processes failed[/b][/color]\n\n")
- console_output.scroll_to_line(console_output.get_line_count() - 1)
- process_successful = false
- if process_cancelled == false:
- progress_window.hide()
- if !console_window.visible:
- console_window.popup_centered()
- else:
- console_output.append_text("[color=#638382]Processes ran successfully[/color]\n\n")
- console_output.scroll_to_line(console_output.get_line_count() - 1)
- else:
- console_output.append_text("[color=#9c2828][b]Processes failed with exit code: %d[/b][/color]\n" % exit_code + "\n")
- console_output.scroll_to_line(console_output.get_line_count() - 1)
- process_successful = false
- if process_cancelled == false:
- progress_window.hide()
- if !console_window.visible:
- console_window.popup_centered()
- if output.contains("as an internal or external command"): #check for cdprogs location error on windows
- console_output.append_text("[color=#9c2828][b]Please make sure your cdprogs folder is set to the correct location in the Settings menu. The default location is C:\\CDPR8\\_cdp\\_cdprogs[/b][/color]\n\n")
- console_output.scroll_to_line(console_output.get_line_count() - 1)
- if output.contains("command not found"): #check for cdprogs location error on unix systems
- console_output.append_text("[color=#9c2828][b]Please make sure your cdprogs folder is set to the correct location in the Settings menu. The default location is ~/cdpr8/_cdp/_cdprogs[/b][/color]\n\n")
- console_output.scroll_to_line(console_output.get_line_count() - 1)
-
- process_running = false
- return output
|