control.gd 57 KB

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