run_thread.gd 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867
  1. extends Node
  2. var control_script
  3. var progress_label
  4. var progress_bar
  5. var graph_edit
  6. var console_output
  7. var progress_window
  8. var console_window
  9. var process_successful #tracks if the last run process was successful
  10. var process_info = {} #tracks the data of the currently running process
  11. var process_running := false #tracks if a process is currently running
  12. var process_cancelled = false #checks if the currently running process has been cancelled
  13. var final_output_dir
  14. # Called when the node enters the scene tree for the first time.
  15. func _ready() -> void:
  16. pass
  17. func init(main_node: Node, progresswindow: Window, progresslabel: Label, progressbar: ProgressBar, graphedit: GraphEdit, consolewindow: Window, consoleoutput: RichTextLabel) -> void:
  18. control_script = main_node
  19. progress_window = progresswindow
  20. progress_label = progresslabel
  21. progress_bar = progressbar
  22. graph_edit = graphedit
  23. console_window = consolewindow
  24. console_output = consoleoutput
  25. func run_thread_with_branches():
  26. process_cancelled = false
  27. process_successful = true
  28. # Detect platform: Determine if the OS is Windows
  29. var is_windows := OS.get_name() == "Windows"
  30. # Choose appropriate commands based on OS
  31. var delete_cmd = "del" if is_windows else "rm"
  32. var rename_cmd = "ren" if is_windows else "mv"
  33. var path_sep := "/" # Always use forward slash for paths
  34. # Get all node connections in the GraphEdit
  35. var connections = graph_edit.get_connection_list()
  36. # Prepare data structures for graph traversal
  37. var graph = {} # forward adjacency list
  38. var reverse_graph = {} # reverse adjacency list (for input lookup)
  39. var indegree = {} # used for topological sort
  40. var all_nodes = {} # map of node name -> GraphNode reference
  41. log_console("Mapping thread.", true)
  42. await get_tree().process_frame # Let UI update
  43. #Step 0: check thread is valid
  44. var is_valid = path_exists_through_all_nodes()
  45. if is_valid == false:
  46. log_console("[color=#9c2828][b]Error: Valid Thread not found[/b][/color]", true)
  47. log_console("Threads must contain at least one processing node and a valid path from the Input File to the Output File.", true)
  48. await get_tree().process_frame # Let UI update
  49. if progress_window.visible:
  50. progress_window.hide()
  51. if !console_window.visible:
  52. console_window.popup_centered()
  53. return
  54. else:
  55. log_console("[color=#638382][b]Valid Thread found[/b][/color]", true)
  56. await get_tree().process_frame # Let UI update
  57. # Step 1: Gather nodes from the GraphEdit
  58. var inputcount = 0 # used for tracking the number of input nodes and trims on input files for progress bar
  59. for child in graph_edit.get_children():
  60. if child is GraphNode:
  61. var includenode = true
  62. var name = str(child.name)
  63. all_nodes[name] = child
  64. if child.has_meta("utility"):
  65. includenode = false
  66. else:
  67. #check if node has inputs
  68. if child.get_input_port_count() > 0:
  69. #if it does scan through those inputs
  70. for i in range(child.get_input_port_count()):
  71. #check if it can find any valid connections
  72. var connected = false
  73. for conn in connections:
  74. if conn["to_node"] == name and conn["to_port"] == i:
  75. connected = true
  76. break
  77. #if no valid connections are found break the for loop to skip checking other inputs and set include to false
  78. if connected == false:
  79. log_console(name + " input is not connected, skipping node.", true)
  80. includenode = false
  81. break
  82. #check if node has outputs
  83. if child.get_output_port_count() > 0:
  84. #if it does scan through those outputs
  85. for i in range(child.get_output_port_count()):
  86. #check if it can find any valid connections
  87. var connected = false
  88. for conn in connections:
  89. if conn["from_node"] == name and conn["to_port"] == i:
  90. connected = true
  91. break
  92. #if no valid connections are found break the for loop to skip checking other inputs and set include to false
  93. if connected == false:
  94. log_console(name + " output is not connected, skipping node.", true)
  95. includenode = false
  96. break
  97. if includenode == true:
  98. graph[name] = []
  99. reverse_graph[name] = []
  100. indegree[name] = 0 # Start with zero incoming edges
  101. if child.get_meta("command") == "inputfile":
  102. inputcount -= 1
  103. if child.get_node("AudioPlayer").get_meta("trimfile"):
  104. inputcount += 1
  105. #do calculations for progress bar
  106. var progress_step
  107. progress_step = 100 / (graph.size() + 3 + inputcount)
  108. # Step 2: Build graph relationships from connections
  109. if process_cancelled:
  110. progress_label.text = "Thread Stopped"
  111. log_console("[b]Thread Stopped[/b]", true)
  112. return
  113. else:
  114. progress_label.text = "Building Thread"
  115. for conn in connections:
  116. var from = str(conn["from_node"])
  117. var to = str(conn["to_node"])
  118. if graph.has(from) and graph.has(to):
  119. graph[from].append(to)
  120. reverse_graph[to].append(from)
  121. indegree[to] += 1 # Count incoming edges
  122. # Step 3: Topological sort to get execution order
  123. var sorted = [] # Sorted list of node names
  124. var queue = [] # Queue of nodes with 0 indegree
  125. for node in graph.keys():
  126. if indegree[node] == 0:
  127. queue.append(node)
  128. while not queue.is_empty():
  129. var current = queue.pop_front()
  130. sorted.append(current)
  131. for neighbor in graph[current]:
  132. indegree[neighbor] -= 1
  133. if indegree[neighbor] == 0:
  134. queue.append(neighbor)
  135. # If not all nodes were processed, there's a cycle
  136. if sorted.size() != graph.size():
  137. log_console("[color=#9c2828][b]Error: Thread not valid[/b][/color]", true)
  138. log_console("Threads cannot contain loops.", true)
  139. return
  140. progress_bar.value = progress_step
  141. # Step 4: Start processing audio
  142. var batch_lines = [] # Holds all batch file commands
  143. var intermediate_files = [] # Files to delete later
  144. var breakfiles = [] #breakfiles to delete later
  145. # Dictionary to keep track of each node's output file
  146. var output_files = {}
  147. var process_count = 0
  148. var current_infile
  149. # Iterate over the processing nodes in topological order
  150. for node_name in sorted:
  151. var node = all_nodes[node_name]
  152. if process_cancelled:
  153. progress_label.text = "Thread Stopped"
  154. log_console("[b]Thread Stopped[/b]", true)
  155. break
  156. else:
  157. progress_label.text = "Running process: " + node.get_title()
  158. # Find upstream nodes connected to the current node
  159. var inputs = reverse_graph[node_name]
  160. var input_files = []
  161. for input_node in inputs:
  162. input_files.append(output_files[input_node])
  163. # Merge inputs if this node has more than one input
  164. if input_files.size() > 1:
  165. # Prepare final merge output file name
  166. var runmerge = await merge_many_files(process_count, input_files)
  167. var merge_output = runmerge[0]
  168. var converted_files = runmerge[1]
  169. # Track the output and intermediate files
  170. current_infile = merge_output
  171. if control_script.delete_intermediate_outputs:
  172. intermediate_files.append(merge_output)
  173. for f in converted_files:
  174. intermediate_files.append(f)
  175. # If only one input, use that
  176. elif input_files.size() == 1:
  177. current_infile = input_files[0]
  178. ## If no input, use the original input file
  179. else:
  180. #if no input i need to skip the node
  181. pass
  182. if node.get_meta("command") == "inputfile":
  183. #get the inputfile from the nodes meta
  184. var loadedfile = node.get_node("AudioPlayer").get_meta("inputfile")
  185. #get wether trim has been enabled
  186. var trimfile = node.get_node("AudioPlayer").get_meta("trimfile")
  187. #if trim is enabled trim the file
  188. if trimfile == true:
  189. #get the start and end points
  190. var start = node.get_node("AudioPlayer").get_meta("trimpoints")[0]
  191. var end = node.get_node("AudioPlayer").get_meta("trimpoints")[1]
  192. if process_cancelled:
  193. #exit out of process if cancelled
  194. progress_label.text = "Thread Stopped"
  195. log_console("[b]Thread Stopped[/b]", true)
  196. return
  197. else:
  198. progress_label.text = "Trimming input audio"
  199. await run_command(control_script.cdpprogs_location + "/sfedit", ["cut", "1", loadedfile, "%s_%d_input_trim.wav" % [Global.outfile, process_count], str(start), str(end)])
  200. output_files[node_name] = "%s_%d_input_trim.wav" % [Global.outfile, process_count]
  201. # Mark trimmed file for cleanup if needed
  202. if control_script.delete_intermediate_outputs:
  203. intermediate_files.append("%s_%d_input_trim.wav" % [Global.outfile, process_count])
  204. progress_bar.value += progress_step
  205. else:
  206. #if trim not enabled pass the loaded file
  207. output_files[node_name] = loadedfile
  208. process_count += 1
  209. else:
  210. # Build the command for the current node's audio processing
  211. var slider_data = _get_slider_values_ordered(node)
  212. if node.get_slot_type_right(0) == 1: #detect if process outputs pvoc data
  213. if typeof(current_infile) == TYPE_ARRAY:
  214. #check if infile is an array meaning that the last pvoc process was run in dual mono mode
  215. # Process left and right seperately
  216. var pvoc_stereo_files = []
  217. for infile in current_infile:
  218. var makeprocess = await make_process(node, process_count, infile, slider_data)
  219. # run the command
  220. await run_command(makeprocess[0], makeprocess[3])
  221. await get_tree().process_frame
  222. var output_file = makeprocess[1]
  223. pvoc_stereo_files.append(output_file)
  224. # Mark file for cleanup if needed
  225. if control_script.delete_intermediate_outputs:
  226. for file in makeprocess[2]:
  227. breakfiles.append(file)
  228. intermediate_files.append(output_file)
  229. process_count += 1
  230. output_files[node_name] = pvoc_stereo_files
  231. else:
  232. var input_stereo = await is_stereo(current_infile)
  233. if input_stereo == true:
  234. #audio file is stereo and needs to be split for pvoc processing
  235. var pvoc_stereo_files = []
  236. ##Split stereo to c1/c2
  237. await run_command(control_script.cdpprogs_location + "/housekeep",["chans", "2", current_infile])
  238. # Process left and right seperately
  239. for channel in ["c1", "c2"]:
  240. var dual_mono_file = current_infile.get_basename() + "_%s.wav" % channel
  241. var makeprocess = await make_process(node, process_count, dual_mono_file, slider_data)
  242. # run the command
  243. await run_command(makeprocess[0], makeprocess[3])
  244. await get_tree().process_frame
  245. var output_file = makeprocess[1]
  246. pvoc_stereo_files.append(output_file)
  247. # Mark file for cleanup if needed
  248. if control_script.delete_intermediate_outputs:
  249. for file in makeprocess[2]:
  250. breakfiles.append(file)
  251. intermediate_files.append(output_file)
  252. #Delete c1 and c2 because they can be in the wrong folder and if the same infile is used more than once
  253. #with this stereo process CDP will throw errors in the console even though its fine
  254. if is_windows:
  255. dual_mono_file = dual_mono_file.replace("/", "\\")
  256. await run_command(delete_cmd, [dual_mono_file])
  257. process_count += 1
  258. # Store output file path for this node
  259. output_files[node_name] = pvoc_stereo_files
  260. else:
  261. #input file is mono run through process
  262. var makeprocess = await make_process(node, process_count, current_infile, slider_data)
  263. # run the command
  264. await run_command(makeprocess[0], makeprocess[3])
  265. await get_tree().process_frame
  266. var output_file = makeprocess[1]
  267. # Store output file path for this node
  268. output_files[node_name] = output_file
  269. # Mark file for cleanup if needed
  270. if control_script.delete_intermediate_outputs:
  271. for file in makeprocess[2]:
  272. breakfiles.append(file)
  273. intermediate_files.append(output_file)
  274. # Increase the process step count
  275. process_count += 1
  276. else:
  277. #Process outputs audio
  278. #check if this is the last pvoc process in a stereo processing chain
  279. if node.get_meta("command") == "pvoc_synth" and typeof(current_infile) == TYPE_ARRAY:
  280. #check if infile is an array meaning that the last pvoc process was run in dual mono mode
  281. # Process left and right seperately
  282. var pvoc_stereo_files = []
  283. for infile in current_infile:
  284. var makeprocess = await make_process(node, process_count, infile, slider_data)
  285. # run the command
  286. await run_command(makeprocess[0], makeprocess[3])
  287. await get_tree().process_frame
  288. var output_file = makeprocess[1]
  289. pvoc_stereo_files.append(output_file)
  290. # Mark file for cleanup if needed
  291. if control_script.delete_intermediate_outputs:
  292. for file in makeprocess[2]:
  293. breakfiles.append(file)
  294. intermediate_files.append(output_file)
  295. process_count += 1
  296. #interleave left and right
  297. var output_file = Global.outfile.get_basename() + str(process_count) + "_interleaved.wav"
  298. await run_command(control_script.cdpprogs_location + "/submix", ["interleave", pvoc_stereo_files[0], pvoc_stereo_files[1], output_file])
  299. # Store output file path for this node
  300. output_files[node_name] = output_file
  301. # Mark file for cleanup if needed
  302. if control_script.delete_intermediate_outputs:
  303. intermediate_files.append(output_file)
  304. else:
  305. #Detect if input file is mono or stereo
  306. var input_stereo = await is_stereo(current_infile)
  307. if input_stereo == true:
  308. if node.get_meta("stereo_input") == true: #audio file is stereo and process is stereo, run file through process
  309. var makeprocess = await make_process(node, process_count, current_infile, slider_data)
  310. # run the command
  311. await run_command(makeprocess[0], makeprocess[3])
  312. await get_tree().process_frame
  313. var output_file = makeprocess[1]
  314. # Store output file path for this node
  315. output_files[node_name] = output_file
  316. # Mark file for cleanup if needed
  317. if control_script.delete_intermediate_outputs:
  318. for file in makeprocess[2]:
  319. breakfiles.append(file)
  320. intermediate_files.append(output_file)
  321. else: #audio file is stereo and process is mono, split stereo, process and recombine
  322. ##Split stereo to c1/c2
  323. await run_command(control_script.cdpprogs_location + "/housekeep",["chans", "2", current_infile])
  324. # Process left and right seperately
  325. var dual_mono_output = []
  326. for channel in ["c1", "c2"]:
  327. var dual_mono_file = current_infile.get_basename() + "_%s.wav" % channel
  328. var makeprocess = await make_process(node, process_count, dual_mono_file, slider_data)
  329. # run the command
  330. await run_command(makeprocess[0], makeprocess[3])
  331. await get_tree().process_frame
  332. var output_file = makeprocess[1]
  333. dual_mono_output.append(output_file)
  334. # Mark file for cleanup if needed
  335. if control_script.delete_intermediate_outputs:
  336. for file in makeprocess[2]:
  337. breakfiles.append(file)
  338. intermediate_files.append(output_file)
  339. #Delete c1 and c2 because they can be in the wrong folder and if the same infile is used more than once
  340. #with this stereo process CDP will throw errors in the console even though its fine
  341. if is_windows:
  342. dual_mono_file = dual_mono_file.replace("/", "\\")
  343. await run_command(delete_cmd, [dual_mono_file])
  344. process_count += 1
  345. var output_file = Global.outfile.get_basename() + str(process_count) + "_interleaved.wav"
  346. await run_command(control_script.cdpprogs_location + "/submix", ["interleave", dual_mono_output[0], dual_mono_output[1], output_file])
  347. # Store output file path for this node
  348. output_files[node_name] = output_file
  349. # Mark file for cleanup if needed
  350. if control_script.delete_intermediate_outputs:
  351. intermediate_files.append(output_file)
  352. else: #audio file is mono, run through the process
  353. var makeprocess = await make_process(node, process_count, current_infile, slider_data)
  354. # run the command
  355. await run_command(makeprocess[0], makeprocess[3])
  356. await get_tree().process_frame
  357. var output_file = makeprocess[1]
  358. # Store output file path for this node
  359. output_files[node_name] = output_file
  360. # Mark file for cleanup if needed
  361. if control_script.delete_intermediate_outputs:
  362. for file in makeprocess[2]:
  363. breakfiles.append(file)
  364. intermediate_files.append(output_file)
  365. # Increase the process step count
  366. process_count += 1
  367. progress_bar.value += progress_step
  368. # FINAL OUTPUT STAGE
  369. # Collect all nodes that are connected to the outputfile node
  370. if process_cancelled:
  371. progress_label.text = "Thread Stopped"
  372. log_console("[b]Thread Stopped[/b]", true)
  373. return
  374. else:
  375. progress_label.text = "Finalising output"
  376. var output_inputs := []
  377. for conn in connections:
  378. var to_node = str(conn["to_node"])
  379. if all_nodes.has(to_node) and all_nodes[to_node].get_meta("command") == "outputfile":
  380. output_inputs.append(str(conn["from_node"]))
  381. # List to hold the final output files to be merged (if needed)
  382. var final_outputs := []
  383. for node_name in output_inputs:
  384. if output_files.has(node_name):
  385. final_outputs.append(output_files[node_name])
  386. # If multiple outputs go to the outputfile node, merge them
  387. if final_outputs.size() > 1:
  388. var runmerge = await merge_many_files(process_count, final_outputs)
  389. final_output_dir = runmerge[0]
  390. var converted_files = runmerge[1]
  391. if control_script.delete_intermediate_outputs:
  392. for f in converted_files:
  393. intermediate_files.append(f)
  394. # Only one output, no merge needed
  395. elif final_outputs.size() == 1:
  396. var single_output = final_outputs[0]
  397. final_output_dir = single_output
  398. intermediate_files.erase(single_output)
  399. progress_bar.value += progress_step
  400. # CLEANUP: Delete intermediate files after processing and rename final output
  401. if process_cancelled:
  402. progress_label.text = "Thread Stopped"
  403. log_console("[b]Thread Stopped[/b]", true)
  404. return
  405. else:
  406. log_console("Cleaning up intermediate files.", true)
  407. progress_label.text = "Cleaning up"
  408. for file_path in intermediate_files:
  409. # Adjust file path format for Windows if needed
  410. var fixed_path = file_path
  411. if is_windows:
  412. fixed_path = fixed_path.replace("/", "\\")
  413. await run_command(delete_cmd, [fixed_path])
  414. await get_tree().process_frame
  415. #delete break files
  416. for file_path in breakfiles:
  417. # Adjust file path format for Windows if needed
  418. var fixed_path = file_path
  419. if is_windows:
  420. fixed_path = fixed_path.replace("/", "\\")
  421. await run_command(delete_cmd, [fixed_path])
  422. await get_tree().process_frame
  423. var final_filename = "%s.wav" % Global.outfile
  424. var final_output_dir_fixed_path = final_output_dir
  425. if is_windows:
  426. final_output_dir_fixed_path = final_output_dir_fixed_path.replace("/", "\\")
  427. await run_command(rename_cmd, [final_output_dir_fixed_path, final_filename.get_file()])
  428. else:
  429. await run_command(rename_cmd, [final_output_dir_fixed_path, "%s.wav" % Global.outfile])
  430. final_output_dir = Global.outfile + ".wav"
  431. control_script.output_audio_player.play_outfile(final_output_dir)
  432. Global.cdpoutput = final_output_dir
  433. progress_bar.value = 100.0
  434. var interface_settings = ConfigHandler.load_interface_settings() #checks if close console is enabled and closes console on a success
  435. progress_window.hide()
  436. if interface_settings.auto_close_console and process_successful == true:
  437. console_window.hide()
  438. func is_stereo(file: String) -> bool:
  439. var output = await run_command(control_script.cdpprogs_location + "/sfprops", ["-c", file])
  440. output = int(output.strip_edges()) #convert output from cmd to clean int
  441. if output == 1:
  442. return false
  443. elif output == 2:
  444. return true
  445. elif output == 1026: #ignore pvoc .ana files
  446. return false
  447. else:
  448. log_console("[color=#9c2828]Error: Only mono and stereo files are supported[/color]", true)
  449. return false
  450. func get_samplerate(file: String) -> int:
  451. var output = await run_command(control_script.cdpprogs_location + "/sfprops", ["-r", file])
  452. output = int(output.strip_edges())
  453. return output
  454. func merge_many_files(process_count: int, input_files: Array) -> Array:
  455. var merge_output = "%s_merge_%d.wav" % [Global.outfile.get_basename(), process_count]
  456. var converted_files := [] # Track any mono->stereo converted files or upsampled files
  457. var inputs_to_merge := [] # Files to be used in the final merge
  458. var mono_files := []
  459. var stereo_files := []
  460. var sample_rates := []
  461. #Get all sample rates
  462. for f in input_files:
  463. var samplerate = await get_samplerate(f)
  464. sample_rates.append(samplerate)
  465. #Check if all sample rates are the same
  466. if sample_rates.all(func(v): return v == sample_rates[0]):
  467. pass
  468. else:
  469. log_console("Different sample rates found, upsampling files to match highest current sample rate before mixing.", true)
  470. #if not find the highest sample rate
  471. var highest_sample_rate = sample_rates.max()
  472. var index = 0
  473. #move through all input files and compare match their index to the sample_rate array
  474. for f in input_files:
  475. #check if sample rate of current file is less than the highest sample rate
  476. if sample_rates[index] < highest_sample_rate:
  477. #up sample it to the highest sample rate if so
  478. var upsample_output = Global.outfile + "_" + str(process_count) + f.get_file().get_slice(".wav", 0) + "_" + str(highest_sample_rate) + ".wav"
  479. await run_command(control_script.cdpprogs_location + "/housekeep", ["respec", "1", f, upsample_output, str(highest_sample_rate)])
  480. #replace the file in the input_file index with the new upsampled file
  481. input_files[index] = upsample_output
  482. converted_files.append(upsample_output)
  483. index += 1
  484. # Check each file's channel count
  485. for f in input_files:
  486. var stereo = await is_stereo(f)
  487. if stereo == false:
  488. mono_files.append(f)
  489. elif stereo == true:
  490. stereo_files.append(f)
  491. # STEP 2: Convert mono to stereo if there is a mix
  492. if mono_files.size() > 0 and stereo_files.size() > 0:
  493. log_console("Mix of mono and stereo files found, interleaving mono files to stereo before mixing.", true)
  494. for mono_file in mono_files:
  495. var stereo_file = Global.outfile + "_" + str(process_count) + mono_file.get_file().get_slice(".wav", 0) + "_stereo.wav"
  496. #var stereo_file = "%s_stereo.wav" % mono_file.get_basename()
  497. await run_command(control_script.cdpprogs_location + "/submix", ["interleave", mono_file, mono_file, stereo_file])
  498. if process_successful == false:
  499. log_console("Failed to interleave mono file: %s" % mono_file, true)
  500. else:
  501. converted_files.append(stereo_file)
  502. inputs_to_merge.append(stereo_file)
  503. # Add existing stereo files
  504. inputs_to_merge += stereo_files
  505. else:
  506. # All mono or all stereo — use input_files directly
  507. inputs_to_merge = input_files.duplicate()
  508. # STEP 3: Merge all input files (converted or original)
  509. log_console("Mixing files to combined input.", true)
  510. var quoted_inputs := []
  511. for f in inputs_to_merge:
  512. quoted_inputs.append(f)
  513. quoted_inputs.insert(0, "mergemany")
  514. quoted_inputs.append(merge_output)
  515. await run_command(control_script.cdpprogs_location + "/submix", quoted_inputs)
  516. if process_successful == false:
  517. log_console("Failed to to merge files to" + merge_output, true)
  518. return [merge_output, converted_files]
  519. func _get_slider_values_ordered(node: Node) -> Array:
  520. var results := []
  521. for child in node.get_children():
  522. if child is Range:
  523. var flag = child.get_meta("flag") if child.has_meta("flag") else ""
  524. var time = child.get_meta("time")
  525. var brk_data = []
  526. var min_slider = child.min_value
  527. var max_slider = child.max_value
  528. if child.has_meta("brk_data"):
  529. brk_data = child.get_meta("brk_data")
  530. results.append([flag, child.value, time, brk_data, min_slider, max_slider])
  531. elif child.get_child_count() > 0:
  532. var nested := _get_slider_values_ordered(child)
  533. results.append_array(nested)
  534. return results
  535. func make_process(node: Node, process_count: int, current_infile: String, slider_data: Array) -> Array:
  536. # Determine output extension: .wav or .ana based on the node's slot type
  537. var extension = ".wav" if node.get_slot_type_right(0) == 0 else ".ana"
  538. # Construct output filename for this step
  539. var output_file = "%s_%d%s" % [Global.outfile.get_basename(), process_count, extension]
  540. # Get the command name from metadata or default to node name
  541. var command_name = str(node.get_meta("command"))
  542. command_name = command_name.split("_", true, 1)
  543. var command = "%s/%s" %[control_script.cdpprogs_location, command_name[0]]
  544. var args = command_name[1].split("_", true, 1)
  545. args.append(current_infile)
  546. args.append(output_file)
  547. # Start building the command line windows
  548. var line = "%s/%s \"%s\" \"%s\" " % [control_script.cdpprogs_location, command_name, current_infile, output_file]
  549. #mac
  550. var cleanup = []
  551. # Append parameter values from the sliders, include flags if present
  552. var slider_count = 0
  553. for entry in slider_data:
  554. var flag = entry[0]
  555. var value = entry[1]
  556. var time = entry[2] #checks if slider is a time percentage slider
  557. var brk_data = entry[3]
  558. var min_slider = entry[4]
  559. var max_slider = entry[5]
  560. if brk_data.size() > 0: #if breakpoint data is present on slider
  561. #Sort all points by time
  562. var sorted_brk_data = []
  563. sorted_brk_data = brk_data.duplicate()
  564. sorted_brk_data.sort_custom(sort_points)
  565. var calculated_brk = []
  566. #get length of input file in seconds
  567. var infile_length = await run_command(control_script.cdpprogs_location + "/sfprops", ["-d", current_infile])
  568. infile_length = float(infile_length.strip_edges())
  569. #scale values from automation window to the right length for file and correct slider values
  570. #need to check how time is handled in all files that accept it, zigzag is x = outfile position, y = infile position
  571. #if time == true:
  572. #for point in sorted_brk_data:
  573. #var new_x = infile_length * (point.x / 700) #time
  574. #var new_y = infile_length * (remap(point.y, 255, 0, min_slider, max_slider) / 100) #slider value scaled as a percentage of infile time
  575. #calculated_brk.append(Vector2(new_x, new_y))
  576. #else:
  577. for i in range(sorted_brk_data.size()):
  578. var point = sorted_brk_data[i]
  579. var new_x = infile_length * (point.x / 700) #time
  580. if i == sorted_brk_data.size() - 1: #check if this is last automation point
  581. new_x = infile_length + 0.1 # force last point's x to infile_length + 100ms to make sure the file is defo over
  582. var new_y = remap(point.y, 255, 0, min_slider, max_slider) #slider value
  583. calculated_brk.append(Vector2(new_x, new_y))
  584. #make text file
  585. var brk_file_path = output_file.get_basename() + "_" + str(slider_count) + ".txt"
  586. write_breakfile(calculated_brk, brk_file_path)
  587. #append text file in place of value
  588. line += ("\"%s\" " % brk_file_path)
  589. args.append(brk_file_path)
  590. cleanup.append(brk_file_path)
  591. else:
  592. if time == true:
  593. var infile_length = await run_command(control_script.cdpprogs_location + "/sfprops", ["-d", current_infile])
  594. infile_length = float(infile_length.strip_edges())
  595. value = infile_length * (value / 100) #calculate percentage time of the input file
  596. line += ("%s%.2f " % [flag, value]) if flag.begins_with("-") else ("%.2f " % value)
  597. args.append(("%s%.2f " % [flag, value]) if flag.begins_with("-") else ("%.2f " % value))
  598. slider_count += 1
  599. return [command, output_file, cleanup, args]
  600. #return [line.strip_edges(), output_file, cleanup]
  601. func sort_points(a, b):
  602. return a.x < b.x
  603. func write_breakfile(points: Array, path: String):
  604. var file = FileAccess.open(path, FileAccess.WRITE)
  605. if file:
  606. for point in points:
  607. var line = str(point.x) + " " + str(point.y) + "\n"
  608. file.store_string(line)
  609. file.close()
  610. else:
  611. print("Failed to open file for writing.")
  612. func _on_kill_process_button_down() -> void:
  613. if process_running and process_info.has("pid"):
  614. progress_window.hide()
  615. # Terminate the process by PID
  616. OS.kill(process_info["pid"])
  617. process_running = false
  618. print("Process cancelled.")
  619. process_cancelled = true
  620. func path_exists_through_all_nodes() -> bool:
  621. var all_nodes = {}
  622. var graph = {}
  623. var input_node_names = []
  624. var output_node_name = ""
  625. # Gather all relevant nodes
  626. for child in graph_edit.get_children():
  627. if child is GraphNode:
  628. var name = str(child.name)
  629. all_nodes[name] = child
  630. var command = child.get_meta("command")
  631. if command == "inputfile":
  632. input_node_names.append(name)
  633. elif command == "outputfile":
  634. output_node_name = name
  635. # Skip utility nodes, include others
  636. if command in ["inputfile", "outputfile"] or not child.has_meta("utility"):
  637. graph[name] = []
  638. # Add edges to graph from the connection list
  639. var connection_list = graph_edit.get_connection_list()
  640. for conn in connection_list:
  641. var from = str(conn["from_node"])
  642. var to = str(conn["to_node"])
  643. if graph.has(from):
  644. graph[from].append(to)
  645. # BFS from each input node
  646. for input_node_name in input_node_names:
  647. var visited = {}
  648. var queue = [{ "node": input_node_name, "depth": 0 }]
  649. while queue.size() > 0:
  650. var current = queue.pop_front()
  651. var current_node = current["node"]
  652. var depth = current["depth"]
  653. if current_node in visited:
  654. continue
  655. visited[current_node] = true
  656. if current_node == output_node_name and depth >= 2:
  657. return true # Found a valid path from this input node
  658. if graph.has(current_node):
  659. for neighbor in graph[current_node]:
  660. queue.append({ "node": neighbor, "depth": depth + 1 })
  661. # If no path from any input node to output node was found
  662. return false
  663. func log_console(text: String, update: bool) -> void:
  664. console_output.append_text(text + "\n \n")
  665. console_output.scroll_to_line(console_output.get_line_count() - 1)
  666. if update == true:
  667. await get_tree().process_frame # Optional: ensure UI updates
  668. func run_command(command: String, args: Array) -> String:
  669. var is_windows = OS.get_name() == "Windows"
  670. console_output.append_text(command + " " + " ".join(args) + "\n")
  671. console_output.scroll_to_line(console_output.get_line_count() - 1)
  672. await get_tree().process_frame
  673. if is_windows and (command == "del" or command == "ren"): #checks if the command is a windows system command and runs it through cmd.exe
  674. args.insert(0, command)
  675. args.insert(0, "/C")
  676. process_info = OS.execute_with_pipe("cmd.exe", args, false)
  677. else:
  678. process_info = OS.execute_with_pipe(command, args, false)
  679. # Check if the process was successfully started
  680. if !process_info.has("pid"):
  681. print("Failed to start process.")
  682. return ""
  683. process_running = true
  684. # Start monitoring the process output and status
  685. return await monitor_process(process_info["pid"], process_info["stdio"], process_info["stderr"])
  686. func monitor_process(pid: int, stdout: FileAccess, stderr: FileAccess) -> String:
  687. var output := ""
  688. while OS.is_process_running(pid):
  689. await get_tree().process_frame
  690. while stdout.get_position() < stdout.get_length():
  691. var line = stdout.get_line()
  692. output += line
  693. console_output.append_text(line + "\n")
  694. console_output.scroll_to_line(console_output.get_line_count() - 1)
  695. while stderr.get_position() < stderr.get_length():
  696. var line = stderr.get_line()
  697. output += line
  698. console_output.append_text(line + "\n")
  699. console_output.scroll_to_line(console_output.get_line_count() - 1)
  700. var exit_code = OS.get_process_exit_code(pid)
  701. if exit_code == 0:
  702. if output.contains("ERROR:"): #checks if CDP reported an error but passed exit code 0 anyway
  703. console_output.append_text("[color=#9c2828][b]Processes failed[/b][/color]\n\n")
  704. console_output.scroll_to_line(console_output.get_line_count() - 1)
  705. process_successful = false
  706. if process_cancelled == false:
  707. progress_window.hide()
  708. if !console_window.visible:
  709. console_window.popup_centered()
  710. else:
  711. console_output.append_text("[color=#638382]Processes ran successfully[/color]\n\n")
  712. console_output.scroll_to_line(console_output.get_line_count() - 1)
  713. else:
  714. console_output.append_text("[color=#9c2828][b]Processes failed with exit code: %d[/b][/color]\n" % exit_code + "\n")
  715. console_output.scroll_to_line(console_output.get_line_count() - 1)
  716. process_successful = false
  717. if process_cancelled == false:
  718. progress_window.hide()
  719. if !console_window.visible:
  720. console_window.popup_centered()
  721. if output.contains("as an internal or external command"): #check for cdprogs location error on windows
  722. 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")
  723. console_output.scroll_to_line(console_output.get_line_count() - 1)
  724. if output.contains("command not found"): #check for cdprogs location error on unix systems
  725. 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")
  726. console_output.scroll_to_line(console_output.get_line_count() - 1)
  727. process_running = false
  728. return output