Add bit crusher, constant, math, envelope follower, phaser, ring modulator, sample and hold, and vocoder nodes
This commit is contained in:
parent
dc32fc4200
commit
0ae168cbca
|
|
@ -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())),
|
||||
|
|
|
|||
|
|
@ -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")),
|
||||
|
|
|
|||
|
|
@ -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<NodePort>,
|
||||
outputs: Vec<NodePort>,
|
||||
parameters: Vec<Parameter>,
|
||||
}
|
||||
|
||||
impl BitCrusherNode {
|
||||
pub fn new(name: impl Into<String>) -> 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<MidiEvent>],
|
||||
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<dyn AudioNode> {
|
||||
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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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<NodePort>,
|
||||
outputs: Vec<NodePort>,
|
||||
parameters: Vec<Parameter>,
|
||||
}
|
||||
|
||||
impl ConstantNode {
|
||||
pub fn new(name: impl Into<String>) -> 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<MidiEvent>],
|
||||
_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<dyn AudioNode> {
|
||||
Box::new(Self {
|
||||
name: self.name.clone(),
|
||||
value: self.value,
|
||||
inputs: self.inputs.clone(),
|
||||
outputs: self.outputs.clone(),
|
||||
parameters: self.parameters.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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<NodePort>,
|
||||
outputs: Vec<NodePort>,
|
||||
parameters: Vec<Parameter>,
|
||||
}
|
||||
|
||||
impl EnvelopeFollowerNode {
|
||||
pub fn new(name: impl Into<String>) -> 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<MidiEvent>],
|
||||
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<dyn AudioNode> {
|
||||
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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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<NodePort>,
|
||||
outputs: Vec<NodePort>,
|
||||
parameters: Vec<Parameter>,
|
||||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<AllPassFilter>,
|
||||
|
||||
// Feedback buffers
|
||||
feedback_left: f32,
|
||||
feedback_right: f32,
|
||||
|
||||
// LFO state
|
||||
lfo_phase: f32,
|
||||
|
||||
sample_rate: u32,
|
||||
|
||||
inputs: Vec<NodePort>,
|
||||
outputs: Vec<NodePort>,
|
||||
parameters: Vec<Parameter>,
|
||||
}
|
||||
|
||||
impl PhaserNode {
|
||||
pub fn new(name: impl Into<String>) -> 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<MidiEvent>],
|
||||
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<dyn AudioNode> {
|
||||
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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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<NodePort>,
|
||||
outputs: Vec<NodePort>,
|
||||
parameters: Vec<Parameter>,
|
||||
}
|
||||
|
||||
impl RingModulatorNode {
|
||||
pub fn new(name: impl Into<String>) -> 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<MidiEvent>],
|
||||
_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<dyn AudioNode> {
|
||||
Box::new(Self {
|
||||
name: self.name.clone(),
|
||||
mix: self.mix,
|
||||
inputs: self.inputs.clone(),
|
||||
outputs: self.outputs.clone(),
|
||||
parameters: self.parameters.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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<NodePort>,
|
||||
outputs: Vec<NodePort>,
|
||||
parameters: Vec<Parameter>,
|
||||
}
|
||||
|
||||
impl SampleHoldNode {
|
||||
pub fn new(name: impl Into<String>) -> 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<MidiEvent>],
|
||||
_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<dyn AudioNode> {
|
||||
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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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<VocoderBand>,
|
||||
|
||||
sample_rate: u32,
|
||||
|
||||
inputs: Vec<NodePort>,
|
||||
outputs: Vec<NodePort>,
|
||||
parameters: Vec<Parameter>,
|
||||
}
|
||||
|
||||
impl VocoderNode {
|
||||
pub fn new(name: impl Into<String>) -> 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<MidiEvent>],
|
||||
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<dyn AudioNode> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
81
src/main.js
81
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) {
|
||||
|
|
|
|||
230
src/nodeTypes.js
230
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) => `
|
||||
<div class="node-content">
|
||||
<div class="node-title">Constant</div>
|
||||
<div class="node-param">
|
||||
<label>Value:</label>
|
||||
<input type="number" class="node-number-input" data-node="${nodeId}" data-param="0" min="-10" max="10" value="0" step="0.01" style="width: 60px; padding: 2px;">
|
||||
<input type="range" data-node="${nodeId}" data-param="0" min="-10" max="10" value="0" step="0.01">
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
},
|
||||
|
||||
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) => `
|
||||
<div class="node-content">
|
||||
<div class="node-title">Math</div>
|
||||
<div class="node-param">
|
||||
<label>Op: <span id="mathop-${nodeId}">Add</span></label>
|
||||
<label>Op: <span id="operation-${nodeId}">Add</span></label>
|
||||
<select class="node-select" data-node="${nodeId}" data-param="0" style="width: 100%; padding: 2px;">
|
||||
<option value="0">Add</option>
|
||||
<option value="1">Subtract</option>
|
||||
|
|
@ -1052,10 +1074,6 @@ export const nodeTypes = {
|
|||
<option value="13">Equal</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="node-param">
|
||||
<label>B: <span id="mathoperand-${nodeId}">1.0</span></label>
|
||||
<input type="range" data-node="${nodeId}" data-param="1" min="-10" max="10" value="1" step="0.1">
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
},
|
||||
|
|
@ -1079,7 +1097,7 @@ export const nodeTypes = {
|
|||
<div class="node-content">
|
||||
<div class="node-title">Quantizer</div>
|
||||
<div class="node-param">
|
||||
<label>Scale: <span id="quantscale-${nodeId}">Chromatic</span></label>
|
||||
<label>Scale: <span id="scale-${nodeId}">Chromatic</span></label>
|
||||
<select class="node-select" data-node="${nodeId}" data-param="0" style="width: 100%; padding: 2px;">
|
||||
<option value="0">Chromatic</option>
|
||||
<option value="1">Major</option>
|
||||
|
|
@ -1095,7 +1113,7 @@ export const nodeTypes = {
|
|||
</select>
|
||||
</div>
|
||||
<div class="node-param">
|
||||
<label>Root: <span id="quantroot-${nodeId}">C</span></label>
|
||||
<label>Root: <span id="root-${nodeId}">C</span></label>
|
||||
<select class="node-select" data-node="${nodeId}" data-param="1" style="width: 100%; padding: 2px;">
|
||||
<option value="0">C</option>
|
||||
<option value="1">C#</option>
|
||||
|
|
@ -1199,6 +1217,200 @@ export const nodeTypes = {
|
|||
</div>
|
||||
</div>
|
||||
`
|
||||
},
|
||||
|
||||
SampleHold: {
|
||||
name: 'Sample & Hold',
|
||||
category: NodeCategory.UTILITY,
|
||||
description: 'Samples CV input when gate signal goes high',
|
||||
inputs: [
|
||||
{ name: 'CV In', type: SignalType.CV, index: 0 },
|
||||
{ name: 'Gate In', type: SignalType.CV, index: 1 }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'CV Out', type: SignalType.CV, index: 0 }
|
||||
],
|
||||
parameters: [],
|
||||
getHTML: (nodeId) => `
|
||||
<div class="node-content">
|
||||
<div class="node-title">Sample & Hold</div>
|
||||
<div style="padding: 8px; font-size: 11px; color: #888;">
|
||||
Samples CV input<br>on gate rising edge
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
},
|
||||
|
||||
EnvelopeFollower: {
|
||||
name: 'Envelope Follower',
|
||||
category: NodeCategory.UTILITY,
|
||||
description: 'Extracts amplitude envelope from audio signal',
|
||||
inputs: [
|
||||
{ name: 'Audio In', type: SignalType.AUDIO, index: 0 }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'CV Out', type: SignalType.CV, index: 0 }
|
||||
],
|
||||
parameters: [
|
||||
{ id: 0, name: 'attack', label: 'Attack', min: 0.001, max: 1.0, default: 0.01, unit: 's' },
|
||||
{ id: 1, name: 'release', label: 'Release', min: 0.001, max: 1.0, default: 0.1, unit: 's' }
|
||||
],
|
||||
getHTML: (nodeId) => `
|
||||
<div class="node-content">
|
||||
<div class="node-title">Envelope Follower</div>
|
||||
<div class="node-param">
|
||||
<label>Attack: <span id="attack-${nodeId}">0.01</span> s</label>
|
||||
<input type="range" data-node="${nodeId}" data-param="0" min="0.001" max="1.0" value="0.01" step="0.001">
|
||||
</div>
|
||||
<div class="node-param">
|
||||
<label>Release: <span id="release-${nodeId}">0.1</span> s</label>
|
||||
<input type="range" data-node="${nodeId}" data-param="1" min="0.001" max="1.0" value="0.1" step="0.001">
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
},
|
||||
|
||||
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) => `
|
||||
<div class="node-content">
|
||||
<div class="node-title">Ring Modulator</div>
|
||||
<div class="node-param">
|
||||
<label>Mix: <span id="mix-${nodeId}">1.00</span></label>
|
||||
<input type="range" data-node="${nodeId}" data-param="0" min="0.0" max="1.0" value="1.0" step="0.01">
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
},
|
||||
|
||||
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) => `
|
||||
<div class="node-content">
|
||||
<div class="node-title">Phaser</div>
|
||||
<div class="node-param">
|
||||
<label>Rate: <span id="rate-${nodeId}">0.5</span> Hz</label>
|
||||
<input type="range" data-node="${nodeId}" data-param="0" min="0.1" max="10.0" value="0.5" step="0.1">
|
||||
</div>
|
||||
<div class="node-param">
|
||||
<label>Depth: <span id="depth-${nodeId}">0.7</span></label>
|
||||
<input type="range" data-node="${nodeId}" data-param="1" min="0.0" max="1.0" value="0.7" step="0.01">
|
||||
</div>
|
||||
<div class="node-param">
|
||||
<label>Stages: <span id="stages-${nodeId}">6</span></label>
|
||||
<input type="range" data-node="${nodeId}" data-param="2" min="2" max="8" value="6" step="2">
|
||||
</div>
|
||||
<div class="node-param">
|
||||
<label>Feedback: <span id="feedback-${nodeId}">0.5</span></label>
|
||||
<input type="range" data-node="${nodeId}" data-param="3" min="-0.95" max="0.95" value="0.5" step="0.01">
|
||||
</div>
|
||||
<div class="node-param">
|
||||
<label>Wet/Dry: <span id="wetdry-${nodeId}">0.5</span></label>
|
||||
<input type="range" data-node="${nodeId}" data-param="4" min="0.0" max="1.0" value="0.5" step="0.01">
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
},
|
||||
|
||||
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) => `
|
||||
<div class="node-content">
|
||||
<div class="node-title">Bit Crusher</div>
|
||||
<div class="node-param">
|
||||
<label>Bit Depth: <span id="bitdepth-${nodeId}">8</span> bits</label>
|
||||
<input type="range" data-node="${nodeId}" data-param="0" min="1" max="16" value="8" step="1">
|
||||
</div>
|
||||
<div class="node-param">
|
||||
<label>Sample Rate: <span id="samplerate-${nodeId}">8000</span> Hz</label>
|
||||
<input type="range" data-node="${nodeId}" data-param="1" min="100" max="48000" value="8000" step="100">
|
||||
</div>
|
||||
<div class="node-param">
|
||||
<label>Mix: <span id="mix-${nodeId}">1.0</span></label>
|
||||
<input type="range" data-node="${nodeId}" data-param="2" min="0.0" max="1.0" value="1.0" step="0.01">
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
},
|
||||
|
||||
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) => `
|
||||
<div class="node-content">
|
||||
<div class="node-title">Vocoder</div>
|
||||
<div class="node-param">
|
||||
<label>Bands: <span id="bands-${nodeId}">16</span></label>
|
||||
<input type="range" data-node="${nodeId}" data-param="0" min="8" max="32" value="16" step="1">
|
||||
</div>
|
||||
<div class="node-param">
|
||||
<label>Attack: <span id="attack-${nodeId}">0.01</span> s</label>
|
||||
<input type="range" data-node="${nodeId}" data-param="1" min="0.001" max="0.1" value="0.01" step="0.001">
|
||||
</div>
|
||||
<div class="node-param">
|
||||
<label>Release: <span id="release-${nodeId}">0.05</span> s</label>
|
||||
<input type="range" data-node="${nodeId}" data-param="2" min="0.001" max="1.0" value="0.05" step="0.001">
|
||||
</div>
|
||||
<div class="node-param">
|
||||
<label>Mix: <span id="mix-${nodeId}">1.0</span></label>
|
||||
<input type="range" data-node="${nodeId}" data-param="3" min="0.0" max="1.0" value="1.0" step="0.01">
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue