control.gd 51 KB

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