control.gd 68 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888
  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. var help_data := {} #stores help data for each node to display in help popup
  25. var HelpWindowScene = preload("res://scenes/main/help_window.tscn")
  26. var uiscale = 1.0 #tracks scaling for retina screens
  27. var process_info = {} #tracks the data of the currently running process
  28. var process_running := false #tracks if a process is currently running
  29. var process_cancelled = false #checks if the currently running process has been cancelled
  30. # Called when the node enters the scene tree for the first time.
  31. func _ready() -> void:
  32. Nodes.hide()
  33. $mainmenu.hide()
  34. #$"mainmenu/select_effect/Time Domain".show()
  35. #$"mainmenu/select_effect/Time Domain/Distort".show()
  36. #$"mainmenu/select_effect/Frequency Domain/Convert".show()
  37. $NoLocationPopup.hide()
  38. $Console.hide()
  39. $NoInputPopup.hide()
  40. $MultipleConnectionsPopup.hide()
  41. $AudioSettings.hide()
  42. $AudioDevicePopup.hide()
  43. $SearchMenu.hide()
  44. $Settings.hide()
  45. $ProgressWindow.hide()
  46. $SaveDialog.access = FileDialog.ACCESS_FILESYSTEM
  47. $SaveDialog.file_mode = FileDialog.FILE_MODE_SAVE_FILE
  48. $SaveDialog.filters = ["*.thd"]
  49. $LoadDialog.access = FileDialog.ACCESS_FILESYSTEM
  50. $LoadDialog.file_mode = FileDialog.FILE_MODE_OPEN_FILE
  51. $LoadDialog.filters = ["*.thd"]
  52. #Goes through all nodes in scene and checks for buttons in the make_node_buttons group
  53. #Associates all buttons with the _on_button_pressed fuction and passes the button as an argument
  54. for child in get_tree().get_nodes_in_group("make_node_buttons"):
  55. if child is Button:
  56. child.pressed.connect(_on_button_pressed.bind(child))
  57. get_node("SearchMenu").make_node.connect(_make_node_from_search_menu)
  58. get_node("mainmenu").make_node.connect(_make_node_from_search_menu)
  59. get_node("mainmenu").open_help.connect(show_help_for_node)
  60. get_node("Settings").open_cdp_location.connect(show_cdp_location)
  61. get_node("Settings").console_on_top.connect(change_console_settings)
  62. check_user_preferences()
  63. get_tree().set_auto_accept_quit(false) #disable closing the app with the x and instead handle it internally
  64. #Check export config for version number and set about menu to current version
  65. #Assumes version of mac + linux builds is the same as windows
  66. #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
  67. var export_config = ConfigFile.new()
  68. export_config.load("res://export_presets.cfg")
  69. $MenuBar/About.set_item_text(0, "SoundThread v" + export_config.get_value("preset.0.options", "application/product_version", "version unknown") + "-alpha")
  70. #checks if display is hidpi and scales ui accordingly hidpi - 144
  71. if DisplayServer.screen_get_dpi(0) >= 144:
  72. uiscale = 2.0
  73. get_window().content_scale_factor = uiscale
  74. #goes through popup_windows group and scales all popups and resizes them
  75. for window in get_tree().get_nodes_in_group("popup_windows"):
  76. window.size = window.size * uiscale
  77. window.content_scale_factor = uiscale
  78. #checks if user has opened a file from the system file menu and loads it
  79. var args = OS.get_cmdline_args()
  80. for arg in args:
  81. var path = arg.strip_edges()
  82. if FileAccess.file_exists(path) and path.get_extension().to_lower() == "thd":
  83. load_graph_edit(path)
  84. break
  85. var file = FileAccess.open("res://scenes/main/process_help.json", FileAccess.READ)
  86. if file:
  87. help_data = JSON.parse_string(file.get_as_text())
  88. new_patch()
  89. check_cdp_location_set()
  90. func new_patch():
  91. #clear old patch
  92. graph_edit.clear_connections()
  93. for node in graph_edit.get_children():
  94. if node is GraphNode:
  95. node.queue_free()
  96. await get_tree().process_frame # Wait for nodes to actually be removed
  97. graph_edit.scroll_offset = Vector2(0, 0)
  98. #Generate input and output nodes
  99. var effect: GraphNode = Nodes.get_node(NodePath("inputfile")).duplicate()
  100. effect.name = "inputfile"
  101. get_node("GraphEdit").add_child(effect, true)
  102. effect.connect("open_help", Callable(self, "show_help_for_node"))
  103. effect.position_offset = Vector2(20,80)
  104. effect = Nodes.get_node(NodePath("outputfile")).duplicate()
  105. effect.name = "outputfile"
  106. get_node("GraphEdit").add_child(effect, true)
  107. effect.connect("open_help", Callable(self, "show_help_for_node"))
  108. effect.position_offset = Vector2((DisplayServer.screen_get_size().x - 480) / uiscale, 80)
  109. _register_node_movement() #link nodes for tracking position changes for changes tracking
  110. changesmade = false #so it stops trying to save unchanged empty files
  111. Global.infile = "no_file" #resets input to stop processes running with old files
  112. get_window().title = "SoundThread"
  113. link_output()
  114. func link_output():
  115. #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
  116. for control in get_tree().get_nodes_in_group("outputnode"): #check all items in outputnode group
  117. if control.get_meta("outputfunction") == "deleteintermediate": #link delete intermediate files toggle to script
  118. control.toggled.connect(_toggle_delete)
  119. control.button_pressed = true
  120. elif control.get_meta("outputfunction") == "runprocess": #link runprocess button
  121. control.button_down.connect(_run_process)
  122. elif control.get_meta("outputfunction") == "recycle": #link recycle button
  123. control.button_down.connect(_recycle_outfile)
  124. elif control.get_meta("outputfunction") == "audioplayer": #link output audio player
  125. output_audio_player = control
  126. elif control.get_meta("outputfunction") == "filename":
  127. control.text = "outfile"
  128. outfilename = control
  129. elif control.get_meta("outputfunction") == "reusefolder":
  130. foldertoggle = control
  131. foldertoggle.button_pressed = true
  132. elif control.get_meta("outputfunction") == "openfolder":
  133. control.button_down.connect(_open_output_folder)
  134. for control in get_tree().get_nodes_in_group("inputnode"):
  135. if control.get_meta("inputfunction") == "audioplayer": #link input for recycle function
  136. print("input player found")
  137. input_audio_player = control
  138. func check_user_preferences():
  139. var interface_settings = ConfigHandler.load_interface_settings()
  140. var audio_settings = ConfigHandler.load_audio_settings()
  141. var audio_devices = AudioServer.get_output_device_list()
  142. $Console.always_on_top = interface_settings.console_on_top
  143. if audio_devices.has(audio_settings.device):
  144. AudioServer.set_output_device(audio_settings.device)
  145. else:
  146. $AudioDevicePopup.popup_centered()
  147. match interface_settings.theme:
  148. 0:
  149. RenderingServer.set_default_clear_color(Color("#2f4f4e"))
  150. 1:
  151. RenderingServer.set_default_clear_color(Color("#000807"))
  152. 2:
  153. RenderingServer.set_default_clear_color(Color("#98d4d2"))
  154. 3:
  155. RenderingServer.set_default_clear_color(Color(interface_settings.theme_custom_colour))
  156. func show_cdp_location():
  157. $CdpLocationDialog.show()
  158. func check_cdp_location_set():
  159. #checks if the location has been set and prompts user to set it
  160. var cdpprogs_settings = ConfigHandler.load_cdpprogs_settings()
  161. if cdpprogs_settings.location == "no_location":
  162. $NoLocationPopup.popup_centered()
  163. else:
  164. #if location is set, stores it in a variable
  165. cdpprogs_location = str(cdpprogs_settings.location)
  166. print(cdpprogs_location)
  167. func _on_ok_button_button_down() -> void:
  168. #after user has read dialog on where to find cdp progs this loads the file browser
  169. $NoLocationPopup.hide()
  170. if OS.get_name() == "Windows":
  171. $CdpLocationDialog.current_dir = "C:/"
  172. else:
  173. $CdpLocationDialog.current_dir = OS.get_environment("HOME")
  174. $CdpLocationDialog.show()
  175. func _on_cdp_location_dialog_dir_selected(dir: String) -> void:
  176. #saves default location for cdp programs in config file
  177. ConfigHandler.save_cdpprogs_settings(dir)
  178. cdpprogs_location = dir
  179. func _on_cdp_location_dialog_canceled() -> void:
  180. #cycles around the set location prompt if user cancels the file dialog
  181. check_cdp_location_set()
  182. # Called every frame. 'delta' is the elapsed time since the previous frame.
  183. func _process(delta: float) -> void:
  184. #showmenu()
  185. pass
  186. func _input(event):
  187. if event.is_action_pressed("copy_node"):
  188. copy_selected_nodes()
  189. get_viewport().set_input_as_handled()
  190. elif event.is_action_pressed("paste_node"):
  191. simulate_mouse_click() #hacky fix to stop tooltips getting stuck
  192. await get_tree().process_frame
  193. paste_copied_nodes()
  194. get_viewport().set_input_as_handled()
  195. elif event.is_action_pressed("undo"):
  196. undo_redo.undo()
  197. elif event.is_action_pressed("redo"):
  198. undo_redo.redo()
  199. elif event.is_action_pressed("save"):
  200. if currentfile == "none":
  201. savestate = "saveas"
  202. $SaveDialog.popup_centered()
  203. else:
  204. save_graph_edit(currentfile)
  205. elif event.is_action_pressed("open_explore"):
  206. open_explore()
  207. func show_help_for_node(node_name: String, node_title: String):
  208. #check if there is already a help window open for this node and pop it up instead of making a new one
  209. for child in get_tree().current_scene.get_children():
  210. if child is Window and child.title == "Help - " + node_title:
  211. # Found existing window, bring it to front
  212. if child.is_visible():
  213. child.hide()
  214. child.popup()
  215. else:
  216. child.popup()
  217. return
  218. if help_data.has(node_name):
  219. #looks up the help data from the json and stores it in info
  220. var info = help_data[node_name]
  221. #makes an instance of the help_window scene
  222. var help_window = HelpWindowScene.instantiate()
  223. help_window.title = "Help - " + node_title
  224. help_window.get_node("HelpTitle").text = node_title
  225. var output = ""
  226. output += info.get("short_description", "") + "\n\n"
  227. var parameters = info.get("parameters", {})
  228. #checks if there are parameters and if there are places them in a table
  229. if parameters.size() > 0:
  230. output += "[table=3]\n"
  231. output += "[cell][b]Parameter Name[/b][/cell][cell][b]Description[/b][/cell][cell][b]Automatable[/b][/cell]\n"
  232. for key in parameters.keys(): #scans through all parameters
  233. var param = parameters[key]
  234. var name = param.get("paramname", "")
  235. var desc = param.get("paramdescription", "")
  236. var automatable = param.get("automatable", false)
  237. var autom_text = "[center]✓[/center]" if automatable else "[center]𐄂[/center]" #replaces true and false with ticks and crosses
  238. output += "[cell]%s[/cell][cell]%s[/cell][cell]%s[/cell]\n" % [name, desc, autom_text] #places each param detail into cells of the table
  239. output += "[/table]\n\n" #ends the table
  240. output += "[b]Functionality[/b]\n"
  241. var description_text = info.get("description", "")
  242. output += description_text.strip_edges()
  243. #check if this is a cdp process or a utility and display the cdp process if it is one
  244. var category = info.get("category", "")
  245. if category != "utility":
  246. output += "\n\n[b]CDP Process[/b]\nThis node runs the CDP Process: " + node_name.replace("_", " ")
  247. help_window.get_node("HelpText").bbcode_text = output
  248. help_window.get_node("HelpText").scroll_to_line(0) #scrolls to the first line of the help file just incase
  249. # Add to the current scene tree to show it
  250. get_tree().current_scene.add_child(help_window)
  251. if help_window.content_scale_factor < uiscale:
  252. help_window.size = help_window.size * uiscale
  253. help_window.content_scale_factor = uiscale
  254. help_window.popup()
  255. else:
  256. # If no help available, even though there always should be, show a window saying no help found
  257. var help_window = HelpWindowScene.instance()
  258. help_window.title = "Help - " + node_title
  259. help_window.get_node("HelpTitle").text = node_title
  260. help_window.get_node("HelpText").bbcode_text = "No help found."
  261. get_tree().current_scene.add_child(help_window)
  262. help_window.popup()
  263. func simulate_mouse_click():
  264. #simulates clicking the middle mouse button in order to hide any visible tooltips
  265. var click_pos = get_viewport().get_mouse_position()
  266. var down_event := InputEventMouseButton.new()
  267. down_event.button_index = MOUSE_BUTTON_MIDDLE
  268. down_event.pressed = true
  269. down_event.position = click_pos
  270. Input.parse_input_event(down_event)
  271. var up_event := InputEventMouseButton.new()
  272. up_event.button_index = MOUSE_BUTTON_MIDDLE
  273. up_event.pressed = false
  274. up_event.position = click_pos
  275. Input.parse_input_event(up_event)
  276. func _make_node_from_search_menu(command: String):
  277. #close menu
  278. $SearchMenu.hide()
  279. #Find node with matching name to button and create a version of it in the graph edit
  280. #and position it close to the origin right click to open the menu
  281. var effect: GraphNode = Nodes.get_node(NodePath(command)).duplicate()
  282. effect.name = command
  283. get_node("GraphEdit").add_child(effect, true)
  284. effect.connect("open_help", Callable(self, "show_help_for_node"))
  285. effect.set_position_offset((effect_position + graph_edit.scroll_offset) / graph_edit.zoom) #set node to current mouse position in graph edit
  286. _register_inputs_in_node(effect) #link sliders for changes tracking
  287. _register_node_movement() #link nodes for tracking position changes for changes tracking
  288. changesmade = true
  289. # Remove node with UndoRedo
  290. undo_redo.create_action("Add Node")
  291. undo_redo.add_undo_method(Callable(graph_edit, "remove_child").bind(effect))
  292. undo_redo.add_undo_method(Callable(effect, "queue_free"))
  293. undo_redo.add_undo_method(Callable(self, "_track_changes"))
  294. undo_redo.commit_action()
  295. func _on_button_pressed(button: Button):
  296. #close menu
  297. $mainmenu.hide()
  298. mainmenu_visible = false
  299. #Find node with matching name to button and create a version of it in the graph edit
  300. #and position it close to the origin right click to open the menu
  301. var effect: GraphNode = Nodes.get_node(NodePath(button.name)).duplicate()
  302. effect.name = button.name
  303. get_node("GraphEdit").add_child(effect, true)
  304. effect.connect("open_help", Callable(self, "show_help_for_node"))
  305. effect.set_position_offset((effect_position + graph_edit.scroll_offset) / graph_edit.zoom) #set node to current mouse position in graph edit
  306. _register_inputs_in_node(effect) #link sliders for changes tracking
  307. _register_node_movement() #link nodes for tracking position changes for changes tracking
  308. changesmade = true
  309. # Remove node with UndoRedo
  310. undo_redo.create_action("Add Node")
  311. undo_redo.add_undo_method(Callable(graph_edit, "remove_child").bind(effect))
  312. undo_redo.add_undo_method(Callable(effect, "queue_free"))
  313. undo_redo.add_undo_method(Callable(self, "_track_changes"))
  314. undo_redo.commit_action()
  315. func _on_graph_edit_connection_request(from_node: StringName, from_port: int, to_node: StringName, to_port: int) -> void:
  316. #get_node("GraphEdit").connect_node(from_node, from_port, to_node, to_port)
  317. var graph_edit = get_node("GraphEdit")
  318. var to_graph_node = graph_edit.get_node(NodePath(to_node))
  319. # Get the type of the input port using GraphNode's built-in method
  320. var port_type = to_graph_node.get_input_port_type(to_port)
  321. # If port type is 1 and already has a connection, reject the request
  322. if port_type == 1:
  323. var connections = graph_edit.get_connection_list()
  324. var existing_connections = 0
  325. for conn in connections:
  326. if conn.to_node == to_node and conn.to_port == to_port:
  327. existing_connections += 1
  328. if existing_connections >= 1:
  329. var interface_settings = ConfigHandler.load_interface_settings()
  330. if interface_settings.disable_pvoc_warning == false:
  331. $MultipleConnectionsPopup.popup_centered()
  332. return
  333. # If no conflict, allow the connection
  334. graph_edit.connect_node(from_node, from_port, to_node, to_port)
  335. changesmade = true
  336. func _on_graph_edit_disconnection_request(from_node: StringName, from_port: int, to_node: StringName, to_port: int) -> void:
  337. get_node("GraphEdit").disconnect_node(from_node, from_port, to_node, to_port)
  338. changesmade = true
  339. func _on_graph_edit_node_selected(node: Node) -> void:
  340. selected_nodes[node] = true
  341. func _on_graph_edit_node_deselected(node: Node) -> void:
  342. selected_nodes[node] = false
  343. func _unhandled_key_input(event: InputEvent) -> void:
  344. if event is InputEventKey and event.pressed and not event.echo:
  345. if event.keycode == KEY_BACKSPACE:
  346. _on_graph_edit_delete_nodes_request(PackedStringArray(selected_nodes.keys().filter(func(k): return selected_nodes[k])))
  347. pass
  348. func _on_graph_edit_delete_nodes_request(nodes: Array[StringName]) -> void:
  349. var graph_edit = get_node("GraphEdit")
  350. undo_redo.create_action("Delete Nodes (Undo only)")
  351. for node in selected_nodes.keys():
  352. if selected_nodes[node]:
  353. if node.get_meta("command") == "inputfile" or node.get_meta("command") == "outputfile":
  354. print("can't delete input or output")
  355. else:
  356. # Store duplicate and state for undo
  357. var node_data = node.duplicate()
  358. var position = node.position_offset
  359. # Store all connections for undo
  360. var conns = []
  361. for con in graph_edit.get_connection_list():
  362. if con["to_node"] == node.name or con["from_node"] == node.name:
  363. conns.append(con)
  364. # Delete
  365. remove_connections_to_node(node)
  366. node.queue_free()
  367. changesmade = true
  368. # Register undo restore
  369. undo_redo.add_undo_method(Callable(graph_edit, "add_child").bind(node_data, true))
  370. undo_redo.add_undo_method(Callable(node_data, "set_position_offset").bind(position))
  371. for con in conns:
  372. undo_redo.add_undo_method(Callable(graph_edit, "connect_node").bind(
  373. con["from_node"], con["from_port"],
  374. con["to_node"], con["to_port"]
  375. ))
  376. undo_redo.add_undo_method(Callable(self, "set_node_selected").bind(node_data, true))
  377. undo_redo.add_undo_method(Callable(self, "_track_changes"))
  378. undo_redo.add_undo_method(Callable(self, "_register_inputs_in_node").bind(node_data)) #link sliders for changes tracking
  379. undo_redo.add_undo_method(Callable(self, "_register_node_movement")) # link nodes for changes tracking
  380. # Clear selection
  381. selected_nodes = {}
  382. undo_redo.commit_action()
  383. func set_node_selected(node: Node, selected: bool) -> void:
  384. selected_nodes[node] = selected
  385. #
  386. func remove_connections_to_node(node):
  387. for con in get_node("GraphEdit").get_connection_list():
  388. if con["to_node"] == node.name or con["from_node"] == node.name:
  389. get_node("GraphEdit").disconnect_node(con["from_node"], con["from_port"], con["to_node"], con["to_port"])
  390. changesmade = true
  391. #copy and paste nodes with vertical offset on paste
  392. func copy_selected_nodes():
  393. copied_nodes_data.clear()
  394. copied_connections.clear()
  395. var graph_edit = get_node("GraphEdit")
  396. # Store selected nodes and their slider values
  397. for node in graph_edit.get_children():
  398. # Check if the node is selected and not an 'inputfile' or 'outputfile'
  399. if node is GraphNode and selected_nodes.get(node, false):
  400. if node.get_meta("command") == "inputfile" or node.get_meta("command") == "outputfile":
  401. continue # Skip these nodes
  402. var node_data = {
  403. "name": node.name,
  404. "type": node.get_class(),
  405. "offset": node.position_offset,
  406. "slider_values": {}
  407. }
  408. for child in node.get_children():
  409. if child is HSlider or child is VSlider:
  410. node_data["slider_values"][child.name] = child.value
  411. copied_nodes_data.append(node_data)
  412. # Store connections between selected nodes
  413. for conn in graph_edit.get_connection_list():
  414. var from_ref = graph_edit.get_node_or_null(NodePath(conn["from_node"]))
  415. var to_ref = graph_edit.get_node_or_null(NodePath(conn["to_node"]))
  416. var is_from_selected = from_ref != null and selected_nodes.get(from_ref, false)
  417. var is_to_selected = to_ref != null and selected_nodes.get(to_ref, false)
  418. # Skip if any of the connected nodes are 'inputfile' or 'outputfile'
  419. if (from_ref != null and (from_ref.get_meta("command") == "inputfile" or from_ref.get_meta("command") == "outputfile")) or (to_ref != null and (to_ref.get_meta("command") == "inputfile" or to_ref.get_meta("command") == "outputfile")):
  420. continue
  421. if is_from_selected and is_to_selected:
  422. # Store connection as dictionary
  423. var conn_data = {
  424. "from_node": conn["from_node"],
  425. "from_port": conn["from_port"],
  426. "to_node": conn["to_node"],
  427. "to_port": conn["to_port"]
  428. }
  429. copied_connections.append(conn_data)
  430. func paste_copied_nodes():
  431. if copied_nodes_data.is_empty():
  432. return
  433. var graph_edit = get_node("GraphEdit")
  434. var name_map = {}
  435. var pasted_nodes = []
  436. # Step 1: Find topmost and bottommost Y of copied nodes
  437. var min_y = INF
  438. var max_y = -INF
  439. for node_data in copied_nodes_data:
  440. var y = node_data["offset"].y
  441. min_y = min(min_y, y)
  442. max_y = max(max_y, y)
  443. # Step 2: Decide where to paste the group
  444. var base_y_offset = max_y + 350 # Pasting below the lowest node
  445. # Step 3: Paste nodes, preserving vertical layout
  446. for node_data in copied_nodes_data:
  447. var original_node = graph_edit.get_node_or_null(NodePath(node_data["name"]))
  448. if not original_node:
  449. continue
  450. var new_node = original_node.duplicate()
  451. new_node.name = node_data["name"] + "_copy_" + str(randi() % 10000)
  452. var relative_y = node_data["offset"].y - min_y
  453. new_node.position_offset = Vector2(
  454. node_data["offset"].x,
  455. base_y_offset + relative_y
  456. )
  457. # Restore sliders
  458. for child in new_node.get_children():
  459. if child.name in node_data["slider_values"]:
  460. child.value = node_data["slider_values"][child.name]
  461. graph_edit.add_child(new_node, true)
  462. new_node.connect("open_help", Callable(self, "show_help_for_node"))
  463. _register_inputs_in_node(new_node) #link sliders for changes tracking
  464. _register_node_movement() # link nodes for changes tracking
  465. name_map[node_data["name"]] = new_node.name
  466. pasted_nodes.append(new_node)
  467. # Step 4: Reconnect new nodes
  468. for conn_data in copied_connections:
  469. var new_from = name_map.get(conn_data["from_node"], null)
  470. var new_to = name_map.get(conn_data["to_node"], null)
  471. if new_from and new_to:
  472. graph_edit.connect_node(new_from, conn_data["from_port"], new_to, conn_data["to_port"])
  473. # Step 5: Select pasted nodes
  474. for pasted_node in pasted_nodes:
  475. graph_edit.set_selected(pasted_node)
  476. selected_nodes[pasted_node] = true
  477. changesmade = true
  478. # Remove node with UndoRedo
  479. undo_redo.create_action("Paste Nodes")
  480. for pasted_node in pasted_nodes:
  481. undo_redo.add_undo_method(Callable(graph_edit, "remove_child").bind(pasted_node))
  482. undo_redo.add_undo_method(Callable(pasted_node, "queue_free"))
  483. undo_redo.add_undo_method(Callable(self, "remove_connections_to_node").bind(pasted_node))
  484. undo_redo.add_undo_method(Callable(self, "_track_changes"))
  485. undo_redo.commit_action()
  486. #functions for tracking changes for save state detection
  487. func _register_inputs_in_node(node: Node):
  488. #tracks input to nodes sliders and codeedit to track if patch is saved
  489. # Track Sliders
  490. for slider in node.find_children("*", "HSlider", true, false):
  491. # Create a Callable to the correct method
  492. var callable = Callable(self, "_on_any_slider_changed")
  493. # Check if it's already connected, and connect if not
  494. if not slider.is_connected("value_changed", callable):
  495. slider.connect("value_changed", callable)
  496. for slider in node.find_children("*", "VBoxContainer", true, false):
  497. # Also connect to meta_changed if the slider has that signal
  498. if slider.has_signal("meta_changed"):
  499. var meta_callable = Callable(self, "_on_any_slider_meta_changed")
  500. if not slider.is_connected("meta_changed", meta_callable):
  501. slider.connect("meta_changed", meta_callable)
  502. # Track CodeEdits
  503. for editor in node.find_children("*", "CodeEdit", true, false):
  504. var callable = Callable(self, "_on_any_input_changed")
  505. if not editor.is_connected("text_changed", callable):
  506. editor.connect("text_changed", callable)
  507. func _on_any_slider_meta_changed():
  508. changesmade = true
  509. print("Meta changed in slider")
  510. func _register_node_movement():
  511. for graphnode in graph_edit.get_children():
  512. if graphnode is GraphNode:
  513. var callable = Callable(self, "_on_graphnode_moved")
  514. if not graphnode.is_connected("position_offset_changed", callable):
  515. graphnode.connect("position_offset_changed", callable)
  516. func _on_graphnode_moved():
  517. changesmade = true
  518. func _on_any_slider_changed(value: float) -> void:
  519. changesmade = true
  520. func _on_any_input_changed():
  521. changesmade = true
  522. func _track_changes():
  523. changesmade = true
  524. func _run_process() -> void:
  525. if Global.infile == "no_file":
  526. $NoInputPopup.popup_centered()
  527. else:
  528. if foldertoggle.button_pressed == true and lastoutputfolder != "none":
  529. _on_file_dialog_dir_selected(lastoutputfolder)
  530. else:
  531. $FileDialog.show()
  532. func _on_file_dialog_dir_selected(dir: String) -> void:
  533. lastoutputfolder = dir
  534. console_output.clear()
  535. var interface_settings = ConfigHandler.load_interface_settings()
  536. if interface_settings.disable_progress_bar == false:
  537. $ProgressWindow.show()
  538. else:
  539. if $Console.is_visible():
  540. $Console.hide()
  541. await get_tree().process_frame # Wait a frame to allow hide to complete
  542. $Console.popup_centered()
  543. else:
  544. $Console.popup_centered()
  545. await get_tree().process_frame
  546. log_console("Generating processing queue", true)
  547. await get_tree().process_frame
  548. #get the current time in hh-mm-ss format as default : causes file name issues
  549. var time_dict = Time.get_time_dict_from_system()
  550. # Pad with zeros to ensure two digits for hour, minute, second
  551. var hour = str(time_dict.hour).pad_zeros(2)
  552. var minute = str(time_dict.minute).pad_zeros(2)
  553. var second = str(time_dict.second).pad_zeros(2)
  554. var time_str = hour + "-" + minute + "-" + second
  555. Global.outfile = dir + "/" + outfilename.text.get_basename() + "_" + Time.get_date_string_from_system() + "_" + time_str
  556. log_console("Output directory and file name(s):" + Global.outfile, true)
  557. await get_tree().process_frame
  558. run_thread_with_branches()
  559. func run_thread_with_branches():
  560. process_cancelled = false
  561. process_successful = true
  562. # Detect platform: Determine if the OS is Windows
  563. var is_windows := OS.get_name() == "Windows"
  564. # Choose appropriate commands based on OS
  565. var delete_cmd = "del" if is_windows else "rm"
  566. var rename_cmd = "ren" if is_windows else "mv"
  567. var path_sep := "/" # Always use forward slash for paths
  568. # Get all node connections in the GraphEdit
  569. var connections = graph_edit.get_connection_list()
  570. # Prepare data structures for graph traversal
  571. var graph = {} # forward adjacency list
  572. var reverse_graph = {} # reverse adjacency list (for input lookup)
  573. var indegree = {} # used for topological sort
  574. var all_nodes = {} # map of node name -> GraphNode reference
  575. log_console("Mapping thread.", true)
  576. await get_tree().process_frame # Let UI update
  577. #Step 0: check thread is valid
  578. var is_valid = path_exists_through_all_nodes()
  579. if is_valid == false:
  580. log_console("[color=#9c2828][b]Error: Valid Thread not found[/b][/color]", true)
  581. log_console("Threads must contain at least one processing node and a valid path from the Input File to the Output File.", true)
  582. await get_tree().process_frame # Let UI update
  583. return
  584. else:
  585. log_console("[color=#638382][b]Valid Thread found[/b][/color]", true)
  586. await get_tree().process_frame # Let UI update
  587. # Step 1: Gather nodes from the GraphEdit
  588. for child in graph_edit.get_children():
  589. if child is GraphNode:
  590. var name = str(child.name)
  591. all_nodes[name] = child
  592. if not child.has_meta("utility"):
  593. graph[name] = []
  594. reverse_graph[name] = []
  595. indegree[name] = 0 # Start with zero incoming edges
  596. #do calculations for progress bar
  597. var progress_step
  598. if Global.trim_infile == true:
  599. progress_step = 100 / (graph.size() + 4)
  600. else:
  601. progress_step = 100 / (graph.size() + 3)
  602. # Step 2: Build graph relationships from connections
  603. if process_cancelled:
  604. $ProgressWindow/ProgressLabel.text = "Thread Stopped"
  605. log_console("[b]Thread Stopped[/b]", true)
  606. return
  607. else:
  608. $ProgressWindow/ProgressLabel.text = "Building Thread"
  609. for conn in connections:
  610. var from = str(conn["from_node"])
  611. var to = str(conn["to_node"])
  612. if graph.has(from) and graph.has(to):
  613. graph[from].append(to)
  614. reverse_graph[to].append(from)
  615. indegree[to] += 1 # Count incoming edges
  616. # Step 3: Topological sort to get execution order
  617. var sorted = [] # Sorted list of node names
  618. var queue = [] # Queue of nodes with 0 indegree
  619. for node in graph.keys():
  620. if indegree[node] == 0:
  621. queue.append(node)
  622. while not queue.is_empty():
  623. var current = queue.pop_front()
  624. sorted.append(current)
  625. for neighbor in graph[current]:
  626. indegree[neighbor] -= 1
  627. if indegree[neighbor] == 0:
  628. queue.append(neighbor)
  629. # If not all nodes were processed, there's a cycle
  630. if sorted.size() != graph.size():
  631. log_console("[color=#9c2828][b]Error: Thread not valid[/b][/color]", true)
  632. log_console("Threads cannot contain loops.", true)
  633. return
  634. $ProgressWindow/ProgressBar.value = progress_step
  635. # Step 4: Start processing audio
  636. var batch_lines = [] # Holds all batch file commands
  637. var intermediate_files = [] # Files to delete later
  638. var breakfiles = [] #breakfiles to delete later
  639. # Dictionary to keep track of each node's output file
  640. var output_files = {}
  641. var process_count = 0
  642. # Start with the original input file
  643. var starting_infile = Global.infile
  644. #If trim is enabled trim input audio
  645. if Global.trim_infile == true:
  646. if process_cancelled:
  647. $ProgressWindow/ProgressLabel.text = "Thread Stopped"
  648. log_console("[b]Thread Stopped[/b]", true)
  649. return
  650. else:
  651. $ProgressWindow/ProgressLabel.text = "Trimming input audio"
  652. await run_command(cdpprogs_location + "/sfedit", ["cut", "1", starting_infile, "%s_trimmed.wav" % Global.outfile, str(Global.infile_start), str(Global.infile_stop)])
  653. starting_infile = Global.outfile + "_trimmed.wav"
  654. # Mark trimmed file for cleanup if needed
  655. if delete_intermediate_outputs:
  656. intermediate_files.append(Global.outfile + "_trimmed.wav")
  657. $ProgressWindow/ProgressBar.value += progress_step
  658. var current_infile = starting_infile
  659. # Iterate over the processing nodes in topological order
  660. for node_name in sorted:
  661. var node = all_nodes[node_name]
  662. if process_cancelled:
  663. $ProgressWindow/ProgressLabel.text = "Thread Stopped"
  664. log_console("[b]Thread Stopped[/b]", true)
  665. break
  666. else:
  667. $ProgressWindow/ProgressLabel.text = "Running process: " + node.get_title()
  668. # Find upstream nodes connected to the current node
  669. var inputs = reverse_graph[node_name]
  670. var input_files = []
  671. for input_node in inputs:
  672. input_files.append(output_files[input_node])
  673. # Merge inputs if this node has more than one input
  674. if input_files.size() > 1:
  675. # Prepare final merge output file name
  676. var runmerge = await merge_many_files(process_count, input_files)
  677. var merge_output = runmerge[0]
  678. var converted_files = runmerge[1]
  679. # Track the output and intermediate files
  680. current_infile = merge_output
  681. if delete_intermediate_outputs:
  682. intermediate_files.append(merge_output)
  683. for f in converted_files:
  684. intermediate_files.append(f)
  685. # If only one input, use that
  686. elif input_files.size() == 1:
  687. current_infile = input_files[0]
  688. ## If no input, use the original input file
  689. else:
  690. current_infile = starting_infile
  691. # Build the command for the current node's audio processing
  692. var slider_data = _get_slider_values_ordered(node)
  693. if node.get_slot_type_right(0) == 1: #detect if process outputs pvoc data
  694. if typeof(current_infile) == TYPE_ARRAY:
  695. #check if infile is an array meaning that the last pvoc process was run in dual mono mode
  696. # Process left and right seperately
  697. var pvoc_stereo_files = []
  698. for infile in current_infile:
  699. var makeprocess = await make_process(node, process_count, infile, slider_data)
  700. # run the command
  701. await run_command(makeprocess[0], makeprocess[3])
  702. await get_tree().process_frame
  703. var output_file = makeprocess[1]
  704. pvoc_stereo_files.append(output_file)
  705. # Mark file for cleanup if needed
  706. if delete_intermediate_outputs:
  707. for file in makeprocess[2]:
  708. breakfiles.append(file)
  709. intermediate_files.append(output_file)
  710. process_count += 1
  711. output_files[node_name] = pvoc_stereo_files
  712. else:
  713. var input_stereo = await is_stereo(current_infile)
  714. if input_stereo == true:
  715. #audio file is stereo and needs to be split for pvoc processing
  716. var pvoc_stereo_files = []
  717. ##Split stereo to c1/c2
  718. await run_command(cdpprogs_location + "/housekeep",["chans", "2", current_infile])
  719. # Process left and right seperately
  720. for channel in ["c1", "c2"]:
  721. var dual_mono_file = current_infile.get_basename() + "_%s.wav" % channel
  722. var makeprocess = await make_process(node, process_count, dual_mono_file, slider_data)
  723. # run the command
  724. await run_command(makeprocess[0], makeprocess[3])
  725. await get_tree().process_frame
  726. var output_file = makeprocess[1]
  727. pvoc_stereo_files.append(output_file)
  728. # Mark file for cleanup if needed
  729. if delete_intermediate_outputs:
  730. for file in makeprocess[2]:
  731. breakfiles.append(file)
  732. intermediate_files.append(output_file)
  733. #Delete c1 and c2 because they can be in the wrong folder and if the same infile is used more than once
  734. #with this stereo process CDP will throw errors in the console even though its fine
  735. if is_windows:
  736. dual_mono_file = dual_mono_file.replace("/", "\\")
  737. await run_command(delete_cmd, [dual_mono_file])
  738. process_count += 1
  739. # Store output file path for this node
  740. output_files[node_name] = pvoc_stereo_files
  741. else:
  742. #input file is mono run through process
  743. var makeprocess = await make_process(node, process_count, current_infile, slider_data)
  744. # run the command
  745. await run_command(makeprocess[0], makeprocess[3])
  746. await get_tree().process_frame
  747. var output_file = makeprocess[1]
  748. # Store output file path for this node
  749. output_files[node_name] = output_file
  750. # Mark file for cleanup if needed
  751. if delete_intermediate_outputs:
  752. for file in makeprocess[2]:
  753. breakfiles.append(file)
  754. intermediate_files.append(output_file)
  755. # Increase the process step count
  756. process_count += 1
  757. else:
  758. #Process outputs audio
  759. #check if this is the last pvoc process in a stereo processing chain
  760. if node.get_meta("command") == "pvoc_synth" and typeof(current_infile) == TYPE_ARRAY:
  761. #check if infile is an array meaning that the last pvoc process was run in dual mono mode
  762. # Process left and right seperately
  763. var pvoc_stereo_files = []
  764. for infile in current_infile:
  765. var makeprocess = await make_process(node, process_count, infile, slider_data)
  766. # run the command
  767. await run_command(makeprocess[0], makeprocess[3])
  768. await get_tree().process_frame
  769. var output_file = makeprocess[1]
  770. pvoc_stereo_files.append(output_file)
  771. # Mark file for cleanup if needed
  772. if delete_intermediate_outputs:
  773. for file in makeprocess[2]:
  774. breakfiles.append(file)
  775. intermediate_files.append(output_file)
  776. process_count += 1
  777. #interleave left and right
  778. var output_file = Global.outfile.get_basename() + str(process_count) + "_interleaved.wav"
  779. await run_command(cdpprogs_location + "/submix", ["interleave", pvoc_stereo_files[0], pvoc_stereo_files[1], output_file])
  780. # Store output file path for this node
  781. output_files[node_name] = output_file
  782. # Mark file for cleanup if needed
  783. if delete_intermediate_outputs:
  784. intermediate_files.append(output_file)
  785. else:
  786. #Detect if input file is mono or stereo
  787. var input_stereo = await is_stereo(current_infile)
  788. if input_stereo == true:
  789. if node.get_meta("stereo_input") == true: #audio file is stereo and process is stereo, run file through process
  790. var makeprocess = await make_process(node, process_count, current_infile, slider_data)
  791. # run the command
  792. await run_command(makeprocess[0], makeprocess[3])
  793. await get_tree().process_frame
  794. var output_file = makeprocess[1]
  795. # Store output file path for this node
  796. output_files[node_name] = output_file
  797. # Mark file for cleanup if needed
  798. if delete_intermediate_outputs:
  799. for file in makeprocess[2]:
  800. breakfiles.append(file)
  801. intermediate_files.append(output_file)
  802. else: #audio file is stereo and process is mono, split stereo, process and recombine
  803. ##Split stereo to c1/c2
  804. await run_command(cdpprogs_location + "/housekeep",["chans", "2", current_infile])
  805. # Process left and right seperately
  806. var dual_mono_output = []
  807. for channel in ["c1", "c2"]:
  808. var dual_mono_file = current_infile.get_basename() + "_%s.wav" % channel
  809. var makeprocess = await make_process(node, process_count, dual_mono_file, slider_data)
  810. # run the command
  811. await run_command(makeprocess[0], makeprocess[3])
  812. await get_tree().process_frame
  813. var output_file = makeprocess[1]
  814. dual_mono_output.append(output_file)
  815. # Mark file for cleanup if needed
  816. if delete_intermediate_outputs:
  817. for file in makeprocess[2]:
  818. breakfiles.append(file)
  819. intermediate_files.append(output_file)
  820. #Delete c1 and c2 because they can be in the wrong folder and if the same infile is used more than once
  821. #with this stereo process CDP will throw errors in the console even though its fine
  822. if is_windows:
  823. dual_mono_file = dual_mono_file.replace("/", "\\")
  824. await run_command(delete_cmd, [dual_mono_file])
  825. process_count += 1
  826. var output_file = Global.outfile.get_basename() + str(process_count) + "_interleaved.wav"
  827. await run_command(cdpprogs_location + "/submix", ["interleave", dual_mono_output[0], dual_mono_output[1], output_file])
  828. # Store output file path for this node
  829. output_files[node_name] = output_file
  830. # Mark file for cleanup if needed
  831. if delete_intermediate_outputs:
  832. intermediate_files.append(output_file)
  833. else: #audio file is mono, run through the process
  834. var makeprocess = await make_process(node, process_count, current_infile, slider_data)
  835. # run the command
  836. await run_command(makeprocess[0], makeprocess[3])
  837. await get_tree().process_frame
  838. var output_file = makeprocess[1]
  839. # Store output file path for this node
  840. output_files[node_name] = output_file
  841. # Mark file for cleanup if needed
  842. if delete_intermediate_outputs:
  843. for file in makeprocess[2]:
  844. breakfiles.append(file)
  845. intermediate_files.append(output_file)
  846. # Increase the process step count
  847. process_count += 1
  848. $ProgressWindow/ProgressBar.value += progress_step
  849. # FINAL OUTPUT STAGE
  850. # Collect all nodes that are connected to the outputfile node
  851. if process_cancelled:
  852. $ProgressWindow/ProgressLabel.text = "Thread Stopped"
  853. log_console("[b]Thread Stopped[/b]", true)
  854. return
  855. else:
  856. $ProgressWindow/ProgressLabel.text = "Finalising output"
  857. var output_inputs := []
  858. for conn in connections:
  859. var to_node = str(conn["to_node"])
  860. if all_nodes.has(to_node) and all_nodes[to_node].get_meta("command") == "outputfile":
  861. output_inputs.append(str(conn["from_node"]))
  862. # List to hold the final output files to be merged (if needed)
  863. var final_outputs := []
  864. for node_name in output_inputs:
  865. if output_files.has(node_name):
  866. final_outputs.append(output_files[node_name])
  867. # If multiple outputs go to the outputfile node, merge them
  868. if final_outputs.size() > 1:
  869. var runmerge = await merge_many_files(process_count, final_outputs)
  870. final_output_dir = runmerge[0]
  871. var converted_files = runmerge[1]
  872. if delete_intermediate_outputs:
  873. for f in converted_files:
  874. intermediate_files.append(f)
  875. # Only one output, no merge needed
  876. elif final_outputs.size() == 1:
  877. var single_output = final_outputs[0]
  878. final_output_dir = single_output
  879. intermediate_files.erase(single_output)
  880. $ProgressWindow/ProgressBar.value += progress_step
  881. # CLEANUP: Delete intermediate files after processing and rename final output
  882. if process_cancelled:
  883. $ProgressWindow/ProgressLabel.text = "Thread Stopped"
  884. log_console("[b]Thread Stopped[/b]", true)
  885. return
  886. else:
  887. log_console("Cleaning up intermediate files.", true)
  888. $ProgressWindow/ProgressLabel.text = "Cleaning up"
  889. for file_path in intermediate_files:
  890. # Adjust file path format for Windows if needed
  891. var fixed_path = file_path
  892. if is_windows:
  893. fixed_path = fixed_path.replace("/", "\\")
  894. await run_command(delete_cmd, [fixed_path])
  895. await get_tree().process_frame
  896. #delete break files
  897. for file_path in breakfiles:
  898. # Adjust file path format for Windows if needed
  899. var fixed_path = file_path
  900. if is_windows:
  901. fixed_path = fixed_path.replace("/", "\\")
  902. await run_command(delete_cmd, [fixed_path])
  903. await get_tree().process_frame
  904. var final_filename = "%s.wav" % Global.outfile
  905. var final_output_dir_fixed_path = final_output_dir
  906. if is_windows:
  907. final_output_dir_fixed_path = final_output_dir_fixed_path.replace("/", "\\")
  908. await run_command(rename_cmd, [final_output_dir_fixed_path, final_filename.get_file()])
  909. else:
  910. await run_command(rename_cmd, [final_output_dir_fixed_path, "%s.wav" % Global.outfile])
  911. final_output_dir = Global.outfile + ".wav"
  912. output_audio_player.play_outfile(final_output_dir)
  913. outfile = final_output_dir
  914. $ProgressWindow/ProgressBar.value = 100.0
  915. var interface_settings = ConfigHandler.load_interface_settings() #checks if close console is enabled and closes console on a success
  916. $ProgressWindow.hide()
  917. if interface_settings.auto_close_console and process_successful == true:
  918. $Console.hide()
  919. func is_stereo(file: String) -> bool:
  920. var output = await run_command(cdpprogs_location + "/sfprops", ["-c", file])
  921. output = int(output.strip_edges()) #convert output from cmd to clean int
  922. if output == 1:
  923. return false
  924. elif output == 2:
  925. return true
  926. elif output == 1026: #ignore pvoc .ana files
  927. return false
  928. else:
  929. log_console("[color=#9c2828]Error: Only mono and stereo files are supported[/color]", true)
  930. return false
  931. func merge_many_files(process_count: int, input_files: Array) -> Array:
  932. var merge_output = "%s_merge_%d.wav" % [Global.outfile.get_basename(), process_count]
  933. var converted_files := [] # Track any mono->stereo converted files
  934. var inputs_to_merge := [] # Files to be used in the final merge
  935. var mono_files := []
  936. var stereo_files := []
  937. # STEP 1: Check each file's channel count
  938. for f in input_files:
  939. var stereo = await is_stereo(f)
  940. if stereo == false:
  941. mono_files.append(f)
  942. elif stereo == true:
  943. stereo_files.append(f)
  944. # STEP 2: Convert mono to stereo if there is a mix
  945. if mono_files.size() > 0 and stereo_files.size() > 0:
  946. for mono_file in mono_files:
  947. var stereo_file = "%s_stereo.wav" % mono_file.get_basename()
  948. await run_command(cdpprogs_location + "/submix", ["interleave", mono_file, mono_file, stereo_file])
  949. if process_successful == false:
  950. log_console("Failed to interleave mono file: %s" % mono_file, true)
  951. else:
  952. converted_files.append(stereo_file)
  953. inputs_to_merge.append(stereo_file)
  954. # Add existing stereo files
  955. inputs_to_merge += stereo_files
  956. else:
  957. # All mono or all stereo — use input_files directly
  958. inputs_to_merge = input_files.duplicate()
  959. # STEP 3: Merge all input files (converted or original)
  960. var quoted_inputs := []
  961. for f in inputs_to_merge:
  962. quoted_inputs.append(f)
  963. quoted_inputs.insert(0, "mergemany")
  964. quoted_inputs.append(merge_output)
  965. await run_command(cdpprogs_location + "/submix", quoted_inputs)
  966. if process_successful == false:
  967. log_console("Failed to to merge files to" + merge_output, true)
  968. return [merge_output, converted_files]
  969. func _get_slider_values_ordered(node: Node) -> Array:
  970. var results := []
  971. for child in node.get_children():
  972. if child is Range:
  973. var flag = child.get_meta("flag") if child.has_meta("flag") else ""
  974. var time
  975. var brk_data = []
  976. var min_slider = child.min_value
  977. var max_slider = child.max_value
  978. if child.has_meta("time"):
  979. time = child.get_meta("time")
  980. else:
  981. time = false
  982. if child.has_meta("brk_data"):
  983. brk_data = child.get_meta("brk_data")
  984. results.append([flag, child.value, time, brk_data, min_slider, max_slider])
  985. elif child.get_child_count() > 0:
  986. var nested := _get_slider_values_ordered(child)
  987. results.append_array(nested)
  988. return results
  989. func make_process(node: Node, process_count: int, current_infile: String, slider_data: Array) -> Array:
  990. # Determine output extension: .wav or .ana based on the node's slot type
  991. var extension = ".wav" if node.get_slot_type_right(0) == 0 else ".ana"
  992. # Construct output filename for this step
  993. var output_file = "%s_%d%s" % [Global.outfile.get_basename(), process_count, extension]
  994. # Get the command name from metadata or default to node name
  995. var command_name = str(node.get_meta("command"))
  996. #command_name = command_name.replace("_", " ")
  997. command_name = command_name.split("_", true, 1)
  998. print(command_name)
  999. var command = "%s/%s" %[cdpprogs_location, command_name[0]]
  1000. print(command)
  1001. var args = command_name[1].split("_", true, 1)
  1002. print(args)
  1003. args.append(current_infile)
  1004. args.append(output_file)
  1005. print(args)
  1006. # Start building the command line windows
  1007. var line = "%s/%s \"%s\" \"%s\" " % [cdpprogs_location, command_name, current_infile, output_file]
  1008. #mac
  1009. var cleanup = []
  1010. # Append parameter values from the sliders, include flags if present
  1011. var slider_count = 0
  1012. for entry in slider_data:
  1013. var flag = entry[0]
  1014. var value = entry[1]
  1015. var time = entry[2] #checks if slider is a time percentage slider
  1016. var brk_data = entry[3]
  1017. var min_slider = entry[4]
  1018. var max_slider = entry[5]
  1019. if brk_data.size() > 0: #if breakpoint data is present on slider
  1020. #Sort all points by time
  1021. var sorted_brk_data = []
  1022. sorted_brk_data = brk_data.duplicate()
  1023. sorted_brk_data.sort_custom(sort_points)
  1024. var calculated_brk = []
  1025. #get length of input file in seconds
  1026. var infile_length = await run_command(cdpprogs_location + "/sfprops", ["-d", current_infile])
  1027. infile_length = float(infile_length.strip_edges())
  1028. #scale values from automation window to the right length for file and correct slider values
  1029. #need to check how time is handled in all files that accept it, zigzag is x = outfile position, y = infile position
  1030. #if time == true:
  1031. #for point in sorted_brk_data:
  1032. #var new_x = infile_length * (point.x / 700) #time
  1033. #var new_y = infile_length * (remap(point.y, 255, 0, min_slider, max_slider) / 100) #slider value scaled as a percentage of infile time
  1034. #calculated_brk.append(Vector2(new_x, new_y))
  1035. #else:
  1036. for i in range(sorted_brk_data.size()):
  1037. var point = sorted_brk_data[i]
  1038. var new_x = infile_length * (point.x / 700) #time
  1039. if i == sorted_brk_data.size() - 1: #check if this is last automation point
  1040. new_x = infile_length + 0.1 # force last point's x to infile_length + 100ms to make sure the file is defo over
  1041. var new_y = remap(point.y, 255, 0, min_slider, max_slider) #slider value
  1042. calculated_brk.append(Vector2(new_x, new_y))
  1043. #make text file
  1044. var brk_file_path = output_file.get_basename() + "_" + str(slider_count) + ".txt"
  1045. write_breakfile(calculated_brk, brk_file_path)
  1046. #append text file in place of value
  1047. line += ("\"%s\" " % brk_file_path)
  1048. args.append(brk_file_path)
  1049. cleanup.append(brk_file_path)
  1050. else:
  1051. if time == true:
  1052. var infile_length = await run_command(cdpprogs_location + "/sfprops", ["-d", current_infile])
  1053. infile_length = float(infile_length.strip_edges())
  1054. value = infile_length * (value / 100) #calculate percentage time of the input file
  1055. line += ("%s%.2f " % [flag, value]) if flag.begins_with("-") else ("%.2f " % value)
  1056. args.append(("%s%.2f " % [flag, value]) if flag.begins_with("-") else ("%.2f " % value))
  1057. slider_count += 1
  1058. return [command, output_file, cleanup, args]
  1059. #return [line.strip_edges(), output_file, cleanup]
  1060. func sort_points(a, b):
  1061. return a.x < b.x
  1062. func write_breakfile(points: Array, path: String):
  1063. var file = FileAccess.open(path, FileAccess.WRITE)
  1064. if file:
  1065. for point in points:
  1066. var line = str(point.x) + " " + str(point.y) + "\n"
  1067. file.store_string(line)
  1068. file.close()
  1069. else:
  1070. print("Failed to open file for writing.")
  1071. func run_command(command: String, args: Array) -> String:
  1072. var is_windows = OS.get_name() == "Windows"
  1073. console_output.append_text(command + " " + " ".join(args) + "\n")
  1074. console_output.scroll_to_line(console_output.get_line_count() - 1)
  1075. await get_tree().process_frame
  1076. if is_windows:
  1077. #exit_code = OS.execute("cmd.exe", ["/C", command], output, true, false)
  1078. args.insert(0, command)
  1079. args.insert(0, "/C")
  1080. process_info = OS.execute_with_pipe("cmd.exe", args, false)
  1081. else:
  1082. process_info = OS.execute_with_pipe(command, args, false)
  1083. # Check if the process was successfully started
  1084. if !process_info.has("pid"):
  1085. print("Failed to start process.")
  1086. return ""
  1087. process_running = true
  1088. # Start monitoring the process output and status
  1089. return await monitor_process(process_info["pid"], process_info["stdio"], process_info["stderr"])
  1090. func monitor_process(pid: int, stdout: FileAccess, stderr: FileAccess) -> String:
  1091. var output := ""
  1092. while OS.is_process_running(pid):
  1093. await get_tree().process_frame
  1094. while stdout.get_position() < stdout.get_length():
  1095. var line = stdout.get_line()
  1096. output += line
  1097. console_output.append_text(line + "\n")
  1098. console_output.scroll_to_line(console_output.get_line_count() - 1)
  1099. while stderr.get_position() < stderr.get_length():
  1100. var line = stderr.get_line()
  1101. output += line
  1102. console_output.append_text(line + "\n")
  1103. console_output.scroll_to_line(console_output.get_line_count() - 1)
  1104. var exit_code = OS.get_process_exit_code(pid)
  1105. if exit_code == 0:
  1106. if output.contains("ERROR:"): #checks if CDP reported an error but passed exit code 0 anyway
  1107. console_output.append_text("[color=#9c2828][b]Processes failed[/b][/color]\n\n")
  1108. console_output.scroll_to_line(console_output.get_line_count() - 1)
  1109. process_successful = false
  1110. if process_cancelled == false:
  1111. $ProgressWindow.hide()
  1112. if !$Console.visible:
  1113. $Console.popup_centered()
  1114. else:
  1115. console_output.append_text("[color=#638382]Processes ran successfully[/color]\n\n")
  1116. console_output.scroll_to_line(console_output.get_line_count() - 1)
  1117. else:
  1118. console_output.append_text("[color=#9c2828][b]Processes failed with exit code: %d[/b][/color]\n" % exit_code + "\n")
  1119. console_output.scroll_to_line(console_output.get_line_count() - 1)
  1120. process_successful = false
  1121. if process_cancelled == false:
  1122. $ProgressWindow.hide()
  1123. if !$Console.visible:
  1124. $Console.popup_centered()
  1125. if output.contains("as an internal or external command"): #check for cdprogs location error on windows
  1126. console_output.append_text("[color=#9c2828][b]Please make sure your cdprogs folder is set to the correct location in the Settings menu. The default location is C:\\CDPR8\\_cdp\\_cdprogs[/b][/color]\n\n")
  1127. console_output.scroll_to_line(console_output.get_line_count() - 1)
  1128. if output.contains("command not found"): #check for cdprogs location error on unix systems
  1129. console_output.append_text("[color=#9c2828][b]Please make sure your cdprogs folder is set to the correct location in the Settings menu. The default location is ~/cdpr8/_cdp/_cdprogs[/b][/color]\n\n")
  1130. console_output.scroll_to_line(console_output.get_line_count() - 1)
  1131. process_running = false
  1132. return output
  1133. func _on_kill_process_button_down() -> void:
  1134. if process_running and process_info.has("pid"):
  1135. $ProgressWindow.hide()
  1136. # Terminate the process by PID
  1137. OS.kill(process_info["pid"])
  1138. process_running = false
  1139. print("Process cancelled.")
  1140. process_cancelled = true
  1141. func path_exists_through_all_nodes() -> bool:
  1142. var all_nodes = {}
  1143. var graph = {}
  1144. var input_node_name = ""
  1145. var output_node_name = ""
  1146. # Gather all relevant nodes
  1147. for child in graph_edit.get_children():
  1148. if child is GraphNode:
  1149. var name = str(child.name)
  1150. all_nodes[name] = child
  1151. var command = child.get_meta("command")
  1152. if command == "inputfile":
  1153. input_node_name = name
  1154. elif command == "outputfile":
  1155. output_node_name = name
  1156. # Skip utility nodes, include others
  1157. if command in ["inputfile", "outputfile"] or not child.has_meta("utility"):
  1158. graph[name] = []
  1159. # Ensure both input and output were found
  1160. if input_node_name == "" or output_node_name == "":
  1161. print("Input or output node not found!")
  1162. return false
  1163. # Add edges to graph from the connection list
  1164. var connection_list = graph_edit.get_connection_list()
  1165. for conn in connection_list:
  1166. var from = str(conn["from_node"])
  1167. var to = str(conn["to_node"])
  1168. if graph.has(from):
  1169. graph[from].append(to)
  1170. # BFS traversal to check path and depth
  1171. var visited = {}
  1172. var queue = [ { "node": input_node_name, "depth": 0 } ]
  1173. var has_intermediate = false
  1174. while queue.size() > 0:
  1175. var current = queue.pop_front()
  1176. var current_node = current["node"]
  1177. var depth = current["depth"]
  1178. if current_node in visited:
  1179. continue
  1180. visited[current_node] = true
  1181. if current_node == output_node_name and depth >= 2:
  1182. has_intermediate = true
  1183. if graph.has(current_node):
  1184. for neighbor in graph[current_node]:
  1185. queue.append({ "node": neighbor, "depth": depth + 1 })
  1186. return has_intermediate
  1187. func _toggle_delete(toggled_on: bool):
  1188. delete_intermediate_outputs = toggled_on
  1189. print(toggled_on)
  1190. func _on_console_close_requested() -> void:
  1191. $Console.hide()
  1192. func log_console(text: String, update: bool) -> void:
  1193. console_output.append_text(text + "\n \n")
  1194. console_output.scroll_to_line(console_output.get_line_count() - 1)
  1195. if update == true:
  1196. await get_tree().process_frame # Optional: ensure UI updates
  1197. func _on_console_open_folder_button_down() -> void:
  1198. $Console.hide()
  1199. OS.shell_open(Global.outfile.get_base_dir())
  1200. func _on_ok_button_2_button_down() -> void:
  1201. $NoInputPopup.hide()
  1202. func _on_ok_button_3_button_down() -> void:
  1203. $MultipleConnectionsPopup.hide()
  1204. func _on_settings_button_index_pressed(index: int) -> void:
  1205. var interface_settings = ConfigHandler.load_interface_settings()
  1206. match index:
  1207. 0:
  1208. $Settings.popup_centered()
  1209. 1:
  1210. $AudioSettings.popup_centered()
  1211. 2:
  1212. if $Console.is_visible():
  1213. $Console.hide()
  1214. await get_tree().process_frame # Wait a frame to allow hide to complete
  1215. $Console.popup_centered()
  1216. else:
  1217. $Console.popup_centered()
  1218. func _on_file_button_index_pressed(index: int) -> void:
  1219. match index:
  1220. 0:
  1221. if changesmade == true:
  1222. savestate = "newfile"
  1223. $SaveChangesPopup.popup_centered()
  1224. else:
  1225. new_patch()
  1226. currentfile = "none" #reset current file to none for save tracking
  1227. 1:
  1228. if currentfile == "none":
  1229. savestate = "saveas"
  1230. $SaveDialog.popup_centered()
  1231. else:
  1232. save_graph_edit(currentfile)
  1233. 2:
  1234. savestate = "saveas"
  1235. $SaveDialog.popup_centered()
  1236. 3:
  1237. if changesmade == true:
  1238. savestate = "load"
  1239. $SaveChangesPopup.popup_centered()
  1240. else:
  1241. $LoadDialog.popup_centered()
  1242. func save_graph_edit(path: String):
  1243. var file = FileAccess.open(path, FileAccess.WRITE)
  1244. if file == null:
  1245. print("Failed to open file for saving")
  1246. return
  1247. var node_data_list = []
  1248. var connection_data_list = []
  1249. var node_id_map = {} # Map node name to numeric ID
  1250. var node_id = 1
  1251. # Assign each node a unique numeric ID and gather node data
  1252. for node in graph_edit.get_children():
  1253. if node is GraphNode:
  1254. node_id_map[node.name] = node_id
  1255. var offset = node.position_offset
  1256. var node_data = {
  1257. "id": node_id,
  1258. "name": node.name,
  1259. "command": node.get_meta("command"),
  1260. "offset": { "x": offset.x, "y": offset.y },
  1261. "slider_values": {},
  1262. "notes": {}
  1263. }
  1264. # Save slider values and metadata
  1265. for child in node.find_children("*", "Slider", true, false):
  1266. var relative_path = node.get_path_to(child)
  1267. var path_str = str(relative_path)
  1268. node_data["slider_values"][path_str] = {
  1269. "value": child.value,
  1270. "editable": child.editable,
  1271. "meta": {}
  1272. }
  1273. for key in child.get_meta_list():
  1274. node_data["slider_values"][path_str]["meta"][str(key)] = child.get_meta(key)
  1275. # Save notes from CodeEdit children
  1276. for child in node.find_children("*", "CodeEdit", true, false):
  1277. node_data["notes"][child.name] = child.text
  1278. node_data_list.append(node_data)
  1279. node_id += 1
  1280. # Save connections using node IDs instead of names
  1281. for conn in graph_edit.get_connection_list():
  1282. # Map from_node and to_node names to IDs
  1283. var from_id = node_id_map.get(conn["from_node"], null)
  1284. var to_id = node_id_map.get(conn["to_node"], null)
  1285. if from_id != null and to_id != null:
  1286. connection_data_list.append({
  1287. "from_node_id": from_id,
  1288. "from_port": conn["from_port"],
  1289. "to_node_id": to_id,
  1290. "to_port": conn["to_port"]
  1291. })
  1292. else:
  1293. print("Warning: Connection references unknown node(s). Skipping connection.")
  1294. var graph_data = {
  1295. "nodes": node_data_list,
  1296. "connections": connection_data_list
  1297. }
  1298. var json = JSON.new()
  1299. var json_string = json.stringify(graph_data, "\t")
  1300. file.store_string(json_string)
  1301. file.close()
  1302. print("Graph saved.")
  1303. changesmade = false
  1304. get_window().title = "SoundThread - " + path.get_file().trim_suffix(".thd")
  1305. func load_graph_edit(path: String):
  1306. var file = FileAccess.open(path, FileAccess.READ)
  1307. if file == null:
  1308. print("Failed to open file for loading")
  1309. return
  1310. var json_text = file.get_as_text()
  1311. file.close()
  1312. var json = JSON.new()
  1313. if json.parse(json_text) != OK:
  1314. print("Error parsing JSON")
  1315. return
  1316. var graph_data = json.get_data()
  1317. graph_edit.clear_connections()
  1318. # Remove all existing GraphNodes from graph_edit
  1319. for node in graph_edit.get_children():
  1320. if node is GraphNode:
  1321. node.queue_free()
  1322. await get_tree().process_frame # Ensure nodes are freed before adding new ones
  1323. var id_to_node = {} # Map node IDs to new node instances
  1324. # Recreate nodes and store them by ID
  1325. for node_data in graph_data["nodes"]:
  1326. var command_name = node_data.get("command", "")
  1327. var template = Nodes.get_node_or_null(command_name)
  1328. if not template:
  1329. print("Template not found for command:", command_name)
  1330. continue
  1331. var new_node: GraphNode = template.duplicate()
  1332. new_node.name = node_data["name"]
  1333. new_node.position_offset = Vector2(node_data["offset"]["x"], node_data["offset"]["y"])
  1334. new_node.set_meta("command", command_name)
  1335. graph_edit.add_child(new_node)
  1336. new_node.connect("open_help", Callable(self, "show_help_for_node"))
  1337. _register_node_movement() # Track node movement changes
  1338. id_to_node[node_data["id"]] = new_node
  1339. # Restore sliders
  1340. for slider_path_str in node_data["slider_values"]:
  1341. var slider = new_node.get_node_or_null(slider_path_str)
  1342. if slider and (slider is HSlider or slider is VSlider):
  1343. var slider_info = node_data["slider_values"][slider_path_str]
  1344. if typeof(slider_info) == TYPE_DICTIONARY:
  1345. slider.value = slider_info.get("value", slider.value)
  1346. if slider_info.has("editable"):
  1347. slider.editable = slider_info["editable"]
  1348. if slider_info.has("meta"):
  1349. for key in slider_info["meta"]:
  1350. var value = slider_info["meta"][key]
  1351. if key == "brk_data" and typeof(value) == TYPE_ARRAY:
  1352. var new_array: Array = []
  1353. for item in value:
  1354. if typeof(item) == TYPE_STRING:
  1355. var numbers: PackedStringArray = item.strip_edges().trim_prefix("(").trim_suffix(")").split(",")
  1356. if numbers.size() == 2:
  1357. var x = float(numbers[0])
  1358. var y = float(numbers[1])
  1359. new_array.append(Vector2(x, y))
  1360. value = new_array
  1361. slider.set_meta(key, value)
  1362. else:
  1363. slider.value = slider_info
  1364. # Restore notes
  1365. for codeedit_name in node_data["notes"]:
  1366. var codeedit = new_node.find_child(codeedit_name, true, false)
  1367. if codeedit and (codeedit is CodeEdit):
  1368. codeedit.text = node_data["notes"][codeedit_name]
  1369. _register_inputs_in_node(new_node) # Track slider changes
  1370. # Recreate connections by looking up nodes by ID
  1371. for conn in graph_data["connections"]:
  1372. var from_node = id_to_node.get(conn["from_node_id"], null)
  1373. var to_node = id_to_node.get(conn["to_node_id"], null)
  1374. if from_node != null and to_node != null:
  1375. graph_edit.connect_node(
  1376. from_node.name, conn["from_port"],
  1377. to_node.name, conn["to_port"]
  1378. )
  1379. else:
  1380. print("Warning: Connection references unknown node ID(s). Skipping connection.")
  1381. link_output()
  1382. print("Graph loaded.")
  1383. get_window().title = "SoundThread - " + path.get_file().trim_suffix(".thd")
  1384. func _on_save_dialog_file_selected(path: String) -> void:
  1385. save_graph_edit(path) #save file
  1386. #check what the user was trying to do before save and do that action
  1387. if savestate == "newfile":
  1388. new_patch()
  1389. currentfile = "none" #reset current file to none for save tracking
  1390. elif savestate == "load":
  1391. $LoadDialog.popup_centered()
  1392. elif savestate == "helpfile":
  1393. currentfile = "none" #reset current file to none for save tracking so user cant save over help file
  1394. load_graph_edit(helpfile)
  1395. elif savestate == "quit":
  1396. await get_tree().create_timer(0.25).timeout #little pause so that it feels like it actually saved even though it did
  1397. get_tree().quit()
  1398. savestate = "none" #reset save state, not really needed but feels good
  1399. func _on_load_dialog_file_selected(path: String) -> void:
  1400. currentfile = path #tracking path here only means "save" only saves patches the user has loaded rather than overwriting help files
  1401. load_graph_edit(path)
  1402. func _on_help_button_index_pressed(index: int) -> void:
  1403. match index:
  1404. 0:
  1405. pass
  1406. 1:
  1407. if changesmade == true:
  1408. savestate = "helpfile"
  1409. helpfile = "res://examples/getting_started.thd"
  1410. $SaveChangesPopup.popup_centered()
  1411. else:
  1412. currentfile = "none" #reset current file to none for save tracking so user cant save over help file
  1413. load_graph_edit("res://examples/getting_started.thd")
  1414. 2:
  1415. if changesmade == true:
  1416. savestate = "helpfile"
  1417. helpfile = "res://examples/navigating.thd"
  1418. $SaveChangesPopup.popup_centered()
  1419. else:
  1420. currentfile = "none" #reset current file to none for save tracking so user cant save over help file
  1421. load_graph_edit("res://examples/navigating.thd")
  1422. 3:
  1423. if changesmade == true:
  1424. savestate = "helpfile"
  1425. helpfile = "res://examples/building_a_thread.thd"
  1426. $SaveChangesPopup.popup_centered()
  1427. else:
  1428. currentfile = "none" #reset current file to none for save tracking so user cant save over help file
  1429. load_graph_edit("res://examples/building_a_thread.thd")
  1430. 4:
  1431. if changesmade == true:
  1432. savestate = "helpfile"
  1433. helpfile = "res://examples/frequency_domain.thd"
  1434. $SaveChangesPopup.popup_centered()
  1435. else:
  1436. currentfile = "none" #reset current file to none for save tracking so user cant save over help file
  1437. load_graph_edit("res://examples/frequency_domain.thd")
  1438. 5:
  1439. if changesmade == true:
  1440. savestate = "helpfile"
  1441. helpfile = "res://examples/automation.thd"
  1442. $SaveChangesPopup.popup_centered()
  1443. else:
  1444. currentfile = "none" #reset current file to none for save tracking so user cant save over help file
  1445. load_graph_edit("res://examples/automation.thd")
  1446. 6:
  1447. if changesmade == true:
  1448. savestate = "helpfile"
  1449. helpfile = "res://examples/trimming.thd"
  1450. $SaveChangesPopup.popup_centered()
  1451. else:
  1452. currentfile = "none" #reset current file to none for save tracking so user cant save over help file
  1453. load_graph_edit("res://examples/trimming.thd")
  1454. 7:
  1455. pass
  1456. 8:
  1457. if changesmade == true:
  1458. savestate = "helpfile"
  1459. helpfile = "res://examples/wetdry.thd"
  1460. $SaveChangesPopup.popup_centered()
  1461. else:
  1462. currentfile = "none" #reset current file to none for save tracking so user cant save over help file
  1463. load_graph_edit("res://examples/wetdry.thd")
  1464. 9:
  1465. if changesmade == true:
  1466. savestate = "helpfile"
  1467. helpfile = "res://examples/resonant_filters.thd"
  1468. $SaveChangesPopup.popup_centered()
  1469. else:
  1470. currentfile = "none" #reset current file to none for save tracking so user cant save over help file
  1471. load_graph_edit("res://examples/resonant_filters.thd")
  1472. 10:
  1473. pass
  1474. 11:
  1475. OS.shell_open("https://www.composersdesktop.com/docs/html/ccdpndex.htm")
  1476. 12:
  1477. OS.shell_open("https://github.com/j-p-higgins/SoundThread/issues")
  1478. func _recycle_outfile():
  1479. if outfile != "no file":
  1480. input_audio_player.recycle_outfile(outfile)
  1481. func _on_save_changes_button_down() -> void:
  1482. $SaveChangesPopup.hide()
  1483. if currentfile == "none":
  1484. $SaveDialog.show()
  1485. else:
  1486. save_graph_edit(currentfile)
  1487. if savestate == "newfile":
  1488. new_patch()
  1489. currentfile = "none" #reset current file to none for save tracking
  1490. elif savestate == "load":
  1491. $LoadDialog.popup_centered()
  1492. elif savestate == "helpfile":
  1493. currentfile = "none" #reset current file to none for save tracking so user cant save over help file
  1494. load_graph_edit(helpfile)
  1495. elif savestate == "quit":
  1496. await get_tree().create_timer(0.25).timeout #little pause so that it feels like it actually saved even though it did
  1497. get_tree().quit()
  1498. savestate = "none"
  1499. func _on_dont_save_changes_button_down() -> void:
  1500. $SaveChangesPopup.hide()
  1501. if savestate == "newfile":
  1502. new_patch()
  1503. currentfile = "none" #reset current file to none for save tracking
  1504. elif savestate == "load":
  1505. $LoadDialog.popup_centered()
  1506. elif savestate == "helpfile":
  1507. currentfile = "none" #reset current file to none for save tracking so user cant save over help file
  1508. load_graph_edit(helpfile)
  1509. elif savestate == "quit":
  1510. get_tree().quit()
  1511. savestate = "none"
  1512. func _notification(what):
  1513. if what == NOTIFICATION_WM_CLOSE_REQUEST:
  1514. _on_kill_process_button_down()
  1515. $Console.hide()
  1516. if changesmade == true:
  1517. savestate = "quit"
  1518. $SaveChangesPopup.popup_centered()
  1519. #$HelpWindow.hide()
  1520. else:
  1521. get_tree().quit() # default behavior
  1522. func _open_output_folder():
  1523. if lastoutputfolder != "none":
  1524. OS.shell_open(lastoutputfolder)
  1525. func _on_rich_text_label_meta_clicked(meta: Variant) -> void:
  1526. print(str(meta))
  1527. OS.shell_open(str(meta))
  1528. func _on_graph_edit_popup_request(at_position: Vector2) -> void:
  1529. effect_position = graph_edit.get_local_mouse_position()
  1530. #get the mouse position in screen coordinates
  1531. var mouse_screen_pos = DisplayServer.mouse_get_position()
  1532. #get the window position in screen coordinates
  1533. var window_screen_pos = get_window().position
  1534. #get the window size relative to its scaling for retina displays
  1535. var window_size = get_window().size * DisplayServer.screen_get_scale()
  1536. #calculate the xy position of the mouse clamped to the size of the window and menu so it doesn't go off the screen
  1537. var clamped_x = clamp(mouse_screen_pos.x, window_screen_pos.x, window_screen_pos.x + window_size.x - $SearchMenu.size.x)
  1538. var clamped_y = clamp(mouse_screen_pos.y, window_screen_pos.y, window_screen_pos.y + window_size.y - (420 * DisplayServer.screen_get_scale()))
  1539. #position and show the menu
  1540. $SearchMenu.position = Vector2(clamped_x, clamped_y)
  1541. $SearchMenu.popup()
  1542. func _on_audio_settings_close_requested() -> void:
  1543. $AudioSettings.hide()
  1544. func _on_open_audio_settings_button_down() -> void:
  1545. $AudioDevicePopup.hide()
  1546. $AudioSettings.popup_centered()
  1547. func _on_audio_device_popup_close_requested() -> void:
  1548. $AudioDevicePopup.hide()
  1549. func _on_mainmenu_close_requested() -> void:
  1550. #closes menu if click is anywhere other than the menu as it is a window with popup set to true
  1551. $mainmenu.hide()
  1552. func open_explore():
  1553. effect_position = graph_edit.get_local_mouse_position()
  1554. #get the mouse position in screen coordinates
  1555. var mouse_screen_pos = DisplayServer.mouse_get_position()
  1556. #get the window position in screen coordinates
  1557. var window_screen_pos = get_window().position
  1558. #get the window size relative to its scaling for retina displays
  1559. var window_size = get_window().size * DisplayServer.screen_get_scale()
  1560. #get the size of the popup menu
  1561. var popup_size = $mainmenu.size
  1562. #calculate the xy position of the mouse clamped to the size of the window and menu so it doesn't go off the screen
  1563. var clamped_x = clamp(mouse_screen_pos.x, window_screen_pos.x, window_screen_pos.x + window_size.x - popup_size.x)
  1564. var clamped_y = clamp(mouse_screen_pos.y, window_screen_pos.y, window_screen_pos.y + window_size.y - popup_size.y)
  1565. #position and show the menu
  1566. $mainmenu.position = Vector2(clamped_x, clamped_y)
  1567. $mainmenu.popup()
  1568. func change_console_settings(toggled: bool):
  1569. $Console.always_on_top = toggled