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