control.gd 71 KB

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