control.gd 52 KB

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