Add parameters to audio nodes and rename Delay node to Echo

This commit is contained in:
Skyler Lehmkuhl 2026-02-15 05:34:28 -05:00
parent 6fcee92d59
commit 1e7001b291
8 changed files with 254 additions and 254 deletions

View File

@ -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())),

View File

@ -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")),

View File

@ -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<Parameter>,
}
impl DelayNode {
impl EchoNode {
pub fn new(name: impl Into<String>) -> 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 {

View File

@ -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;

View File

@ -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

View File

@ -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<u32>,
#[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,12 +470,50 @@ impl WidgetValueTrait for ValueType {
_node_data: &Self::NodeData,
) -> Vec<Self::Response> {
match self {
ValueType::Float { value } => {
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| {
ui.label(param_name);
@ -583,7 +570,7 @@ impl NodeTemplateIter for AllNodeTemplates {
// Effects
NodeTemplate::Filter,
NodeTemplate::Gain,
NodeTemplate::Delay,
NodeTemplate::Echo,
NodeTemplate::Reverb,
NodeTemplate::Chorus,
NodeTemplate::Flanger,

View File

@ -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 (&param_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<InputId> = 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,19 +472,16 @@ 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) {
if let Some(param_id) = backend_param_id {
eprintln!("[DEBUG] Parameter changed: node {:?} param {} from {:?} to {}",
backend_id, param_index, previous_value, current_value);
// Create action to update backend
backend_id, param_id, previous_value, current_value);
let action = Box::new(actions::NodeGraphAction::SetParameter(
actions::SetParameterAction::new(
track_id,
backend_id,
param_index as u32,
param_id,
current_value as f64,
)
));
@ -488,7 +489,6 @@ impl NodeGraphPane {
}
}
}
}
// Update stored value
self.parameter_values.insert(input_id, current_value);

View File

@ -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(),
},
);