From 1e7001b29198cbe8a3e2c157e59604a5cdff91d2 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Sun, 15 Feb 2026 05:34:28 -0500 Subject: [PATCH] Add parameters to audio nodes and rename Delay node to Echo --- daw-backend/src/audio/engine.rs | 6 +- daw-backend/src/audio/node_graph/graph.rs | 2 +- .../node_graph/nodes/{delay.rs => echo.rs} | 10 +- daw-backend/src/audio/node_graph/nodes/mod.rs | 4 +- daw-backend/src/audio/project.rs | 11 + .../src/panes/node_graph/graph_data.rs | 405 +++++++++--------- .../src/panes/node_graph/mod.rs | 58 +-- .../src/panes/node_graph/node_types.rs | 12 +- 8 files changed, 254 insertions(+), 254 deletions(-) rename daw-backend/src/audio/node_graph/nodes/{delay.rs => echo.rs} (98%) diff --git a/daw-backend/src/audio/engine.rs b/daw-backend/src/audio/engine.rs index f8666bc..b329493 100644 --- a/daw-backend/src/audio/engine.rs +++ b/daw-backend/src/audio/engine.rs @@ -501,6 +501,8 @@ impl Engine { .store(self.playhead, Ordering::Relaxed); // Stop all MIDI notes when seeking to prevent stuck notes self.project.stop_all_notes(); + // Reset all node graphs to clear effect buffers (echo, reverb, etc.) + self.project.reset_all_graphs(); // Notify disk reader to refill buffers from new position if let Some(ref mut dr) = self.disk_reader { dr.send(crate::audio::disk_reader::DiskReaderCommand::Seek { frame: frames }); @@ -1080,7 +1082,7 @@ impl Engine { "Splitter" => Box::new(SplitterNode::new("Splitter".to_string())), "Pan" => Box::new(PanNode::new("Pan".to_string())), "Quantizer" => Box::new(QuantizerNode::new("Quantizer".to_string())), - "Delay" => Box::new(DelayNode::new("Delay".to_string())), + "Echo" | "Delay" => Box::new(EchoNode::new("Echo".to_string())), "Distortion" => Box::new(DistortionNode::new("Distortion".to_string())), "Reverb" => Box::new(ReverbNode::new("Reverb".to_string())), "Chorus" => Box::new(ChorusNode::new("Chorus".to_string())), @@ -1165,7 +1167,7 @@ impl Engine { "Splitter" => Box::new(SplitterNode::new("Splitter".to_string())), "Pan" => Box::new(PanNode::new("Pan".to_string())), "Quantizer" => Box::new(QuantizerNode::new("Quantizer".to_string())), - "Delay" => Box::new(DelayNode::new("Delay".to_string())), + "Echo" | "Delay" => Box::new(EchoNode::new("Echo".to_string())), "Distortion" => Box::new(DistortionNode::new("Distortion".to_string())), "Reverb" => Box::new(ReverbNode::new("Reverb".to_string())), "Chorus" => Box::new(ChorusNode::new("Chorus".to_string())), diff --git a/daw-backend/src/audio/node_graph/graph.rs b/daw-backend/src/audio/node_graph/graph.rs index bd24e5c..5c65043 100644 --- a/daw-backend/src/audio/node_graph/graph.rs +++ b/daw-backend/src/audio/node_graph/graph.rs @@ -865,7 +865,7 @@ impl AudioGraph { "Splitter" => Box::new(SplitterNode::new("Splitter")), "Pan" => Box::new(PanNode::new("Pan")), "Quantizer" => Box::new(QuantizerNode::new("Quantizer")), - "Delay" => Box::new(DelayNode::new("Delay")), + "Echo" | "Delay" => Box::new(EchoNode::new("Echo")), "Distortion" => Box::new(DistortionNode::new("Distortion")), "Reverb" => Box::new(ReverbNode::new("Reverb")), "Chorus" => Box::new(ChorusNode::new("Chorus")), diff --git a/daw-backend/src/audio/node_graph/nodes/delay.rs b/daw-backend/src/audio/node_graph/nodes/echo.rs similarity index 98% rename from daw-backend/src/audio/node_graph/nodes/delay.rs rename to daw-backend/src/audio/node_graph/nodes/echo.rs index 0960618..f2ba870 100644 --- a/daw-backend/src/audio/node_graph/nodes/delay.rs +++ b/daw-backend/src/audio/node_graph/nodes/echo.rs @@ -7,8 +7,8 @@ const PARAM_WET_DRY: u32 = 2; const MAX_DELAY_SECONDS: f32 = 2.0; -/// Stereo delay node with feedback -pub struct DelayNode { +/// Stereo echo node with feedback +pub struct EchoNode { name: String, delay_time: f32, // seconds feedback: f32, // 0.0 to 0.95 @@ -26,7 +26,7 @@ pub struct DelayNode { parameters: Vec, } -impl DelayNode { +impl EchoNode { pub fn new(name: impl Into) -> Self { let name = name.into(); @@ -79,7 +79,7 @@ impl DelayNode { } } -impl AudioNode for DelayNode { +impl AudioNode for EchoNode { fn category(&self) -> NodeCategory { NodeCategory::Effect } @@ -185,7 +185,7 @@ impl AudioNode for DelayNode { } fn node_type(&self) -> &str { - "Delay" + "Echo" } fn name(&self) -> &str { diff --git a/daw-backend/src/audio/node_graph/nodes/mod.rs b/daw-backend/src/audio/node_graph/nodes/mod.rs index 9a94602..60abb2b 100644 --- a/daw-backend/src/audio/node_graph/nodes/mod.rs +++ b/daw-backend/src/audio/node_graph/nodes/mod.rs @@ -7,7 +7,7 @@ mod bpm_detector; mod chorus; mod compressor; mod constant; -mod delay; +mod echo; mod distortion; mod envelope_follower; mod eq; @@ -49,7 +49,7 @@ pub use bpm_detector::BpmDetectorNode; pub use chorus::ChorusNode; pub use compressor::CompressorNode; pub use constant::ConstantNode; -pub use delay::DelayNode; +pub use echo::EchoNode; pub use distortion::DistortionNode; pub use envelope_follower::EnvelopeFollowerNode; pub use eq::EQNode; diff --git a/daw-backend/src/audio/project.rs b/daw-backend/src/audio/project.rs index 68f5d74..107f646 100644 --- a/daw-backend/src/audio/project.rs +++ b/daw-backend/src/audio/project.rs @@ -506,6 +506,17 @@ impl Project { } } + /// Reset all node graphs (clears effect buffers on seek) + pub fn reset_all_graphs(&mut self) { + for track in self.tracks.values_mut() { + match track { + TrackNode::Audio(t) => t.effects_graph.reset(), + TrackNode::Midi(t) => t.instrument_graph.reset(), + TrackNode::Group(_) => {} + } + } + } + /// Process live MIDI input from all MIDI tracks (called even when not playing) pub fn process_live_midi(&mut self, output: &mut [f32], sample_rate: u32, channels: u32) { // Process all MIDI tracks to handle queued live input events diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/graph_data.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/graph_data.rs index 29e1549..2538337 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/graph_data.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/graph_data.rs @@ -33,7 +33,7 @@ pub enum NodeTemplate { // Effects Filter, Gain, - Delay, + Echo, Reverb, Chorus, Flanger, @@ -89,16 +89,70 @@ pub enum UserResponse {} impl UserResponseTrait for UserResponse {} +fn default_unit() -> &'static str { "" } + /// Value types for inline parameters #[derive(Clone, Debug, Serialize, Deserialize)] pub enum ValueType { - Float { value: f32 }, + Float { + value: f32, + #[serde(skip, default)] + min: f32, + #[serde(skip, default)] + max: f32, + #[serde(skip, default = "default_unit")] + unit: &'static str, + #[serde(skip)] + backend_param_id: Option, + #[serde(skip)] + enum_labels: Option<&'static [&'static str]>, + }, String { value: String }, } +impl ValueType { + /// Plain float value (for connection inputs, no parameter metadata) + pub fn float(value: f32) -> Self { + ValueType::Float { + value, + min: 0.0, + max: 0.0, + unit: "", + backend_param_id: None, + enum_labels: None, + } + } + + /// Float parameter with full metadata for inline editing + pub fn float_param( + value: f32, + min: f32, + max: f32, + unit: &'static str, + param_id: u32, + enum_labels: Option<&'static [&'static str]>, + ) -> Self { + ValueType::Float { + value, + min, + max, + unit, + backend_param_id: Some(param_id), + enum_labels, + } + } +} + impl Default for ValueType { fn default() -> Self { - ValueType::Float { value: 0.0 } + ValueType::Float { + value: 0.0, + min: 0.0, + max: 0.0, + unit: "", + backend_param_id: None, + enum_labels: None, + } } } @@ -145,7 +199,7 @@ impl NodeTemplateTrait for NodeTemplate { // Effects NodeTemplate::Filter => "Filter".into(), NodeTemplate::Gain => "Gain".into(), - NodeTemplate::Delay => "Delay".into(), + NodeTemplate::Echo => "Echo".into(), NodeTemplate::Reverb => "Reverb".into(), NodeTemplate::Chorus => "Chorus".into(), NodeTemplate::Flanger => "Flanger".into(), @@ -187,7 +241,7 @@ impl NodeTemplateTrait for NodeTemplate { NodeTemplate::MidiInput | NodeTemplate::AudioInput | NodeTemplate::AutomationInput => vec!["Inputs"], NodeTemplate::Oscillator | NodeTemplate::WavetableOscillator | NodeTemplate::FmSynth | NodeTemplate::Noise | NodeTemplate::SimpleSampler | NodeTemplate::MultiSampler => vec!["Generators"], - NodeTemplate::Filter | NodeTemplate::Gain | NodeTemplate::Delay | NodeTemplate::Reverb + NodeTemplate::Filter | NodeTemplate::Gain | NodeTemplate::Echo | NodeTemplate::Reverb | NodeTemplate::Chorus | NodeTemplate::Flanger | NodeTemplate::Phaser | NodeTemplate::Distortion | NodeTemplate::BitCrusher | NodeTemplate::Compressor | NodeTemplate::Limiter | NodeTemplate::Eq | NodeTemplate::Pan | NodeTemplate::RingModulator | NodeTemplate::Vocoder => vec!["Effects"], @@ -217,124 +271,66 @@ impl NodeTemplateTrait for NodeTemplate { ) { match self { NodeTemplate::Oscillator => { - // V/Oct input (pitch control voltage) - graph.add_input_param( - node_id, - "V/Oct".into(), - DataType::CV, - ValueType::Float { value: 0.0 }, - InputParamKind::ConnectionOrConstant, - true, - ); - // FM input (frequency modulation) - graph.add_input_param( - node_id, - "FM".into(), - DataType::CV, - ValueType::Float { value: 0.0 }, - InputParamKind::ConnectionOnly, - true, - ); - // Audio output + // Connection inputs + graph.add_input_param(node_id, "V/Oct".into(), DataType::CV, ValueType::float(0.0), InputParamKind::ConnectionOrConstant, true); + graph.add_input_param(node_id, "FM".into(), DataType::CV, ValueType::float(0.0), InputParamKind::ConnectionOnly, true); + // Parameters + graph.add_input_param(node_id, "Frequency".into(), DataType::CV, + ValueType::float_param(440.0, 20.0, 20000.0, " Hz", 0, None), InputParamKind::ConstantOnly, true); + graph.add_input_param(node_id, "Amplitude".into(), DataType::CV, + ValueType::float_param(0.5, 0.0, 1.0, "", 1, None), InputParamKind::ConstantOnly, true); + graph.add_input_param(node_id, "Waveform".into(), DataType::CV, + ValueType::float_param(0.0, 0.0, 3.0, "", 2, Some(&["Sine", "Saw", "Square", "Triangle"])), InputParamKind::ConstantOnly, true); graph.add_output_param(node_id, "Audio Out".into(), DataType::Audio); } NodeTemplate::Noise => { + graph.add_input_param(node_id, "Color".into(), DataType::CV, + ValueType::float_param(0.0, 0.0, 2.0, "", 0, Some(&["White", "Pink", "Brown"])), InputParamKind::ConstantOnly, true); graph.add_output_param(node_id, "Audio Out".into(), DataType::Audio); } NodeTemplate::Filter => { - graph.add_input_param( - node_id, - "Audio In".into(), - DataType::Audio, - ValueType::Float { value: 0.0 }, - InputParamKind::ConnectionOnly, - true, - ); - graph.add_input_param( - node_id, - "Cutoff CV".into(), - DataType::CV, - ValueType::Float { value: 0.0 }, - InputParamKind::ConnectionOnly, - true, - ); + graph.add_input_param(node_id, "Audio In".into(), DataType::Audio, ValueType::float(0.0), InputParamKind::ConnectionOnly, true); + graph.add_input_param(node_id, "Cutoff CV".into(), DataType::CV, ValueType::float(0.0), InputParamKind::ConnectionOnly, true); + // Parameters + graph.add_input_param(node_id, "Cutoff".into(), DataType::CV, + ValueType::float_param(1000.0, 20.0, 20000.0, " Hz", 0, None), InputParamKind::ConstantOnly, true); + graph.add_input_param(node_id, "Resonance".into(), DataType::CV, + ValueType::float_param(0.0, 0.0, 1.0, "", 1, None), InputParamKind::ConstantOnly, true); + graph.add_input_param(node_id, "Type".into(), DataType::CV, + ValueType::float_param(0.0, 0.0, 3.0, "", 2, Some(&["LPF", "HPF", "BPF", "Notch"])), InputParamKind::ConstantOnly, true); graph.add_output_param(node_id, "Audio Out".into(), DataType::Audio); } NodeTemplate::Gain => { - graph.add_input_param( - node_id, - "Audio In".into(), - DataType::Audio, - ValueType::Float { value: 0.0 }, - InputParamKind::ConnectionOnly, - true, - ); - graph.add_input_param( - node_id, - "Gain CV".into(), - DataType::CV, - ValueType::Float { value: 0.0 }, - InputParamKind::ConnectionOnly, - true, - ); + graph.add_input_param(node_id, "Audio In".into(), DataType::Audio, ValueType::float(0.0), InputParamKind::ConnectionOnly, true); + graph.add_input_param(node_id, "Gain CV".into(), DataType::CV, ValueType::float(0.0), InputParamKind::ConnectionOnly, true); + // Parameters + graph.add_input_param(node_id, "Gain".into(), DataType::CV, + ValueType::float_param(0.0, -60.0, 12.0, " dB", 0, None), InputParamKind::ConstantOnly, true); graph.add_output_param(node_id, "Audio Out".into(), DataType::Audio); } NodeTemplate::Adsr => { - graph.add_input_param( - node_id, - "Gate".into(), - DataType::CV, - ValueType::Float { value: 0.0 }, - InputParamKind::ConnectionOnly, - true, - ); + graph.add_input_param(node_id, "Gate".into(), DataType::CV, ValueType::float(0.0), InputParamKind::ConnectionOnly, true); // Parameters - graph.add_input_param( - node_id, - "Attack".into(), - DataType::CV, - ValueType::Float { value: 0.01 }, - InputParamKind::ConstantOnly, - true, - ); - graph.add_input_param( - node_id, - "Decay".into(), - DataType::CV, - ValueType::Float { value: 0.1 }, - InputParamKind::ConstantOnly, - true, - ); - graph.add_input_param( - node_id, - "Sustain".into(), - DataType::CV, - ValueType::Float { value: 0.7 }, - InputParamKind::ConstantOnly, - true, - ); - graph.add_input_param( - node_id, - "Release".into(), - DataType::CV, - ValueType::Float { value: 0.2 }, - InputParamKind::ConstantOnly, - true, - ); + graph.add_input_param(node_id, "Attack".into(), DataType::CV, + ValueType::float_param(10.0, 0.1, 2000.0, " ms", 0, None), InputParamKind::ConstantOnly, true); + graph.add_input_param(node_id, "Decay".into(), DataType::CV, + ValueType::float_param(100.0, 0.1, 2000.0, " ms", 1, None), InputParamKind::ConstantOnly, true); + graph.add_input_param(node_id, "Sustain".into(), DataType::CV, + ValueType::float_param(0.7, 0.0, 1.0, "", 2, None), InputParamKind::ConstantOnly, true); + graph.add_input_param(node_id, "Release".into(), DataType::CV, + ValueType::float_param(200.0, 0.1, 5000.0, " ms", 3, None), InputParamKind::ConstantOnly, true); graph.add_output_param(node_id, "Envelope Out".into(), DataType::CV); } NodeTemplate::Lfo => { + // Parameters + graph.add_input_param(node_id, "Rate".into(), DataType::CV, + ValueType::float_param(1.0, 0.01, 20.0, " Hz", 0, None), InputParamKind::ConstantOnly, true); + graph.add_input_param(node_id, "Waveform".into(), DataType::CV, + ValueType::float_param(0.0, 0.0, 3.0, "", 1, Some(&["Sine", "Triangle", "Square", "Saw"])), InputParamKind::ConstantOnly, true); graph.add_output_param(node_id, "CV Out".into(), DataType::CV); } NodeTemplate::AudioOutput => { - graph.add_input_param( - node_id, - "Audio In".into(), - DataType::Audio, - ValueType::Float { value: 0.0 }, - InputParamKind::ConnectionOnly, - true, - ); + graph.add_input_param(node_id, "Audio In".into(), DataType::Audio, ValueType::float(0.0), InputParamKind::ConnectionOnly, true); } NodeTemplate::AudioInput => { graph.add_output_param(node_id, "Audio Out".into(), DataType::Audio); @@ -342,105 +338,47 @@ impl NodeTemplateTrait for NodeTemplate { NodeTemplate::MidiInput => { graph.add_output_param(node_id, "MIDI Out".into(), DataType::Midi); } - NodeTemplate::Delay => { - graph.add_input_param( - node_id, - "Audio In".into(), - DataType::Audio, - ValueType::Float { value: 0.0 }, - InputParamKind::ConnectionOnly, - true, - ); + NodeTemplate::Echo => { + graph.add_input_param(node_id, "Audio In".into(), DataType::Audio, ValueType::float(0.0), InputParamKind::ConnectionOnly, true); // Parameters - graph.add_input_param( - node_id, - "Delay Time".into(), - DataType::CV, - ValueType::Float { value: 0.5 }, - InputParamKind::ConstantOnly, - true, - ); - graph.add_input_param( - node_id, - "Feedback".into(), - DataType::CV, - ValueType::Float { value: 0.5 }, - InputParamKind::ConstantOnly, - true, - ); - graph.add_input_param( - node_id, - "Wet/Dry".into(), - DataType::CV, - ValueType::Float { value: 0.5 }, - InputParamKind::ConstantOnly, - true, - ); + graph.add_input_param(node_id, "Delay Time".into(), DataType::CV, + ValueType::float_param(250.0, 1.0, 2000.0, " ms", 0, None), InputParamKind::ConstantOnly, true); + graph.add_input_param(node_id, "Feedback".into(), DataType::CV, + ValueType::float_param(0.3, 0.0, 0.95, "", 1, None), InputParamKind::ConstantOnly, true); + graph.add_input_param(node_id, "Mix".into(), DataType::CV, + ValueType::float_param(0.5, 0.0, 1.0, "", 2, None), InputParamKind::ConstantOnly, true); graph.add_output_param(node_id, "Audio Out".into(), DataType::Audio); } NodeTemplate::Mixer => { - graph.add_input_param( - node_id, - "Input 1".into(), - DataType::Audio, - ValueType::Float { value: 0.0 }, - InputParamKind::ConnectionOnly, - true, - ); - graph.add_input_param( - node_id, - "Input 2".into(), - DataType::Audio, - ValueType::Float { value: 0.0 }, - InputParamKind::ConnectionOnly, - true, - ); - graph.add_input_param( - node_id, - "Input 3".into(), - DataType::Audio, - ValueType::Float { value: 0.0 }, - InputParamKind::ConnectionOnly, - true, - ); - graph.add_input_param( - node_id, - "Input 4".into(), - DataType::Audio, - ValueType::Float { value: 0.0 }, - InputParamKind::ConnectionOnly, - true, - ); + graph.add_input_param(node_id, "Input 1".into(), DataType::Audio, ValueType::float(0.0), InputParamKind::ConnectionOnly, true); + graph.add_input_param(node_id, "Input 2".into(), DataType::Audio, ValueType::float(0.0), InputParamKind::ConnectionOnly, true); + graph.add_input_param(node_id, "Input 3".into(), DataType::Audio, ValueType::float(0.0), InputParamKind::ConnectionOnly, true); + graph.add_input_param(node_id, "Input 4".into(), DataType::Audio, ValueType::float(0.0), InputParamKind::ConnectionOnly, true); + // Level parameters + graph.add_input_param(node_id, "Level 1".into(), DataType::CV, + ValueType::float_param(1.0, 0.0, 1.0, "", 0, None), InputParamKind::ConstantOnly, true); + graph.add_input_param(node_id, "Level 2".into(), DataType::CV, + ValueType::float_param(1.0, 0.0, 1.0, "", 1, None), InputParamKind::ConstantOnly, true); + graph.add_input_param(node_id, "Level 3".into(), DataType::CV, + ValueType::float_param(1.0, 0.0, 1.0, "", 2, None), InputParamKind::ConstantOnly, true); + graph.add_input_param(node_id, "Level 4".into(), DataType::CV, + ValueType::float_param(1.0, 0.0, 1.0, "", 3, None), InputParamKind::ConstantOnly, true); graph.add_output_param(node_id, "Mixed Out".into(), DataType::Audio); } NodeTemplate::Splitter => { - graph.add_input_param( - node_id, - "Audio In".into(), - DataType::Audio, - ValueType::Float { value: 0.0 }, - InputParamKind::ConnectionOnly, - true, - ); + graph.add_input_param(node_id, "Audio In".into(), DataType::Audio, ValueType::float(0.0), InputParamKind::ConnectionOnly, true); graph.add_output_param(node_id, "Out 1".into(), DataType::Audio); graph.add_output_param(node_id, "Out 2".into(), DataType::Audio); graph.add_output_param(node_id, "Out 3".into(), DataType::Audio); graph.add_output_param(node_id, "Out 4".into(), DataType::Audio); } NodeTemplate::Constant => { - // No inputs - value is set via parameter - graph.add_input_param( - node_id, - "Value".into(), - DataType::CV, - ValueType::Float { value: 0.0 }, - InputParamKind::ConstantOnly, - true, - ); + graph.add_input_param(node_id, "Value".into(), DataType::CV, + ValueType::float_param(0.0, -1.0, 1.0, "", 0, None), InputParamKind::ConstantOnly, true); graph.add_output_param(node_id, "CV Out".into(), DataType::CV); } NodeTemplate::MidiToCv => { - graph.add_input_param(node_id, "MIDI In".into(), DataType::Midi, ValueType::Float { value: 0.0 }, InputParamKind::ConnectionOnly, true); + graph.add_input_param(node_id, "MIDI In".into(), DataType::Midi, ValueType::float(0.0), InputParamKind::ConnectionOnly, true); graph.add_output_param(node_id, "V/Oct".into(), DataType::CV); graph.add_output_param(node_id, "Gate".into(), DataType::CV); graph.add_output_param(node_id, "Velocity".into(), DataType::CV); @@ -450,56 +388,67 @@ impl NodeTemplateTrait for NodeTemplate { graph.add_output_param(node_id, "CV Out".into(), DataType::CV); } NodeTemplate::WavetableOscillator => { - graph.add_input_param(node_id, "V/Oct".into(), DataType::CV, ValueType::Float { value: 0.0 }, InputParamKind::ConnectionOnly, true); + graph.add_input_param(node_id, "V/Oct".into(), DataType::CV, ValueType::float(0.0), InputParamKind::ConnectionOnly, true); graph.add_output_param(node_id, "Audio Out".into(), DataType::Audio); } NodeTemplate::FmSynth => { - graph.add_input_param(node_id, "V/Oct".into(), DataType::CV, ValueType::Float { value: 0.0 }, InputParamKind::ConnectionOnly, true); + graph.add_input_param(node_id, "V/Oct".into(), DataType::CV, ValueType::float(0.0), InputParamKind::ConnectionOnly, true); graph.add_output_param(node_id, "Audio Out".into(), DataType::Audio); } NodeTemplate::SimpleSampler => { - graph.add_input_param(node_id, "Gate".into(), DataType::CV, ValueType::Float { value: 0.0 }, InputParamKind::ConnectionOnly, true); + graph.add_input_param(node_id, "Gate".into(), DataType::CV, ValueType::float(0.0), InputParamKind::ConnectionOnly, true); graph.add_output_param(node_id, "Audio Out".into(), DataType::Audio); } NodeTemplate::MultiSampler => { - graph.add_input_param(node_id, "MIDI In".into(), DataType::Midi, ValueType::Float { value: 0.0 }, InputParamKind::ConnectionOnly, true); + graph.add_input_param(node_id, "MIDI In".into(), DataType::Midi, ValueType::float(0.0), InputParamKind::ConnectionOnly, true); graph.add_output_param(node_id, "Audio Out".into(), DataType::Audio); } - NodeTemplate::Reverb | NodeTemplate::Chorus | NodeTemplate::Flanger | NodeTemplate::Phaser + NodeTemplate::Reverb => { + graph.add_input_param(node_id, "Audio In".into(), DataType::Audio, ValueType::float(0.0), InputParamKind::ConnectionOnly, true); + // Parameters + graph.add_input_param(node_id, "Room Size".into(), DataType::CV, + ValueType::float_param(0.5, 0.0, 1.0, "", 0, None), InputParamKind::ConstantOnly, true); + graph.add_input_param(node_id, "Damping".into(), DataType::CV, + ValueType::float_param(0.5, 0.0, 1.0, "", 1, None), InputParamKind::ConstantOnly, true); + graph.add_input_param(node_id, "Wet/Dry".into(), DataType::CV, + ValueType::float_param(0.3, 0.0, 1.0, "", 2, None), InputParamKind::ConstantOnly, true); + graph.add_output_param(node_id, "Audio Out".into(), DataType::Audio); + } + NodeTemplate::Chorus | NodeTemplate::Flanger | NodeTemplate::Phaser | NodeTemplate::Distortion | NodeTemplate::BitCrusher | NodeTemplate::Compressor | NodeTemplate::Limiter | NodeTemplate::Eq | NodeTemplate::Pan | NodeTemplate::RingModulator | NodeTemplate::Vocoder => { - graph.add_input_param(node_id, "Audio In".into(), DataType::Audio, ValueType::Float { value: 0.0 }, InputParamKind::ConnectionOnly, true); + graph.add_input_param(node_id, "Audio In".into(), DataType::Audio, ValueType::float(0.0), InputParamKind::ConnectionOnly, true); graph.add_output_param(node_id, "Audio Out".into(), DataType::Audio); } NodeTemplate::AudioToCv => { - graph.add_input_param(node_id, "Audio In".into(), DataType::Audio, ValueType::Float { value: 0.0 }, InputParamKind::ConnectionOnly, true); + graph.add_input_param(node_id, "Audio In".into(), DataType::Audio, ValueType::float(0.0), InputParamKind::ConnectionOnly, true); graph.add_output_param(node_id, "CV Out".into(), DataType::CV); } NodeTemplate::Math => { - graph.add_input_param(node_id, "A".into(), DataType::CV, ValueType::Float { value: 0.0 }, InputParamKind::ConnectionOrConstant, true); - graph.add_input_param(node_id, "B".into(), DataType::CV, ValueType::Float { value: 0.0 }, InputParamKind::ConnectionOrConstant, true); + graph.add_input_param(node_id, "A".into(), DataType::CV, ValueType::float(0.0), InputParamKind::ConnectionOrConstant, true); + graph.add_input_param(node_id, "B".into(), DataType::CV, ValueType::float(0.0), InputParamKind::ConnectionOrConstant, true); graph.add_output_param(node_id, "Out".into(), DataType::CV); } NodeTemplate::SampleHold | NodeTemplate::SlewLimiter | NodeTemplate::Quantizer | NodeTemplate::EnvelopeFollower => { - graph.add_input_param(node_id, "In".into(), DataType::CV, ValueType::Float { value: 0.0 }, InputParamKind::ConnectionOnly, true); + graph.add_input_param(node_id, "In".into(), DataType::CV, ValueType::float(0.0), InputParamKind::ConnectionOnly, true); graph.add_output_param(node_id, "Out".into(), DataType::CV); } NodeTemplate::BpmDetector => { - graph.add_input_param(node_id, "Audio In".into(), DataType::Audio, ValueType::Float { value: 0.0 }, InputParamKind::ConnectionOnly, true); + graph.add_input_param(node_id, "Audio In".into(), DataType::Audio, ValueType::float(0.0), InputParamKind::ConnectionOnly, true); graph.add_output_param(node_id, "BPM".into(), DataType::CV); } NodeTemplate::Mod => { - graph.add_input_param(node_id, "Carrier".into(), DataType::Audio, ValueType::Float { value: 0.0 }, InputParamKind::ConnectionOnly, true); - graph.add_input_param(node_id, "Modulator".into(), DataType::CV, ValueType::Float { value: 0.0 }, InputParamKind::ConnectionOnly, true); + graph.add_input_param(node_id, "Carrier".into(), DataType::Audio, ValueType::float(0.0), InputParamKind::ConnectionOnly, true); + graph.add_input_param(node_id, "Modulator".into(), DataType::CV, ValueType::float(0.0), InputParamKind::ConnectionOnly, true); graph.add_output_param(node_id, "Out".into(), DataType::Audio); } NodeTemplate::Oscilloscope => { - graph.add_input_param(node_id, "Audio In".into(), DataType::Audio, ValueType::Float { value: 0.0 }, InputParamKind::ConnectionOnly, true); - graph.add_input_param(node_id, "CV In".into(), DataType::CV, ValueType::Float { value: 0.0 }, InputParamKind::ConnectionOnly, true); + graph.add_input_param(node_id, "Audio In".into(), DataType::Audio, ValueType::float(0.0), InputParamKind::ConnectionOnly, true); + graph.add_input_param(node_id, "CV In".into(), DataType::CV, ValueType::float(0.0), InputParamKind::ConnectionOnly, true); } NodeTemplate::VoiceAllocator => { - graph.add_input_param(node_id, "MIDI In".into(), DataType::Midi, ValueType::Float { value: 0.0 }, InputParamKind::ConnectionOnly, true); + graph.add_input_param(node_id, "MIDI In".into(), DataType::Midi, ValueType::float(0.0), InputParamKind::ConnectionOnly, true); graph.add_output_param(node_id, "Audio Out".into(), DataType::Audio); } } @@ -521,11 +470,49 @@ impl WidgetValueTrait for ValueType { _node_data: &Self::NodeData, ) -> Vec { match self { - ValueType::Float { value } => { - ui.horizontal(|ui| { - ui.label(param_name); - ui.add(egui::DragValue::new(value).speed(0.1)); - }); + ValueType::Float { value, min, max, unit, enum_labels, .. } => { + let has_range = *max > *min; + if let Some(labels) = enum_labels { + // Enum parameter: render as ComboBox dropdown + let mut selected = (*value as usize).min(labels.len().saturating_sub(1)); + ui.horizontal(|ui| { + ui.label(param_name); + egui::ComboBox::from_id_salt(param_name) + .selected_text(labels.get(selected).copied().unwrap_or("?")) + .width(90.0) + .show_ui(ui, |ui| { + for (i, label) in labels.iter().enumerate() { + ui.selectable_value(&mut selected, i, *label); + } + }); + }); + *value = selected as f32; + } else if has_range { + // Ranged parameter: render clamped DragValue with unit suffix + let range = *max - *min; + let speed = if range > 1000.0 { + // Logarithmic-ish speed for large ranges (frequency, time) + (*value).max(1.0) * 0.01 + } else { + range / 300.0 + }; + ui.horizontal(|ui| { + ui.label(param_name); + let mut dv = egui::DragValue::new(value) + .speed(speed) + .range(*min..=*max); + if !unit.is_empty() { + dv = dv.suffix(*unit); + } + ui.add(dv); + }); + } else { + // Plain float (no metadata) + ui.horizontal(|ui| { + ui.label(param_name); + ui.add(egui::DragValue::new(value).speed(0.1)); + }); + } } ValueType::String { value } => { ui.horizontal(|ui| { @@ -583,7 +570,7 @@ impl NodeTemplateIter for AllNodeTemplates { // Effects NodeTemplate::Filter, NodeTemplate::Gain, - NodeTemplate::Delay, + NodeTemplate::Echo, NodeTemplate::Reverb, NodeTemplate::Chorus, NodeTemplate::Flanger, diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/mod.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/mod.rs index d5e3468..09ddc72 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/mod.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/mod.rs @@ -137,7 +137,7 @@ impl NodeGraphPane { // Effects "Filter" => graph_data::NodeTemplate::Filter, "Gain" => graph_data::NodeTemplate::Gain, - "Delay" => graph_data::NodeTemplate::Delay, + "Echo" | "Delay" => graph_data::NodeTemplate::Echo, "Reverb" => graph_data::NodeTemplate::Reverb, "Chorus" => graph_data::NodeTemplate::Chorus, "Flanger" => graph_data::NodeTemplate::Flanger, @@ -205,13 +205,17 @@ impl NodeGraphPane { self.node_id_map.insert(frontend_id, backend_id); self.backend_to_frontend_map.insert(backend_id, frontend_id); - // Set parameter values - for (¶m_id, &value) in &node.parameters { - // Find the input param in the graph and set its value - if let Some(_node_data) = self.state.graph.nodes.get_mut(frontend_id) { - // TODO: Set parameter values on the node's input params - // This requires matching param_id to the input param by index - let _ = (param_id, value); // Silence unused warning for now + // Set parameter values from backend + if let Some(node_data) = self.state.graph.nodes.get(frontend_id) { + let input_ids: Vec = node_data.inputs.iter().map(|(_, id)| *id).collect(); + for input_id in input_ids { + if let Some(input_param) = self.state.graph.inputs.get_mut(input_id) { + if let ValueType::Float { value, backend_param_id: Some(pid), .. } = &mut input_param.value { + if let Some(&backend_value) = node.parameters.get(pid) { + *value = backend_value as f32; + } + } + } } } } @@ -440,11 +444,11 @@ impl NodeGraphPane { continue; } - // Get current value - let current_value = match &input_param.value { - ValueType::Float { value } => { + // Get current value and backend param ID + let (current_value, backend_param_id) = match &input_param.value { + ValueType::Float { value, backend_param_id, .. } => { _checked_count += 1; - *value + (*value, *backend_param_id) }, other => { _non_float_count += 1; @@ -468,24 +472,20 @@ impl NodeGraphPane { if let Some(track_id) = self.track_id { let node_id = input_param.node; - // Get backend node ID + // Get backend node ID and use stored param ID if let Some(&backend_id) = self.node_id_map.get(&node_id) { - // Get parameter index (position in node's inputs array) - if let Some(node) = self.state.graph.nodes.get(node_id) { - if let Some(param_index) = node.inputs.iter().position(|(_, id)| *id == input_id) { - eprintln!("[DEBUG] Parameter changed: node {:?} param {} from {:?} to {}", - backend_id, param_index, previous_value, current_value); - // Create action to update backend - let action = Box::new(actions::NodeGraphAction::SetParameter( - actions::SetParameterAction::new( - track_id, - backend_id, - param_index as u32, - current_value as f64, - ) - )); - self.pending_action = Some(action); - } + if let Some(param_id) = backend_param_id { + eprintln!("[DEBUG] Parameter changed: node {:?} param {} from {:?} to {}", + backend_id, param_id, previous_value, current_value); + let action = Box::new(actions::NodeGraphAction::SetParameter( + actions::SetParameterAction::new( + track_id, + backend_id, + param_id, + current_value as f64, + ) + )); + self.pending_action = Some(action); } } } diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/node_types.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/node_types.rs index 3a6d325..d345574 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/node_types.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/node_types.rs @@ -332,10 +332,10 @@ impl NodeTypeRegistry { ); types.insert( - "Delay".to_string(), + "Echo".to_string(), NodeTypeInfo { - id: "Delay".to_string(), - display_name: "Delay".to_string(), + id: "Echo".to_string(), + display_name: "Echo".to_string(), category: NodeCategory::Effects, inputs: vec![PortInfo { index: 0, @@ -347,7 +347,7 @@ impl NodeTypeRegistry { index: 0, name: "Out".to_string(), signal_type: DataType::Audio, - description: "Delayed audio output".to_string(), + description: "Echo audio output".to_string(), }], parameters: vec![ ParameterInfo { @@ -357,7 +357,7 @@ impl NodeTypeRegistry { min: 1.0, max: 2000.0, unit: ParameterUnit::Milliseconds, - description: "Delay time".to_string(), + description: "Echo time".to_string(), }, ParameterInfo { id: 1, @@ -378,7 +378,7 @@ impl NodeTypeRegistry { description: "Dry/wet mix".to_string(), }, ], - description: "Time-based delay effect".to_string(), + description: "Echo effect with feedback".to_string(), }, );