control.gd 50 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450
  1. extends Control
  2. var mainmenu_visible : bool = false #used to test if mainmenu is open
  3. var effect_position = Vector2(40,40) #tracks mouse position for node placement offset
  4. @onready var graph_edit = $GraphEdit
  5. var selected_nodes = {} #used to track which nodes in the GraphEdit are selected
  6. var cdpprogs_location #stores the cdp programs location from user prefs for easy access
  7. var delete_intermediate_outputs # tracks state of delete intermediate outputs toggle
  8. @onready var console_output: RichTextLabel = $Console/ConsoleOutput
  9. var final_output_dir
  10. var copied_nodes_data = [] #stores node data on ctrl+c
  11. var copied_connections = [] #stores all connections on ctrl+c
  12. var undo_redo := UndoRedo.new()
  13. var output_audio_player #tracks the node that is the current output player for linking
  14. var input_audio_player #tracks node that is the current input player for linking
  15. var outfile = "no file" #tracks dir of output file from cdp process
  16. var currentfile = "none" #tracks dir of currently loaded file for saving
  17. var changesmade = false #tracks if user has made changes to the currently loaded save file
  18. var savestate # tracks what the user is trying to do when savechangespopup is called
  19. var helpfile #tracks which help file the user was trying to load when savechangespopup is called
  20. var outfilename #links to the user name for outputfile field
  21. var foldertoggle #links to the reuse folder button
  22. var lastoutputfolder = "none" #tracks last output folder, this can in future be used to replace global.outfile but i cba right now
  23. # Called when the node enters the scene tree for the first time.
  24. func _ready() -> void:
  25. Nodes.hide()
  26. $mainmenu.hide()
  27. $NoLocationPopup.hide()
  28. $Console.hide()
  29. $NoInputPopup.hide()
  30. $MultipleConnectionsPopup.hide()
  31. $SaveDialog.access = FileDialog.ACCESS_FILESYSTEM
  32. $SaveDialog.file_mode = FileDialog.FILE_MODE_SAVE_FILE
  33. $SaveDialog.filters = ["*.thd"]
  34. $LoadDialog.access = FileDialog.ACCESS_FILESYSTEM
  35. $LoadDialog.file_mode = FileDialog.FILE_MODE_OPEN_FILE
  36. $LoadDialog.filters = ["*.thd"]
  37. #Goes through all nodes in scene and checks for buttons in the make_node_buttons group
  38. #Associates all buttons with the _on_button_pressed fuction and passes the button as an argument
  39. for child in get_tree().get_nodes_in_group("make_node_buttons"):
  40. if child is Button:
  41. child.pressed.connect(_on_button_pressed.bind(child))
  42. check_cdp_location_set()
  43. check_user_preferences()
  44. new_patch()
  45. get_tree().set_auto_accept_quit(false)
  46. #link output file to input file to enable audio output file loopback
  47. #$GraphEdit/outputfile/AudioPlayer.recycle_outfile_trigger.connect($GraphEdit/inputfile/AudioPlayer.recycle_outfile)
  48. func new_patch():
  49. #clear old patch
  50. graph_edit.clear_connections()
  51. for node in graph_edit.get_children():
  52. if node is GraphNode:
  53. node.queue_free()
  54. #Generate input and output nodes
  55. var effect: GraphNode = Nodes.get_node(NodePath("inputfile")).duplicate()
  56. get_node("GraphEdit").add_child(effect, true)
  57. effect.position_offset = Vector2(20,80)
  58. effect = Nodes.get_node(NodePath("outputfile")).duplicate()
  59. get_node("GraphEdit").add_child(effect, true)
  60. effect.position_offset = Vector2((DisplayServer.screen_get_size().x - 480) ,80)
  61. _register_node_movement() #link nodes for tracking position changes for changes tracking
  62. changesmade = false #so it stops trying to save unchanged empty files
  63. get_window().title = "SoundThread"
  64. link_output()
  65. func link_output():
  66. #links various buttons and function in the input nodes - this is called after they are created so that it still works on new and loading files
  67. for control in get_tree().get_nodes_in_group("outputnode"): #check all items in outputnode group
  68. if control.get_meta("outputfunction") == "deleteintermediate": #link delete intermediate files toggle to script
  69. control.toggled.connect(_toggle_delete)
  70. control.button_pressed = true
  71. elif control.get_meta("outputfunction") == "runprocess": #link runprocess button
  72. control.button_down.connect(_run_process)
  73. elif control.get_meta("outputfunction") == "recycle": #link recycle button
  74. control.button_down.connect(_recycle_outfile)
  75. elif control.get_meta("outputfunction") == "audioplayer": #link output audio player
  76. output_audio_player = control
  77. elif control.get_meta("outputfunction") == "filename":
  78. control.text = "outfile"
  79. outfilename = control
  80. elif control.get_meta("outputfunction") == "reusefolder":
  81. foldertoggle = control
  82. foldertoggle.button_pressed = true
  83. elif control.get_meta("outputfunction") == "openfolder":
  84. control.button_down.connect(_open_output_folder)
  85. for control in get_tree().get_nodes_in_group("inputnode"):
  86. if control.get_meta("inputfunction") == "audioplayer": #link input for recycle function
  87. print("input player found")
  88. input_audio_player = control
  89. func check_user_preferences():
  90. var interface_settings = ConfigHandler.load_interface_settings()
  91. $MenuBar/SettingsButton.set_item_checked(1, interface_settings.disable_pvoc_warning)
  92. $MenuBar/SettingsButton.set_item_checked(2, interface_settings.auto_close_console)
  93. func check_cdp_location_set():
  94. #checks if the location has been set and prompts user to set it
  95. var cdpprogs_settings = ConfigHandler.load_cdpprogs_settings()
  96. if cdpprogs_settings.location == "no_location":
  97. $NoLocationPopup.show()
  98. else:
  99. #if location is set, stores it in a variable
  100. cdpprogs_location = str(cdpprogs_settings.location)
  101. print(cdpprogs_location)
  102. func _on_ok_button_button_down() -> void:
  103. #after user has read dialog on where to find cdp progs this loads the file browser
  104. $NoLocationPopup.hide()
  105. if OS.get_name() == "Windows":
  106. $CdpLocationDialog.current_dir = "C:/"
  107. else:
  108. $CdpLocationDialog.current_dir = "~/"
  109. $CdpLocationDialog.show()
  110. func _on_cdp_location_dialog_dir_selected(dir: String) -> void:
  111. #saves default location for cdp programs in config file
  112. ConfigHandler.save_cdpprogs_settings(dir)
  113. func _on_cdp_location_dialog_canceled() -> void:
  114. #cycles around the set location prompt if user cancels the file dialog
  115. check_cdp_location_set()
  116. # Called every frame. 'delta' is the elapsed time since the previous frame.
  117. func _process(delta: float) -> void:
  118. showmenu()
  119. func _input(event):
  120. if event.is_action_pressed("copy_node"):
  121. copy_selected_nodes()
  122. get_viewport().set_input_as_handled()
  123. elif event.is_action_pressed("paste_node"):
  124. paste_copied_nodes()
  125. get_viewport().set_input_as_handled()
  126. elif event.is_action_pressed("undo"):
  127. undo_redo.undo()
  128. elif event.is_action_pressed("redo"):
  129. undo_redo.redo()
  130. elif event.is_action_pressed("save"):
  131. if currentfile == "none":
  132. savestate = "saveas"
  133. $SaveDialog.popup_centered()
  134. else:
  135. save_graph_edit(currentfile)
  136. #logic for making, connecting, disconnecting, copy pasting and deleteing nodes and connections in GraphEdit
  137. #mostly taken from https://gdscript.com/solutions/godot-graphnode-and-graphedit-tutorial/
  138. func showmenu():
  139. #check for mouse input and if menu is already open and then open or close the menu
  140. #stores mouse position at time of right click to later place a node in that location
  141. if Input.is_action_just_pressed("open_menu"):
  142. if mainmenu_visible == false:
  143. effect_position = get_viewport().get_mouse_position()
  144. $mainmenu.show()
  145. mainmenu_visible = true
  146. else:
  147. $mainmenu.hide()
  148. mainmenu_visible = false
  149. # creates nodes from menu
  150. func _on_button_pressed(button: Button):
  151. #close menu
  152. $mainmenu.hide()
  153. mainmenu_visible = false
  154. #Find node with matching name to button and create a version of it in the graph edit
  155. #and position it close to the origin right click to open the menu
  156. var effect: GraphNode = Nodes.get_node(NodePath(button.name)).duplicate()
  157. get_node("GraphEdit").add_child(effect, true)
  158. effect.position_offset = effect_position
  159. _register_inputs_in_node(effect) #link sliders for changes tracking
  160. _register_node_movement() #link nodes for tracking position changes for changes tracking
  161. changesmade = true
  162. # Remove node with UndoRedo
  163. undo_redo.create_action("Add Node")
  164. undo_redo.add_undo_method(Callable(graph_edit, "remove_child").bind(effect))
  165. undo_redo.add_undo_method(Callable(effect, "queue_free"))
  166. undo_redo.add_undo_method(Callable(self, "_track_changes"))
  167. undo_redo.commit_action()
  168. func _on_graph_edit_connection_request(from_node: StringName, from_port: int, to_node: StringName, to_port: int) -> void:
  169. #get_node("GraphEdit").connect_node(from_node, from_port, to_node, to_port)
  170. var graph_edit = get_node("GraphEdit")
  171. var to_graph_node = graph_edit.get_node(NodePath(to_node))
  172. # Get the type of the input port using GraphNode's built-in method
  173. var port_type = to_graph_node.get_input_port_type(to_port)
  174. # If port type is 1 and already has a connection, reject the request
  175. if port_type == 1:
  176. var connections = graph_edit.get_connection_list()
  177. var existing_connections = 0
  178. for conn in connections:
  179. if conn.to_node == to_node and conn.to_port == to_port:
  180. existing_connections += 1
  181. if existing_connections >= 1:
  182. var interface_settings = ConfigHandler.load_interface_settings()
  183. if interface_settings.disable_pvoc_warning == false:
  184. $MultipleConnectionsPopup.show()
  185. return
  186. # If no conflict, allow the connection
  187. graph_edit.connect_node(from_node, from_port, to_node, to_port)
  188. changesmade = true
  189. func _on_graph_edit_disconnection_request(from_node: StringName, from_port: int, to_node: StringName, to_port: int) -> void:
  190. get_node("GraphEdit").disconnect_node(from_node, from_port, to_node, to_port)
  191. changesmade = true
  192. func _on_graph_edit_node_selected(node: Node) -> void:
  193. selected_nodes[node] = true
  194. func _on_graph_edit_node_deselected(node: Node) -> void:
  195. selected_nodes[node] = false
  196. func _on_graph_edit_delete_nodes_request(nodes: Array[StringName]) -> void:
  197. var graph_edit = get_node("GraphEdit")
  198. undo_redo.create_action("Delete Nodes (Undo only)")
  199. for node in selected_nodes.keys():
  200. if selected_nodes[node]:
  201. if node.name in ["inputfile", "outputfile"]:
  202. print("can't delete input or output")
  203. else:
  204. # Store duplicate and state for undo
  205. var node_data = node.duplicate()
  206. var position = node.position_offset
  207. # Store all connections for undo
  208. var conns = []
  209. for con in graph_edit.get_connection_list():
  210. if con["to_node"] == node.name or con["from_node"] == node.name:
  211. conns.append(con)
  212. # Delete
  213. remove_connections_to_node(node)
  214. node.queue_free()
  215. changesmade = true
  216. # Register undo restore
  217. undo_redo.add_undo_method(Callable(graph_edit, "add_child").bind(node_data, true))
  218. undo_redo.add_undo_method(Callable(node_data, "set_position_offset").bind(position))
  219. for con in conns:
  220. undo_redo.add_undo_method(Callable(graph_edit, "connect_node").bind(
  221. con["from_node"], con["from_port"],
  222. con["to_node"], con["to_port"]
  223. ))
  224. undo_redo.add_undo_method(Callable(self, "set_node_selected").bind(node_data, true))
  225. undo_redo.add_undo_method(Callable(self, "_track_changes"))
  226. undo_redo.add_undo_method(Callable(self, "_register_inputs_in_node").bind(node_data)) #link sliders for changes tracking
  227. undo_redo.add_undo_method(Callable(self, "_register_node_movement")) # link nodes for changes tracking
  228. # Clear selection
  229. selected_nodes = {}
  230. undo_redo.commit_action()
  231. func set_node_selected(node: Node, selected: bool) -> void:
  232. selected_nodes[node] = selected
  233. #
  234. func remove_connections_to_node(node):
  235. for con in get_node("GraphEdit").get_connection_list():
  236. if con["to_node"] == node.name or con["from_node"] == node.name:
  237. get_node("GraphEdit").disconnect_node(con["from_node"], con["from_port"], con["to_node"], con["to_port"])
  238. changesmade = true
  239. #copy and paste nodes with vertical offset on paste
  240. func copy_selected_nodes():
  241. copied_nodes_data.clear()
  242. copied_connections.clear()
  243. var graph_edit = get_node("GraphEdit")
  244. # Store selected nodes and their slider values
  245. for node in graph_edit.get_children():
  246. # Check if the node is selected and not an 'inputfile' or 'outputfile'
  247. if node is GraphNode and selected_nodes.get(node, false):
  248. if node.name == "inputfile" or node.name == "outputfile":
  249. continue # Skip these nodes
  250. var node_data = {
  251. "name": node.name,
  252. "type": node.get_class(),
  253. "offset": node.position_offset,
  254. "slider_values": {}
  255. }
  256. for child in node.get_children():
  257. if child is HSlider or child is VSlider:
  258. node_data["slider_values"][child.name] = child.value
  259. copied_nodes_data.append(node_data)
  260. # Store connections between selected nodes
  261. for conn in graph_edit.get_connection_list():
  262. var from_ref = graph_edit.get_node_or_null(NodePath(conn["from_node"]))
  263. var to_ref = graph_edit.get_node_or_null(NodePath(conn["to_node"]))
  264. var is_from_selected = from_ref != null and selected_nodes.get(from_ref, false)
  265. var is_to_selected = to_ref != null and selected_nodes.get(to_ref, false)
  266. # Skip if any of the connected nodes are 'inputfile' or 'outputfile'
  267. if (from_ref != null and (from_ref.name == "inputfile" or from_ref.name == "outputfile")) or (to_ref != null and (to_ref.name == "inputfile" or to_ref.name == "outputfile")):
  268. continue
  269. if is_from_selected and is_to_selected:
  270. # Store connection as dictionary
  271. var conn_data = {
  272. "from_node": conn["from_node"],
  273. "from_port": conn["from_port"],
  274. "to_node": conn["to_node"],
  275. "to_port": conn["to_port"]
  276. }
  277. copied_connections.append(conn_data)
  278. func paste_copied_nodes():
  279. if copied_nodes_data.is_empty():
  280. return
  281. var graph_edit = get_node("GraphEdit")
  282. var name_map = {}
  283. var pasted_nodes = []
  284. # Step 1: Find topmost and bottommost Y of copied nodes
  285. var min_y = INF
  286. var max_y = -INF
  287. for node_data in copied_nodes_data:
  288. var y = node_data["offset"].y
  289. min_y = min(min_y, y)
  290. max_y = max(max_y, y)
  291. # Step 2: Decide where to paste the group
  292. var base_y_offset = max_y + 350 # Pasting below the lowest node
  293. # Step 3: Paste nodes, preserving vertical layout
  294. for node_data in copied_nodes_data:
  295. var original_node = graph_edit.get_node_or_null(NodePath(node_data["name"]))
  296. if not original_node:
  297. continue
  298. var new_node = original_node.duplicate()
  299. new_node.name = node_data["name"] + "_copy_" + str(randi() % 10000)
  300. var relative_y = node_data["offset"].y - min_y
  301. new_node.position_offset = Vector2(
  302. node_data["offset"].x,
  303. base_y_offset + relative_y
  304. )
  305. # Restore sliders
  306. for child in new_node.get_children():
  307. if child.name in node_data["slider_values"]:
  308. child.value = node_data["slider_values"][child.name]
  309. graph_edit.add_child(new_node, true)
  310. _register_inputs_in_node(new_node) #link sliders for changes tracking
  311. _register_node_movement() # link nodes for changes tracking
  312. name_map[node_data["name"]] = new_node.name
  313. pasted_nodes.append(new_node)
  314. # Step 4: Reconnect new nodes
  315. for conn_data in copied_connections:
  316. var new_from = name_map.get(conn_data["from_node"], null)
  317. var new_to = name_map.get(conn_data["to_node"], null)
  318. if new_from and new_to:
  319. graph_edit.connect_node(new_from, conn_data["from_port"], new_to, conn_data["to_port"])
  320. # Step 5: Select pasted nodes
  321. for pasted_node in pasted_nodes:
  322. graph_edit.set_selected(pasted_node)
  323. selected_nodes[pasted_node] = true
  324. changesmade = true
  325. # Remove node with UndoRedo
  326. undo_redo.create_action("Paste Nodes")
  327. for pasted_node in pasted_nodes:
  328. undo_redo.add_undo_method(Callable(graph_edit, "remove_child").bind(pasted_node))
  329. undo_redo.add_undo_method(Callable(pasted_node, "queue_free"))
  330. undo_redo.add_undo_method(Callable(self, "remove_connections_to_node").bind(pasted_node))
  331. undo_redo.add_undo_method(Callable(self, "_track_changes"))
  332. undo_redo.commit_action()
  333. #functions for tracking changes for save state detection
  334. func _register_inputs_in_node(node: Node):
  335. #tracks input to nodes sliders and codeedit to track if patch is saved
  336. # Track Sliders
  337. for slider in node.find_children("*", "HSlider", true, false):
  338. # Create a Callable to the correct method
  339. var callable = Callable(self, "_on_any_slider_changed")
  340. # Check if it's already connected, and connect if not
  341. if not slider.is_connected("value_changed", callable):
  342. slider.connect("value_changed", callable)
  343. # Track CodeEdits (if necessary)
  344. for editor in node.find_children("*", "CodeEdit", true, false):
  345. var callable = Callable(self, "_on_any_input_changed")
  346. if not editor.is_connected("text_changed", callable):
  347. editor.connect("text_changed", callable)
  348. func _register_node_movement():
  349. for graphnode in graph_edit.get_children():
  350. if graphnode is GraphNode:
  351. var callable = Callable(self, "_on_graphnode_moved")
  352. if not graphnode.is_connected("position_offset_changed", callable):
  353. graphnode.connect("position_offset_changed", callable)
  354. func _on_graphnode_moved():
  355. changesmade = true
  356. func _on_any_slider_changed(value: float) -> void:
  357. changesmade = true
  358. func _on_any_input_changed():
  359. changesmade = true
  360. func _track_changes():
  361. changesmade = true
  362. ######## Here be dragons #########
  363. ##################################
  364. ####### Don't let them out #######
  365. #Scans through all nodes and generates a batch file based on their order
  366. func _run_process() -> void:
  367. if Global.infile == "no_file":
  368. $NoInputPopup.show()
  369. else:
  370. if foldertoggle.button_pressed == true and lastoutputfolder != "none":
  371. _on_file_dialog_dir_selected(lastoutputfolder)
  372. else:
  373. $FileDialog.show()
  374. func _on_file_dialog_dir_selected(dir: String) -> void:
  375. lastoutputfolder = dir
  376. console_output.clear()
  377. $Console.show()
  378. await get_tree().process_frame
  379. log_console("Generating processing queue", true)
  380. await get_tree().process_frame
  381. #get the current time in hh-mm-ss format as default : causes file name issues
  382. var time_dict = Time.get_time_dict_from_system()
  383. # Pad with zeros to ensure two digits for hour, minute, second
  384. var hour = str(time_dict.hour).pad_zeros(2)
  385. var minute = str(time_dict.minute).pad_zeros(2)
  386. var second = str(time_dict.second).pad_zeros(2)
  387. var time_str = hour + "-" + minute + "-" + second
  388. Global.outfile = dir + "/" + outfilename.text + "_" + Time.get_date_string_from_system() + "_" + time_str
  389. log_console("Output directory and file name(s):" + Global.outfile, true)
  390. await get_tree().process_frame
  391. generate_batch_file_with_branches()
  392. func generate_batch_file_with_branches():
  393. #mac windows compatibility
  394. var is_windows := OS.get_name() == "Windows"
  395. var script_ext = ".bat" if is_windows else ".sh"
  396. var delete_cmd = "del" if is_windows else "rm"
  397. var path_sep := "/" # Use forward slash for compatibility
  398. var connections = graph_edit.get_connection_list()
  399. var graph = {}
  400. var reverse_graph = {}
  401. var indegree = {}
  402. var all_nodes = {}
  403. log_console("Generating batch file.", true)
  404. await get_tree().process_frame
  405. # Step 1: Collect nodes
  406. for child in graph_edit.get_children():
  407. if child is GraphNode:
  408. var name = str(child.name)
  409. all_nodes[name] = child
  410. if name != "inputfile" and name != "outputfile":
  411. graph[name] = []
  412. reverse_graph[name] = []
  413. indegree[name] = 0
  414. # Step 2: Build the graph
  415. for conn in connections:
  416. var from = str(conn["from_node"])
  417. var to = str(conn["to_node"])
  418. if graph.has(from) and graph.has(to):
  419. graph[from].append(to)
  420. reverse_graph[to].append(from)
  421. indegree[to] += 1
  422. # Step 3: Topological sort
  423. var sorted = []
  424. var queue = []
  425. for node in graph.keys():
  426. if indegree[node] == 0:
  427. queue.append(node)
  428. while not queue.is_empty():
  429. var current = queue.pop_front()
  430. sorted.append(current)
  431. for neighbor in graph[current]:
  432. indegree[neighbor] -= 1
  433. if indegree[neighbor] == 0:
  434. queue.append(neighbor)
  435. if sorted.size() != graph.size():
  436. log_console("Cycle detected or disconnected nodes", true)
  437. return
  438. # Step 4: Batch file generation
  439. var batch_lines = []
  440. var intermediate_files = []
  441. var stereo_outputs = {}
  442. if Global.infile_stereo:
  443. log_console("Input file is stereo, note this may cause left/right decorrelation with some processes.", true)
  444. await get_tree().process_frame
  445. # Step 4.1: Split stereo to c1/c2
  446. batch_lines.append("%s/housekeep chans 2 \"%s\"" % [cdpprogs_location, Global.infile])
  447. # Process for each channel
  448. for channel in ["c1", "c2"]:
  449. var current_infile = Global.infile.get_basename() + "_%s.wav" % channel
  450. var output_files = {}
  451. var process_count = 0
  452. for node_name in sorted:
  453. var node = all_nodes[node_name]
  454. var inputs = reverse_graph[node_name]
  455. var input_files = []
  456. for input_node in inputs:
  457. if output_files.has(input_node):
  458. input_files.append(output_files[input_node])
  459. if input_files.size() > 1:
  460. var merge_output = "%s_%s_merge_%d.wav" % [Global.outfile.get_basename(), channel, process_count]
  461. var quoted_inputs = []
  462. for f in input_files:
  463. quoted_inputs.append("\"%s\"" % f)
  464. var merge_cmd = cdpprogs_location + "/submix mergemany " + " ".join(quoted_inputs) + " \"%s\"" % merge_output
  465. batch_lines.append(merge_cmd)
  466. intermediate_files.append(merge_output)
  467. current_infile = merge_output
  468. elif input_files.size() == 1:
  469. current_infile = input_files[0]
  470. else:
  471. current_infile = Global.infile.get_basename() + "_%s.wav" % channel
  472. var slider_data = _get_slider_values_ordered(node)
  473. var extension = ".wav" if node.get_slot_type_right(0) == 0 else ".ana"
  474. var output_file = "%s_%s_%d%s" % [Global.outfile.get_basename(), channel, process_count, extension]
  475. var command_name = str(node.get_meta("command")) if node.has_meta("command") else node_name
  476. command_name = command_name.replace("_", " ")
  477. var line = "%s/%s \"%s\" \"%s\" " % [cdpprogs_location, command_name, current_infile, output_file]
  478. #checks if slider has a flag meta value and appends the flag before the parameter
  479. for entry in slider_data:
  480. var flag = entry[0]
  481. var value = entry[1]
  482. line += ("%s%.2f " % [flag, value]) if flag.begins_with("-") else ("%.2f " % value)
  483. batch_lines.append(line.strip_edges())
  484. output_files[node_name] = output_file
  485. if delete_intermediate_outputs:
  486. intermediate_files.append(output_file)
  487. process_count += 1
  488. # Handle output node
  489. var output_inputs = []
  490. for conn in connections:
  491. if conn["to_node"] == "outputfile":
  492. output_inputs.append(str(conn["from_node"]))
  493. var final_output = ""
  494. if output_inputs.size() > 1:
  495. var quoted_inputs = []
  496. for fnode in output_inputs:
  497. if output_files.has(fnode):
  498. quoted_inputs.append("\"%s\"" % output_files[fnode])
  499. #intermediate_files.append(output_files[fnode])
  500. final_output = "%s_%s_final.wav" % [Global.outfile.get_basename(), channel]
  501. batch_lines.append("%s/submix mergemany %s \"%s\"" % [cdpprogs_location, " ".join(quoted_inputs), final_output])
  502. elif output_inputs.size() == 1:
  503. final_output = output_files[output_inputs[0]]
  504. intermediate_files.erase(final_output)
  505. stereo_outputs[channel] = final_output
  506. # Interleave final
  507. if stereo_outputs.has("c1") and stereo_outputs.has("c2"):
  508. if stereo_outputs["c1"].ends_with(".wav") and stereo_outputs["c2"].ends_with(".wav"):
  509. var final_stereo = Global.outfile.get_basename() + "_stereo.wav"
  510. batch_lines.append("%s/submix interleave \"%s\" \"%s\" \"%s\"" % [cdpprogs_location, stereo_outputs["c1"], stereo_outputs["c2"], final_stereo])
  511. final_output_dir = final_stereo
  512. if delete_intermediate_outputs:
  513. intermediate_files.append(stereo_outputs["c1"])
  514. intermediate_files.append(stereo_outputs["c2"])
  515. #add delete command for not needed files
  516. #always delete mono split as they are in a weird location
  517. intermediate_files.append(Global.infile.get_basename() + "_c1.wav")
  518. intermediate_files.append(Global.infile.get_basename() + "_c2.wav")
  519. for file_path in intermediate_files:
  520. var fixed_path = file_path
  521. if is_windows:
  522. fixed_path = fixed_path.replace("/", "\\")
  523. batch_lines.append("%s \"%s\"" % [delete_cmd, fixed_path])
  524. else:
  525. # Use mono logic as before
  526. # Step 4: Process chain
  527. var output_files = {} # node -> output file
  528. var process_count = 0
  529. var current_infile = Global.infile
  530. for node_name in sorted:
  531. var node = all_nodes[node_name]
  532. var inputs = reverse_graph[node_name]
  533. var input_files = []
  534. for input_node in inputs:
  535. input_files.append(output_files[input_node])
  536. # If multiple inputs, merge with submix mergemany
  537. if input_files.size() > 1:
  538. var merge_output = "%s_merge_%d.wav" % [Global.outfile.get_basename(), process_count]
  539. var quoted_inputs := []
  540. for f in input_files:
  541. quoted_inputs.append("\"%s\"" % f)
  542. var merge_cmd = cdpprogs_location + "/submix mergemany " + " ".join(quoted_inputs) + " \"%s\"" % merge_output
  543. batch_lines.append(merge_cmd)
  544. intermediate_files.append(merge_output)
  545. current_infile = merge_output
  546. elif input_files.size() == 1:
  547. current_infile = input_files[0]
  548. else:
  549. current_infile = Global.infile
  550. # Build node command
  551. var slider_data = _get_slider_values_ordered(node)
  552. var extension = ".wav" if node.get_slot_type_right(0) == 0 else ".ana"
  553. var output_file = "%s_%d%s" % [Global.outfile.get_basename(), process_count, extension]
  554. var command_name = str(node.get_meta("command")) if node.has_meta("command") else node_name
  555. command_name = command_name.replace("_", " ")
  556. var line = "%s/%s \"%s\" \"%s\" " % [cdpprogs_location, command_name, current_infile, output_file]
  557. #checks if slider has a flag meta value and appends the flag before the parameter
  558. for entry in slider_data:
  559. var flag = entry[0]
  560. var value = entry[1]
  561. line += ("%s%.2f " % [flag, value]) if flag.begins_with("-") else ("%.2f " % value)
  562. batch_lines.append(line.strip_edges())
  563. output_files[node_name] = output_file
  564. if delete_intermediate_outputs:
  565. intermediate_files.append(output_file)
  566. process_count += 1
  567. # Step 4.5: Handle nodes connected to outputfile
  568. var output_inputs := []
  569. for conn in connections:
  570. if conn["to_node"] == "outputfile":
  571. output_inputs.append(str(conn["from_node"]))
  572. var final_outputs := []
  573. for node_name in output_inputs:
  574. if output_files.has(node_name):
  575. final_outputs.append(output_files[node_name])
  576. if final_outputs.size() > 1:
  577. var quoted_inputs := []
  578. for f in final_outputs:
  579. quoted_inputs.append("\"%s\"" % f)
  580. intermediate_files.append(f)
  581. var merge_cmd = cdpprogs_location + "/submix mergemany " + " ".join(quoted_inputs) + " \"%s\"" % Global.outfile + ".wav"
  582. final_output_dir = Global.outfile + ".wav"
  583. batch_lines.append(merge_cmd)
  584. for f in final_outputs:
  585. intermediate_files.erase(f)
  586. elif final_outputs.size() == 1:
  587. var single_output = final_outputs[0]
  588. final_output_dir = single_output
  589. intermediate_files.erase(single_output)
  590. # Step 5: Cleanup commands
  591. log_console("Adding cleanup commands for intermediate files.", true)
  592. for file_path in intermediate_files:
  593. var fixed_path = file_path
  594. if is_windows:
  595. fixed_path = fixed_path.replace("/", "\\")
  596. batch_lines.append("%s \"%s\"" % [delete_cmd, fixed_path])
  597. # Step 6: Write batch file
  598. var script_path = "user://ordered_script%s" % script_ext
  599. var file = FileAccess.open(script_path, FileAccess.WRITE)
  600. for line in batch_lines:
  601. file.store_line(line)
  602. file.close()
  603. log_console("Batch file complete.", true)
  604. log_console("Processing audio, please wait.", true)
  605. await get_tree().process_frame
  606. run_batch_file()
  607. #func generate_batch_file_with_branches():
  608. #var connections = graph_edit.get_connection_list()
  609. #var graph = {}
  610. #var reverse_graph = {}
  611. #var indegree = {}
  612. #var all_nodes = {}
  613. #
  614. #log_console("Generating batch file.", true)
  615. #await get_tree().process_frame
  616. #
  617. ## Step 1: Collect nodes
  618. #for child in graph_edit.get_children():
  619. #if child is GraphNode:
  620. #var name = str(child.name)
  621. #all_nodes[name] = child
  622. #if name != "inputfile" and name != "outputfile":
  623. #graph[name] = []
  624. #reverse_graph[name] = []
  625. #indegree[name] = 0
  626. #
  627. ## Step 2: Build the graph
  628. #for conn in connections:
  629. #var from = str(conn["from_node"])
  630. #var to = str(conn["to_node"])
  631. #if graph.has(from) and graph.has(to):
  632. #graph[from].append(to)
  633. #reverse_graph[to].append(from)
  634. #indegree[to] += 1
  635. #
  636. ## Step 3: Topological sort
  637. #var sorted = []
  638. #var queue = []
  639. #for node in graph.keys():
  640. #if indegree[node] == 0:
  641. #queue.append(node)
  642. #while not queue.is_empty():
  643. #var current = queue.pop_front()
  644. #sorted.append(current)
  645. #for neighbor in graph[current]:
  646. #indegree[neighbor] -= 1
  647. #if indegree[neighbor] == 0:
  648. #queue.append(neighbor)
  649. #if sorted.size() != graph.size():
  650. #log_console("Cycle detected or disconnected nodes", true)
  651. #return
  652. #
  653. ## Step 4: Batch file generation
  654. #var batch_lines = []
  655. #var intermediate_files = []
  656. #var stereo_outputs = {}
  657. #
  658. #if Global.infile_stereo:
  659. #log_console("Input file is stereo, note this may cause left/right decorrelation with some processes.", true)
  660. #await get_tree().process_frame
  661. #
  662. ## Step 4.1: Split stereo to c1/c2
  663. #batch_lines.append("%s/housekeep chans 2 \"%s\"" % [cdpprogs_location, Global.infile])
  664. #
  665. ## Process for each channel
  666. #for channel in ["c1", "c2"]:
  667. #var current_infile = Global.infile.get_basename() + "_%s.wav" % channel
  668. #var output_files = {}
  669. #var process_count = 0
  670. #
  671. #for node_name in sorted:
  672. #var node = all_nodes[node_name]
  673. #var inputs = reverse_graph[node_name]
  674. #var input_files = []
  675. #for input_node in inputs:
  676. #if output_files.has(input_node):
  677. #input_files.append(output_files[input_node])
  678. #
  679. #if input_files.size() > 1:
  680. #var merge_output = "%s_%s_merge_%d.wav" % [Global.outfile.get_basename(), channel, process_count]
  681. #var quoted_inputs = []
  682. #for f in input_files:
  683. #quoted_inputs.append("\"%s\"" % f)
  684. #var merge_cmd = cdpprogs_location + "/submix mergemany " + " ".join(quoted_inputs) + " \"%s\"" % merge_output
  685. #batch_lines.append(merge_cmd)
  686. #intermediate_files.append(merge_output)
  687. #current_infile = merge_output
  688. #elif input_files.size() == 1:
  689. #current_infile = input_files[0]
  690. #else:
  691. #current_infile = Global.infile.get_basename() + "_%s.wav" % channel
  692. #
  693. #var slider_data = _get_slider_values_ordered(node)
  694. #var extension = ".wav" if node.get_slot_type_right(0) == 0 else ".ana"
  695. #var output_file = "%s_%s_%d%s" % [Global.outfile.get_basename(), channel, process_count, extension]
  696. #var command_name = str(node.get_meta("command")) if node.has_meta("command") else node_name
  697. #command_name = command_name.replace("_", " ")
  698. #var line = "%s/%s \"%s\" \"%s\" " % [cdpprogs_location, command_name, current_infile, output_file]
  699. ##checks if slider has a flag meta value and appends the flag before the parameter
  700. #for entry in slider_data:
  701. #var flag = entry[0]
  702. #var value = entry[1]
  703. #line += ("%s%.2f " % [flag, value]) if flag.begins_with("-") else ("%.2f " % value)
  704. #batch_lines.append(line.strip_edges())
  705. #output_files[node_name] = output_file
  706. #if delete_intermediate_outputs:
  707. #intermediate_files.append(output_file)
  708. #process_count += 1
  709. #
  710. ## Handle output node
  711. #var output_inputs = []
  712. #for conn in connections:
  713. #if conn["to_node"] == "outputfile":
  714. #output_inputs.append(str(conn["from_node"]))
  715. #var final_output = ""
  716. #if output_inputs.size() > 1:
  717. #var quoted_inputs = []
  718. #for fnode in output_inputs:
  719. #if output_files.has(fnode):
  720. #quoted_inputs.append("\"%s\"" % output_files[fnode])
  721. ##intermediate_files.append(output_files[fnode])
  722. #final_output = "%s_%s_final.wav" % [Global.outfile.get_basename(), channel]
  723. #batch_lines.append("%s/submix mergemany %s \"%s\"" % [cdpprogs_location, " ".join(quoted_inputs), final_output])
  724. #elif output_inputs.size() == 1:
  725. #final_output = output_files[output_inputs[0]]
  726. #intermediate_files.erase(final_output)
  727. #stereo_outputs[channel] = final_output
  728. #
  729. ## Interleave final
  730. #if stereo_outputs.has("c1") and stereo_outputs.has("c2"):
  731. #if stereo_outputs["c1"].ends_with(".wav") and stereo_outputs["c2"].ends_with(".wav"):
  732. #var final_stereo = Global.outfile.get_basename() + "_stereo.wav"
  733. #batch_lines.append("%s/submix interleave \"%s\" \"%s\" \"%s\"" % [cdpprogs_location, stereo_outputs["c1"], stereo_outputs["c2"], final_stereo])
  734. #final_output_dir = final_stereo
  735. #if delete_intermediate_outputs:
  736. #intermediate_files.append(stereo_outputs["c1"])
  737. #intermediate_files.append(stereo_outputs["c2"])
  738. #
  739. ##add del command for not needed files
  740. ##always delete mono split as they are in a weird location
  741. #intermediate_files.append(Global.infile.get_basename() + "_c1.wav")
  742. #intermediate_files.append(Global.infile.get_basename() + "_c2.wav")
  743. #
  744. #for file_path in intermediate_files:
  745. #batch_lines.append("del \"%s\"" % file_path.replace("/", "\\"))
  746. #
  747. #else:
  748. ## Use mono logic as before
  749. ## Step 4: Process chain
  750. #var output_files = {} # node -> output file
  751. #var process_count = 0
  752. #var current_infile = Global.infile
  753. #
  754. #for node_name in sorted:
  755. #var node = all_nodes[node_name]
  756. #var inputs = reverse_graph[node_name]
  757. #var input_files = []
  758. #for input_node in inputs:
  759. #input_files.append(output_files[input_node])
  760. #
  761. ## If multiple inputs, merge with submix mergemany
  762. #if input_files.size() > 1:
  763. #var merge_output = "%s_merge_%d.wav" % [Global.outfile.get_basename(), process_count]
  764. #var quoted_inputs := []
  765. #for f in input_files:
  766. #quoted_inputs.append("\"%s\"" % f)
  767. #var merge_cmd = cdpprogs_location + "/submix mergemany " + " ".join(quoted_inputs) + " \"%s\"" % merge_output
  768. #batch_lines.append(merge_cmd)
  769. #intermediate_files.append(merge_output)
  770. #current_infile = merge_output
  771. #elif input_files.size() == 1:
  772. #current_infile = input_files[0]
  773. #else:
  774. #current_infile = Global.infile
  775. #
  776. ## Build node command
  777. #var slider_data = _get_slider_values_ordered(node)
  778. #var extension = ".wav" if node.get_slot_type_right(0) == 0 else ".ana"
  779. #var output_file = "%s_%d%s" % [Global.outfile.get_basename(), process_count, extension]
  780. #var command_name = str(node.get_meta("command")) if node.has_meta("command") else node_name
  781. #command_name = command_name.replace("_", " ")
  782. #var line = "%s/%s \"%s\" \"%s\" " % [cdpprogs_location, command_name, current_infile, output_file]
  783. ##checks if slider has a flag meta value and appends the flag before the parameter
  784. #for entry in slider_data:
  785. #var flag = entry[0]
  786. #var value = entry[1]
  787. #line += ("%s%.2f " % [flag, value]) if flag.begins_with("-") else ("%.2f " % value)
  788. #batch_lines.append(line.strip_edges())
  789. #output_files[node_name] = output_file
  790. #if delete_intermediate_outputs:
  791. #intermediate_files.append(output_file)
  792. #process_count += 1
  793. #
  794. ## Step 4.5: Handle nodes connected to outputfile
  795. #var output_inputs := []
  796. #for conn in connections:
  797. #if conn["to_node"] == "outputfile":
  798. #output_inputs.append(str(conn["from_node"]))
  799. #
  800. #var final_outputs := []
  801. #for node_name in output_inputs:
  802. #if output_files.has(node_name):
  803. #final_outputs.append(output_files[node_name])
  804. #
  805. #if final_outputs.size() > 1:
  806. #var quoted_inputs := []
  807. #for f in final_outputs:
  808. #quoted_inputs.append("\"%s\"" % f)
  809. #intermediate_files.append(f)
  810. #var merge_cmd = cdpprogs_location + "/submix mergemany " + " ".join(quoted_inputs) + " \"%s\"" % Global.outfile + ".wav"
  811. #final_output_dir = Global.outfile + ".wav"
  812. #batch_lines.append(merge_cmd)
  813. #for f in final_outputs:
  814. #intermediate_files.erase(f)
  815. #elif final_outputs.size() == 1:
  816. #var single_output = final_outputs[0]
  817. #final_output_dir = single_output
  818. #intermediate_files.erase(single_output)
  819. #
  820. ## Step 5: Cleanup commands
  821. #log_console("Adding cleanup commands for intermediate files.", true)
  822. #for file_path in intermediate_files:
  823. #batch_lines.append("del \"%s\"" % file_path.replace("/", "\\"))
  824. #
  825. ## Step 6: Write batch file
  826. #var file = FileAccess.open("user://ordered_script.bat", FileAccess.WRITE)
  827. #for line in batch_lines:
  828. #file.store_line(line)
  829. #file.close()
  830. #
  831. #log_console("Batch file complete.", true)
  832. #log_console("Processing audio, please wait.", true)
  833. #await get_tree().process_frame
  834. #run_batch_file()
  835. func _get_slider_values_ordered(node: Node) -> Array:
  836. var results := []
  837. for child in node.get_children():
  838. if child is Range:
  839. var flag = child.get_meta("flag") if child.has_meta("flag") else ""
  840. results.append([flag, child.value])
  841. elif child.get_child_count() > 0:
  842. var nested := _get_slider_values_ordered(child)
  843. results.append_array(nested)
  844. return results
  845. func build_graph_from_connections(graph_edit: GraphEdit) -> Dictionary:
  846. var connections = graph_edit.get_connection_list()
  847. var graph := {}
  848. var reverse_graph := {}
  849. var all_nodes := {}
  850. # Collect all GraphNode names
  851. for child in graph_edit.get_children():
  852. if child is GraphNode:
  853. var name = str(child.name)
  854. all_nodes[name] = true
  855. graph[name] = []
  856. reverse_graph[name] = []
  857. # Build forward and reverse graphs
  858. for conn in connections:
  859. var from = str(conn["from_node"])
  860. var to = str(conn["to_node"])
  861. if graph.has(from) and graph.has(to):
  862. graph[from].append(to)
  863. reverse_graph[to].append(from)
  864. # Perform BFS from "inputfile"
  865. var reachable := {}
  866. var queue := ["inputfile"]
  867. while not queue.is_empty():
  868. var current = queue.pop_front()
  869. if reachable.has(current):
  870. continue
  871. reachable[current] = true
  872. for neighbor in graph.get(current, []):
  873. queue.append(neighbor)
  874. # Reverse BFS from "outputfile"
  875. var required := {}
  876. queue = ["outputfile"]
  877. while not queue.is_empty():
  878. var current = queue.pop_front()
  879. if required.has(current):
  880. continue
  881. required[current] = true
  882. for parent in reverse_graph.get(current, []):
  883. queue.append(parent)
  884. # Keep only nodes that are reachable both ways
  885. var used_nodes := []
  886. for node in reachable.keys():
  887. if required.has(node):
  888. used_nodes.append(node)
  889. var pruned_graph := {}
  890. for node in used_nodes:
  891. var filtered_neighbors := []
  892. for neighbor in graph.get(node, []):
  893. if used_nodes.has(neighbor):
  894. filtered_neighbors.append(neighbor)
  895. pruned_graph[node] = filtered_neighbors
  896. return {
  897. "graph": pruned_graph,
  898. "nodes": used_nodes
  899. }
  900. func topological_sort(graph: Dictionary, nodes: Array) -> Array:
  901. var indegree := {}
  902. for node in nodes:
  903. indegree[node] = 0
  904. for node in nodes:
  905. for neighbor in graph[node]:
  906. indegree[neighbor] += 1
  907. var queue := []
  908. for node in nodes:
  909. if indegree[node] == 0:
  910. queue.append(node)
  911. var sorted := []
  912. while not queue.is_empty():
  913. var current = queue.pop_front()
  914. sorted.append(current)
  915. for neighbor in graph[current]:
  916. indegree[neighbor] -= 1
  917. if indegree[neighbor] == 0:
  918. queue.append(neighbor)
  919. if sorted.size() != nodes.size():
  920. push_error("Cycle detected or disconnected graph.")
  921. return []
  922. return sorted
  923. func run_batch_file():
  924. var is_windows = OS.get_name() == "Windows"
  925. var script_ext = ".bat" if is_windows else ".sh"
  926. var script_name = "ordered_script" + script_ext
  927. var script_path = ProjectSettings.globalize_path("user://%s" % script_name)
  928. var output: Array = []
  929. var error: Array = []
  930. var exit_code := 0
  931. if is_windows:
  932. exit_code = OS.execute("cmd.exe", ["/c", script_path], output, true, true)
  933. else:
  934. exit_code = OS.execute("sh", [script_path], output, true, true)
  935. var output_str := ""
  936. for item in output:
  937. output_str += item + "\n"
  938. var error_str := ""
  939. for item in error:
  940. error_str += item + "\n"
  941. if exit_code == 0:
  942. console_output.append_text("[color=green]Processes ran successfully[/color]\n\n")
  943. console_output.append_text("[b]Output:[/b]\n")
  944. console_output.scroll_to_line(console_output.get_line_count() - 1)
  945. console_output.append_text(output_str + "\n")
  946. if final_output_dir.ends_with(".wav"):
  947. output_audio_player.play_outfile(final_output_dir)
  948. outfile = final_output_dir
  949. var interface_settings = ConfigHandler.load_interface_settings()
  950. if interface_settings.auto_close_console:
  951. $Console.hide()
  952. else:
  953. console_output.append_text("[color=red][b]Processes failed with exit code: %d[/b][/color]\n\n" % exit_code)
  954. console_output.append_text("[b]Error:[/b]\n")
  955. console_output.scroll_to_line(console_output.get_line_count() - 1)
  956. console_output.append_text(error_str + "\n")
  957. #func run_batch_file():
  958. #var bat_path = ProjectSettings.globalize_path("user://ordered_script.bat")
  959. #var output : Array = []
  960. #var error : Array = []
  961. #
  962. #var exit_code = OS.execute("cmd.exe", ["/c", bat_path], output, true, true)
  963. #
  964. #var output_str = ""
  965. #for item in output:
  966. #output_str += item + "\n"
  967. #
  968. #var error_str = ""
  969. #for item in error:
  970. #error_str += item + "\n"
  971. #
  972. #if exit_code == 0:
  973. #console_output.append_text("[color=green]Processes ran successfully[/color]\n \n")
  974. #console_output.append_text("[b]Output:[/b]\n")
  975. #console_output.scroll_to_line(console_output.get_line_count() - 1)
  976. #console_output.append_text(output_str + "/n")
  977. #if final_output_dir.ends_with(".wav"):
  978. #output_audio_player.play_outfile(final_output_dir)
  979. #outfile = final_output_dir
  980. #var interface_settings = ConfigHandler.load_interface_settings()
  981. #if interface_settings.auto_close_console == true:
  982. #$Console.hide()
  983. #else:
  984. #console_output.append_text("[color=red][b]Processes failed with exit code: %d[/b][/color]\n" % exit_code + "\n \n")
  985. #console_output.append_text("[b]Error:[/b]\n" )
  986. #console_output.scroll_to_line(console_output.get_line_count() - 1)
  987. #console_output.append_text(error_str + "/n")
  988. ######## Realtively free from dragons from here
  989. func _toggle_delete(toggled_on: bool):
  990. delete_intermediate_outputs = toggled_on
  991. print(toggled_on)
  992. func _on_console_close_requested() -> void:
  993. $Console.hide()
  994. func log_console(text: String, update: bool) -> void:
  995. console_output.append_text(text + "\n \n")
  996. console_output.scroll_to_line(console_output.get_line_count() - 1)
  997. if update == true:
  998. await get_tree().process_frame # Optional: ensure UI updates
  999. func _on_console_open_folder_button_down() -> void:
  1000. $Console.hide()
  1001. OS.shell_open(Global.outfile.get_base_dir())
  1002. func _on_ok_button_2_button_down() -> void:
  1003. $NoInputPopup.hide()
  1004. func _on_ok_button_3_button_down() -> void:
  1005. $MultipleConnectionsPopup.hide()
  1006. func _on_settings_button_index_pressed(index: int) -> void:
  1007. var interface_settings = ConfigHandler.load_interface_settings()
  1008. match index:
  1009. 0:
  1010. $CdpLocationDialog.show()
  1011. 1:
  1012. if interface_settings.disable_pvoc_warning == false:
  1013. $MenuBar/SettingsButton.set_item_checked(index, true)
  1014. ConfigHandler.save_interface_settings("disable_pvoc_warning", true)
  1015. else:
  1016. $MenuBar/SettingsButton.set_item_checked(index, false)
  1017. ConfigHandler.save_interface_settings("disable_pvoc_warning", false)
  1018. 2:
  1019. if interface_settings.auto_close_console == false:
  1020. $MenuBar/SettingsButton.set_item_checked(index, true)
  1021. ConfigHandler.save_interface_settings("auto_close_console", true)
  1022. else:
  1023. $MenuBar/SettingsButton.set_item_checked(index, false)
  1024. ConfigHandler.save_interface_settings("auto_close_console", false)
  1025. 3:
  1026. $Console.show()
  1027. func _on_file_button_index_pressed(index: int) -> void:
  1028. match index:
  1029. 0:
  1030. if changesmade == true:
  1031. savestate = "newfile"
  1032. $SaveChangesPopup.show()
  1033. else:
  1034. new_patch()
  1035. currentfile = "none" #reset current file to none for save tracking
  1036. 1:
  1037. if currentfile == "none":
  1038. savestate = "saveas"
  1039. $SaveDialog.popup_centered()
  1040. else:
  1041. save_graph_edit(currentfile)
  1042. 2:
  1043. savestate = "saveas"
  1044. $SaveDialog.popup_centered()
  1045. 3:
  1046. if changesmade == true:
  1047. savestate = "load"
  1048. $SaveChangesPopup.show()
  1049. else:
  1050. $LoadDialog.popup_centered()
  1051. func save_graph_edit(path: String):
  1052. var file = FileAccess.open(path, FileAccess.WRITE)
  1053. if file == null:
  1054. print("Failed to open file for saving")
  1055. return
  1056. var node_data_list = []
  1057. var connection_data_list = []
  1058. for node in graph_edit.get_children():
  1059. if node is GraphNode:
  1060. var offset = node.position_offset
  1061. var node_data = {
  1062. "name": node.name,
  1063. "command": node.get_meta("command"),
  1064. "offset": { "x": offset.x, "y": offset.y },
  1065. "slider_values": {},
  1066. "notes":{}
  1067. }
  1068. for child in node.find_children("*", "Slider", true, false):
  1069. var relative_path = node.get_path_to(child)
  1070. node_data["slider_values"][str(relative_path)] = child.value
  1071. for child in node.find_children("*", "CodeEdit", true, false):
  1072. node_data["notes"][child.name] = child.text
  1073. node_data_list.append(node_data)
  1074. for conn in graph_edit.get_connection_list():
  1075. connection_data_list.append({
  1076. "from_node": conn["from_node"],
  1077. "from_port": conn["from_port"],
  1078. "to_node": conn["to_node"],
  1079. "to_port": conn["to_port"]
  1080. })
  1081. var graph_data = {
  1082. "nodes": node_data_list,
  1083. "connections": connection_data_list
  1084. }
  1085. var json = JSON.new()
  1086. var json_string = json.stringify(graph_data, "\t")
  1087. file.store_string(json_string)
  1088. file.close()
  1089. print("Graph saved.")
  1090. changesmade = false
  1091. get_window().title = "SoundThread - " + path.get_file().trim_suffix(".thd")
  1092. func load_graph_edit(path: String):
  1093. var file = FileAccess.open(path, FileAccess.READ)
  1094. if file == null:
  1095. print("Failed to open file for loading")
  1096. return
  1097. var json_text = file.get_as_text()
  1098. file.close()
  1099. var json = JSON.new()
  1100. if json.parse(json_text) != OK:
  1101. print("Error parsing JSON")
  1102. return
  1103. var graph_data = json.get_data()
  1104. graph_edit.clear_connections()
  1105. for node in graph_edit.get_children():
  1106. if node is GraphNode:
  1107. node.queue_free()
  1108. await get_tree().process_frame # Ensure nodes are cleared
  1109. for node_data in graph_data["nodes"]:
  1110. var command_name = node_data.get("command", "")
  1111. var template = Nodes.get_node_or_null(command_name)
  1112. if not template:
  1113. print("Template not found for command:", command_name)
  1114. continue
  1115. var new_node: GraphNode = template.duplicate()
  1116. new_node.name = node_data["name"]
  1117. new_node.position_offset = Vector2(node_data["offset"]["x"], node_data["offset"]["y"])
  1118. new_node.set_meta("command", command_name)
  1119. graph_edit.add_child(new_node)
  1120. _register_node_movement() #link nodes for tracking position changes for changes tracking
  1121. # Restore sliders
  1122. for slider_path_str in node_data["slider_values"]:
  1123. var slider = new_node.get_node_or_null(slider_path_str)
  1124. if slider and (slider is HSlider or slider is VSlider):
  1125. slider.value = node_data["slider_values"][slider_path_str]
  1126. # Restore notes
  1127. for codeedit_name in node_data["notes"]:
  1128. var codeedit = new_node.find_child(codeedit_name, true, false)
  1129. if codeedit and (codeedit is CodeEdit):
  1130. codeedit.text = node_data["notes"][codeedit_name]
  1131. _register_inputs_in_node(new_node) #link sliders for changes tracking
  1132. # Restore connections
  1133. for conn in graph_data["connections"]:
  1134. graph_edit.connect_node(
  1135. conn["from_node"], conn["from_port"],
  1136. conn["to_node"], conn["to_port"]
  1137. )
  1138. link_output()
  1139. print("Graph loaded.")
  1140. get_window().title = "SoundThread - " + path.get_file().trim_suffix(".thd")
  1141. func _on_save_dialog_file_selected(path: String) -> void:
  1142. save_graph_edit(path) #save file
  1143. #check what the user was trying to do before save and do that action
  1144. if savestate == "newfile":
  1145. new_patch()
  1146. currentfile = "none" #reset current file to none for save tracking
  1147. elif savestate == "load":
  1148. $LoadDialog.popup_centered()
  1149. elif savestate == "helpfile":
  1150. currentfile = "none" #reset current file to none for save tracking so user cant save over help file
  1151. load_graph_edit(helpfile)
  1152. elif savestate == "quit":
  1153. await get_tree().create_timer(0.25).timeout #little pause so that it feels like it actually saved even though it did
  1154. get_tree().quit()
  1155. savestate = "none" #reset save state, not really needed but feels good
  1156. func _on_load_dialog_file_selected(path: String) -> void:
  1157. currentfile = path #tracking path here only means "save" only saves patches the user has loaded rather than overwriting help files
  1158. load_graph_edit(path)
  1159. func _on_help_button_index_pressed(index: int) -> void:
  1160. match index:
  1161. 0:
  1162. pass
  1163. 1:
  1164. if changesmade == true:
  1165. savestate = "helpfile"
  1166. helpfile = "res://examples/getting_started.thd"
  1167. $SaveChangesPopup.show()
  1168. else:
  1169. currentfile = "none" #reset current file to none for save tracking so user cant save over help file
  1170. load_graph_edit("res://examples/getting_started.thd")
  1171. 2:
  1172. if changesmade == true:
  1173. savestate = "helpfile"
  1174. helpfile = "res://examples/navigating.thd"
  1175. $SaveChangesPopup.show()
  1176. else:
  1177. currentfile = "none" #reset current file to none for save tracking so user cant save over help file
  1178. load_graph_edit("res://examples/navigating.thd")
  1179. 3:
  1180. if changesmade == true:
  1181. savestate = "helpfile"
  1182. helpfile = "res://examples/building_a_thread.thd"
  1183. $SaveChangesPopup.show()
  1184. else:
  1185. currentfile = "none" #reset current file to none for save tracking so user cant save over help file
  1186. load_graph_edit("res://examples/building_a_thread.thd")
  1187. 4:
  1188. if changesmade == true:
  1189. savestate = "helpfile"
  1190. helpfile = "res://examples/frequency_domain.thd"
  1191. $SaveChangesPopup.show()
  1192. else:
  1193. currentfile = "none" #reset current file to none for save tracking so user cant save over help file
  1194. load_graph_edit("res://examples/frequency_domain.thd")
  1195. 5:
  1196. if changesmade == true:
  1197. savestate = "helpfile"
  1198. helpfile = "res://examples/quirks.thd"
  1199. $SaveChangesPopup.show()
  1200. else:
  1201. currentfile = "none" #reset current file to none for save tracking so user cant save over help file
  1202. load_graph_edit("res://examples/quirks.thd")
  1203. 6:
  1204. pass
  1205. 7:
  1206. OS.shell_open("https://www.composersdesktop.com/docs/html/cdphome.htm")
  1207. func _recycle_outfile():
  1208. if outfile != "no file":
  1209. input_audio_player.recycle_outfile(outfile)
  1210. func _on_save_changes_button_down() -> void:
  1211. $SaveChangesPopup.hide()
  1212. if currentfile == "none":
  1213. $SaveDialog.show()
  1214. else:
  1215. save_graph_edit(currentfile)
  1216. if savestate == "newfile":
  1217. new_patch()
  1218. currentfile = "none" #reset current file to none for save tracking
  1219. elif savestate == "load":
  1220. $LoadDialog.popup_centered()
  1221. elif savestate == "helpfile":
  1222. currentfile = "none" #reset current file to none for save tracking so user cant save over help file
  1223. load_graph_edit(helpfile)
  1224. elif savestate == "quit":
  1225. await get_tree().create_timer(0.25).timeout #little pause so that it feels like it actually saved even though it did
  1226. get_tree().quit()
  1227. savestate = "none"
  1228. func _on_dont_save_changes_button_down() -> void:
  1229. $SaveChangesPopup.hide()
  1230. if savestate == "newfile":
  1231. new_patch()
  1232. currentfile = "none" #reset current file to none for save tracking
  1233. elif savestate == "load":
  1234. $LoadDialog.popup_centered()
  1235. elif savestate == "helpfile":
  1236. currentfile = "none" #reset current file to none for save tracking so user cant save over help file
  1237. load_graph_edit(helpfile)
  1238. elif savestate == "quit":
  1239. get_tree().quit()
  1240. savestate = "none"
  1241. func _notification(what):
  1242. if what == NOTIFICATION_WM_CLOSE_REQUEST:
  1243. if changesmade == true:
  1244. savestate = "quit"
  1245. $SaveChangesPopup.show()
  1246. else:
  1247. get_tree().quit() # default behavior
  1248. func _open_output_folder():
  1249. if lastoutputfolder != "none":
  1250. OS.shell_open(lastoutputfolder)
  1251. func _on_rich_text_label_meta_clicked(meta: Variant) -> void:
  1252. print(str(meta))
  1253. OS.shell_open(str(meta))