Преглед на файлове

Merge pull request #183 from j-p-higgins/multiple-inputs

Multiple inputs
Jonathan Higgins преди 3 месеца
родител
ревизия
3f5e1abbae

+ 113 - 88
addons/audio_preview/voice_preview_generator.gd

@@ -28,108 +28,133 @@ var must_abort := false
 func generate_preview(stream: AudioStreamWAV, image_max_width: int = 500):
 	if not stream:
 		return
-	
 	if stream.format == AudioStreamWAV.FORMAT_IMA_ADPCM:
 		return
-	
 	if image_max_width <= 0:
 		return
-	
+
+	# If already working, abort previous job first.
 	if is_working:
 		must_abort = true
 		while is_working:
 			await get_tree().process_frame
-	
-	is_working = true
-	
-	var data = stream.data
-	var data_size = data.size()
-	var is_16bit = (stream.format == AudioStreamWAV.FORMAT_16_BITS)
-	var is_stereo = stream.stereo
-	
-	var sample_interval = 1
-	if stream.mix_rate > SAMPLING_RATE:
-		sample_interval = int(round(stream.mix_rate / SAMPLING_RATE))
-	if is_16bit:
-		sample_interval *= 2
-	if is_stereo:
-		sample_interval *= 2
-	
-	var reduced_data = PackedByteArray()
-	var reduced_data_size = int(floor(data_size / float(sample_interval)))
-	reduced_data.resize(reduced_data_size)
-	
-	var sample_in_i := 1 if is_16bit else 0
-	var sample_out_i := 0
-	while (sample_in_i < data_size) and (sample_out_i < reduced_data_size):
-		reduced_data[sample_out_i] = data[sample_in_i]
-		sample_in_i += sample_interval
-		sample_out_i += 1
-		
-		if must_abort:
-			is_working = false
-			must_abort = false
-			return
-	
-	image_compression = ceil(reduced_data_size / float(image_max_width))
-	var img_width = floor(reduced_data_size / image_compression)
-	var img = Image.create(img_width, IMAGE_HEIGHT, true, Image.FORMAT_RGBA8)
-	img.fill(background_color)
-	
-	var sample_i = 0
-	var img_x = 0
-	var final_sample_i = reduced_data_size - image_compression
-	
-	while sample_i < final_sample_i:
-		var min_val_left := 128
-		var max_val_left := 128
-		var min_val_right := 128
-		var max_val_right := 128
-		
-		for block_i in range(image_compression):
-			if sample_i >= reduced_data_size:
-				break
-			var sample_val = reduced_data[sample_i] + 128
-			if sample_val >= 256:
-				sample_val -= 256
 
-			if is_stereo:
-				if (sample_i % 2) == 0:
-					if sample_val < min_val_left:
-						min_val_left = sample_val
-					if sample_val > max_val_left:
-						max_val_left = sample_val
-				else:
-					if sample_val < min_val_right:
-						min_val_right = sample_val
-					if sample_val > max_val_right:
-						max_val_right = sample_val
-			else:
-				if sample_val < min_val_left:
-					min_val_left = sample_val
-				if sample_val > max_val_left:
-					max_val_left = sample_val
+	is_working = true
 
-			sample_i += 1
+	var data: PackedByteArray = stream.data
+	var data_size: int = data.size()
+	var is_stereo: bool = stream.stereo
+	var is_16bit: bool = (stream.format == AudioStreamWAV.FORMAT_16_BITS)
+	# Assume non-ADPCM, non-16-bit is 24-bit PCM for our preview purposes
+	var bytes_per_sample: int = 2 if is_16bit else 3
+	var channels: int = 2 if is_stereo else 1
+	var frame_bytes: int = bytes_per_sample * channels
+
+	if frame_bytes <= 0:
+		is_working = false
+		return
 
-			if is_stereo:
-				# Draw top (left)
-				_draw_half_waveform(img, img_x, min_val_left, max_val_left, 0, IMAGE_HEIGHT / 2)
-				# Draw bottom (right)
-				_draw_half_waveform(img, img_x, min_val_right, max_val_right, IMAGE_HEIGHT / 2, IMAGE_HEIGHT / 2)
-			else:
-				# Mono: use full height
-				_draw_half_waveform(img, img_x, min_val_left, max_val_left, 0, IMAGE_HEIGHT)
+	var total_frames: int = int(floor(data_size / frame_bytes))
+	if total_frames <= 0:
+		is_working = false
+		return
 
-		img_x += 1
+	# Decide how many frames contribute to each pixel column
+	var frames_per_pixel: int = int(ceil(total_frames / float(image_max_width)))
+	var img_width: int = int(floor(total_frames / float(frames_per_pixel)))
+	if img_width <= 0:
+		img_width = 1
 
-		if must_abort:
-			is_working = false
-			must_abort = false
-			return
+	var img := Image.create(img_width, IMAGE_HEIGHT, true, Image.FORMAT_RGBA8)
+	img.fill(background_color)
 
-		if (sample_i % 100) == 0:
-			var progress = sample_i / final_sample_i
+	# For speed, sample only a subset of frames in each pixel column.
+	# Tweak this to trade accuracy for speed (e.g., 8 or 12 samples per column).
+	var samples_per_pixel_target: int = 4
+	var inner_step: int = max(1, int(floor(frames_per_pixel / float(samples_per_pixel_target))))
+
+	var x: int = 0
+	var frames_processed: int = 0
+
+	while x < img_width:
+		var start_frame: int = x * frames_per_pixel
+		var end_frame: int = min(start_frame + frames_per_pixel, total_frames)
+
+		var min_l := 128
+		var max_l := 128
+		var min_r := 128
+		var max_r := 128
+
+		var f: int = start_frame
+		while f < end_frame:
+			var base := f * frame_bytes
+
+			# ---- Decode LEFT -> fast 8-bit signed, then map to 0..255
+			var l_u8: int
+			if is_16bit:
+				var l_lo := data[base]
+				var l_hi := data[base + 1]
+				var l16 := (l_hi << 8) | l_lo
+				if (l_hi & 0x80) != 0:
+					l16 -= 0x10000
+				l_u8 = ((l16 >> 8) + 128)
+			else:
+				var l_b0 := data[base]
+				var l_b1 := data[base + 1]
+				var l_b2 := data[base + 2]
+				var l24 := (l_b2 << 16) | (l_b1 << 8) | l_b0
+				if (l_b2 & 0x80) != 0:
+					l24 -= 0x1000000
+				l_u8 = ((l24 >> 16) + 128)
+			l_u8 = clamp(l_u8, 0, 255)
+
+			# ---- Decode RIGHT (or mirror left for mono)
+			var r_u8: int = l_u8
+			if is_stereo:
+				var ro := bytes_per_sample # right channel offset inside frame
+				if is_16bit:
+					var r_lo := data[base + ro]
+					var r_hi := data[base + ro + 1]
+					var r16 := (r_hi << 8) | r_lo
+					if (r_hi & 0x80) != 0:
+						r16 -= 0x10000
+					r_u8 = ((r16 >> 8) + 128)
+				else:
+					var r_b0 := data[base + ro]
+					var r_b1 := data[base + ro + 1]
+					var r_b2 := data[base + ro + 2]
+					var r24 := (r_b2 << 16) | (r_b1 << 8) | r_b0
+					if (r_b2 & 0x80) != 0:
+						r24 -= 0x1000000
+					r_u8 = ((r24 >> 16) + 128)
+				r_u8 = clamp(r_u8, 0, 255)
+
+			# Update min/max per channel
+			if l_u8 < min_l: min_l = l_u8
+			if l_u8 > max_l: max_l = l_u8
+			if r_u8 < min_r: min_r = r_u8
+			if r_u8 > max_r: max_r = r_u8
+
+			f += inner_step
+			frames_processed += inner_step
+
+			if must_abort:
+				is_working = false
+				must_abort = false
+				return
+
+		# Draw column
+		if is_stereo:
+			_draw_half_waveform(img, x, min_l, max_l, 0, IMAGE_HEIGHT / 2)
+			_draw_half_waveform(img, x, min_r, max_r, IMAGE_HEIGHT / 2, IMAGE_HEIGHT / 2)
+		else:
+			_draw_half_waveform(img, x, min_l, max_l, 0, IMAGE_HEIGHT)
+
+		x += 1
+
+		# Lightweight progress update
+		if (x % 16) == 0:
+			var progress := float(x) / float(img_width)
 			emit_signal("generation_progress", progress)
 			await get_tree().process_frame
 

+ 2 - 0
dev_tools/json_editor/json_editor.gd

@@ -86,6 +86,7 @@ func edit_node(key: String):
 		$HBoxContainer/VBoxContainer2/HBoxContainer5/shortdescription.text = info.get("short_description", "")
 		$HBoxContainer/VBoxContainer2/HBoxContainer7/longdescription.text = info.get("description", "")
 		$HBoxContainer/VBoxContainer2/HBoxContainer6/stereo.button_pressed = bool(info.get("stereo"))
+		$HBoxContainer/VBoxContainer2/HBoxContainer8/outputisstereo.button_pressed = bool(info.get("outputisstereo"))
 		$HBoxContainer/VBoxContainer2/HBoxContainer9/inputtype.text = str(info.get("inputtype", ""))
 		$HBoxContainer/VBoxContainer2/HBoxContainer11/outputtype.text = str(info.get("outputtype", ""))
 		
@@ -205,6 +206,7 @@ func save_node(is_new: bool) -> void:
 		"short_description": $HBoxContainer/VBoxContainer2/HBoxContainer5/shortdescription.text,
 		"description": $HBoxContainer/VBoxContainer2/HBoxContainer7/longdescription.text,
 		"stereo": $HBoxContainer/VBoxContainer2/HBoxContainer6/stereo.button_pressed,
+		"outputisstereo": $HBoxContainer/VBoxContainer2/HBoxContainer8/outputisstereo.button_pressed,
 		"inputtype": $HBoxContainer/VBoxContainer2/HBoxContainer9/inputtype.text,
 		"outputtype": $HBoxContainer/VBoxContainer2/HBoxContainer11/outputtype.text,
 		"parameters": {}

+ 11 - 0
dev_tools/json_editor/json_editor.tscn

@@ -180,6 +180,17 @@ text = "Stereo:"
 [node name="stereo" type="CheckBox" parent="HBoxContainer/VBoxContainer2/HBoxContainer6"]
 layout_mode = 2
 
+[node name="HBoxContainer8" type="HBoxContainer" parent="HBoxContainer/VBoxContainer2"]
+layout_mode = 2
+
+[node name="Label" type="Label" parent="HBoxContainer/VBoxContainer2/HBoxContainer8"]
+custom_minimum_size = Vector2(250, 0)
+layout_mode = 2
+text = "Output is always stereo:"
+
+[node name="outputisstereo" type="CheckBox" parent="HBoxContainer/VBoxContainer2/HBoxContainer8"]
+layout_mode = 2
+
 [node name="HBoxContainer9" type="HBoxContainer" parent="HBoxContainer/VBoxContainer2"]
 layout_mode = 2
 

Файловите разлики са ограничени, защото са твърде много
+ 4974 - 742
dev_tools/json_editor/process_help_backup.json


+ 3 - 7
project.godot

@@ -14,6 +14,8 @@ config/name="SoundThread"
 config/description="Node based interface for the Composers Desktop Project"
 run/main_scene="uid://bcs87y7ptx3ke"
 config/features=PackedStringArray("4.4", "Forward Plus")
+run/max_fps=30
+run/low_processor_mode=true
 boot_splash/bg_color=Color(0.160784, 0.25098, 0.247059, 1)
 boot_splash/image="uid://hprx8lbgmd1n"
 config/icon="uid://5648pc85dknw"
@@ -51,12 +53,6 @@ undo={
 "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"command_or_control_autoremap":true,"alt_pressed":false,"shift_pressed":false,"pressed":false,"keycode":0,"physical_keycode":90,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
 ]
 }
-redo={
-"deadzone": 0.2,
-"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":true,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":89,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
-, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"command_or_control_autoremap":true,"alt_pressed":false,"shift_pressed":true,"pressed":false,"keycode":0,"physical_keycode":90,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
-]
-}
 save={
 "deadzone": 0.2,
 "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"command_or_control_autoremap":true,"alt_pressed":false,"shift_pressed":false,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
@@ -84,7 +80,7 @@ new={
 }
 save_as={
 "deadzone": 0.2,
-"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"command_or_control_autoremap":true,"alt_pressed":false,"shift_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194325,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"command_or_control_autoremap":true,"alt_pressed":false,"shift_pressed":true,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":83,"location":0,"echo":false,"script":null)
 ]
 }
 

+ 49 - 0
scenes/Nodes/addremoveinlets.gd

@@ -0,0 +1,49 @@
+extends Control
+
+signal add_inlet
+signal remove_inlet
+var minimum_inlet_count
+var maximum_inlet_count
+var current_inlet_count
+
+func _ready() -> void:
+	minimum_inlet_count = get_meta("min")
+	maximum_inlet_count = get_meta("max")
+	current_inlet_count = get_meta("default")
+	check_buttons()
+
+
+func _on_add_inlet_button_button_down() -> void:
+	add_inlet.emit()
+	current_inlet_count += 1
+	set_meta("inlet_count", current_inlet_count)
+	check_buttons()
+
+
+func _on_remove_inlet_button_button_down() -> void:
+	remove_inlet.emit()
+	current_inlet_count -= 1
+	set_meta("inlet_count", current_inlet_count)
+	check_buttons()
+
+func check_buttons():
+	if current_inlet_count == maximum_inlet_count:
+		$VBoxContainer/HBoxContainer/AddInletButton.disabled = true
+	else:
+		$VBoxContainer/HBoxContainer/AddInletButton.disabled = false
+		
+	if current_inlet_count == minimum_inlet_count:
+		$VBoxContainer/HBoxContainer/RemoveInletButton.disabled = true
+	else:
+		$VBoxContainer/HBoxContainer/RemoveInletButton.disabled = false
+		
+func restore_inlets():
+	print("current meta for inlet count is " + str(get_meta("inlet_count")))
+	var restore_inlet_count = get_meta("inlet_count")
+	while restore_inlet_count > current_inlet_count:
+		_on_add_inlet_button_button_down()
+	
+	while restore_inlet_count < current_inlet_count:
+		_on_remove_inlet_button_button_down()
+		
+	print("current inlet count is " + str(current_inlet_count))

+ 1 - 0
scenes/Nodes/addremoveinlets.gd.uid

@@ -0,0 +1 @@
+uid://ddy27yplob87l

+ 43 - 0
scenes/Nodes/addremoveinlets.tscn

@@ -0,0 +1,43 @@
+[gd_scene load_steps=2 format=3 uid="uid://cho3fvni7kadi"]
+
+[ext_resource type="Script" uid="uid://ddy27yplob87l" path="res://scenes/Nodes/addremoveinlets.gd" id="1_1pgec"]
+
+[node name="Control" type="Control"]
+custom_minimum_size = Vector2(270, 57)
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_right = -1010.0
+offset_bottom = -663.0
+grow_horizontal = 2
+grow_vertical = 2
+script = ExtResource("1_1pgec")
+
+[node name="VBoxContainer" type="VBoxContainer" parent="."]
+layout_mode = 0
+offset_right = 40.0
+offset_bottom = 40.0
+
+[node name="Label" type="Label" parent="VBoxContainer"]
+layout_mode = 2
+text = "Add/Remove Inlets"
+
+[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"]
+custom_minimum_size = Vector2(270, 0)
+layout_mode = 2
+
+[node name="AddInletButton" type="Button" parent="VBoxContainer/HBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+tooltip_text = "Add new inlet to node"
+text = "+"
+
+[node name="RemoveInletButton" type="Button" parent="VBoxContainer/HBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+tooltip_text = "Remove last inlet from node"
+text = "-"
+
+[connection signal="button_down" from="VBoxContainer/HBoxContainer/AddInletButton" to="." method="_on_add_inlet_button_button_down"]
+[connection signal="button_down" from="VBoxContainer/HBoxContainer/RemoveInletButton" to="." method="_on_remove_inlet_button_button_down"]

+ 3 - 0
scenes/Nodes/audioplayer.gd

@@ -76,6 +76,9 @@ func _on_file_selected(path: String):
 	else:
 		$EndLabel.text = "00:00.00"
 	set_meta("inputfile", path)
+	set_meta("sample_rate", audio_player.stream.get_mix_rate())
+	set_meta("stereo", audio_player.stream.is_stereo())
+	set_meta("upsampled", false)
 	reset_playback()
 	
 	#output signal that the input has loaded and it is safe to continue with running thread

+ 46 - 0
scenes/Nodes/node_logic.gd

@@ -2,6 +2,7 @@ extends GraphNode
 
 @export var min_gap: float = 0.5  # editable value in inspector for the minimum gap between min and max
 signal open_help
+signal inlet_removed
 
 func _ready() -> void:
 	var sliders := _get_all_hsliders(self) #finds all sliders
@@ -16,6 +17,13 @@ func _ready() -> void:
 	btn.tooltip_text = "Open help for " + self.title
 	btn.connect("pressed", Callable(self, "_open_help")) #pass key (process name) when button is pressed
 	titlebar.add_child(btn)
+	await get_tree().process_frame
+	#reset_size()
+	
+	if self.has_node("addremoveinlets"):
+		var addremove = self.get_node("addremoveinlets")
+		addremove.add_inlet.connect(add_inlet_to_node)
+		addremove.remove_inlet.connect(remove_inlet_from_node)
 	
 
 func _get_all_hsliders(node: Node) -> Array:
@@ -60,3 +68,41 @@ func _on_slider_value_changed(value: float, changed_slider: HSlider) -> void:
 
 func _open_help():
 	open_help.emit(self.get_meta("command"), self.title)
+
+func add_inlet_to_node():
+	#called when the + button is pressed on an addremoveinlets node in the graphnode
+	var inlet_count = self.get_input_port_count()
+	var child_count = self.get_child_count()
+	
+	#check if the number of children is less than the new inlet count
+	if child_count < inlet_count + 1:
+		#if so add a new control node for the inlet to connect to
+		var control = Control.new()
+		control.custom_minimum_size.y = 57
+		#give it this meta so it can be found and removed later if needed
+		control.set_meta("dummynode", true)
+		add_child(control)
+		#move the ui for adding/removing inlets to the bottom of the node
+		move_child(get_node("addremoveinlets"), get_child_count() - 1)
+	
+	#add the inlet using the same parameters as the first inlet
+	set_slot(inlet_count, true, get_input_port_type(0), get_input_port_color(0), false, 0, get_input_port_color(0))
+	
+func remove_inlet_from_node():
+	var inlet_count = self.get_input_port_count()
+	var child_count = self.get_child_count()
+	
+	#emit a signal to the graphedit script to remove any connections to this inlet
+	inlet_removed.emit(self.get_name(), inlet_count - 1)
+	#remove the inlet note inlet idx starts at 0 hence inlet_count -1
+	set_slot(inlet_count - 1, false, get_input_port_type(0), get_input_port_color(0), false, 0, get_input_port_color(0))
+	
+	#check if a dummy control node has been added to make this inlet -2 because bottom node is the ui for adding removing inlets and idx starts at 0
+	if get_child(child_count - 2).has_meta("dummynode"):
+		#remove the dummy node
+		get_child(child_count - 2).queue_free()
+		#wait a frame for it to be removed
+		await get_tree().process_frame
+		#update the size of the graphnode to shrink to fit smaller ui
+		update_minimum_size()
+		size.y = get_combined_minimum_size().y

+ 44 - 0
scenes/Nodes/nodes.tscn

@@ -802,6 +802,50 @@ size_flags_horizontal = 3
 text = "+"
 metadata/calc = "+"
 
+[node name="GraphNode" type="GraphNode" parent="."]
+layout_mode = 0
+offset_left = 1273.0
+offset_top = 422.0
+offset_right = 1615.0
+offset_bottom = 509.0
+slot/0/left_enabled = true
+slot/0/left_type = 0
+slot/0/left_color = Color(1, 1, 1, 1)
+slot/0/left_icon = null
+slot/0/right_enabled = false
+slot/0/right_type = 0
+slot/0/right_color = Color(1, 1, 1, 1)
+slot/0/right_icon = null
+slot/0/draw_stylebox = true
+slot/1/left_enabled = true
+slot/1/left_type = 0
+slot/1/left_color = Color(1, 1, 1, 1)
+slot/1/left_icon = null
+slot/1/right_enabled = false
+slot/1/right_type = 0
+slot/1/right_color = Color(1, 1, 1, 1)
+slot/1/right_icon = null
+slot/1/draw_stylebox = true
+slot/2/left_enabled = false
+slot/2/left_type = 0
+slot/2/left_color = Color(1, 1, 1, 1)
+slot/2/left_icon = null
+slot/2/right_enabled = false
+slot/2/right_type = 0
+slot/2/right_color = Color(1, 1, 1, 1)
+slot/2/right_icon = null
+slot/2/draw_stylebox = true
+
+[node name="Control" type="Control" parent="GraphNode"]
+custom_minimum_size = Vector2(0, 20)
+layout_mode = 2
+
+[node name="Control2" type="Control" parent="GraphNode"]
+layout_mode = 2
+
+[node name="HSlider" type="HSlider" parent="GraphNode"]
+layout_mode = 2
+
 [connection signal="text_submitted" from="outputfile/FileNameField" to="outputfile" method="_on_file_name_field_text_submitted"]
 [connection signal="toggled" from="outputfile/DeleteIntermediateFilesToggle" to="outputfile" method="_on_delete_intermediate_files_toggle_toggled"]
 [connection signal="toggled" from="outputfile/ReuseFolderToggle" to="outputfile" method="_on_reuse_folder_toggle_toggled"]

Файловите разлики са ограничени, защото са твърде много
+ 551 - 168
scenes/main/process_help.json


+ 74 - 31
scenes/main/scripts/graph_edit.gd

@@ -9,6 +9,7 @@ var copied_nodes_data = [] #stores node data on ctrl+c
 var copied_connections = [] #stores all connections on ctrl+c
 var node_data = {} #stores json with all nodes in it
 var valueslider = preload("res://scenes/Nodes/valueslider.tscn") #slider scene for use in nodes
+var addremoveinlets = preload("res://scenes/Nodes/addremoveinlets.tscn") #add remove inlets scene for use in nodes
 var node_logic = preload("res://scenes/Nodes/node_logic.gd") #load the script logic
 
 
@@ -71,44 +72,18 @@ func _make_node(command: String, skip_undo_redo := false) -> GraphNode:
 			
 			#get node properties
 			var stereo = node_info.get("stereo", false)
+			var outputisstereo = node_info.get("outputisstereo", false) #used to identify the few processes that always output in stereo making the thread need to be stereo
 			var inputs = JSON.parse_string(node_info.get("inputtype", ""))
 			var outputs = JSON.parse_string(node_info.get("outputtype", ""))
 			var portcount = max(inputs.size(), outputs.size())
 			var parameters = node_info.get("parameters", {})
 			
 			var graphnode = GraphNode.new()
-			for i in range(portcount):
-				#add a number of control nodes equal to whatever is higher input or output ports
-				var control = Control.new()
-				graphnode.add_child(control)
-				
-				#check if input or output is enabled
-				var enable_input = i < inputs.size()
-				var enable_output = i < outputs.size()
-				
-				#get the colour of the port for time or pvoc ins/outs
-				var input_colour = Color("#ffffff90")
-				var output_colour = Color("#ffffff90")
-				
-				if enable_input:
-					if inputs[i] == 1:
-						input_colour = Color("#000000b0")
-				if enable_output:
-					if outputs[i] == 1:
-						output_colour = Color("#000000b0")
-				
-				#enable and set ports
-				if enable_input == true and enable_output == false:
-					graphnode.set_slot(i, true, inputs[i], input_colour, false, 0, output_colour)
-				elif enable_input == false and enable_output == true:
-					graphnode.set_slot(i, false, 0, input_colour, true, outputs[i], output_colour)
-				elif enable_input == true and enable_output == true:
-					graphnode.set_slot(i, true, inputs[i], input_colour, true, outputs[i], output_colour)
-				else:
-					pass
+			
 			#set meta data for the process
 			graphnode.set_meta("command", command)
 			graphnode.set_meta("stereo_input", stereo)
+			graphnode.set_meta("output_is_stereo", outputisstereo)
 			if inputs.size() == 0 and outputs.size() > 0:
 				graphnode.set_meta("input", true)
 			else:
@@ -122,9 +97,16 @@ func _make_node(command: String, skip_undo_redo := false) -> GraphNode:
 			graphnode.set_position_offset((control_script.effect_position + graph_edit.scroll_offset) / graph_edit.zoom)
 			graphnode.name = command
 			
+			#add one small control node to the top of the node to aline first inlet to top
+			var first_inlet = Control.new()
+			graphnode.add_child(first_inlet)
+			
 			if parameters.is_empty():
 				var noparams = Label.new()
 				noparams.text = "No adjustable parameters"
+				noparams.custom_minimum_size.x = 270
+				noparams.custom_minimum_size.y = 57
+				noparams.vertical_alignment = 1
 				
 				graphnode.add_child(noparams)
 			else:
@@ -225,7 +207,11 @@ func _make_node(command: String, skip_undo_redo := false) -> GraphNode:
 						var optionbutton_tooltip  = param_data.get("paramdescription", "")
 						
 						#name optionbutton
-						optionbutton.name = optionbutton_label.replace(" ", "")
+						optionbutton.name = optionbutton_label.replace(" ", "").to_lower()
+						
+						#add meta flag if this is a sample rate selector for running thread sample rate checks
+						if optionbutton.name == "samplerate":
+							graphnode.set_meta("node_sets_sample_rate", true)
 						
 						#get optionbutton properties
 						var options = JSON.parse_string(param_data.get("step", ""))
@@ -246,20 +232,71 @@ func _make_node(command: String, skip_undo_redo := false) -> GraphNode:
 						#set flag meta
 						optionbutton.set_meta("flag", flag)
 						
-						#add margin size for ertical spacing
+						#add margin size for vertical spacing
 						margin.add_theme_constant_override("margin_bottom", 4)
 						
 						graphnode.add_child(label)
 						graphnode.add_child(optionbutton)
 						graphnode.add_child(margin)
+					elif param_data.get("uitype", "") == "addremoveinlets":
+						var addremove = addremoveinlets.instantiate()
+						addremove.name = "addremoveinlets"
+						
+						#get parameters
+						var min_inlets = param_data.get("minrange", 0)
+						var max_inlets = param_data.get("maxrange", 10)
+						var default_inlets = param_data.get("value", 1)
+						
+						#set meta
+						addremove.set_meta("min", min_inlets)
+						addremove.set_meta("max", max_inlets)
+						addremove.set_meta("default", default_inlets)
+						
+						graphnode.add_child(addremove)
 				
 				control_script.changesmade = true
 			
+			#add control nodes if number of child nodes is lower than the number of inlets or outlets
+			for i in range(portcount - graphnode.get_child_count()):
+				#add a number of control nodes equal to whatever is higher input or output ports
+				var control = Control.new()
+				control.custom_minimum_size.y = 57
+				graphnode.add_child(control)
+				if graphnode.has_node("addremoveinlets"):
+					graphnode.move_child(graphnode.get_node("addremoveinlets"), graphnode.get_child_count() - 1)
+			
+			#add ports
+			for i in range(portcount):
+				#check if input or output is enabled
+				var enable_input = i < inputs.size()
+				var enable_output = i < outputs.size()
+				
+				#get the colour of the port for time or pvoc ins/outs
+				var input_colour = Color("#ffffff90")
+				var output_colour = Color("#ffffff90")
+				
+				if enable_input:
+					if inputs[i] == 1:
+						input_colour = Color("#000000b0")
+				if enable_output:
+					if outputs[i] == 1:
+						output_colour = Color("#000000b0")
+				
+				#enable and set ports
+				if enable_input == true and enable_output == false:
+					graphnode.set_slot(i, true, inputs[i], input_colour, false, 0, output_colour)
+				elif enable_input == false and enable_output == true:
+					graphnode.set_slot(i, false, 0, input_colour, true, outputs[i], output_colour)
+				elif enable_input == true and enable_output == true:
+					graphnode.set_slot(i, true, inputs[i], input_colour, true, outputs[i], output_colour)
+				else:
+					pass
 			
 			graphnode.set_script(node_logic)
 			
 			add_child(graphnode, true)
 			graphnode.connect("open_help", open_help)
+			graphnode.connect("inlet_removed", Callable(self, "on_inlet_removed"))
 			_register_inputs_in_node(graphnode) #link sliders for changes tracking
 			_register_node_movement() #link nodes for tracking position changes for changes tracking
 			
@@ -548,3 +585,9 @@ func _on_paste_nodes_request() -> void:
 	control_script.simulate_mouse_click() #hacky fix to stop tooltips getting stuck
 	await get_tree().process_frame
 	graph_edit.paste_copied_nodes()
+
+func on_inlet_removed(node_name: StringName, port_index: int):
+	var connections = get_connection_list()
+	for conn in connections:
+		if conn.to_node == node_name and conn.to_port == port_index:
+			disconnect_node(conn.from_node, conn.from_port, conn.to_node, conn.to_port)

Файловите разлики са ограничени, защото са твърде много
+ 708 - 326
scenes/main/scripts/run_thread.gd


+ 15 - 0
scenes/main/scripts/save_load.gd

@@ -44,6 +44,7 @@ func save_graph_edit(path: String):
 				"command": node.get_meta("command"),
 				"offset": { "x": offset.x, "y": offset.y },
 				"slider_values": {},
+				"addremoveinlets":{},
 				"notes": {},
 				"checkbutton_states": {},
 				"optionbutton_values": {}
@@ -62,6 +63,11 @@ func save_graph_edit(path: String):
 				for key in child.get_meta_list():
 					node_data["slider_values"][path_str]["meta"][str(key)] = child.get_meta(key)
 				
+			#save add remove inlet meta data
+			if node.has_node("addremoveinlets"):
+				if node.get_node("addremoveinlets").has_meta("inlet_count"):
+					node_data["addremoveinlets"]["inlet_count"] = node.get_node("addremoveinlets").get_meta("inlet_count")
+					
 			# Save notes from CodeEdit children
 			for child in node.find_children("*", "CodeEdit", true, false):
 				node_data["notes"][child.name] = child.text
@@ -191,6 +197,15 @@ func load_graph_edit(path: String):
 				if optionbutton and (optionbutton is OptionButton):
 					optionbutton.selected = node_data["optionbutton_values"][optionbutton_name]
 
+		#restore dynamic inlets
+		if node_data.has("addremoveinlets") and new_node.has_node("addremoveinlets"):
+			print("restoring inlets")
+			var addremoveinlets = new_node.get_node("addremoveinlets")
+			addremoveinlets.set_meta("inlet_count", node_data["addremoveinlets"]["inlet_count"])
+			await get_tree().process_frame
+			addremoveinlets.restore_inlets()
+		
+		
 		register_input.call(new_node)
 
 	# Recreate connections

+ 2 - 0
scenes/menu/explore_menu.gd

@@ -57,6 +57,8 @@ func fill_menu():
 				container = $"Control/select_effect/Frequency Domain/Convert/MarginContainer/ScrollContainer/PVOCConvertContainer"
 			elif subcategory == "amplitude" or subcategory == "pitch":
 				container = $"Control/select_effect/Frequency Domain/Amplitude and Pitch/MarginContainer/ScrollContainer/PVOCAmplitudePitchContainer"
+			elif subcategory == "combine":
+				container = $"Control/select_effect/Frequency Domain/Combine/MarginContainer/ScrollContainer/PVOCCombineContainer"
 			elif subcategory == "formants":
 				container = $"Control/select_effect/Frequency Domain/Formants/MarginContainer/ScrollContainer/PVOCFormantsContainer"
 			elif subcategory == "time":

+ 39 - 6
scenes/menu/menu.tscn

@@ -31,9 +31,10 @@ offset_right = 325.0
 offset_bottom = 250.0
 grow_horizontal = 2
 grow_vertical = 2
-current_tab = 0
+current_tab = 1
 
 [node name="Time Domain" type="TabContainer" parent="Control/select_effect"]
+visible = false
 layout_mode = 2
 current_tab = 5
 metadata/_tab_index = 0
@@ -261,9 +262,8 @@ layout_mode = 2
 theme_override_constants/margin_bottom = 5
 
 [node name="Frequency Domain" type="TabContainer" parent="Control/select_effect"]
-visible = false
 layout_mode = 2
-current_tab = 4
+current_tab = 2
 metadata/_tab_index = 1
 
 [node name="Convert" type="VBoxContainer" parent="Control/select_effect/Frequency Domain"]
@@ -332,10 +332,42 @@ autowrap_mode = 3
 layout_mode = 2
 theme_override_constants/margin_bottom = 5
 
+[node name="Combine" type="VBoxContainer" parent="Control/select_effect/Frequency Domain"]
+layout_mode = 2
+metadata/_tab_index = 2
+
+[node name="MarginContainer" type="MarginContainer" parent="Control/select_effect/Frequency Domain/Combine"]
+layout_mode = 2
+theme_override_constants/margin_left = 15
+theme_override_constants/margin_top = 10
+theme_override_constants/margin_right = 5
+theme_override_constants/margin_bottom = 10
+
+[node name="ScrollContainer" type="ScrollContainer" parent="Control/select_effect/Frequency Domain/Combine/MarginContainer"]
+custom_minimum_size = Vector2(620, 425)
+layout_mode = 2
+size_flags_horizontal = 0
+horizontal_scroll_mode = 0
+
+[node name="PVOCCombineContainer" type="VBoxContainer" parent="Control/select_effect/Frequency Domain/Combine/MarginContainer/ScrollContainer"]
+custom_minimum_size = Vector2(620, 0)
+layout_mode = 2
+
+[node name="Label" type="Label" parent="Control/select_effect/Frequency Domain/Combine/MarginContainer/ScrollContainer/PVOCCombineContainer"]
+custom_minimum_size = Vector2(620, 0)
+layout_mode = 2
+size_flags_horizontal = 4
+text = "These processes take multiple inputs and combine them into one output. This is not mixing in the conventional sense, instead these processes can be used to morph between sounds or create hybrids of sounds."
+autowrap_mode = 3
+
+[node name="MarginContainer4" type="MarginContainer" parent="Control/select_effect/Frequency Domain/Combine/MarginContainer/ScrollContainer/PVOCCombineContainer"]
+layout_mode = 2
+theme_override_constants/margin_bottom = 5
+
 [node name="Formants" type="VBoxContainer" parent="Control/select_effect/Frequency Domain"]
 visible = false
 layout_mode = 2
-metadata/_tab_index = 2
+metadata/_tab_index = 3
 
 [node name="MarginContainer" type="MarginContainer" parent="Control/select_effect/Frequency Domain/Formants"]
 layout_mode = 2
@@ -368,7 +400,7 @@ theme_override_constants/margin_bottom = 5
 [node name="Time" type="VBoxContainer" parent="Control/select_effect/Frequency Domain"]
 visible = false
 layout_mode = 2
-metadata/_tab_index = 3
+metadata/_tab_index = 4
 
 [node name="MarginContainer" type="MarginContainer" parent="Control/select_effect/Frequency Domain/Time"]
 layout_mode = 2
@@ -399,8 +431,9 @@ layout_mode = 2
 theme_override_constants/margin_bottom = 5
 
 [node name="Spectrum" type="VBoxContainer" parent="Control/select_effect/Frequency Domain"]
+visible = false
 layout_mode = 2
-metadata/_tab_index = 4
+metadata/_tab_index = 5
 
 [node name="MarginContainer" type="MarginContainer" parent="Control/select_effect/Frequency Domain/Spectrum"]
 layout_mode = 2

+ 10 - 1
theme/main_theme.tres

@@ -1,4 +1,4 @@
-[gd_resource type="Theme" load_steps=68 format=3 uid="uid://cefwkdcoxihro"]
+[gd_resource type="Theme" load_steps=69 format=3 uid="uid://cefwkdcoxihro"]
 
 [ext_resource type="Texture2D" uid="uid://b4o8vm5o4uptk" path="res://theme/images/toggle_checked.png" id="1_cibxr"]
 [ext_resource type="Texture2D" uid="uid://d0dubcywvqtkw" path="res://theme/images/toggle_unchecked.png" id="2_adhqp"]
@@ -13,6 +13,14 @@
 [ext_resource type="Texture2D" uid="uid://bjg8l0k0ypi4f" path="res://theme/images/zoomout.png" id="8_4xa5k"]
 [ext_resource type="Texture2D" uid="uid://br24jo4nhi88d" path="res://theme/images/reset.png" id="9_fqy7u"]
 
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_41w5c"]
+content_margin_left = 4.0
+content_margin_top = 4.0
+content_margin_right = 4.0
+content_margin_bottom = 4.0
+bg_color = Color(0.1, 0.1, 0.1, 0.3)
+corner_detail = 5
+
 [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_470mv"]
 content_margin_left = 4.0
 content_margin_top = 4.0
@@ -561,6 +569,7 @@ expand_margin_right = 2.0
 expand_margin_bottom = 2.0
 
 [resource]
+Button/styles/disabled = SubResource("StyleBoxFlat_41w5c")
 Button/styles/focus = SubResource("StyleBoxFlat_470mv")
 Button/styles/hover = SubResource("StyleBoxFlat_466ly")
 Button/styles/normal = SubResource("StyleBoxFlat_52mx2")

Някои файлове не бяха показани, защото твърде много файлове са промени