From 0ae168cbcad067eb21cc6b08cd9e50fa24a578e1 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Wed, 29 Oct 2025 03:14:01 -0400 Subject: [PATCH] Add bit crusher, constant, math, envelope follower, phaser, ring modulator, sample and hold, and vocoder nodes --- daw-backend/src/audio/engine.rs | 14 + daw-backend/src/audio/node_graph/graph.rs | 7 + .../src/audio/node_graph/nodes/bit_crusher.rs | 187 +++++++++ .../src/audio/node_graph/nodes/constant.rs | 113 ++++++ .../node_graph/nodes/envelope_follower.rs | 158 ++++++++ .../src/audio/node_graph/nodes/math.rs | 36 +- daw-backend/src/audio/node_graph/nodes/mod.rs | 14 + .../src/audio/node_graph/nodes/phaser.rs | 289 ++++++++++++++ .../src/audio/node_graph/nodes/quantizer.rs | 20 +- .../audio/node_graph/nodes/ring_modulator.rs | 137 +++++++ .../src/audio/node_graph/nodes/sample_hold.rs | 137 +++++++ .../src/audio/node_graph/nodes/vocoder.rs | 362 ++++++++++++++++++ src/main.js | 81 ++++ src/nodeTypes.js | 230 ++++++++++- 14 files changed, 1743 insertions(+), 42 deletions(-) create mode 100644 daw-backend/src/audio/node_graph/nodes/bit_crusher.rs create mode 100644 daw-backend/src/audio/node_graph/nodes/constant.rs create mode 100644 daw-backend/src/audio/node_graph/nodes/envelope_follower.rs create mode 100644 daw-backend/src/audio/node_graph/nodes/phaser.rs create mode 100644 daw-backend/src/audio/node_graph/nodes/ring_modulator.rs create mode 100644 daw-backend/src/audio/node_graph/nodes/sample_hold.rs create mode 100644 daw-backend/src/audio/node_graph/nodes/vocoder.rs diff --git a/daw-backend/src/audio/engine.rs b/daw-backend/src/audio/engine.rs index b7e3279..6e389be 100644 --- a/daw-backend/src/audio/engine.rs +++ b/daw-backend/src/audio/engine.rs @@ -710,11 +710,18 @@ impl Engine { "Reverb" => Box::new(ReverbNode::new("Reverb".to_string())), "Chorus" => Box::new(ChorusNode::new("Chorus".to_string())), "Compressor" => Box::new(CompressorNode::new("Compressor".to_string())), + "Constant" => Box::new(ConstantNode::new("Constant".to_string())), + "EnvelopeFollower" => Box::new(EnvelopeFollowerNode::new("Envelope Follower".to_string())), "Limiter" => Box::new(LimiterNode::new("Limiter".to_string())), "Math" => Box::new(MathNode::new("Math".to_string())), "EQ" => Box::new(EQNode::new("EQ".to_string())), "Flanger" => Box::new(FlangerNode::new("Flanger".to_string())), "FMSynth" => Box::new(FMSynthNode::new("FM Synth".to_string())), + "Phaser" => Box::new(PhaserNode::new("Phaser".to_string())), + "BitCrusher" => Box::new(BitCrusherNode::new("Bit Crusher".to_string())), + "Vocoder" => Box::new(VocoderNode::new("Vocoder".to_string())), + "RingModulator" => Box::new(RingModulatorNode::new("Ring Modulator".to_string())), + "SampleHold" => Box::new(SampleHoldNode::new("Sample & Hold".to_string())), "WavetableOscillator" => Box::new(WavetableOscillatorNode::new("Wavetable".to_string())), "SimpleSampler" => Box::new(SimpleSamplerNode::new("Sampler".to_string())), "SlewLimiter" => Box::new(SlewLimiterNode::new("Slew Limiter".to_string())), @@ -777,11 +784,18 @@ impl Engine { "Reverb" => Box::new(ReverbNode::new("Reverb".to_string())), "Chorus" => Box::new(ChorusNode::new("Chorus".to_string())), "Compressor" => Box::new(CompressorNode::new("Compressor".to_string())), + "Constant" => Box::new(ConstantNode::new("Constant".to_string())), + "EnvelopeFollower" => Box::new(EnvelopeFollowerNode::new("Envelope Follower".to_string())), "Limiter" => Box::new(LimiterNode::new("Limiter".to_string())), "Math" => Box::new(MathNode::new("Math".to_string())), "EQ" => Box::new(EQNode::new("EQ".to_string())), "Flanger" => Box::new(FlangerNode::new("Flanger".to_string())), "FMSynth" => Box::new(FMSynthNode::new("FM Synth".to_string())), + "Phaser" => Box::new(PhaserNode::new("Phaser".to_string())), + "BitCrusher" => Box::new(BitCrusherNode::new("Bit Crusher".to_string())), + "Vocoder" => Box::new(VocoderNode::new("Vocoder".to_string())), + "RingModulator" => Box::new(RingModulatorNode::new("Ring Modulator".to_string())), + "SampleHold" => Box::new(SampleHoldNode::new("Sample & Hold".to_string())), "WavetableOscillator" => Box::new(WavetableOscillatorNode::new("Wavetable".to_string())), "SimpleSampler" => Box::new(SimpleSamplerNode::new("Sampler".to_string())), "SlewLimiter" => Box::new(SlewLimiterNode::new("Slew Limiter".to_string())), diff --git a/daw-backend/src/audio/node_graph/graph.rs b/daw-backend/src/audio/node_graph/graph.rs index 95eb355..7bff3b5 100644 --- a/daw-backend/src/audio/node_graph/graph.rs +++ b/daw-backend/src/audio/node_graph/graph.rs @@ -797,11 +797,18 @@ impl InstrumentGraph { "Reverb" => Box::new(ReverbNode::new("Reverb")), "Chorus" => Box::new(ChorusNode::new("Chorus")), "Compressor" => Box::new(CompressorNode::new("Compressor")), + "Constant" => Box::new(ConstantNode::new("Constant")), + "EnvelopeFollower" => Box::new(EnvelopeFollowerNode::new("Envelope Follower")), "Limiter" => Box::new(LimiterNode::new("Limiter")), "Math" => Box::new(MathNode::new("Math")), "EQ" => Box::new(EQNode::new("EQ")), "Flanger" => Box::new(FlangerNode::new("Flanger")), "FMSynth" => Box::new(FMSynthNode::new("FM Synth")), + "Phaser" => Box::new(PhaserNode::new("Phaser")), + "BitCrusher" => Box::new(BitCrusherNode::new("Bit Crusher")), + "Vocoder" => Box::new(VocoderNode::new("Vocoder")), + "RingModulator" => Box::new(RingModulatorNode::new("Ring Modulator")), + "SampleHold" => Box::new(SampleHoldNode::new("Sample & Hold")), "WavetableOscillator" => Box::new(WavetableOscillatorNode::new("Wavetable")), "SimpleSampler" => Box::new(SimpleSamplerNode::new("Sampler")), "SlewLimiter" => Box::new(SlewLimiterNode::new("Slew Limiter")), diff --git a/daw-backend/src/audio/node_graph/nodes/bit_crusher.rs b/daw-backend/src/audio/node_graph/nodes/bit_crusher.rs new file mode 100644 index 0000000..7452d86 --- /dev/null +++ b/daw-backend/src/audio/node_graph/nodes/bit_crusher.rs @@ -0,0 +1,187 @@ +use crate::audio::node_graph::{AudioNode, NodeCategory, NodePort, Parameter, ParameterUnit, SignalType}; +use crate::audio::midi::MidiEvent; + +const PARAM_BIT_DEPTH: u32 = 0; +const PARAM_SAMPLE_RATE_REDUCTION: u32 = 1; +const PARAM_MIX: u32 = 2; + +/// Bit Crusher effect - reduces bit depth and sample rate for lo-fi sound +pub struct BitCrusherNode { + name: String, + bit_depth: f32, // 1 to 16 bits + sample_rate_reduction: f32, // 1 to 48000 Hz (crushed sample rate) + mix: f32, // 0.0 to 1.0 (dry/wet) + + // State for sample rate reduction + hold_left: f32, + hold_right: f32, + hold_counter: f32, + + sample_rate: u32, + + inputs: Vec, + outputs: Vec, + parameters: Vec, +} + +impl BitCrusherNode { + pub fn new(name: impl Into) -> Self { + let name = name.into(); + + let inputs = vec![ + NodePort::new("Audio In", SignalType::Audio, 0), + ]; + + let outputs = vec![ + NodePort::new("Audio Out", SignalType::Audio, 0), + ]; + + let parameters = vec![ + Parameter::new(PARAM_BIT_DEPTH, "Bit Depth", 1.0, 16.0, 8.0, ParameterUnit::Generic), + Parameter::new(PARAM_SAMPLE_RATE_REDUCTION, "Sample Rate", 100.0, 48000.0, 8000.0, ParameterUnit::Frequency), + Parameter::new(PARAM_MIX, "Mix", 0.0, 1.0, 1.0, ParameterUnit::Generic), + ]; + + Self { + name, + bit_depth: 8.0, + sample_rate_reduction: 8000.0, + mix: 1.0, + hold_left: 0.0, + hold_right: 0.0, + hold_counter: 0.0, + sample_rate: 48000, + inputs, + outputs, + parameters, + } + } + + /// Quantize sample to specified bit depth + fn quantize(&self, sample: f32) -> f32 { + // Calculate number of quantization levels + let levels = 2.0_f32.powf(self.bit_depth); + + // Quantize: scale up, round, scale back down + let scaled = sample * levels / 2.0; + let quantized = scaled.round(); + quantized * 2.0 / levels + } +} + +impl AudioNode for BitCrusherNode { + fn category(&self) -> NodeCategory { + NodeCategory::Effect + } + + fn inputs(&self) -> &[NodePort] { + &self.inputs + } + + fn outputs(&self) -> &[NodePort] { + &self.outputs + } + + fn parameters(&self) -> &[Parameter] { + &self.parameters + } + + fn set_parameter(&mut self, id: u32, value: f32) { + match id { + PARAM_BIT_DEPTH => self.bit_depth = value.clamp(1.0, 16.0), + PARAM_SAMPLE_RATE_REDUCTION => self.sample_rate_reduction = value.clamp(100.0, 48000.0), + PARAM_MIX => self.mix = value.clamp(0.0, 1.0), + _ => {} + } + } + + fn get_parameter(&self, id: u32) -> f32 { + match id { + PARAM_BIT_DEPTH => self.bit_depth, + PARAM_SAMPLE_RATE_REDUCTION => self.sample_rate_reduction, + PARAM_MIX => self.mix, + _ => 0.0, + } + } + + fn process( + &mut self, + inputs: &[&[f32]], + outputs: &mut [&mut [f32]], + _midi_inputs: &[&[MidiEvent]], + _midi_outputs: &mut [&mut Vec], + sample_rate: u32, + ) { + if inputs.is_empty() || outputs.is_empty() { + return; + } + + // Update sample rate if changed + if self.sample_rate != sample_rate { + self.sample_rate = sample_rate; + } + + let input = inputs[0]; + let output = &mut outputs[0]; + + // Audio signals are stereo (interleaved L/R) + let frames = input.len() / 2; + let output_frames = output.len() / 2; + let frames_to_process = frames.min(output_frames); + + // Calculate sample hold period + let hold_period = self.sample_rate as f32 / self.sample_rate_reduction; + + for frame in 0..frames_to_process { + let left_in = input[frame * 2]; + let right_in = input[frame * 2 + 1]; + + // Sample rate reduction: hold samples + if self.hold_counter <= 0.0 { + // Time to sample a new value + self.hold_left = self.quantize(left_in); + self.hold_right = self.quantize(right_in); + self.hold_counter = hold_period; + } + + self.hold_counter -= 1.0; + + // Mix dry and wet + let wet_left = self.hold_left; + let wet_right = self.hold_right; + + output[frame * 2] = left_in * (1.0 - self.mix) + wet_left * self.mix; + output[frame * 2 + 1] = right_in * (1.0 - self.mix) + wet_right * self.mix; + } + } + + fn reset(&mut self) { + self.hold_left = 0.0; + self.hold_right = 0.0; + self.hold_counter = 0.0; + } + + fn node_type(&self) -> &str { + "BitCrusher" + } + + fn name(&self) -> &str { + &self.name + } + + fn clone_node(&self) -> Box { + Box::new(Self { + name: self.name.clone(), + bit_depth: self.bit_depth, + sample_rate_reduction: self.sample_rate_reduction, + mix: self.mix, + hold_left: 0.0, + hold_right: 0.0, + hold_counter: 0.0, + sample_rate: self.sample_rate, + inputs: self.inputs.clone(), + outputs: self.outputs.clone(), + parameters: self.parameters.clone(), + }) + } +} diff --git a/daw-backend/src/audio/node_graph/nodes/constant.rs b/daw-backend/src/audio/node_graph/nodes/constant.rs new file mode 100644 index 0000000..5e96937 --- /dev/null +++ b/daw-backend/src/audio/node_graph/nodes/constant.rs @@ -0,0 +1,113 @@ +use crate::audio::node_graph::{AudioNode, NodeCategory, NodePort, Parameter, ParameterUnit, SignalType}; +use crate::audio::midi::MidiEvent; + +const PARAM_VALUE: u32 = 0; + +/// Constant CV source - outputs a constant voltage +/// Useful for providing fixed values to CV inputs, offsets, etc. +pub struct ConstantNode { + name: String, + value: f32, + inputs: Vec, + outputs: Vec, + parameters: Vec, +} + +impl ConstantNode { + pub fn new(name: impl Into) -> Self { + let name = name.into(); + + let inputs = vec![]; + + let outputs = vec![ + NodePort::new("CV Out", SignalType::CV, 0), + ]; + + let parameters = vec![ + Parameter::new(PARAM_VALUE, "Value", -10.0, 10.0, 0.0, ParameterUnit::Generic), + ]; + + Self { + name, + value: 0.0, + inputs, + outputs, + parameters, + } + } +} + +impl AudioNode for ConstantNode { + fn category(&self) -> NodeCategory { + NodeCategory::Utility + } + + fn inputs(&self) -> &[NodePort] { + &self.inputs + } + + fn outputs(&self) -> &[NodePort] { + &self.outputs + } + + fn parameters(&self) -> &[Parameter] { + &self.parameters + } + + fn set_parameter(&mut self, id: u32, value: f32) { + match id { + PARAM_VALUE => self.value = value.clamp(-10.0, 10.0), + _ => {} + } + } + + fn get_parameter(&self, id: u32) -> f32 { + match id { + PARAM_VALUE => self.value, + _ => 0.0, + } + } + + fn process( + &mut self, + _inputs: &[&[f32]], + outputs: &mut [&mut [f32]], + _midi_inputs: &[&[MidiEvent]], + _midi_outputs: &mut [&mut Vec], + _sample_rate: u32, + ) { + if outputs.is_empty() { + return; + } + + let output = &mut outputs[0]; + let length = output.len(); + + // Fill output with constant value + for i in 0..length { + output[i] = self.value; + } + } + + fn reset(&mut self) { + // No state to reset + } + + fn node_type(&self) -> &str { + "Constant" + } + + fn name(&self) -> &str { + &self.name + } + + fn clone_node(&self) -> Box { + Box::new(Self { + name: self.name.clone(), + value: self.value, + inputs: self.inputs.clone(), + outputs: self.outputs.clone(), + parameters: self.parameters.clone(), + }) + } +} diff --git a/daw-backend/src/audio/node_graph/nodes/envelope_follower.rs b/daw-backend/src/audio/node_graph/nodes/envelope_follower.rs new file mode 100644 index 0000000..693eb1c --- /dev/null +++ b/daw-backend/src/audio/node_graph/nodes/envelope_follower.rs @@ -0,0 +1,158 @@ +use crate::audio::node_graph::{AudioNode, NodeCategory, NodePort, Parameter, ParameterUnit, SignalType}; +use crate::audio::midi::MidiEvent; + +const PARAM_ATTACK: u32 = 0; +const PARAM_RELEASE: u32 = 1; + +/// Envelope Follower - extracts amplitude envelope from audio signal +/// Outputs a CV signal that follows the loudness of the input +pub struct EnvelopeFollowerNode { + name: String, + attack_time: f32, // seconds + release_time: f32, // seconds + envelope: f32, // current envelope level + inputs: Vec, + outputs: Vec, + parameters: Vec, +} + +impl EnvelopeFollowerNode { + pub fn new(name: impl Into) -> Self { + let name = name.into(); + + let inputs = vec![ + NodePort::new("Audio In", SignalType::Audio, 0), + ]; + + let outputs = vec![ + NodePort::new("CV Out", SignalType::CV, 0), + ]; + + let parameters = vec![ + Parameter::new(PARAM_ATTACK, "Attack", 0.001, 1.0, 0.01, ParameterUnit::Time), + Parameter::new(PARAM_RELEASE, "Release", 0.001, 1.0, 0.1, ParameterUnit::Time), + ]; + + Self { + name, + attack_time: 0.01, + release_time: 0.1, + envelope: 0.0, + inputs, + outputs, + parameters, + } + } +} + +impl AudioNode for EnvelopeFollowerNode { + fn category(&self) -> NodeCategory { + NodeCategory::Utility + } + + fn inputs(&self) -> &[NodePort] { + &self.inputs + } + + fn outputs(&self) -> &[NodePort] { + &self.outputs + } + + fn parameters(&self) -> &[Parameter] { + &self.parameters + } + + fn set_parameter(&mut self, id: u32, value: f32) { + match id { + PARAM_ATTACK => self.attack_time = value.clamp(0.001, 1.0), + PARAM_RELEASE => self.release_time = value.clamp(0.001, 1.0), + _ => {} + } + } + + fn get_parameter(&self, id: u32) -> f32 { + match id { + PARAM_ATTACK => self.attack_time, + PARAM_RELEASE => self.release_time, + _ => 0.0, + } + } + + fn process( + &mut self, + inputs: &[&[f32]], + outputs: &mut [&mut [f32]], + _midi_inputs: &[&[MidiEvent]], + _midi_outputs: &mut [&mut Vec], + sample_rate: u32, + ) { + if outputs.is_empty() { + return; + } + + let output = &mut outputs[0]; + let length = output.len(); + + let input = if !inputs.is_empty() && !inputs[0].is_empty() { + inputs[0] + } else { + &[] + }; + + // Calculate filter coefficients + // One-pole filter: y[n] = y[n-1] + coefficient * (x[n] - y[n-1]) + let sample_duration = 1.0 / sample_rate as f32; + + // Time constant τ = time to reach ~63% of target + // Coefficient = 1 - e^(-1/(τ * sample_rate)) + // Simplified approximation: coefficient ≈ sample_duration / time_constant + let attack_coeff = (sample_duration / self.attack_time).min(1.0); + let release_coeff = (sample_duration / self.release_time).min(1.0); + + // Process each sample + for i in 0..length { + // Get absolute value of input (rectify) + let input_level = if i < input.len() { + input[i].abs() + } else { + 0.0 + }; + + // Apply attack or release + let coeff = if input_level > self.envelope { + attack_coeff // Rising - use attack time + } else { + release_coeff // Falling - use release time + }; + + // One-pole filter + self.envelope += (input_level - self.envelope) * coeff; + + output[i] = self.envelope; + } + } + + fn reset(&mut self) { + self.envelope = 0.0; + } + + fn node_type(&self) -> &str { + "EnvelopeFollower" + } + + fn name(&self) -> &str { + &self.name + } + + fn clone_node(&self) -> Box { + Box::new(Self { + name: self.name.clone(), + attack_time: self.attack_time, + release_time: self.release_time, + envelope: self.envelope, + inputs: self.inputs.clone(), + outputs: self.outputs.clone(), + parameters: self.parameters.clone(), + }) + } +} diff --git a/daw-backend/src/audio/node_graph/nodes/math.rs b/daw-backend/src/audio/node_graph/nodes/math.rs index c34e6fb..4f3c52f 100644 --- a/daw-backend/src/audio/node_graph/nodes/math.rs +++ b/daw-backend/src/audio/node_graph/nodes/math.rs @@ -2,7 +2,6 @@ use crate::audio::node_graph::{AudioNode, NodeCategory, NodePort, Parameter, Par use crate::audio::midi::MidiEvent; const PARAM_OPERATION: u32 = 0; -const PARAM_OPERAND: u32 = 1; /// Mathematical and logical operations on CV signals /// Operations: @@ -14,7 +13,6 @@ const PARAM_OPERAND: u32 = 1; pub struct MathNode { name: String, operation: u32, - operand: f32, inputs: Vec, outputs: Vec, parameters: Vec, @@ -35,13 +33,11 @@ impl MathNode { let parameters = vec![ Parameter::new(PARAM_OPERATION, "Operation", 0.0, 13.0, 0.0, ParameterUnit::Generic), - Parameter::new(PARAM_OPERAND, "Operand", -10.0, 10.0, 1.0, ParameterUnit::Generic), ]; Self { name, operation: 0, - operand: 1.0, inputs, outputs, parameters, @@ -98,7 +94,6 @@ impl AudioNode for MathNode { fn set_parameter(&mut self, id: u32, value: f32) { match id { PARAM_OPERATION => self.operation = (value as u32).clamp(0, 13), - PARAM_OPERAND => self.operand = value.clamp(-10.0, 10.0), _ => {} } } @@ -106,7 +101,6 @@ impl AudioNode for MathNode { fn get_parameter(&self, id: u32) -> f32 { match id { PARAM_OPERATION => self.operation as f32, - PARAM_OPERAND => self.operand, _ => 0.0, } } @@ -126,27 +120,20 @@ impl AudioNode for MathNode { let output = &mut outputs[0]; let length = output.len(); - // Get input A (or use 0.0) - let input_a = if !inputs.is_empty() && !inputs[0].is_empty() { - inputs[0] - } else { - &[] - }; - - // Get input B (or use operand parameter) - let input_b = if inputs.len() > 1 && !inputs[1].is_empty() { - inputs[1] - } else { - &[] - }; - // Process each sample for i in 0..length { - let a = if i < input_a.len() { input_a[i] } else { 0.0 }; - let b = if i < input_b.len() { - input_b[i] + // Get input A (or 0.0 if not connected) + let a = if !inputs.is_empty() && i < inputs[0].len() { + inputs[0][i] } else { - self.operand + 0.0 + }; + + // Get input B (or 0.0 if not connected) + let b = if inputs.len() > 1 && i < inputs[1].len() { + inputs[1][i] + } else { + 0.0 }; output[i] = self.apply_operation(a, b); @@ -169,7 +156,6 @@ impl AudioNode for MathNode { Box::new(Self { name: self.name.clone(), operation: self.operation, - operand: self.operand, inputs: self.inputs.clone(), outputs: self.outputs.clone(), parameters: self.parameters.clone(), diff --git a/daw-backend/src/audio/node_graph/nodes/mod.rs b/daw-backend/src/audio/node_graph/nodes/mod.rs index 4f190da..252ab77 100644 --- a/daw-backend/src/audio/node_graph/nodes/mod.rs +++ b/daw-backend/src/audio/node_graph/nodes/mod.rs @@ -1,9 +1,12 @@ mod adsr; mod audio_to_cv; +mod bit_crusher; mod chorus; mod compressor; +mod constant; mod delay; mod distortion; +mod envelope_follower; mod eq; mod filter; mod flanger; @@ -21,21 +24,28 @@ mod oscillator; mod oscilloscope; mod output; mod pan; +mod phaser; mod quantizer; mod reverb; +mod ring_modulator; +mod sample_hold; mod simple_sampler; mod slew_limiter; mod splitter; mod template_io; +mod vocoder; mod voice_allocator; mod wavetable_oscillator; pub use adsr::ADSRNode; pub use audio_to_cv::AudioToCVNode; +pub use bit_crusher::BitCrusherNode; pub use chorus::ChorusNode; pub use compressor::CompressorNode; +pub use constant::ConstantNode; pub use delay::DelayNode; pub use distortion::DistortionNode; +pub use envelope_follower::EnvelopeFollowerNode; pub use eq::EQNode; pub use filter::FilterNode; pub use flanger::FlangerNode; @@ -53,11 +63,15 @@ pub use oscillator::OscillatorNode; pub use oscilloscope::OscilloscopeNode; pub use output::AudioOutputNode; pub use pan::PanNode; +pub use phaser::PhaserNode; pub use quantizer::QuantizerNode; pub use reverb::ReverbNode; +pub use ring_modulator::RingModulatorNode; +pub use sample_hold::SampleHoldNode; pub use simple_sampler::SimpleSamplerNode; pub use slew_limiter::SlewLimiterNode; pub use splitter::SplitterNode; pub use template_io::{TemplateInputNode, TemplateOutputNode}; +pub use vocoder::VocoderNode; pub use voice_allocator::VoiceAllocatorNode; pub use wavetable_oscillator::WavetableOscillatorNode; diff --git a/daw-backend/src/audio/node_graph/nodes/phaser.rs b/daw-backend/src/audio/node_graph/nodes/phaser.rs new file mode 100644 index 0000000..d0723df --- /dev/null +++ b/daw-backend/src/audio/node_graph/nodes/phaser.rs @@ -0,0 +1,289 @@ +use crate::audio::node_graph::{AudioNode, NodeCategory, NodePort, Parameter, ParameterUnit, SignalType}; +use crate::audio::midi::MidiEvent; +use std::f32::consts::PI; + +const PARAM_RATE: u32 = 0; +const PARAM_DEPTH: u32 = 1; +const PARAM_STAGES: u32 = 2; +const PARAM_FEEDBACK: u32 = 3; +const PARAM_WET_DRY: u32 = 4; + +const MAX_STAGES: usize = 8; + +/// First-order all-pass filter for phaser +struct AllPassFilter { + a1: f32, + zm1_left: f32, + zm1_right: f32, +} + +impl AllPassFilter { + fn new() -> Self { + Self { + a1: 0.0, + zm1_left: 0.0, + zm1_right: 0.0, + } + } + + fn set_coefficient(&mut self, frequency: f32, sample_rate: f32) { + // First-order all-pass coefficient + // a1 = (tan(π*f/fs) - 1) / (tan(π*f/fs) + 1) + let tan_val = ((PI * frequency) / sample_rate).tan(); + self.a1 = (tan_val - 1.0) / (tan_val + 1.0); + } + + fn process(&mut self, input: f32, is_left: bool) -> f32 { + let zm1 = if is_left { + &mut self.zm1_left + } else { + &mut self.zm1_right + }; + + // All-pass filter: y[n] = a1*x[n] + x[n-1] - a1*y[n-1] + let output = self.a1 * input + *zm1; + *zm1 = input - self.a1 * output; + output + } + + fn reset(&mut self) { + self.zm1_left = 0.0; + self.zm1_right = 0.0; + } +} + +/// Phaser effect using cascaded all-pass filters +pub struct PhaserNode { + name: String, + rate: f32, // LFO rate in Hz (0.1 to 10 Hz) + depth: f32, // Modulation depth 0.0 to 1.0 + stages: usize, // Number of all-pass stages (2, 4, 6, or 8) + feedback: f32, // Feedback amount -0.95 to 0.95 + wet_dry: f32, // 0.0 = dry only, 1.0 = wet only + + // All-pass filters + filters: Vec, + + // Feedback buffers + feedback_left: f32, + feedback_right: f32, + + // LFO state + lfo_phase: f32, + + sample_rate: u32, + + inputs: Vec, + outputs: Vec, + parameters: Vec, +} + +impl PhaserNode { + pub fn new(name: impl Into) -> Self { + let name = name.into(); + + let inputs = vec![ + NodePort::new("Audio In", SignalType::Audio, 0), + ]; + + let outputs = vec![ + NodePort::new("Audio Out", SignalType::Audio, 0), + ]; + + let parameters = vec![ + Parameter::new(PARAM_RATE, "Rate", 0.1, 10.0, 0.5, ParameterUnit::Frequency), + Parameter::new(PARAM_DEPTH, "Depth", 0.0, 1.0, 0.7, ParameterUnit::Generic), + Parameter::new(PARAM_STAGES, "Stages", 2.0, 8.0, 6.0, ParameterUnit::Generic), + Parameter::new(PARAM_FEEDBACK, "Feedback", -0.95, 0.95, 0.5, ParameterUnit::Generic), + Parameter::new(PARAM_WET_DRY, "Wet/Dry", 0.0, 1.0, 0.5, ParameterUnit::Generic), + ]; + + let mut filters = Vec::with_capacity(MAX_STAGES); + for _ in 0..MAX_STAGES { + filters.push(AllPassFilter::new()); + } + + Self { + name, + rate: 0.5, + depth: 0.7, + stages: 6, + feedback: 0.5, + wet_dry: 0.5, + filters, + feedback_left: 0.0, + feedback_right: 0.0, + lfo_phase: 0.0, + sample_rate: 48000, + inputs, + outputs, + parameters, + } + } +} + +impl AudioNode for PhaserNode { + fn category(&self) -> NodeCategory { + NodeCategory::Effect + } + + fn inputs(&self) -> &[NodePort] { + &self.inputs + } + + fn outputs(&self) -> &[NodePort] { + &self.outputs + } + + fn parameters(&self) -> &[Parameter] { + &self.parameters + } + + fn set_parameter(&mut self, id: u32, value: f32) { + match id { + PARAM_RATE => { + self.rate = value.clamp(0.1, 10.0); + } + PARAM_DEPTH => { + self.depth = value.clamp(0.0, 1.0); + } + PARAM_STAGES => { + // Round to even numbers: 2, 4, 6, 8 + let stages = (value.round() as usize).clamp(2, 8); + self.stages = if stages % 2 == 0 { stages } else { stages + 1 }; + } + PARAM_FEEDBACK => { + self.feedback = value.clamp(-0.95, 0.95); + } + PARAM_WET_DRY => { + self.wet_dry = value.clamp(0.0, 1.0); + } + _ => {} + } + } + + fn get_parameter(&self, id: u32) -> f32 { + match id { + PARAM_RATE => self.rate, + PARAM_DEPTH => self.depth, + PARAM_STAGES => self.stages as f32, + PARAM_FEEDBACK => self.feedback, + PARAM_WET_DRY => self.wet_dry, + _ => 0.0, + } + } + + fn process( + &mut self, + inputs: &[&[f32]], + outputs: &mut [&mut [f32]], + _midi_inputs: &[&[MidiEvent]], + _midi_outputs: &mut [&mut Vec], + sample_rate: u32, + ) { + if inputs.is_empty() || outputs.is_empty() { + return; + } + + // Update sample rate if changed + if self.sample_rate != sample_rate { + self.sample_rate = sample_rate; + } + + let input = inputs[0]; + let output = &mut outputs[0]; + + // Audio signals are stereo (interleaved L/R) + let frames = input.len() / 2; + let output_frames = output.len() / 2; + let frames_to_process = frames.min(output_frames); + + let dry_gain = 1.0 - self.wet_dry; + let wet_gain = self.wet_dry; + + // Frequency range for all-pass filters (200 Hz to 2000 Hz) + let min_freq = 200.0; + let max_freq = 2000.0; + + for frame in 0..frames_to_process { + let left_in = input[frame * 2]; + let right_in = input[frame * 2 + 1]; + + // Generate LFO value (sine wave, 0 to 1) + let lfo_value = (self.lfo_phase * 2.0 * PI).sin() * 0.5 + 0.5; + + // Calculate modulated frequency + let frequency = min_freq + (max_freq - min_freq) * lfo_value * self.depth; + + // Update all filter coefficients + for filter in self.filters.iter_mut().take(self.stages) { + filter.set_coefficient(frequency, self.sample_rate as f32); + } + + // Add feedback + let mut left_sig = left_in + self.feedback_left * self.feedback; + let mut right_sig = right_in + self.feedback_right * self.feedback; + + // Process through all-pass filter chain + for i in 0..self.stages { + left_sig = self.filters[i].process(left_sig, true); + right_sig = self.filters[i].process(right_sig, false); + } + + // Store feedback + self.feedback_left = left_sig; + self.feedback_right = right_sig; + + // Mix dry and wet signals + output[frame * 2] = left_in * dry_gain + left_sig * wet_gain; + output[frame * 2 + 1] = right_in * dry_gain + right_sig * wet_gain; + + // Advance LFO phase + self.lfo_phase += self.rate / self.sample_rate as f32; + if self.lfo_phase >= 1.0 { + self.lfo_phase -= 1.0; + } + } + } + + fn reset(&mut self) { + for filter in &mut self.filters { + filter.reset(); + } + self.feedback_left = 0.0; + self.feedback_right = 0.0; + self.lfo_phase = 0.0; + } + + fn node_type(&self) -> &str { + "Phaser" + } + + fn name(&self) -> &str { + &self.name + } + + fn clone_node(&self) -> Box { + let mut filters = Vec::with_capacity(MAX_STAGES); + for _ in 0..MAX_STAGES { + filters.push(AllPassFilter::new()); + } + + Box::new(Self { + name: self.name.clone(), + rate: self.rate, + depth: self.depth, + stages: self.stages, + feedback: self.feedback, + wet_dry: self.wet_dry, + filters, + feedback_left: 0.0, + feedback_right: 0.0, + lfo_phase: 0.0, + sample_rate: self.sample_rate, + inputs: self.inputs.clone(), + outputs: self.outputs.clone(), + parameters: self.parameters.clone(), + }) + } +} diff --git a/daw-backend/src/audio/node_graph/nodes/quantizer.rs b/daw-backend/src/audio/node_graph/nodes/quantizer.rs index c71e211..3bf2331 100644 --- a/daw-backend/src/audio/node_graph/nodes/quantizer.rs +++ b/daw-backend/src/audio/node_graph/nodes/quantizer.rs @@ -83,28 +83,32 @@ impl QuantizerNode { // Clamp to reasonable range let input_midi_note = input_midi_note.clamp(0.0, 127.0); - // Get scale intervals + // Get scale intervals (relative to root) let intervals = self.get_scale_intervals(); // Find which octave we're in (relative to C) let octave = (input_midi_note / 12.0).floor() as i32; - let note_in_octave = (input_midi_note % 12.0) as u32; + let note_in_octave = input_midi_note % 12.0; - // Find the nearest note in the scale + // Adjust note relative to root (e.g., if root is D (2), then C becomes 10, D becomes 0) + let note_relative_to_root = (note_in_octave - self.root_note as f32 + 12.0) % 12.0; + + // Find the nearest note in the scale (scale intervals are relative to root) let mut closest_interval = intervals[0]; - let mut min_distance = (note_in_octave as i32 - closest_interval as i32).abs(); + let mut min_distance = (note_relative_to_root - closest_interval as f32).abs(); for &interval in &intervals { - let distance = (note_in_octave as i32 - interval as i32).abs(); + let distance = (note_relative_to_root - interval as f32).abs(); if distance < min_distance { min_distance = distance; closest_interval = interval; } } - // Calculate final MIDI note (adjusted for root note) - // Start from the octave * 12, add root note, add scale interval - let quantized_midi_note = (octave * 12) as f32 + self.root_note as f32 + closest_interval as f32; + // Calculate final MIDI note + // The scale interval is relative to root, so add root back to get absolute note + let quantized_note_in_octave = (self.root_note + closest_interval) % 12; + let quantized_midi_note = (octave * 12) as f32 + quantized_note_in_octave as f32; // Clamp result let quantized_midi_note = quantized_midi_note.clamp(0.0, 127.0); diff --git a/daw-backend/src/audio/node_graph/nodes/ring_modulator.rs b/daw-backend/src/audio/node_graph/nodes/ring_modulator.rs new file mode 100644 index 0000000..e9f2b61 --- /dev/null +++ b/daw-backend/src/audio/node_graph/nodes/ring_modulator.rs @@ -0,0 +1,137 @@ +use crate::audio::node_graph::{AudioNode, NodeCategory, NodePort, Parameter, ParameterUnit, SignalType}; +use crate::audio::midi::MidiEvent; + +const PARAM_MIX: u32 = 0; + +/// Ring Modulator - multiplies two signals together +/// Creates metallic, inharmonic timbres by multiplying carrier and modulator +pub struct RingModulatorNode { + name: String, + mix: f32, // 0.0 = dry (carrier only), 1.0 = fully modulated + inputs: Vec, + outputs: Vec, + parameters: Vec, +} + +impl RingModulatorNode { + pub fn new(name: impl Into) -> Self { + let name = name.into(); + + let inputs = vec![ + NodePort::new("Carrier", SignalType::Audio, 0), + NodePort::new("Modulator", SignalType::Audio, 1), + ]; + + let outputs = vec![ + NodePort::new("Audio Out", SignalType::Audio, 0), + ]; + + let parameters = vec![ + Parameter::new(PARAM_MIX, "Mix", 0.0, 1.0, 1.0, ParameterUnit::Generic), + ]; + + Self { + name, + mix: 1.0, + inputs, + outputs, + parameters, + } + } +} + +impl AudioNode for RingModulatorNode { + fn category(&self) -> NodeCategory { + NodeCategory::Effect + } + + fn inputs(&self) -> &[NodePort] { + &self.inputs + } + + fn outputs(&self) -> &[NodePort] { + &self.outputs + } + + fn parameters(&self) -> &[Parameter] { + &self.parameters + } + + fn set_parameter(&mut self, id: u32, value: f32) { + match id { + PARAM_MIX => self.mix = value.clamp(0.0, 1.0), + _ => {} + } + } + + fn get_parameter(&self, id: u32) -> f32 { + match id { + PARAM_MIX => self.mix, + _ => 0.0, + } + } + + fn process( + &mut self, + inputs: &[&[f32]], + outputs: &mut [&mut [f32]], + _midi_inputs: &[&[MidiEvent]], + _midi_outputs: &mut [&mut Vec], + _sample_rate: u32, + ) { + if outputs.is_empty() { + return; + } + + let output = &mut outputs[0]; + let length = output.len(); + + // Get carrier input + let carrier = if !inputs.is_empty() && !inputs[0].is_empty() { + inputs[0] + } else { + &[] + }; + + // Get modulator input + let modulator = if inputs.len() > 1 && !inputs[1].is_empty() { + inputs[1] + } else { + &[] + }; + + // Process each sample + for i in 0..length { + let carrier_sample = if i < carrier.len() { carrier[i] } else { 0.0 }; + let modulator_sample = if i < modulator.len() { modulator[i] } else { 0.0 }; + + // Ring modulation: multiply the two signals + let modulated = carrier_sample * modulator_sample; + + // Mix between dry (carrier) and wet (modulated) + output[i] = carrier_sample * (1.0 - self.mix) + modulated * self.mix; + } + } + + fn reset(&mut self) { + // No state to reset + } + + fn node_type(&self) -> &str { + "RingModulator" + } + + fn name(&self) -> &str { + &self.name + } + + fn clone_node(&self) -> Box { + Box::new(Self { + name: self.name.clone(), + mix: self.mix, + inputs: self.inputs.clone(), + outputs: self.outputs.clone(), + parameters: self.parameters.clone(), + }) + } +} diff --git a/daw-backend/src/audio/node_graph/nodes/sample_hold.rs b/daw-backend/src/audio/node_graph/nodes/sample_hold.rs new file mode 100644 index 0000000..dc5b8d0 --- /dev/null +++ b/daw-backend/src/audio/node_graph/nodes/sample_hold.rs @@ -0,0 +1,137 @@ +use crate::audio::node_graph::{AudioNode, NodeCategory, NodePort, Parameter, SignalType}; +use crate::audio::midi::MidiEvent; + +/// Sample & Hold - samples input CV when triggered by a gate signal +/// Classic modular synth utility for creating stepped sequences +pub struct SampleHoldNode { + name: String, + held_value: f32, + last_gate: f32, + inputs: Vec, + outputs: Vec, + parameters: Vec, +} + +impl SampleHoldNode { + pub fn new(name: impl Into) -> Self { + let name = name.into(); + + let inputs = vec![ + NodePort::new("CV In", SignalType::CV, 0), + NodePort::new("Gate In", SignalType::CV, 1), + ]; + + let outputs = vec![ + NodePort::new("CV Out", SignalType::CV, 0), + ]; + + let parameters = vec![]; + + Self { + name, + held_value: 0.0, + last_gate: 0.0, + inputs, + outputs, + parameters, + } + } +} + +impl AudioNode for SampleHoldNode { + fn category(&self) -> NodeCategory { + NodeCategory::Utility + } + + fn inputs(&self) -> &[NodePort] { + &self.inputs + } + + fn outputs(&self) -> &[NodePort] { + &self.outputs + } + + fn parameters(&self) -> &[Parameter] { + &self.parameters + } + + fn set_parameter(&mut self, _id: u32, _value: f32) { + // No parameters + } + + fn get_parameter(&self, _id: u32) -> f32 { + 0.0 + } + + fn process( + &mut self, + inputs: &[&[f32]], + outputs: &mut [&mut [f32]], + _midi_inputs: &[&[MidiEvent]], + _midi_outputs: &mut [&mut Vec], + _sample_rate: u32, + ) { + if outputs.is_empty() { + return; + } + + let output = &mut outputs[0]; + let length = output.len(); + + // Get CV input + let cv_input = if !inputs.is_empty() && !inputs[0].is_empty() { + inputs[0] + } else { + &[] + }; + + // Get Gate input + let gate_input = if inputs.len() > 1 && !inputs[1].is_empty() { + inputs[1] + } else { + &[] + }; + + // Process each sample + for i in 0..length { + let cv = if i < cv_input.len() { cv_input[i] } else { 0.0 }; + let gate = if i < gate_input.len() { gate_input[i] } else { 0.0 }; + + // Detect rising edge (trigger) + let gate_active = gate > 0.5; + let last_gate_active = self.last_gate > 0.5; + + if gate_active && !last_gate_active { + // Rising edge detected - sample the input + self.held_value = cv; + } + + self.last_gate = gate; + output[i] = self.held_value; + } + } + + fn reset(&mut self) { + self.held_value = 0.0; + self.last_gate = 0.0; + } + + fn node_type(&self) -> &str { + "SampleHold" + } + + fn name(&self) -> &str { + &self.name + } + + fn clone_node(&self) -> Box { + Box::new(Self { + name: self.name.clone(), + held_value: self.held_value, + last_gate: self.last_gate, + inputs: self.inputs.clone(), + outputs: self.outputs.clone(), + parameters: self.parameters.clone(), + }) + } +} diff --git a/daw-backend/src/audio/node_graph/nodes/vocoder.rs b/daw-backend/src/audio/node_graph/nodes/vocoder.rs new file mode 100644 index 0000000..8240048 --- /dev/null +++ b/daw-backend/src/audio/node_graph/nodes/vocoder.rs @@ -0,0 +1,362 @@ +use crate::audio::node_graph::{AudioNode, NodeCategory, NodePort, Parameter, ParameterUnit, SignalType}; +use crate::audio::midi::MidiEvent; +use std::f32::consts::PI; + +const PARAM_BANDS: u32 = 0; +const PARAM_ATTACK: u32 = 1; +const PARAM_RELEASE: u32 = 2; +const PARAM_MIX: u32 = 3; + +const MAX_BANDS: usize = 32; + +/// Simple bandpass filter using biquad +struct BandpassFilter { + // Biquad coefficients + b0: f32, + b1: f32, + b2: f32, + a1: f32, + a2: f32, + + // State variables (separate for modulator and carrier, L/R channels) + mod_z1_left: f32, + mod_z2_left: f32, + mod_z1_right: f32, + mod_z2_right: f32, + car_z1_left: f32, + car_z2_left: f32, + car_z1_right: f32, + car_z2_right: f32, +} + +impl BandpassFilter { + fn new() -> Self { + Self { + b0: 0.0, + b1: 0.0, + b2: 0.0, + a1: 0.0, + a2: 0.0, + mod_z1_left: 0.0, + mod_z2_left: 0.0, + mod_z1_right: 0.0, + mod_z2_right: 0.0, + car_z1_left: 0.0, + car_z2_left: 0.0, + car_z1_right: 0.0, + car_z2_right: 0.0, + } + } + + fn set_bandpass(&mut self, frequency: f32, q: f32, sample_rate: f32) { + let omega = 2.0 * PI * frequency / sample_rate; + let sin_omega = omega.sin(); + let cos_omega = omega.cos(); + let alpha = sin_omega / (2.0 * q); + + let a0 = 1.0 + alpha; + self.b0 = alpha / a0; + self.b1 = 0.0; + self.b2 = -alpha / a0; + self.a1 = -2.0 * cos_omega / a0; + self.a2 = (1.0 - alpha) / a0; + } + + fn process_modulator(&mut self, input: f32, is_left: bool) -> f32 { + let (z1, z2) = if is_left { + (&mut self.mod_z1_left, &mut self.mod_z2_left) + } else { + (&mut self.mod_z1_right, &mut self.mod_z2_right) + }; + + let output = self.b0 * input + self.b1 * *z1 + self.b2 * *z2 - self.a1 * *z1 - self.a2 * *z2; + *z2 = *z1; + *z1 = output; + output + } + + fn process_carrier(&mut self, input: f32, is_left: bool) -> f32 { + let (z1, z2) = if is_left { + (&mut self.car_z1_left, &mut self.car_z2_left) + } else { + (&mut self.car_z1_right, &mut self.car_z2_right) + }; + + let output = self.b0 * input + self.b1 * *z1 + self.b2 * *z2 - self.a1 * *z1 - self.a2 * *z2; + *z2 = *z1; + *z1 = output; + output + } + + fn reset(&mut self) { + self.mod_z1_left = 0.0; + self.mod_z2_left = 0.0; + self.mod_z1_right = 0.0; + self.mod_z2_right = 0.0; + self.car_z1_left = 0.0; + self.car_z2_left = 0.0; + self.car_z1_right = 0.0; + self.car_z2_right = 0.0; + } +} + +/// Vocoder band with filter and envelope follower +struct VocoderBand { + filter: BandpassFilter, + envelope_left: f32, + envelope_right: f32, +} + +impl VocoderBand { + fn new() -> Self { + Self { + filter: BandpassFilter::new(), + envelope_left: 0.0, + envelope_right: 0.0, + } + } + + fn reset(&mut self) { + self.filter.reset(); + self.envelope_left = 0.0; + self.envelope_right = 0.0; + } +} + +/// Vocoder effect - imposes spectral envelope of modulator onto carrier +pub struct VocoderNode { + name: String, + num_bands: usize, // 8 to 32 bands + attack_time: f32, // 0.001 to 0.1 seconds + release_time: f32, // 0.001 to 1.0 seconds + mix: f32, // 0.0 to 1.0 + + bands: Vec, + + sample_rate: u32, + + inputs: Vec, + outputs: Vec, + parameters: Vec, +} + +impl VocoderNode { + pub fn new(name: impl Into) -> Self { + let name = name.into(); + + let inputs = vec![ + NodePort::new("Modulator", SignalType::Audio, 0), + NodePort::new("Carrier", SignalType::Audio, 1), + ]; + + let outputs = vec![ + NodePort::new("Audio Out", SignalType::Audio, 0), + ]; + + let parameters = vec![ + Parameter::new(PARAM_BANDS, "Bands", 8.0, 32.0, 16.0, ParameterUnit::Generic), + Parameter::new(PARAM_ATTACK, "Attack", 0.001, 0.1, 0.01, ParameterUnit::Time), + Parameter::new(PARAM_RELEASE, "Release", 0.001, 1.0, 0.05, ParameterUnit::Time), + Parameter::new(PARAM_MIX, "Mix", 0.0, 1.0, 1.0, ParameterUnit::Generic), + ]; + + let mut bands = Vec::with_capacity(MAX_BANDS); + for _ in 0..MAX_BANDS { + bands.push(VocoderBand::new()); + } + + Self { + name, + num_bands: 16, + attack_time: 0.01, + release_time: 0.05, + mix: 1.0, + bands, + sample_rate: 48000, + inputs, + outputs, + parameters, + } + } + + fn setup_bands(&mut self) { + // Distribute bands logarithmically from 200 Hz to 5000 Hz + let min_freq: f32 = 200.0; + let max_freq: f32 = 5000.0; + let q: f32 = 4.0; // Fairly narrow bands + + for i in 0..self.num_bands { + let t = i as f32 / (self.num_bands - 1) as f32; + let freq = min_freq * (max_freq / min_freq).powf(t); + self.bands[i].filter.set_bandpass(freq, q, self.sample_rate as f32); + } + } +} + +impl AudioNode for VocoderNode { + fn category(&self) -> NodeCategory { + NodeCategory::Effect + } + + fn inputs(&self) -> &[NodePort] { + &self.inputs + } + + fn outputs(&self) -> &[NodePort] { + &self.outputs + } + + fn parameters(&self) -> &[Parameter] { + &self.parameters + } + + fn set_parameter(&mut self, id: u32, value: f32) { + match id { + PARAM_BANDS => { + let bands = (value.round() as usize).clamp(8, 32); + if bands != self.num_bands { + self.num_bands = bands; + self.setup_bands(); + } + } + PARAM_ATTACK => self.attack_time = value.clamp(0.001, 0.1), + PARAM_RELEASE => self.release_time = value.clamp(0.001, 1.0), + PARAM_MIX => self.mix = value.clamp(0.0, 1.0), + _ => {} + } + } + + fn get_parameter(&self, id: u32) -> f32 { + match id { + PARAM_BANDS => self.num_bands as f32, + PARAM_ATTACK => self.attack_time, + PARAM_RELEASE => self.release_time, + PARAM_MIX => self.mix, + _ => 0.0, + } + } + + fn process( + &mut self, + inputs: &[&[f32]], + outputs: &mut [&mut [f32]], + _midi_inputs: &[&[MidiEvent]], + _midi_outputs: &mut [&mut Vec], + sample_rate: u32, + ) { + if inputs.len() < 2 || outputs.is_empty() { + return; + } + + // Update sample rate if changed + if self.sample_rate != sample_rate { + self.sample_rate = sample_rate; + self.setup_bands(); + } + + let modulator = inputs[0]; + let carrier = inputs[1]; + let output = &mut outputs[0]; + + // Audio signals are stereo (interleaved L/R) + let mod_frames = modulator.len() / 2; + let car_frames = carrier.len() / 2; + let out_frames = output.len() / 2; + let frames_to_process = mod_frames.min(car_frames).min(out_frames); + + // Calculate envelope follower coefficients + let sample_duration = 1.0 / self.sample_rate as f32; + let attack_coeff = (sample_duration / self.attack_time).min(1.0); + let release_coeff = (sample_duration / self.release_time).min(1.0); + + for frame in 0..frames_to_process { + let mod_left = modulator[frame * 2]; + let mod_right = modulator[frame * 2 + 1]; + let car_left = carrier[frame * 2]; + let car_right = carrier[frame * 2 + 1]; + + let mut out_left = 0.0; + let mut out_right = 0.0; + + // Process each band + for i in 0..self.num_bands { + let band = &mut self.bands[i]; + + // Filter modulator and carrier through bandpass + let mod_band_left = band.filter.process_modulator(mod_left, true); + let mod_band_right = band.filter.process_modulator(mod_right, false); + let car_band_left = band.filter.process_carrier(car_left, true); + let car_band_right = band.filter.process_carrier(car_right, false); + + // Extract envelope from modulator band (rectify + smooth) + let mod_level_left = mod_band_left.abs(); + let mod_level_right = mod_band_right.abs(); + + // Envelope follower + let coeff_left = if mod_level_left > band.envelope_left { + attack_coeff + } else { + release_coeff + }; + let coeff_right = if mod_level_right > band.envelope_right { + attack_coeff + } else { + release_coeff + }; + + band.envelope_left += (mod_level_left - band.envelope_left) * coeff_left; + band.envelope_right += (mod_level_right - band.envelope_right) * coeff_right; + + // Apply envelope to carrier band + out_left += car_band_left * band.envelope_left; + out_right += car_band_right * band.envelope_right; + } + + // Normalize output (roughly compensate for band summing) + let norm_factor = 1.0 / (self.num_bands as f32).sqrt(); + out_left *= norm_factor; + out_right *= norm_factor; + + // Mix with carrier (dry signal) + output[frame * 2] = car_left * (1.0 - self.mix) + out_left * self.mix; + output[frame * 2 + 1] = car_right * (1.0 - self.mix) + out_right * self.mix; + } + } + + fn reset(&mut self) { + for band in &mut self.bands { + band.reset(); + } + } + + fn node_type(&self) -> &str { + "Vocoder" + } + + fn name(&self) -> &str { + &self.name + } + + fn clone_node(&self) -> Box { + let mut bands = Vec::with_capacity(MAX_BANDS); + for _ in 0..MAX_BANDS { + bands.push(VocoderBand::new()); + } + + let mut node = Self { + name: self.name.clone(), + num_bands: self.num_bands, + attack_time: self.attack_time, + release_time: self.release_time, + mix: self.mix, + bands, + sample_rate: self.sample_rate, + inputs: self.inputs.clone(), + outputs: self.outputs.clone(), + parameters: self.parameters.clone(), + }; + + node.setup_bands(); + Box::new(node) + } +} diff --git a/src/main.js b/src/main.js index 64fc96d..90ec49a 100644 --- a/src/main.js +++ b/src/main.js @@ -7467,6 +7467,87 @@ function nodeEditor() { }); }); + // Handle number inputs + const numberInputs = nodeElement.querySelectorAll('input[type="number"][data-param]'); + numberInputs.forEach(numberInput => { + // Track parameter change action for undo/redo + let paramAction = null; + + // Prevent node dragging when interacting with number input + numberInput.addEventListener("mousedown", (e) => { + e.stopPropagation(); + + // Initialize undo action + const paramId = parseInt(e.target.getAttribute("data-param")); + const currentValue = parseFloat(e.target.value); + const nodeData = editor.getNodeFromId(nodeId); + + if (nodeData && nodeData.data.backendId !== null) { + const currentTrackId = getCurrentMidiTrack(); + if (currentTrackId !== null) { + paramAction = actions.graphSetParameter.initialize( + currentTrackId, + nodeData.data.backendId, + paramId, + nodeId, + currentValue + ); + } + } + }); + numberInput.addEventListener("pointerdown", (e) => { + e.stopPropagation(); + }); + + numberInput.addEventListener("input", (e) => { + const paramId = parseInt(e.target.getAttribute("data-param")); + let value = parseFloat(e.target.value); + + // Validate and clamp to min/max + const min = parseFloat(e.target.min); + const max = parseFloat(e.target.max); + if (!isNaN(value)) { + value = Math.max(min, Math.min(max, value)); + } else { + value = 0; + } + + console.log(`[setupNodeParameters] Number input - nodeId: ${nodeId}, paramId: ${paramId}, value: ${value}`); + + // Update corresponding slider + const slider = nodeElement.querySelector(`input[type="range"][data-param="${paramId}"]`); + if (slider) { + slider.value = value; + } + + // Send to backend + const nodeData = editor.getNodeFromId(nodeId); + if (nodeData && nodeData.data.backendId !== null) { + const currentTrackId = getCurrentMidiTrack(); + if (currentTrackId !== null) { + invoke("graph_set_parameter", { + trackId: currentTrackId, + nodeId: nodeData.data.backendId, + paramId: paramId, + value: value + }).catch(err => { + console.error("Failed to set parameter:", err); + }); + } + } + }); + + numberInput.addEventListener("change", (e) => { + const value = parseFloat(e.target.value); + + // Finalize undo action + if (paramAction) { + actions.graphSetParameter.finalize(paramAction, value); + paramAction = null; + } + }); + }); + // Handle Load Sample button for SimpleSampler const loadSampleBtn = nodeElement.querySelector(".load-sample-btn"); if (loadSampleBtn) { diff --git a/src/nodeTypes.js b/src/nodeTypes.js index 449008c..16067ee 100644 --- a/src/nodeTypes.js +++ b/src/nodeTypes.js @@ -1015,6 +1015,29 @@ export const nodeTypes = { ` }, + Constant: { + name: 'Constant', + category: NodeCategory.UTILITY, + description: 'Constant CV source - outputs a fixed voltage', + inputs: [], + outputs: [ + { name: 'CV Out', type: SignalType.CV, index: 0 } + ], + parameters: [ + { id: 0, name: 'value', label: 'Value', min: -10, max: 10, default: 0, unit: '' } + ], + getHTML: (nodeId) => ` +
+
Constant
+
+ + + +
+
+ ` + }, + Math: { name: 'Math', category: NodeCategory.UTILITY, @@ -1027,14 +1050,13 @@ export const nodeTypes = { { name: 'CV Out', type: SignalType.CV, index: 0 } ], parameters: [ - { id: 0, name: 'operation', label: 'Operation', min: 0, max: 13, default: 0, unit: '' }, - { id: 1, name: 'operand', label: 'Operand', min: -10, max: 10, default: 1, unit: '' } + { id: 0, name: 'operation', label: 'Operation', min: 0, max: 13, default: 0, unit: '' } ], getHTML: (nodeId) => `
Math
- +
-
- - -
` }, @@ -1079,7 +1097,7 @@ export const nodeTypes = {
Quantizer
- +
- + +
+
+ + +
+
+ ` + }, + + RingModulator: { + name: 'Ring Modulator', + category: NodeCategory.EFFECT, + description: 'Multiplies carrier and modulator for metallic timbres', + inputs: [ + { name: 'Carrier', type: SignalType.AUDIO, index: 0 }, + { name: 'Modulator', type: SignalType.AUDIO, index: 1 } + ], + outputs: [ + { name: 'Audio Out', type: SignalType.AUDIO, index: 0 } + ], + parameters: [ + { id: 0, name: 'mix', label: 'Mix', min: 0.0, max: 1.0, default: 1.0, unit: '' } + ], + getHTML: (nodeId) => ` +
+
Ring Modulator
+
+ + +
+
+ ` + }, + + Phaser: { + name: 'Phaser', + category: NodeCategory.EFFECT, + description: 'Sweeping all-pass filters for phase shifting effect', + inputs: [ + { name: 'Audio In', type: SignalType.AUDIO, index: 0 } + ], + outputs: [ + { name: 'Audio Out', type: SignalType.AUDIO, index: 0 } + ], + parameters: [ + { id: 0, name: 'rate', label: 'Rate', min: 0.1, max: 10.0, default: 0.5, unit: 'Hz' }, + { id: 1, name: 'depth', label: 'Depth', min: 0.0, max: 1.0, default: 0.7, unit: '' }, + { id: 2, name: 'stages', label: 'Stages', min: 2, max: 8, default: 6, unit: '' }, + { id: 3, name: 'feedback', label: 'Feedback', min: -0.95, max: 0.95, default: 0.5, unit: '' }, + { id: 4, name: 'wetdry', label: 'Wet/Dry', min: 0.0, max: 1.0, default: 0.5, unit: '' } + ], + getHTML: (nodeId) => ` +
+
Phaser
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ ` + }, + + BitCrusher: { + name: 'Bit Crusher', + category: NodeCategory.EFFECT, + description: 'Lo-fi effect via bit depth and sample rate reduction', + inputs: [ + { name: 'Audio In', type: SignalType.AUDIO, index: 0 } + ], + outputs: [ + { name: 'Audio Out', type: SignalType.AUDIO, index: 0 } + ], + parameters: [ + { id: 0, name: 'bitdepth', label: 'Bit Depth', min: 1, max: 16, default: 8, unit: 'bits' }, + { id: 1, name: 'samplerate', label: 'Sample Rate', min: 100, max: 48000, default: 8000, unit: 'Hz' }, + { id: 2, name: 'mix', label: 'Mix', min: 0.0, max: 1.0, default: 1.0, unit: '' } + ], + getHTML: (nodeId) => ` +
+
Bit Crusher
+
+ + +
+
+ + +
+
+ + +
+
+ ` + }, + + Vocoder: { + name: 'Vocoder', + category: NodeCategory.EFFECT, + description: 'Multi-band vocoder - modulator controls carrier spectrum', + inputs: [ + { name: 'Modulator', type: SignalType.AUDIO, index: 0 }, + { name: 'Carrier', type: SignalType.AUDIO, index: 1 } + ], + outputs: [ + { name: 'Audio Out', type: SignalType.AUDIO, index: 0 } + ], + parameters: [ + { id: 0, name: 'bands', label: 'Bands', min: 8, max: 32, default: 16, unit: '' }, + { id: 1, name: 'attack', label: 'Attack', min: 0.001, max: 0.1, default: 0.01, unit: 's' }, + { id: 2, name: 'release', label: 'Release', min: 0.001, max: 1.0, default: 0.05, unit: 's' }, + { id: 3, name: 'mix', label: 'Mix', min: 0.0, max: 1.0, default: 1.0, unit: '' } + ], + getHTML: (nodeId) => ` +
+
Vocoder
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ ` } };