run_thread.gd 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949
  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 or Synthesis node 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. else:
  179. #if no input i need to skip the node
  180. pass
  181. #check if node is some form of input node
  182. if node.get_input_port_count() == 0:
  183. if node.get_meta("command") == "inputfile":
  184. #get the inputfile from the nodes meta
  185. var loadedfile = node.get_node("AudioPlayer").get_meta("inputfile")
  186. #get wether trim has been enabled
  187. var trimfile = node.get_node("AudioPlayer").get_meta("trimfile")
  188. #if trim is enabled trim the file
  189. if trimfile == true:
  190. #get the start and end points
  191. var start = node.get_node("AudioPlayer").get_meta("trimpoints")[0]
  192. var end = node.get_node("AudioPlayer").get_meta("trimpoints")[1]
  193. if process_cancelled:
  194. #exit out of process if cancelled
  195. progress_label.text = "Thread Stopped"
  196. log_console("[b]Thread Stopped[/b]", true)
  197. return
  198. else:
  199. progress_label.text = "Trimming input audio"
  200. await run_command(control_script.cdpprogs_location + "/sfedit", ["cut", "1", loadedfile, "%s_%d_input_trim.wav" % [Global.outfile, process_count], str(start), str(end)])
  201. output_files[node_name] = "%s_%d_input_trim.wav" % [Global.outfile, process_count]
  202. # Mark trimmed file for cleanup if needed
  203. if control_script.delete_intermediate_outputs:
  204. intermediate_files.append("%s_%d_input_trim.wav" % [Global.outfile, process_count])
  205. progress_bar.value += progress_step
  206. else:
  207. #if trim not enabled pass the loaded file
  208. output_files[node_name] = loadedfile
  209. process_count += 1
  210. else: #not an audio file must be synthesis
  211. var slider_data = _get_slider_values_ordered(node)
  212. var makeprocess = await make_process(node, process_count, "none", slider_data)
  213. # run the command
  214. await run_command(makeprocess[0], makeprocess[3])
  215. await get_tree().process_frame
  216. var output_file = makeprocess[1]
  217. # Store output file path for this node
  218. output_files[node_name] = output_file
  219. # Mark file for cleanup if needed
  220. if control_script.delete_intermediate_outputs:
  221. for file in makeprocess[2]:
  222. breakfiles.append(file)
  223. intermediate_files.append(output_file)
  224. process_count += 1
  225. else:
  226. # Build the command for the current node's audio processing
  227. var slider_data = _get_slider_values_ordered(node)
  228. if node.get_slot_type_right(0) == 1: #detect if process outputs pvoc data
  229. if typeof(current_infile) == TYPE_ARRAY:
  230. #check if infile is an array meaning that the last pvoc process was run in dual mono mode
  231. # Process left and right seperately
  232. var pvoc_stereo_files = []
  233. for infile in current_infile:
  234. var makeprocess = await make_process(node, process_count, infile, slider_data)
  235. # run the command
  236. await run_command(makeprocess[0], makeprocess[3])
  237. await get_tree().process_frame
  238. var output_file = makeprocess[1]
  239. pvoc_stereo_files.append(output_file)
  240. # Mark file for cleanup if needed
  241. if control_script.delete_intermediate_outputs:
  242. for file in makeprocess[2]:
  243. breakfiles.append(file)
  244. intermediate_files.append(output_file)
  245. process_count += 1
  246. output_files[node_name] = pvoc_stereo_files
  247. else:
  248. var input_stereo = await is_stereo(current_infile)
  249. if input_stereo == true:
  250. #audio file is stereo and needs to be split for pvoc processing
  251. var pvoc_stereo_files = []
  252. ##Split stereo to c1/c2
  253. await run_command(control_script.cdpprogs_location + "/housekeep",["chans", "2", current_infile])
  254. # Process left and right seperately
  255. for channel in ["c1", "c2"]:
  256. var dual_mono_file = current_infile.get_basename() + "_%s.wav" % channel
  257. var makeprocess = await make_process(node, process_count, dual_mono_file, slider_data)
  258. # run the command
  259. await run_command(makeprocess[0], makeprocess[3])
  260. await get_tree().process_frame
  261. var output_file = makeprocess[1]
  262. pvoc_stereo_files.append(output_file)
  263. # Mark file for cleanup if needed
  264. if control_script.delete_intermediate_outputs:
  265. for file in makeprocess[2]:
  266. breakfiles.append(file)
  267. intermediate_files.append(output_file)
  268. #Delete c1 and c2 because they can be in the wrong folder and if the same infile is used more than once
  269. #with this stereo process CDP will throw errors in the console even though its fine
  270. if is_windows:
  271. dual_mono_file = dual_mono_file.replace("/", "\\")
  272. await run_command(delete_cmd, [dual_mono_file])
  273. process_count += 1
  274. # Store output file path for this node
  275. output_files[node_name] = pvoc_stereo_files
  276. else:
  277. #input file is mono run through process
  278. var makeprocess = await make_process(node, process_count, current_infile, slider_data)
  279. # run the command
  280. await run_command(makeprocess[0], makeprocess[3])
  281. await get_tree().process_frame
  282. var output_file = makeprocess[1]
  283. # Store output file path for this node
  284. output_files[node_name] = output_file
  285. # Mark file for cleanup if needed
  286. if control_script.delete_intermediate_outputs:
  287. for file in makeprocess[2]:
  288. breakfiles.append(file)
  289. intermediate_files.append(output_file)
  290. # Increase the process step count
  291. process_count += 1
  292. else:
  293. #Process outputs audio
  294. #check if this is the last pvoc process in a stereo processing chain
  295. if node.get_meta("command") == "pvoc_synth" and typeof(current_infile) == TYPE_ARRAY:
  296. #check if infile is an array meaning that the last pvoc process was run in dual mono mode
  297. # Process left and right seperately
  298. var pvoc_stereo_files = []
  299. for infile in current_infile:
  300. var makeprocess = await make_process(node, process_count, infile, slider_data)
  301. # run the command
  302. await run_command(makeprocess[0], makeprocess[3])
  303. await get_tree().process_frame
  304. var output_file = makeprocess[1]
  305. pvoc_stereo_files.append(output_file)
  306. # Mark file for cleanup if needed
  307. if control_script.delete_intermediate_outputs:
  308. for file in makeprocess[2]:
  309. breakfiles.append(file)
  310. intermediate_files.append(output_file)
  311. process_count += 1
  312. #interleave left and right
  313. var output_file = Global.outfile.get_basename() + str(process_count) + "_interleaved.wav"
  314. await run_command(control_script.cdpprogs_location + "/submix", ["interleave", pvoc_stereo_files[0], pvoc_stereo_files[1], output_file])
  315. # Store output file path for this node
  316. output_files[node_name] = output_file
  317. # Mark file for cleanup if needed
  318. if control_script.delete_intermediate_outputs:
  319. intermediate_files.append(output_file)
  320. else:
  321. #Detect if input file is mono or stereo
  322. var input_stereo = await is_stereo(current_infile)
  323. if input_stereo == true:
  324. if node.get_meta("stereo_input") == true: #audio file is stereo and process is stereo, run file through process
  325. var makeprocess = await make_process(node, process_count, current_infile, slider_data)
  326. # run the command
  327. await run_command(makeprocess[0], makeprocess[3])
  328. await get_tree().process_frame
  329. var output_file = makeprocess[1]
  330. # Store output file path for this node
  331. output_files[node_name] = output_file
  332. # Mark file for cleanup if needed
  333. if control_script.delete_intermediate_outputs:
  334. for file in makeprocess[2]:
  335. breakfiles.append(file)
  336. intermediate_files.append(output_file)
  337. else: #audio file is stereo and process is mono, split stereo, process and recombine
  338. ##Split stereo to c1/c2
  339. await run_command(control_script.cdpprogs_location + "/housekeep",["chans", "2", current_infile])
  340. # Process left and right seperately
  341. var dual_mono_output = []
  342. for channel in ["c1", "c2"]:
  343. var dual_mono_file = current_infile.get_basename() + "_%s.wav" % channel
  344. var makeprocess = await make_process(node, process_count, dual_mono_file, slider_data)
  345. # run the command
  346. await run_command(makeprocess[0], makeprocess[3])
  347. await get_tree().process_frame
  348. var output_file = makeprocess[1]
  349. dual_mono_output.append(output_file)
  350. # Mark file for cleanup if needed
  351. if control_script.delete_intermediate_outputs:
  352. for file in makeprocess[2]:
  353. breakfiles.append(file)
  354. intermediate_files.append(output_file)
  355. #Delete c1 and c2 because they can be in the wrong folder and if the same infile is used more than once
  356. #with this stereo process CDP will throw errors in the console even though its fine
  357. if is_windows:
  358. dual_mono_file = dual_mono_file.replace("/", "\\")
  359. await run_command(delete_cmd, [dual_mono_file])
  360. process_count += 1
  361. var output_file = Global.outfile.get_basename() + str(process_count) + "_interleaved.wav"
  362. await run_command(control_script.cdpprogs_location + "/submix", ["interleave", dual_mono_output[0], dual_mono_output[1], output_file])
  363. # Store output file path for this node
  364. output_files[node_name] = output_file
  365. # Mark file for cleanup if needed
  366. if control_script.delete_intermediate_outputs:
  367. intermediate_files.append(output_file)
  368. else: #audio file is mono, run through the process
  369. var makeprocess = await make_process(node, process_count, current_infile, slider_data)
  370. # run the command
  371. await run_command(makeprocess[0], makeprocess[3])
  372. await get_tree().process_frame
  373. var output_file = makeprocess[1]
  374. # Store output file path for this node
  375. output_files[node_name] = output_file
  376. # Mark file for cleanup if needed
  377. if control_script.delete_intermediate_outputs:
  378. for file in makeprocess[2]:
  379. breakfiles.append(file)
  380. intermediate_files.append(output_file)
  381. # Increase the process step count
  382. process_count += 1
  383. progress_bar.value += progress_step
  384. # FINAL OUTPUT STAGE
  385. # Collect all nodes that are connected to the outputfile node
  386. if process_cancelled:
  387. progress_label.text = "Thread Stopped"
  388. log_console("[b]Thread Stopped[/b]", true)
  389. return
  390. else:
  391. progress_label.text = "Finalising output"
  392. var output_inputs := []
  393. for conn in connections:
  394. var to_node = str(conn["to_node"])
  395. if all_nodes.has(to_node) and all_nodes[to_node].get_meta("command") == "outputfile":
  396. output_inputs.append(str(conn["from_node"]))
  397. # List to hold the final output files to be merged (if needed)
  398. var final_outputs := []
  399. for node_name in output_inputs:
  400. if output_files.has(node_name):
  401. final_outputs.append(output_files[node_name])
  402. # If multiple outputs go to the outputfile node, merge them
  403. if final_outputs.size() > 1:
  404. var runmerge = await merge_many_files(process_count, final_outputs)
  405. final_output_dir = runmerge[0]
  406. var converted_files = runmerge[1]
  407. if control_script.delete_intermediate_outputs:
  408. for f in converted_files:
  409. intermediate_files.append(f)
  410. # Only one output, no merge needed
  411. elif final_outputs.size() == 1:
  412. var single_output = final_outputs[0]
  413. final_output_dir = single_output
  414. intermediate_files.erase(single_output)
  415. progress_bar.value += progress_step
  416. # CLEANUP: Delete intermediate files after processing and rename final output
  417. if process_cancelled:
  418. progress_label.text = "Thread Stopped"
  419. log_console("[b]Thread Stopped[/b]", true)
  420. return
  421. else:
  422. log_console("Cleaning up intermediate files.", true)
  423. progress_label.text = "Cleaning up"
  424. for file_path in intermediate_files:
  425. # Adjust file path format for Windows if needed
  426. var fixed_path = file_path
  427. if is_windows:
  428. fixed_path = fixed_path.replace("/", "\\")
  429. await run_command(delete_cmd, [fixed_path])
  430. await get_tree().process_frame
  431. #delete break files
  432. for file_path in breakfiles:
  433. # Adjust file path format for Windows if needed
  434. var fixed_path = file_path
  435. if is_windows:
  436. fixed_path = fixed_path.replace("/", "\\")
  437. await run_command(delete_cmd, [fixed_path])
  438. await get_tree().process_frame
  439. var final_filename = "%s.wav" % Global.outfile
  440. var final_output_dir_fixed_path = final_output_dir
  441. if is_windows:
  442. final_output_dir_fixed_path = final_output_dir_fixed_path.replace("/", "\\")
  443. await run_command(rename_cmd, [final_output_dir_fixed_path, final_filename.get_file()])
  444. else:
  445. await run_command(rename_cmd, [final_output_dir_fixed_path, "%s.wav" % Global.outfile])
  446. final_output_dir = Global.outfile + ".wav"
  447. control_script.output_audio_player.play_outfile(final_output_dir)
  448. Global.cdpoutput = final_output_dir
  449. progress_bar.value = 100.0
  450. var interface_settings = ConfigHandler.load_interface_settings() #checks if close console is enabled and closes console on a success
  451. progress_window.hide()
  452. if interface_settings.auto_close_console and process_successful == true:
  453. console_window.hide()
  454. func is_stereo(file: String) -> bool:
  455. if file != "none":
  456. var output = await run_command(control_script.cdpprogs_location + "/sfprops", ["-c", file])
  457. output = int(output.strip_edges()) #convert output from cmd to clean int
  458. if output == 1:
  459. return false
  460. elif output == 2:
  461. return true
  462. elif output == 1026: #ignore pvoc .ana files
  463. return false
  464. else:
  465. log_console("[color=#9c2828]Error: Only mono and stereo files are supported[/color]", true)
  466. return false
  467. return true
  468. func get_samplerate(file: String) -> int:
  469. var output = await run_command(control_script.cdpprogs_location + "/sfprops", ["-r", file])
  470. output = int(output.strip_edges())
  471. return output
  472. func merge_many_files(process_count: int, input_files: Array) -> Array:
  473. var merge_output = "%s_merge_%d.wav" % [Global.outfile.get_basename(), process_count]
  474. var converted_files := [] # Track any mono->stereo converted files or upsampled files
  475. var inputs_to_merge := [] # Files to be used in the final merge
  476. var mono_files := []
  477. var stereo_files := []
  478. var sample_rates := []
  479. #Get all sample rates
  480. for f in input_files:
  481. var samplerate = await get_samplerate(f)
  482. sample_rates.append(samplerate)
  483. #Check if all sample rates are the same
  484. if sample_rates.all(func(v): return v == sample_rates[0]):
  485. pass
  486. else:
  487. log_console("Different sample rates found, upsampling files to match highest current sample rate before mixing.", true)
  488. #if not find the highest sample rate
  489. var highest_sample_rate = sample_rates.max()
  490. var index = 0
  491. #move through all input files and compare match their index to the sample_rate array
  492. for f in input_files:
  493. #check if sample rate of current file is less than the highest sample rate
  494. if sample_rates[index] < highest_sample_rate:
  495. #up sample it to the highest sample rate if so
  496. var upsample_output = Global.outfile + "_" + str(process_count) + f.get_file().get_slice(".wav", 0) + "_" + str(highest_sample_rate) + ".wav"
  497. await run_command(control_script.cdpprogs_location + "/housekeep", ["respec", "1", f, upsample_output, str(highest_sample_rate)])
  498. #replace the file in the input_file index with the new upsampled file
  499. input_files[index] = upsample_output
  500. converted_files.append(upsample_output)
  501. index += 1
  502. # Check each file's channel count
  503. for f in input_files:
  504. var stereo = await is_stereo(f)
  505. if stereo == false:
  506. mono_files.append(f)
  507. elif stereo == true:
  508. stereo_files.append(f)
  509. # STEP 2: Convert mono to stereo if there is a mix
  510. if mono_files.size() > 0 and stereo_files.size() > 0:
  511. log_console("Mix of mono and stereo files found, interleaving mono files to stereo before mixing.", true)
  512. for mono_file in mono_files:
  513. var stereo_file = Global.outfile + "_" + str(process_count) + mono_file.get_file().get_slice(".wav", 0) + "_stereo.wav"
  514. #var stereo_file = "%s_stereo.wav" % mono_file.get_basename()
  515. await run_command(control_script.cdpprogs_location + "/submix", ["interleave", mono_file, mono_file, stereo_file])
  516. if process_successful == false:
  517. log_console("Failed to interleave mono file: %s" % mono_file, true)
  518. else:
  519. converted_files.append(stereo_file)
  520. inputs_to_merge.append(stereo_file)
  521. # Add existing stereo files
  522. inputs_to_merge += stereo_files
  523. else:
  524. # All mono or all stereo — use input_files directly
  525. inputs_to_merge = input_files.duplicate()
  526. # STEP 3: Merge all input files (converted or original)
  527. log_console("Mixing files to combined input.", true)
  528. var quoted_inputs := []
  529. for f in inputs_to_merge:
  530. quoted_inputs.append(f)
  531. quoted_inputs.insert(0, "mergemany")
  532. quoted_inputs.append(merge_output)
  533. await run_command(control_script.cdpprogs_location + "/submix", quoted_inputs)
  534. if process_successful == false:
  535. log_console("Failed to to merge files to" + merge_output, true)
  536. return [merge_output, converted_files]
  537. func _get_slider_values_ordered(node: Node) -> Array:
  538. var results := []
  539. for child in node.get_children():
  540. if child is Range:
  541. var flag = child.get_meta("flag") if child.has_meta("flag") else ""
  542. var time = child.get_meta("time")
  543. var brk_data = []
  544. var min_slider = child.min_value
  545. var max_slider = child.max_value
  546. var exp = child.exp_edit
  547. if child.has_meta("brk_data"):
  548. brk_data = child.get_meta("brk_data")
  549. results.append(["slider", flag, child.value, time, brk_data, min_slider, max_slider, exp])
  550. elif child is CheckButton:
  551. var flag = child.get_meta("flag") if child.has_meta("flag") else ""
  552. results.append(["checkbutton", flag, child.button_pressed])
  553. elif child is OptionButton:
  554. var flag = child.get_meta("flag") if child.has_meta("flag") else ""
  555. var value = child.get_item_text(child.selected)
  556. results.append(["optionbutton", flag, value])
  557. #call this function recursively to find any nested sliders in scenes
  558. if child.get_child_count() > 0:
  559. var nested := _get_slider_values_ordered(child)
  560. results.append_array(nested)
  561. return results
  562. func make_process(node: Node, process_count: int, current_infile: String, slider_data: Array) -> Array:
  563. # Determine output extension: .wav or .ana based on the node's slot type
  564. var extension = ".wav" if node.get_slot_type_right(0) == 0 else ".ana"
  565. # Construct output filename for this step
  566. var output_file = "%s_%d%s" % [Global.outfile.get_basename(), process_count, extension]
  567. # Get the command name from metadata or default to node name
  568. var command_name = str(node.get_meta("command"))
  569. command_name = command_name.split("_", true, 1)
  570. var command = "%s/%s" %[control_script.cdpprogs_location, command_name[0]]
  571. var args = command_name[1].split("_", true, 1)
  572. if current_infile != "none":
  573. #check if input is none, e.g. synthesis nodes, otherwise append input file to arguments
  574. args.append(current_infile)
  575. args.append(output_file)
  576. # Start building the command line windows i dont think this is used anymore
  577. #var line = "%s/%s \"%s\" \"%s\" " % [control_script.cdpprogs_location, command_name, current_infile, output_file]
  578. #mac
  579. var cleanup = []
  580. # Append parameter values from the sliders, include flags if present
  581. var slider_count = 0
  582. for entry in slider_data:
  583. if entry[0] == "slider":
  584. var flag = entry[1]
  585. var value = entry[2]
  586. #if value == int(value):
  587. #value = int(value)
  588. var time = entry[3] #checks if slider is a time percentage slider
  589. var brk_data = entry[4]
  590. var min_slider = entry[5]
  591. var max_slider = entry[6]
  592. var exp = entry[7]
  593. if brk_data.size() > 0: #if breakpoint data is present on slider
  594. #Sort all points by time
  595. var sorted_brk_data = []
  596. sorted_brk_data = brk_data.duplicate()
  597. sorted_brk_data.sort_custom(sort_points)
  598. var calculated_brk = []
  599. #get length of input file in seconds
  600. var infile_length = 1 #set infile length to dummy value just incase it does get used where it shouldn't to avoid crashes
  601. if current_infile != "none":
  602. infile_length = await run_command(control_script.cdpprogs_location + "/sfprops", ["-d", current_infile])
  603. infile_length = float(infile_length.strip_edges())
  604. #scale values from automation window to the right length for file and correct slider values
  605. #if node has an output duration then breakpoint files should be x = outputduration y= slider value else x=input duration, y=value
  606. if node.has_meta("outputduration"):
  607. for i in range(sorted_brk_data.size()):
  608. var point = sorted_brk_data[i]
  609. var new_x = float(node.get_meta("outputduration")) * (point.x / 700) #output time
  610. if i == sorted_brk_data.size() - 1: #check if this is last automation point
  611. 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
  612. var new_y
  613. #check if slider is exponential and scale automation
  614. if exp:
  615. new_y = remap_y_to_log_scale(point.y, 0.0, 255.0, min_slider, max_slider)
  616. else:
  617. new_y = remap(point.y, 255, 0, min_slider, max_slider) #slider value
  618. if time: #check if this is a time slider and convert to percentage of input file
  619. new_y = infile_length * (new_y / 100)
  620. calculated_brk.append(Vector2(new_x, new_y))
  621. else:
  622. for i in range(sorted_brk_data.size()):
  623. var point = sorted_brk_data[i]
  624. var new_x = infile_length * (point.x / 700) #time
  625. if i == sorted_brk_data.size() - 1: #check if this is last automation point
  626. new_x = infile_length + 0.1 # force last point's x to infile_length + 100ms to make sure the file is defo over
  627. var new_y
  628. #check if slider is exponential and scale automation
  629. if exp:
  630. new_y = remap_y_to_log_scale(point.y, 0.0, 255.0, min_slider, max_slider)
  631. else:
  632. new_y = remap(point.y, 255, 0, min_slider, max_slider) #slider value
  633. calculated_brk.append(Vector2(new_x, new_y))
  634. #make text file
  635. var brk_file_path = output_file.get_basename() + "_" + str(slider_count) + ".txt"
  636. write_breakfile(calculated_brk, brk_file_path)
  637. #add breakfile to cleanup before adding flag
  638. cleanup.append(brk_file_path)
  639. #append text file in place of value
  640. #include flag if this param has a flag
  641. if flag.begins_with("-"):
  642. brk_file_path = flag + brk_file_path
  643. args.append(brk_file_path)
  644. else: #no break file append slider value
  645. if time == true:
  646. var infile_length = await run_command(control_script.cdpprogs_location + "/sfprops", ["-d", current_infile])
  647. infile_length = float(infile_length.strip_edges())
  648. value = infile_length * (value / 100) #calculate percentage time of the input file
  649. #line += ("%s%.2f " % [flag, value]) if flag.begins_with("-") else ("%.2f " % value)
  650. args.append(("%s%.2f " % [flag, value]) if flag.begins_with("-") else str(value))
  651. elif entry[0] == "checkbutton":
  652. var flag = entry[1]
  653. var value = entry[2]
  654. #if button is pressed add the flag to the arguments list
  655. if value == true:
  656. args.append(flag)
  657. elif entry[0] == "optionbutton":
  658. var flag = entry[1]
  659. var value = entry[2]
  660. args.append(("%s%.2f " % [flag, value]) if flag.begins_with("-") else str(value))
  661. slider_count += 1
  662. return [command, output_file, cleanup, args]
  663. #return [line.strip_edges(), output_file, cleanup]
  664. func remap_y_to_log_scale(y: float, min_y: float, max_y: float, min_val: float, max_val: float) -> float:
  665. var t = clamp((y - min_y) / (max_y - min_y), 0.0, 1.0)
  666. # Since y goes top-down (0 = top, 255 = bottom), we invert t
  667. t = 1.0 - t
  668. var log_min = log(min_val) / log(10)
  669. var log_max = log(max_val) / log(10)
  670. var log_val = lerp(log_min, log_max, t)
  671. return pow(10.0, log_val)
  672. func sort_points(a, b):
  673. return a.x < b.x
  674. func write_breakfile(points: Array, path: String):
  675. var file = FileAccess.open(path, FileAccess.WRITE)
  676. if file:
  677. for point in points:
  678. var line = str(point.x) + " " + str(point.y) + "\n"
  679. file.store_string(line)
  680. file.close()
  681. else:
  682. print("Failed to open file for writing.")
  683. func _on_kill_process_button_down() -> void:
  684. if process_running and process_info.has("pid"):
  685. progress_window.hide()
  686. # Terminate the process by PID
  687. OS.kill(process_info["pid"])
  688. process_running = false
  689. print("Process cancelled.")
  690. process_cancelled = true
  691. func path_exists_through_all_nodes() -> bool:
  692. var all_nodes = {}
  693. var graph = {}
  694. var input_node_names = []
  695. var output_node_name = ""
  696. # Gather all relevant nodes
  697. for child in graph_edit.get_children():
  698. if child is GraphNode:
  699. var name = str(child.name)
  700. all_nodes[name] = child
  701. var command = child.get_meta("command")
  702. var input = child.get_meta("input")
  703. if input:
  704. input_node_names.append(name)
  705. elif command == "outputfile":
  706. output_node_name = name
  707. # Skip utility nodes, include others
  708. if command in ["inputfile", "outputfile"] or not child.has_meta("utility"):
  709. graph[name] = []
  710. # Add edges to graph from the connection list
  711. var connection_list = graph_edit.get_connection_list()
  712. for conn in connection_list:
  713. var from = str(conn["from_node"])
  714. var to = str(conn["to_node"])
  715. if graph.has(from):
  716. graph[from].append(to)
  717. # BFS from each input node
  718. for input_node_name in input_node_names:
  719. var visited = {}
  720. var queue = [{ "node": input_node_name, "depth": 0 }]
  721. while queue.size() > 0:
  722. var current = queue.pop_front()
  723. var current_node = current["node"]
  724. var depth = current["depth"]
  725. if current_node in visited:
  726. continue
  727. visited[current_node] = true
  728. if current_node == output_node_name and depth >= 2:
  729. return true # Found a valid path from this input node
  730. if graph.has(current_node):
  731. for neighbor in graph[current_node]:
  732. queue.append({ "node": neighbor, "depth": depth + 1 })
  733. # If no path from any input node to output node was found
  734. return false
  735. func log_console(text: String, update: bool) -> void:
  736. console_output.append_text(text + "\n \n")
  737. console_output.scroll_to_line(console_output.get_line_count() - 1)
  738. if update == true:
  739. await get_tree().process_frame # Optional: ensure UI updates
  740. func run_command(command: String, args: Array) -> String:
  741. var is_windows = OS.get_name() == "Windows"
  742. console_output.append_text(command + " " + " ".join(args) + "\n")
  743. console_output.scroll_to_line(console_output.get_line_count() - 1)
  744. await get_tree().process_frame
  745. if is_windows and (command == "del" or command == "ren"): #checks if the command is a windows system command and runs it through cmd.exe
  746. args.insert(0, command)
  747. args.insert(0, "/C")
  748. process_info = OS.execute_with_pipe("cmd.exe", args, false)
  749. else:
  750. process_info = OS.execute_with_pipe(command, args, false)
  751. # Check if the process was successfully started
  752. if !process_info.has("pid"):
  753. print("Failed to start process.")
  754. return ""
  755. process_running = true
  756. # Start monitoring the process output and status
  757. return await monitor_process(process_info["pid"], process_info["stdio"], process_info["stderr"])
  758. func monitor_process(pid: int, stdout: FileAccess, stderr: FileAccess) -> String:
  759. var output := ""
  760. while OS.is_process_running(pid):
  761. await get_tree().process_frame
  762. while stdout.get_position() < stdout.get_length():
  763. var line = stdout.get_line()
  764. output += line
  765. console_output.append_text(line + "\n")
  766. console_output.scroll_to_line(console_output.get_line_count() - 1)
  767. while stderr.get_position() < stderr.get_length():
  768. var line = stderr.get_line()
  769. output += line
  770. console_output.append_text(line + "\n")
  771. console_output.scroll_to_line(console_output.get_line_count() - 1)
  772. var exit_code = OS.get_process_exit_code(pid)
  773. if exit_code == 0:
  774. if output.contains("ERROR:"): #checks if CDP reported an error but passed exit code 0 anyway
  775. console_output.append_text("[color=#9c2828][b]Processes failed[/b][/color]\n\n")
  776. console_output.scroll_to_line(console_output.get_line_count() - 1)
  777. process_successful = false
  778. if process_cancelled == false:
  779. progress_window.hide()
  780. if !console_window.visible:
  781. console_window.popup_centered()
  782. else:
  783. console_output.append_text("[color=#638382]Processes ran successfully[/color]\n\n")
  784. console_output.scroll_to_line(console_output.get_line_count() - 1)
  785. else:
  786. console_output.append_text("[color=#9c2828][b]Processes failed with exit code: %d[/b][/color]\n" % exit_code + "\n")
  787. console_output.scroll_to_line(console_output.get_line_count() - 1)
  788. process_successful = false
  789. if process_cancelled == false:
  790. progress_window.hide()
  791. if !console_window.visible:
  792. console_window.popup_centered()
  793. if output.contains("as an internal or external command"): #check for cdprogs location error on windows
  794. 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")
  795. console_output.scroll_to_line(console_output.get_line_count() - 1)
  796. if output.contains("command not found"): #check for cdprogs location error on unix systems
  797. 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")
  798. console_output.scroll_to_line(console_output.get_line_count() - 1)
  799. process_running = false
  800. return output