use super::common::{sanitize_audio, EdgeDetector, GATE_HIGH_V, GATE_THRESHOLD_V};
use crate::port::{GraphModule, ParamDef, ParamId, PortDef, PortSpec, PortValues, SignalKind};
use crate::rng;
use alloc::format;
use alloc::vec;
use alloc::vec::Vec;
use core::f64::consts::TAU;
use libm::Libm;
pub struct Mixer {
num_channels: usize,
spec: PortSpec,
}
impl Mixer {
pub fn new(num_channels: usize) -> Self {
let inputs = (0..num_channels)
.map(|i| {
PortDef::new(i as u32, format!("ch{}", i), SignalKind::Audio).with_attenuverter()
})
.collect();
Self {
num_channels,
spec: PortSpec {
inputs,
outputs: vec![PortDef::new(100, "out", SignalKind::Audio)],
},
}
}
}
impl Default for Mixer {
fn default() -> Self {
Self::new(4)
}
}
impl GraphModule for Mixer {
fn port_spec(&self) -> &PortSpec {
&self.spec
}
fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
let sum: f64 = (0..self.num_channels)
.map(|i| inputs.get_or(i as u32, 0.0))
.sum();
outputs.set(100, sum);
}
fn reset(&mut self) {}
fn set_sample_rate(&mut self, _: f64) {}
fn type_id(&self) -> &'static str {
"mixer"
}
}
pub struct Offset {
pub(crate) offset: f64,
spec: PortSpec,
}
impl Offset {
pub fn new(offset: f64) -> Self {
Self {
offset,
spec: PortSpec {
inputs: vec![PortDef::new(0, "in", SignalKind::CvBipolar)],
outputs: vec![PortDef::new(10, "out", SignalKind::CvBipolar)],
},
}
}
pub fn set_offset(&mut self, offset: f64) {
self.offset = offset;
}
}
impl Default for Offset {
fn default() -> Self {
Self::new(0.0)
}
}
impl GraphModule for Offset {
fn port_spec(&self) -> &PortSpec {
&self.spec
}
fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
let input = inputs.get_or(0, 0.0);
outputs.set(10, input + self.offset);
}
fn reset(&mut self) {}
fn set_sample_rate(&mut self, _: f64) {}
fn type_id(&self) -> &'static str {
"offset"
}
fn params(&self) -> &[ParamDef] {
static PARAMS: &[ParamDef] = &[];
PARAMS
}
fn get_param(&self, id: ParamId) -> Option<f64> {
if id == 0 {
Some(self.offset)
} else {
None
}
}
fn set_param(&mut self, id: ParamId, value: f64) {
if id == 0 {
self.offset = value;
}
}
crate::impl_introspect!();
}
const NOTE_HYSTERESIS_SEMITONES: f64 = 0.3;
fn hysteretic_note(
last: Option<f64>,
input_v: f64,
candidate_v: f64,
hysteresis_semitones: f64,
) -> f64 {
match last {
None => candidate_v,
Some(last_v) => {
if candidate_v == last_v {
return last_v;
}
let in_s = input_v * 12.0;
let last_s = last_v * 12.0;
let cand_s = candidate_v * 12.0;
let boundary = (last_s + cand_s) * 0.5;
let commit = if cand_s > last_s {
in_s >= boundary + hysteresis_semitones
} else {
in_s <= boundary - hysteresis_semitones
};
if commit {
candidate_v
} else {
last_v
}
}
}
}
pub struct ScaleQuantizer {
last_output: Option<f64>,
custom_cents: Vec<f64>,
spec: PortSpec,
}
impl ScaleQuantizer {
const CHROMATIC: [u8; 12] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
const MAJOR: [u8; 7] = [0, 2, 4, 5, 7, 9, 11];
const MINOR: [u8; 7] = [0, 2, 3, 5, 7, 8, 10];
const PENT_MAJOR: [u8; 5] = [0, 2, 4, 7, 9];
const PENT_MINOR: [u8; 5] = [0, 3, 5, 7, 10];
const DORIAN: [u8; 7] = [0, 2, 3, 5, 7, 9, 10];
const BLUES: [u8; 6] = [0, 3, 5, 6, 7, 10];
pub fn new(_sample_rate: f64) -> Self {
Self {
last_output: None,
custom_cents: Vec::new(),
spec: PortSpec {
inputs: vec![
PortDef::new(0, "in", SignalKind::VoltPerOctave),
PortDef::new(1, "root", SignalKind::CvUnipolar)
.with_default(0.0)
.with_attenuverter(),
PortDef::new(2, "scale", SignalKind::CvUnipolar)
.with_default(0.0)
.with_attenuverter(),
],
outputs: vec![
PortDef::new(10, "out", SignalKind::VoltPerOctave),
PortDef::new(11, "trigger", SignalKind::Trigger),
],
},
}
}
fn quantize_to_scale(note: i32, scale: &[u8]) -> i32 {
let octave = note.div_euclid(12);
let semitone = note.rem_euclid(12);
let mut closest = scale[0] as i32;
let mut min_dist = i32::MAX;
for &s in scale {
let s = s as i32;
let dist = (semitone - s).abs();
if dist < min_dist {
min_dist = dist;
closest = s;
}
let dist_wrap = (semitone - (s + 12)).abs();
if dist_wrap < min_dist {
min_dist = dist_wrap;
closest = s + 12;
}
}
octave * 12 + closest
}
pub fn has_custom_scale(&self) -> bool {
!self.custom_cents.is_empty()
}
#[cfg(feature = "alloc")]
pub fn set_custom_scale(&mut self, cents: &[f64]) {
let mut degrees: Vec<f64> = cents
.iter()
.map(|&c| {
let mut r = Libm::<f64>::fmod(c, 1200.0);
if r < 0.0 {
r += 1200.0;
}
r
})
.collect();
degrees.sort_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal));
degrees.dedup_by(|a, b| (*a - *b).abs() < 1e-6);
self.custom_cents = degrees;
}
#[cfg(feature = "alloc")]
pub fn clear_custom_scale(&mut self) {
self.custom_cents.clear();
}
#[cfg(feature = "alloc")]
pub fn load_scala(&mut self, source: &str) -> Result<(), crate::scala::ScalaError> {
let scale = crate::scala::ScalaScale::parse(source)?;
self.set_custom_scale(&scale.degrees_within_octave());
Ok(())
}
fn quantize_custom_cents(input_cents: f64, degrees: &[f64]) -> f64 {
if degrees.is_empty() {
return input_cents;
}
let octave = Libm::<f64>::floor(input_cents / 1200.0);
let within = input_cents - octave * 1200.0;
let mut closest = degrees[0];
let mut min_dist = f64::MAX;
for &d in degrees {
let dist = Libm::<f64>::fabs(within - d);
if dist < min_dist {
min_dist = dist;
closest = d;
}
let dist_wrap = Libm::<f64>::fabs(within - (d + 1200.0));
if dist_wrap < min_dist {
min_dist = dist_wrap;
closest = d + 1200.0;
}
}
octave * 1200.0 + closest
}
}
impl Default for ScaleQuantizer {
fn default() -> Self {
Self::new(44100.0)
}
}
impl GraphModule for ScaleQuantizer {
fn port_spec(&self) -> &PortSpec {
&self.spec
}
fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
let input = inputs.get_or(0, 0.0);
let root_cv = inputs.get_or(1, 0.0).clamp(0.0, 1.0);
let scale_cv = inputs.get_or(2, 0.0).clamp(0.0, 1.0);
let root = (root_cv * 11.99) as i32;
let candidate_voct = if !self.custom_cents.is_empty() {
let root_cents = root as f64 * 100.0;
let input_cents = input * 1200.0 - root_cents;
let q_cents = Self::quantize_custom_cents(input_cents, &self.custom_cents);
(q_cents + root_cents) / 1200.0
} else {
let semitones_from_c4 = Libm::<f64>::round(input * 12.0) as i32;
let relative_note = semitones_from_c4 - root;
let scale_idx = (scale_cv * 6.99) as u8;
let quantized = match scale_idx {
0 => Self::quantize_to_scale(relative_note, &Self::CHROMATIC),
1 => Self::quantize_to_scale(relative_note, &Self::MAJOR),
2 => Self::quantize_to_scale(relative_note, &Self::MINOR),
3 => Self::quantize_to_scale(relative_note, &Self::PENT_MAJOR),
4 => Self::quantize_to_scale(relative_note, &Self::PENT_MINOR),
5 => Self::quantize_to_scale(relative_note, &Self::DORIAN),
_ => Self::quantize_to_scale(relative_note, &Self::BLUES),
};
(quantized + root) as f64 / 12.0
};
let prev = self.last_output;
let output_voct = hysteretic_note(prev, input, candidate_voct, NOTE_HYSTERESIS_SEMITONES);
let trigger = match prev {
Some(p) if (p - output_voct).abs() > 1e-9 => GATE_HIGH_V,
_ => 0.0,
};
self.last_output = Some(output_voct);
outputs.set(10, output_voct);
outputs.set(11, trigger);
}
fn reset(&mut self) {
self.last_output = None;
}
fn set_sample_rate(&mut self, _: f64) {}
fn type_id(&self) -> &'static str {
"scale_quantizer"
}
#[cfg(feature = "alloc")]
fn serialize_state(&self) -> Option<serde_json::Value> {
if self.custom_cents.is_empty() {
return None;
}
let cents = serde_json::to_value(&self.custom_cents).ok()?;
let mut map = serde_json::Map::new();
map.insert(alloc::string::String::from("custom_cents"), cents);
Some(serde_json::Value::Object(map))
}
#[cfg(feature = "alloc")]
fn deserialize_state(
&mut self,
state: &serde_json::Value,
) -> Result<(), alloc::string::String> {
let Some(cents_val) = state.get("custom_cents") else {
return Ok(());
};
let cents: Vec<f64> = serde_json::from_value(cents_val.clone())
.map_err(|e| format!("ScaleQuantizer custom_cents: {e}"))?;
self.set_custom_scale(¢s);
Ok(())
}
}
pub struct Euclidean {
step: usize,
pattern: Vec<bool>,
last_pulses: usize,
clock_edge: EdgeDetector,
reset_edge: EdgeDetector,
cycle_accented: bool,
spec: PortSpec,
}
impl Euclidean {
pub fn new(_sample_rate: f64) -> Self {
Self {
step: 0,
pattern: vec![true; 16],
last_pulses: 16,
clock_edge: EdgeDetector::new(),
reset_edge: EdgeDetector::new(),
cycle_accented: false,
spec: PortSpec {
inputs: vec![
PortDef::new(0, "clock", SignalKind::Trigger),
PortDef::new(1, "steps", SignalKind::CvUnipolar)
.with_default(0.5)
.with_attenuverter(),
PortDef::new(2, "pulses", SignalKind::CvUnipolar)
.with_default(0.25)
.with_attenuverter(),
PortDef::new(3, "rotation", SignalKind::CvUnipolar)
.with_default(0.0)
.with_attenuverter(),
PortDef::new(4, "reset", SignalKind::Trigger),
],
outputs: vec![
PortDef::new(10, "out", SignalKind::Trigger),
PortDef::new(11, "accent", SignalKind::Trigger),
],
},
}
}
fn generate_pattern(steps: usize, pulses: usize) -> Vec<bool> {
if steps == 0 || pulses == 0 {
return vec![false; steps.max(1)];
}
let pulses = pulses.min(steps);
let mut pattern = vec![false; steps];
let mut bucket = 0;
for slot in pattern.iter_mut().take(steps) {
bucket += pulses;
if bucket >= steps {
bucket -= steps;
*slot = true;
}
}
pattern
}
}
impl Default for Euclidean {
fn default() -> Self {
Self::new(44100.0)
}
}
impl GraphModule for Euclidean {
fn port_spec(&self) -> &PortSpec {
&self.spec
}
fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
let clock = inputs.get_or(0, 0.0);
let steps_cv = inputs.get_or(1, 0.5).clamp(0.0, 1.0);
let pulses_cv = inputs.get_or(2, 0.25).clamp(0.0, 1.0);
let rotation_cv = inputs.get_or(3, 0.0).clamp(0.0, 1.0);
let reset = inputs.get_or(4, 0.0);
let steps = 2 + (steps_cv * 14.99) as usize;
let pulses = (pulses_cv * steps as f64) as usize;
if self.pattern.len() != steps || self.last_pulses != pulses {
self.pattern = Self::generate_pattern(steps, pulses);
self.last_pulses = pulses;
}
if self.reset_edge.rising(reset) {
self.step = 0;
self.cycle_accented = false;
}
let trigger = self.clock_edge.rising(clock);
let mut out = 0.0;
let mut accent = 0.0;
if trigger {
let rotation = ((rotation_cv * steps as f64) as usize).min(steps - 1);
if self.step == 0 {
self.cycle_accented = false;
}
let rotated_step = (self.step + rotation) % steps;
if self.pattern[rotated_step] {
out = GATE_HIGH_V;
if !self.cycle_accented {
accent = GATE_HIGH_V;
self.cycle_accented = true;
}
}
self.step = (self.step + 1) % steps;
}
outputs.set(10, out);
outputs.set(11, accent);
}
fn reset(&mut self) {
self.step = 0;
self.cycle_accented = false;
self.clock_edge.reset();
self.reset_edge.reset();
}
fn set_sample_rate(&mut self, _: f64) {}
fn type_id(&self) -> &'static str {
"euclidean"
}
}
pub struct Crosstalk {
sample_rate: f64,
hf_state: [f64; 2],
spec: PortSpec,
}
impl Crosstalk {
pub fn new(sample_rate: f64) -> Self {
Self {
sample_rate,
hf_state: [0.0; 2],
spec: PortSpec {
inputs: vec![
PortDef::new(0, "in_a", SignalKind::Audio),
PortDef::new(1, "in_b", SignalKind::Audio),
PortDef::new(2, "amount", SignalKind::CvUnipolar).with_default(0.01),
PortDef::new(3, "hf_emphasis", SignalKind::CvUnipolar).with_default(0.5),
],
outputs: vec![
PortDef::new(10, "out_a", SignalKind::Audio),
PortDef::new(11, "out_b", SignalKind::Audio),
],
},
}
}
}
impl Default for Crosstalk {
fn default() -> Self {
Self::new(44100.0)
}
}
impl GraphModule for Crosstalk {
fn port_spec(&self) -> &PortSpec {
&self.spec
}
fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
let in_a = sanitize_audio(inputs.get_or(0, 0.0));
let in_b = sanitize_audio(inputs.get_or(1, 0.0));
let amount = inputs.get_or(2, 0.01).clamp(0.0, 0.5);
let hf_emphasis = inputs.get_or(3, 0.5).clamp(0.0, 1.0);
let hf_coef = 0.1 + hf_emphasis * 0.4;
let hf_a = in_a - self.hf_state[0];
let hf_b = in_b - self.hf_state[1];
self.hf_state[0] += hf_coef * (in_a - self.hf_state[0]);
self.hf_state[1] += hf_coef * (in_b - self.hf_state[1]);
let crosstalk_to_a = (in_b * (1.0 - hf_emphasis) + hf_b * hf_emphasis) * amount;
let crosstalk_to_b = (in_a * (1.0 - hf_emphasis) + hf_a * hf_emphasis) * amount;
outputs.set(10, in_a + crosstalk_to_a);
outputs.set(11, in_b + crosstalk_to_b);
}
fn reset(&mut self) {
self.hf_state = [0.0; 2];
}
fn set_sample_rate(&mut self, sample_rate: f64) {
self.sample_rate = sample_rate;
}
fn type_id(&self) -> &'static str {
"crosstalk"
}
}
pub struct GroundLoop {
sample_rate: f64,
phase: f64,
pub(crate) frequency: f64,
thermal_state: f64,
spec: PortSpec,
}
impl GroundLoop {
pub fn new(sample_rate: f64) -> Self {
Self {
sample_rate,
phase: 0.0,
frequency: 60.0, thermal_state: 0.0,
spec: PortSpec {
inputs: vec![
PortDef::new(0, "in", SignalKind::Audio),
PortDef::new(1, "level", SignalKind::CvUnipolar).with_default(0.005),
PortDef::new(2, "modulation", SignalKind::CvUnipolar).with_default(0.1),
PortDef::new(3, "freq_select", SignalKind::CvUnipolar).with_default(1.0),
],
outputs: vec![PortDef::new(10, "out", SignalKind::Audio)],
},
}
}
pub fn hz_50(sample_rate: f64) -> Self {
let mut gl = Self::new(sample_rate);
gl.frequency = 50.0;
gl
}
pub fn hz_60(sample_rate: f64) -> Self {
let mut gl = Self::new(sample_rate);
gl.frequency = 60.0;
gl
}
}
impl Default for GroundLoop {
fn default() -> Self {
Self::new(44100.0)
}
}
impl GraphModule for GroundLoop {
fn port_spec(&self) -> &PortSpec {
&self.spec
}
fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
let input = sanitize_audio(inputs.get_or(0, 0.0));
let level = inputs.get_or(1, 0.005).clamp(0.0, 0.1);
let modulation = inputs.get_or(2, 0.1).clamp(0.0, 1.0);
let freq_select = inputs.get_or(3, 1.0);
let freq = if freq_select > 0.5 { 60.0 } else { 50.0 };
let signal_energy = Libm::<f64>::pow(input / 5.0, 2.0);
self.thermal_state += (signal_energy - self.thermal_state) * 0.0001;
let modulated_level = level * (1.0 + self.thermal_state * modulation * 10.0);
let fundamental = Libm::<f64>::sin(self.phase * TAU);
let second_harmonic = Libm::<f64>::sin(self.phase * 2.0 * TAU) * 0.5;
let third_harmonic = Libm::<f64>::sin(self.phase * 3.0 * TAU) * 0.25;
let hum = (fundamental + second_harmonic + third_harmonic) * modulated_level * 5.0;
let new_phase = self.phase + freq / self.sample_rate;
self.phase = new_phase - Libm::<f64>::floor(new_phase);
outputs.set(10, input + hum);
}
fn reset(&mut self) {
self.phase = 0.0;
self.thermal_state = 0.0;
}
fn set_sample_rate(&mut self, sample_rate: f64) {
self.sample_rate = sample_rate;
}
fn type_id(&self) -> &'static str {
"ground_loop"
}
}
pub struct StepSequencer {
steps: [f64; 8],
gates: [bool; 8],
current: usize,
prev_clock: f64,
prev_reset: f64,
spec: PortSpec,
}
impl StepSequencer {
pub fn new() -> Self {
Self {
steps: [0.0; 8],
gates: [true; 8],
current: 0,
prev_clock: 0.0,
prev_reset: 0.0,
spec: PortSpec {
inputs: vec![
PortDef::new(0, "clock", SignalKind::Clock),
PortDef::new(1, "reset", SignalKind::Trigger),
],
outputs: vec![
PortDef::new(10, "cv", SignalKind::VoltPerOctave),
PortDef::new(11, "gate", SignalKind::Gate),
PortDef::new(12, "trig", SignalKind::Trigger),
],
},
}
}
pub fn set_step(&mut self, index: usize, voltage: f64, gate: bool) {
if index < 8 {
self.steps[index] = voltage;
self.gates[index] = gate;
}
}
pub fn get_step(&self, index: usize) -> Option<(f64, bool)> {
if index < 8 {
Some((self.steps[index], self.gates[index]))
} else {
None
}
}
}
impl Default for StepSequencer {
fn default() -> Self {
Self::new()
}
}
impl GraphModule for StepSequencer {
fn port_spec(&self) -> &PortSpec {
&self.spec
}
fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
let clock = inputs.get_or(0, 0.0);
let reset = inputs.get_or(1, 0.0);
let clock_rising = clock > GATE_THRESHOLD_V && self.prev_clock <= GATE_THRESHOLD_V;
let reset_rising = reset > GATE_THRESHOLD_V && self.prev_reset <= GATE_THRESHOLD_V;
let mut trigger = 0.0;
if reset_rising {
self.current = 0;
trigger = GATE_HIGH_V;
} else if clock_rising {
self.current = (self.current + 1) % 8;
trigger = GATE_HIGH_V;
}
self.prev_clock = clock;
self.prev_reset = reset;
let cv = self.steps[self.current];
let gate = if self.gates[self.current] && clock > GATE_THRESHOLD_V {
5.0
} else {
0.0
};
outputs.set(10, cv);
outputs.set(11, gate);
outputs.set(12, trigger);
}
fn reset(&mut self) {
self.current = 0;
self.prev_clock = 0.0;
self.prev_reset = 0.0;
}
fn set_sample_rate(&mut self, _: f64) {}
fn type_id(&self) -> &'static str {
"step_sequencer"
}
crate::impl_introspect!();
}
pub struct StereoOutput {
spec: PortSpec,
}
impl StereoOutput {
pub fn new() -> Self {
Self {
spec: PortSpec {
inputs: vec![
PortDef::new(0, "left", SignalKind::Audio),
PortDef::new(1, "right", SignalKind::Audio).normalled_to(0),
],
outputs: vec![
PortDef::new(0, "left", SignalKind::Audio),
PortDef::new(1, "right", SignalKind::Audio),
],
},
}
}
}
impl Default for StereoOutput {
fn default() -> Self {
Self::new()
}
}
impl GraphModule for StereoOutput {
fn port_spec(&self) -> &PortSpec {
&self.spec
}
fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
let left = inputs.get_or(0, 0.0);
let right = inputs.get_or(1, left);
outputs.set(0, left);
outputs.set(1, right);
}
fn reset(&mut self) {}
fn set_sample_rate(&mut self, _: f64) {}
fn type_id(&self) -> &'static str {
"stereo_output"
}
}
pub struct SampleAndHold {
held_value: f64,
trigger_edge: EdgeDetector,
spec: PortSpec,
}
impl SampleAndHold {
pub fn new() -> Self {
Self {
held_value: 0.0,
trigger_edge: EdgeDetector::new(),
spec: PortSpec {
inputs: vec![
PortDef::new(0, "in", SignalKind::CvBipolar),
PortDef::new(1, "trig", SignalKind::Trigger),
],
outputs: vec![PortDef::new(10, "out", SignalKind::CvBipolar)],
},
}
}
}
impl Default for SampleAndHold {
fn default() -> Self {
Self::new()
}
}
impl GraphModule for SampleAndHold {
fn port_spec(&self) -> &PortSpec {
&self.spec
}
fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
let input = inputs.get_or(0, 0.0);
let trigger = inputs.get_or(1, 0.0);
if self.trigger_edge.rising(trigger) {
self.held_value = input;
}
outputs.set(10, self.held_value);
}
fn reset(&mut self) {
self.held_value = 0.0;
self.trigger_edge.reset();
}
fn set_sample_rate(&mut self, _: f64) {}
fn type_id(&self) -> &'static str {
"sample_hold"
}
}
pub struct SlewLimiter {
current: f64,
sample_rate: f64,
spec: PortSpec,
}
impl SlewLimiter {
pub fn new(sample_rate: f64) -> Self {
Self {
current: 0.0,
sample_rate,
spec: PortSpec {
inputs: vec![
PortDef::new(0, "in", SignalKind::CvBipolar),
PortDef::new(1, "rise", SignalKind::CvUnipolar)
.with_default(0.5)
.with_attenuverter(),
PortDef::new(2, "fall", SignalKind::CvUnipolar)
.with_default(0.5)
.with_attenuverter(),
],
outputs: vec![PortDef::new(10, "out", SignalKind::CvBipolar)],
},
}
}
fn cv_to_rate(&self, cv: f64) -> f64 {
let time = 0.001 + Libm::<f64>::pow(cv.clamp(0.0, 1.0), 2.0) * 10.0; 1.0 / (time * self.sample_rate)
}
}
impl Default for SlewLimiter {
fn default() -> Self {
Self::new(44100.0)
}
}
impl GraphModule for SlewLimiter {
fn port_spec(&self) -> &PortSpec {
&self.spec
}
fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
let target = inputs.get_or(0, 0.0);
let rise_cv = inputs.get_or(1, 0.5);
let fall_cv = inputs.get_or(2, 0.5);
let diff = target - self.current;
if diff > 0.0 {
let rate = self.cv_to_rate(rise_cv);
self.current += Libm::<f64>::fmin(diff, rate * 10.0); } else if diff < 0.0 {
let rate = self.cv_to_rate(fall_cv);
self.current += Libm::<f64>::fmax(diff, -rate * 10.0);
}
outputs.set(10, self.current);
}
fn reset(&mut self) {
self.current = 0.0;
}
fn set_sample_rate(&mut self, sample_rate: f64) {
self.sample_rate = sample_rate;
}
fn type_id(&self) -> &'static str {
"slew_limiter"
}
}
pub struct Quantizer {
pub(crate) scale: Scale,
last_output: Option<f64>,
spec: PortSpec,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Scale {
Chromatic,
Major,
Minor,
PentatonicMajor,
PentatonicMinor,
Dorian,
Mixolydian,
Blues,
}
impl Scale {
fn semitones(&self) -> &'static [i32] {
match self {
Scale::Chromatic => &[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
Scale::Major => &[0, 2, 4, 5, 7, 9, 11],
Scale::Minor => &[0, 2, 3, 5, 7, 8, 10],
Scale::PentatonicMajor => &[0, 2, 4, 7, 9],
Scale::PentatonicMinor => &[0, 3, 5, 7, 10],
Scale::Dorian => &[0, 2, 3, 5, 7, 9, 10],
Scale::Mixolydian => &[0, 2, 4, 5, 7, 9, 10],
Scale::Blues => &[0, 3, 5, 6, 7, 10],
}
}
}
impl Quantizer {
pub fn new(scale: Scale) -> Self {
Self {
scale,
last_output: None,
spec: PortSpec {
inputs: vec![PortDef::new(0, "in", SignalKind::VoltPerOctave)],
outputs: vec![PortDef::new(10, "out", SignalKind::VoltPerOctave)],
},
}
}
pub fn chromatic() -> Self {
Self::new(Scale::Chromatic)
}
pub fn major() -> Self {
Self::new(Scale::Major)
}
pub fn minor() -> Self {
Self::new(Scale::Minor)
}
pub fn set_scale(&mut self, scale: Scale) {
self.scale = scale;
}
fn quantize(&self, voltage: f64) -> f64 {
let semitones = self.scale.semitones();
let total_semitones = voltage * 12.0;
let octave = Libm::<f64>::floor(total_semitones / 12.0);
let within_octave = total_semitones - octave * 12.0;
let mut nearest = semitones[0];
let mut min_dist = f64::MAX;
for &semi in semitones {
let dist = (within_octave - semi as f64).abs();
if dist < min_dist {
min_dist = dist;
nearest = semi;
}
let dist_wrap = (within_octave - (semi + 12) as f64).abs();
if dist_wrap < min_dist {
min_dist = dist_wrap;
nearest = semi + 12;
}
}
(octave * 12.0 + nearest as f64) / 12.0
}
}
impl Default for Quantizer {
fn default() -> Self {
Self::chromatic()
}
}
impl GraphModule for Quantizer {
fn port_spec(&self) -> &PortSpec {
&self.spec
}
fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
let input = inputs.get_or(0, 0.0);
let candidate = self.quantize(input);
let committed = hysteretic_note(
self.last_output,
input,
candidate,
NOTE_HYSTERESIS_SEMITONES,
);
self.last_output = Some(committed);
outputs.set(10, committed);
}
fn reset(&mut self) {
self.last_output = None;
}
fn set_sample_rate(&mut self, _: f64) {}
fn type_id(&self) -> &'static str {
"quantizer"
}
crate::impl_introspect!();
}
pub struct Clock {
phase: f64,
cycle: u64,
sample_rate: f64,
spec: PortSpec,
}
impl Clock {
const DEFAULT_BPM_CV: f64 = 6.616_418_958_920_283;
pub fn new(sample_rate: f64) -> Self {
Self {
phase: 0.0,
cycle: 0,
sample_rate,
spec: PortSpec {
inputs: vec![
PortDef::new(0, "bpm", SignalKind::CvUnipolar)
.with_default(Self::DEFAULT_BPM_CV) .with_attenuverter(),
PortDef::new(1, "reset", SignalKind::Trigger),
],
outputs: vec![
PortDef::new(10, "out", SignalKind::Clock),
PortDef::new(11, "div2", SignalKind::Clock),
PortDef::new(12, "div4", SignalKind::Clock),
],
},
}
}
fn cv_to_bpm(cv: f64) -> f64 {
20.0 * Libm::<f64>::pow(15.0, cv / 10.0)
}
}
impl Default for Clock {
fn default() -> Self {
Self::new(44100.0)
}
}
impl GraphModule for Clock {
fn port_spec(&self) -> &PortSpec {
&self.spec
}
fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
let bpm_cv = inputs.get_or(0, Self::DEFAULT_BPM_CV); let reset = inputs.get_or(1, 0.0);
let bpm = Self::cv_to_bpm(bpm_cv);
let freq = bpm / 60.0;
if reset > GATE_THRESHOLD_V {
self.phase = 0.0;
self.cycle = 0;
}
let pulse_width = 0.1; let in_pulse = self.phase < pulse_width;
let main_out = if in_pulse { GATE_HIGH_V } else { 0.0 };
let div2_out = if in_pulse && (self.cycle & 1) == 0 {
GATE_HIGH_V
} else {
0.0
};
let div4_out = if in_pulse && (self.cycle & 3) == 0 {
GATE_HIGH_V
} else {
0.0
};
outputs.set(10, main_out);
outputs.set(11, div2_out);
outputs.set(12, div4_out);
let new_phase = self.phase + freq / self.sample_rate;
let wraps = Libm::<f64>::floor(new_phase);
if wraps > 0.0 {
self.cycle = self.cycle.wrapping_add(wraps as u64);
}
self.phase = new_phase - wraps;
}
fn reset(&mut self) {
self.phase = 0.0;
self.cycle = 0;
}
fn set_sample_rate(&mut self, sample_rate: f64) {
self.sample_rate = sample_rate;
}
fn type_id(&self) -> &'static str {
"clock"
}
}
pub struct Attenuverter {
spec: PortSpec,
}
impl Attenuverter {
pub fn new() -> Self {
Self {
spec: PortSpec {
inputs: vec![
PortDef::new(0, "in", SignalKind::CvBipolar),
PortDef::new(1, "level", SignalKind::CvBipolar).with_default(5.0), ],
outputs: vec![PortDef::new(10, "out", SignalKind::CvBipolar)],
},
}
}
}
impl Default for Attenuverter {
fn default() -> Self {
Self::new()
}
}
impl GraphModule for Attenuverter {
fn port_spec(&self) -> &PortSpec {
&self.spec
}
fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
let input = inputs.get_or(0, 0.0);
let level = inputs.get_or(1, 5.0) / 5.0;
outputs.set(10, input * level);
}
fn reset(&mut self) {}
fn set_sample_rate(&mut self, _: f64) {}
fn type_id(&self) -> &'static str {
"attenuverter"
}
}
pub struct Multiple {
spec: PortSpec,
}
impl Multiple {
pub fn new() -> Self {
Self {
spec: PortSpec {
inputs: vec![PortDef::new(0, "in", SignalKind::CvBipolar)],
outputs: vec![
PortDef::new(10, "out1", SignalKind::CvBipolar),
PortDef::new(11, "out2", SignalKind::CvBipolar),
PortDef::new(12, "out3", SignalKind::CvBipolar),
PortDef::new(13, "out4", SignalKind::CvBipolar),
],
},
}
}
}
impl Default for Multiple {
fn default() -> Self {
Self::new()
}
}
impl GraphModule for Multiple {
fn port_spec(&self) -> &PortSpec {
&self.spec
}
fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
let input = inputs.get_or(0, 0.0);
outputs.set(10, input);
outputs.set(11, input);
outputs.set(12, input);
outputs.set(13, input);
}
fn reset(&mut self) {}
fn set_sample_rate(&mut self, _: f64) {}
fn type_id(&self) -> &'static str {
"multiple"
}
}
pub struct Crossfader {
spec: PortSpec,
}
impl Crossfader {
pub fn new() -> Self {
Self {
spec: PortSpec {
inputs: vec![
PortDef::new(0, "a", SignalKind::Audio),
PortDef::new(1, "b", SignalKind::Audio),
PortDef::new(2, "pos", SignalKind::CvBipolar).with_default(0.0),
],
outputs: vec![
PortDef::new(10, "out", SignalKind::Audio),
PortDef::new(11, "left", SignalKind::Audio),
PortDef::new(12, "right", SignalKind::Audio),
],
},
}
}
}
impl Default for Crossfader {
fn default() -> Self {
Self::new()
}
}
impl GraphModule for Crossfader {
fn port_spec(&self) -> &PortSpec {
&self.spec
}
fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
let a = inputs.get_or(0, 0.0);
let b = inputs.get_or(1, 0.0);
let pos = inputs.get_or(2, 0.0);
let mix = ((pos / 5.0) + 1.0) / 2.0;
let mix = mix.clamp(0.0, 1.0);
let a_gain = Libm::<f64>::sqrt(1.0 - mix);
let b_gain = Libm::<f64>::sqrt(mix);
let out = a * a_gain + b * b_gain;
outputs.set(10, out);
outputs.set(11, out * a_gain); outputs.set(12, out * b_gain); }
fn reset(&mut self) {}
fn set_sample_rate(&mut self, _: f64) {}
fn type_id(&self) -> &'static str {
"crossfader"
}
}
pub struct LogicAnd {
spec: PortSpec,
}
impl LogicAnd {
pub fn new() -> Self {
Self {
spec: PortSpec {
inputs: vec![
PortDef::new(0, "a", SignalKind::Gate),
PortDef::new(1, "b", SignalKind::Gate),
],
outputs: vec![PortDef::new(10, "out", SignalKind::Gate)],
},
}
}
}
impl Default for LogicAnd {
fn default() -> Self {
Self::new()
}
}
impl GraphModule for LogicAnd {
fn port_spec(&self) -> &PortSpec {
&self.spec
}
fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
let a = inputs.get_or(0, 0.0) > GATE_THRESHOLD_V;
let b = inputs.get_or(1, 0.0) > GATE_THRESHOLD_V;
outputs.set(10, if a && b { GATE_HIGH_V } else { 0.0 });
}
fn reset(&mut self) {}
fn set_sample_rate(&mut self, _: f64) {}
fn type_id(&self) -> &'static str {
"logic_and"
}
}
pub struct LogicOr {
spec: PortSpec,
}
impl LogicOr {
pub fn new() -> Self {
Self {
spec: PortSpec {
inputs: vec![
PortDef::new(0, "a", SignalKind::Gate),
PortDef::new(1, "b", SignalKind::Gate),
],
outputs: vec![PortDef::new(10, "out", SignalKind::Gate)],
},
}
}
}
impl Default for LogicOr {
fn default() -> Self {
Self::new()
}
}
impl GraphModule for LogicOr {
fn port_spec(&self) -> &PortSpec {
&self.spec
}
fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
let a = inputs.get_or(0, 0.0) > GATE_THRESHOLD_V;
let b = inputs.get_or(1, 0.0) > GATE_THRESHOLD_V;
outputs.set(10, if a || b { GATE_HIGH_V } else { 0.0 });
}
fn reset(&mut self) {}
fn set_sample_rate(&mut self, _: f64) {}
fn type_id(&self) -> &'static str {
"logic_or"
}
}
pub struct LogicXor {
spec: PortSpec,
}
impl LogicXor {
pub fn new() -> Self {
Self {
spec: PortSpec {
inputs: vec![
PortDef::new(0, "a", SignalKind::Gate),
PortDef::new(1, "b", SignalKind::Gate),
],
outputs: vec![PortDef::new(10, "out", SignalKind::Gate)],
},
}
}
}
impl Default for LogicXor {
fn default() -> Self {
Self::new()
}
}
impl GraphModule for LogicXor {
fn port_spec(&self) -> &PortSpec {
&self.spec
}
fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
let a = inputs.get_or(0, 0.0) > GATE_THRESHOLD_V;
let b = inputs.get_or(1, 0.0) > GATE_THRESHOLD_V;
outputs.set(10, if a ^ b { GATE_HIGH_V } else { 0.0 });
}
fn reset(&mut self) {}
fn set_sample_rate(&mut self, _: f64) {}
fn type_id(&self) -> &'static str {
"logic_xor"
}
}
pub struct LogicNot {
spec: PortSpec,
}
impl LogicNot {
pub fn new() -> Self {
Self {
spec: PortSpec {
inputs: vec![PortDef::new(0, "in", SignalKind::Gate)],
outputs: vec![PortDef::new(10, "out", SignalKind::Gate)],
},
}
}
}
impl Default for LogicNot {
fn default() -> Self {
Self::new()
}
}
impl GraphModule for LogicNot {
fn port_spec(&self) -> &PortSpec {
&self.spec
}
fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
let input = inputs.get_or(0, 0.0) > GATE_THRESHOLD_V;
outputs.set(10, if input { 0.0 } else { GATE_HIGH_V });
}
fn reset(&mut self) {}
fn set_sample_rate(&mut self, _: f64) {}
fn type_id(&self) -> &'static str {
"logic_not"
}
}
pub struct Comparator {
state: i8,
spec: PortSpec,
}
impl Comparator {
const DEADBAND_V: f64 = 0.01;
const HYSTERESIS_V: f64 = 0.02;
pub fn new() -> Self {
Self {
state: 0,
spec: PortSpec {
inputs: vec![
PortDef::new(0, "a", SignalKind::CvBipolar),
PortDef::new(1, "b", SignalKind::CvBipolar),
],
outputs: vec![
PortDef::new(10, "gt", SignalKind::Gate), PortDef::new(11, "lt", SignalKind::Gate), PortDef::new(12, "eq", SignalKind::Gate), ],
},
}
}
}
impl Default for Comparator {
fn default() -> Self {
Self::new()
}
}
impl GraphModule for Comparator {
fn port_spec(&self) -> &PortSpec {
&self.spec
}
fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
let a = inputs.get_or(0, 0.0);
let b = inputs.get_or(1, 0.0);
let d = a - b;
let t = Self::DEADBAND_V;
let hy = Self::HYSTERESIS_V;
let mut gt = self.state == 1;
let mut lt = self.state == -1;
if gt {
if d < t {
gt = false;
}
} else if d >= t + hy {
gt = true;
}
if lt {
if d > -t {
lt = false;
}
} else if d <= -t - hy {
lt = true;
}
self.state = if gt {
1
} else if lt {
-1
} else {
0
};
outputs.set(10, if gt { GATE_HIGH_V } else { 0.0 });
outputs.set(11, if lt { GATE_HIGH_V } else { 0.0 });
outputs.set(12, if self.state == 0 { GATE_HIGH_V } else { 0.0 });
}
fn reset(&mut self) {
self.state = 0;
}
fn set_sample_rate(&mut self, _: f64) {}
fn type_id(&self) -> &'static str {
"comparator"
}
}
pub struct Rectifier {
spec: PortSpec,
}
impl Rectifier {
pub fn new() -> Self {
Self {
spec: PortSpec {
inputs: vec![PortDef::new(0, "in", SignalKind::Audio)],
outputs: vec![
PortDef::new(10, "full", SignalKind::Audio), PortDef::new(11, "half_pos", SignalKind::Audio), PortDef::new(12, "half_neg", SignalKind::Audio), PortDef::new(13, "abs", SignalKind::CvUnipolar), ],
},
}
}
}
impl Default for Rectifier {
fn default() -> Self {
Self::new()
}
}
impl GraphModule for Rectifier {
fn port_spec(&self) -> &PortSpec {
&self.spec
}
fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
let input = inputs.get_or(0, 0.0);
outputs.set(10, Libm::<f64>::fabs(input));
outputs.set(11, Libm::<f64>::fmax(input, 0.0));
outputs.set(12, Libm::<f64>::fmax(-input, 0.0));
outputs.set(13, Libm::<f64>::fabs(input) * 2.0);
}
fn reset(&mut self) {}
fn set_sample_rate(&mut self, _: f64) {}
fn type_id(&self) -> &'static str {
"rectifier"
}
}
pub struct PrecisionAdder {
spec: PortSpec,
}
impl PrecisionAdder {
pub fn new() -> Self {
Self {
spec: PortSpec {
inputs: vec![
PortDef::new(0, "in1", SignalKind::VoltPerOctave),
PortDef::new(1, "in2", SignalKind::VoltPerOctave),
PortDef::new(2, "in3", SignalKind::CvBipolar),
PortDef::new(3, "in4", SignalKind::CvBipolar),
],
outputs: vec![
PortDef::new(10, "sum", SignalKind::VoltPerOctave),
PortDef::new(11, "inv", SignalKind::VoltPerOctave), ],
},
}
}
}
impl Default for PrecisionAdder {
fn default() -> Self {
Self::new()
}
}
impl GraphModule for PrecisionAdder {
fn port_spec(&self) -> &PortSpec {
&self.spec
}
fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
let sum = inputs.get_or(0, 0.0)
+ inputs.get_or(1, 0.0)
+ inputs.get_or(2, 0.0)
+ inputs.get_or(3, 0.0);
outputs.set(10, sum);
outputs.set(11, -sum);
}
fn reset(&mut self) {}
fn set_sample_rate(&mut self, _: f64) {}
fn type_id(&self) -> &'static str {
"precision_adder"
}
}
pub struct VcSwitch {
spec: PortSpec,
}
impl VcSwitch {
pub fn new() -> Self {
Self {
spec: PortSpec {
inputs: vec![
PortDef::new(0, "a", SignalKind::Audio),
PortDef::new(1, "b", SignalKind::Audio),
PortDef::new(2, "cv", SignalKind::Gate).with_default(0.0),
],
outputs: vec![
PortDef::new(10, "out", SignalKind::Audio), PortDef::new(11, "a_out", SignalKind::Audio), PortDef::new(12, "b_out", SignalKind::Audio), ],
},
}
}
}
impl Default for VcSwitch {
fn default() -> Self {
Self::new()
}
}
impl GraphModule for VcSwitch {
fn port_spec(&self) -> &PortSpec {
&self.spec
}
fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
let a = inputs.get_or(0, 0.0);
let b = inputs.get_or(1, 0.0);
let cv = inputs.get_or(2, 0.0);
let select_b = cv > GATE_THRESHOLD_V;
if select_b {
outputs.set(10, b);
outputs.set(11, 0.0);
outputs.set(12, b);
} else {
outputs.set(10, a);
outputs.set(11, a);
outputs.set(12, 0.0);
}
}
fn reset(&mut self) {}
fn set_sample_rate(&mut self, _: f64) {}
fn type_id(&self) -> &'static str {
"vc_switch"
}
}
pub struct BernoulliGate {
prev_trigger: f64,
gate_a: f64,
gate_b: f64,
spec: PortSpec,
}
impl BernoulliGate {
pub fn new() -> Self {
Self {
prev_trigger: 0.0,
gate_a: 0.0,
gate_b: 0.0,
spec: PortSpec {
inputs: vec![
PortDef::new(0, "trig", SignalKind::Trigger),
PortDef::new(1, "prob", SignalKind::CvUnipolar).with_default(5.0), ],
outputs: vec![
PortDef::new(10, "a", SignalKind::Trigger), PortDef::new(11, "b", SignalKind::Trigger), PortDef::new(12, "gate_a", SignalKind::Gate), PortDef::new(13, "gate_b", SignalKind::Gate), ],
},
}
}
}
impl Default for BernoulliGate {
fn default() -> Self {
Self::new()
}
}
impl GraphModule for BernoulliGate {
fn port_spec(&self) -> &PortSpec {
&self.spec
}
fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
let trigger = inputs.get_or(0, 0.0);
let prob = (inputs.get_or(1, 5.0) / 10.0).clamp(0.0, 1.0);
let rising_edge = trigger > GATE_THRESHOLD_V && self.prev_trigger <= GATE_THRESHOLD_V;
self.prev_trigger = trigger;
let mut trig_a = 0.0;
let mut trig_b = 0.0;
if rising_edge {
let rand_val: f64 = rng::random();
if rand_val < prob {
trig_a = GATE_HIGH_V;
} else {
trig_b = GATE_HIGH_V;
}
}
outputs.set(10, trig_a);
outputs.set(11, trig_b);
if trig_a > 0.0 {
self.gate_a = GATE_HIGH_V;
self.gate_b = 0.0;
} else if trig_b > 0.0 {
self.gate_a = 0.0;
self.gate_b = GATE_HIGH_V;
}
outputs.set(12, self.gate_a);
outputs.set(13, self.gate_b);
}
fn reset(&mut self) {
self.prev_trigger = 0.0;
self.gate_a = 0.0;
self.gate_b = 0.0;
}
fn set_sample_rate(&mut self, _: f64) {}
fn type_id(&self) -> &'static str {
"bernoulli_gate"
}
}
pub struct Min {
spec: PortSpec,
}
impl Min {
pub fn new() -> Self {
Self {
spec: PortSpec {
inputs: vec![
PortDef::new(0, "a", SignalKind::CvBipolar),
PortDef::new(1, "b", SignalKind::CvBipolar),
],
outputs: vec![PortDef::new(10, "out", SignalKind::CvBipolar)],
},
}
}
}
impl Default for Min {
fn default() -> Self {
Self::new()
}
}
impl GraphModule for Min {
fn port_spec(&self) -> &PortSpec {
&self.spec
}
fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
let a = inputs.get_or(0, 0.0);
let b = inputs.get_or(1, 0.0);
outputs.set(10, Libm::<f64>::fmin(a, b));
}
fn reset(&mut self) {}
fn set_sample_rate(&mut self, _: f64) {}
fn type_id(&self) -> &'static str {
"min"
}
}
pub struct Max {
spec: PortSpec,
}
impl Max {
pub fn new() -> Self {
Self {
spec: PortSpec {
inputs: vec![
PortDef::new(0, "a", SignalKind::CvBipolar),
PortDef::new(1, "b", SignalKind::CvBipolar),
],
outputs: vec![PortDef::new(10, "out", SignalKind::CvBipolar)],
},
}
}
}
impl Default for Max {
fn default() -> Self {
Self::new()
}
}
impl GraphModule for Max {
fn port_spec(&self) -> &PortSpec {
&self.spec
}
fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
let a = inputs.get_or(0, 0.0);
let b = inputs.get_or(1, 0.0);
outputs.set(10, Libm::<f64>::fmax(a, b));
}
fn reset(&mut self) {}
fn set_sample_rate(&mut self, _: f64) {}
fn type_id(&self) -> &'static str {
"max"
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ChordType {
Major,
Minor,
Seventh,
MajorSeventh,
MinorSeventh,
Diminished,
Augmented,
Sus2,
Sus4,
}
impl ChordType {
fn intervals(&self) -> &'static [i32] {
match self {
ChordType::Major => &[0, 4, 7],
ChordType::Minor => &[0, 3, 7],
ChordType::Seventh => &[0, 4, 7, 10],
ChordType::MajorSeventh => &[0, 4, 7, 11],
ChordType::MinorSeventh => &[0, 3, 7, 10],
ChordType::Diminished => &[0, 3, 6],
ChordType::Augmented => &[0, 4, 8],
ChordType::Sus2 => &[0, 2, 7],
ChordType::Sus4 => &[0, 5, 7],
}
}
fn from_cv(cv: f64) -> Self {
match (cv * 8.99) as u8 {
0 => ChordType::Major,
1 => ChordType::Minor,
2 => ChordType::Seventh,
3 => ChordType::MajorSeventh,
4 => ChordType::MinorSeventh,
5 => ChordType::Diminished,
6 => ChordType::Augmented,
7 => ChordType::Sus2,
_ => ChordType::Sus4,
}
}
}
pub struct ChordMemory {
spec: PortSpec,
}
impl ChordMemory {
pub fn new() -> Self {
Self {
spec: PortSpec {
inputs: vec![
PortDef::new(0, "root", SignalKind::VoltPerOctave),
PortDef::new(1, "chord", SignalKind::CvUnipolar)
.with_default(0.0)
.with_attenuverter(),
PortDef::new(2, "inversion", SignalKind::CvUnipolar)
.with_default(0.0)
.with_attenuverter(),
PortDef::new(3, "spread", SignalKind::CvUnipolar)
.with_default(0.0)
.with_attenuverter(),
],
outputs: vec![
PortDef::new(10, "voice1", SignalKind::VoltPerOctave),
PortDef::new(11, "voice2", SignalKind::VoltPerOctave),
PortDef::new(12, "voice3", SignalKind::VoltPerOctave),
PortDef::new(13, "voice4", SignalKind::VoltPerOctave),
],
},
}
}
}
impl Default for ChordMemory {
fn default() -> Self {
Self::new()
}
}
impl GraphModule for ChordMemory {
fn port_spec(&self) -> &PortSpec {
&self.spec
}
fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
let root = inputs.get_or(0, 0.0);
let chord_cv = inputs.get_or(1, 0.0).clamp(0.0, 1.0);
let inversion_cv = inputs.get_or(2, 0.0).clamp(0.0, 1.0);
let spread = inputs.get_or(3, 0.0).clamp(0.0, 1.0);
let chord_type = ChordType::from_cv(chord_cv);
let intervals = chord_type.intervals();
let num_notes = intervals.len();
let inversion = ((inversion_cv * num_notes as f64) as usize) % num_notes;
let mut voices = [0.0f64; 4];
for (i, voice) in voices.iter_mut().enumerate() {
if i < num_notes {
let interval_idx = (i + inversion) % num_notes;
let semitones = intervals[interval_idx];
let octave_offset = if i + inversion >= num_notes { 1.0 } else { 0.0 };
let spread_offset = spread * (i as f64 / 3.0);
*voice = root + semitones as f64 / 12.0 + octave_offset + spread_offset;
} else {
let spread_offset = spread * (i as f64 / 3.0);
*voice = root + 1.0 + spread_offset;
}
}
outputs.set(10, voices[0]);
outputs.set(11, voices[1]);
outputs.set(12, voices[2]);
outputs.set(13, voices[3]);
}
fn reset(&mut self) {}
fn set_sample_rate(&mut self, _: f64) {}
fn type_id(&self) -> &'static str {
"chord_memory"
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ArpPattern {
Up,
Down,
UpDown,
Random,
}
impl ArpPattern {
fn from_cv(cv: f64) -> Self {
let cv = cv.clamp(0.0, 1.0);
if cv < 0.25 {
ArpPattern::Up
} else if cv < 0.5 {
ArpPattern::Down
} else if cv < 0.75 {
ArpPattern::UpDown
} else {
ArpPattern::Random
}
}
}
pub struct Arpeggiator {
held_notes: [f64; 8],
num_notes: usize,
current_step: usize,
direction_up: bool,
prev_gate: f64,
captured_note: Option<f64>,
prev_clock: f64,
prev_reset: f64,
rng: crate::rng::Rng,
gate_out: f64,
trigger_countdown: usize,
sample_rate: f64,
spec: PortSpec,
}
impl Arpeggiator {
const TRIGGER_MS: f64 = 1.0;
pub fn new(sample_rate: f64) -> Self {
let spec = PortSpec {
inputs: vec![
PortDef::new(0, "v_oct", SignalKind::VoltPerOctave).with_default(0.0),
PortDef::new(1, "gate", SignalKind::Gate).with_default(0.0),
PortDef::new(2, "clock", SignalKind::Clock).with_default(0.0),
PortDef::new(3, "pattern", SignalKind::CvUnipolar).with_default(0.0),
PortDef::new(4, "octaves", SignalKind::CvUnipolar).with_default(0.0),
PortDef::new(5, "reset", SignalKind::Gate).with_default(0.0),
],
outputs: vec![
PortDef::new(10, "v_oct_out", SignalKind::VoltPerOctave),
PortDef::new(11, "gate_out", SignalKind::Gate),
PortDef::new(12, "trigger", SignalKind::Trigger),
],
};
Self {
held_notes: [0.0; 8],
num_notes: 0,
current_step: 0,
direction_up: true,
prev_gate: 0.0,
captured_note: None,
prev_clock: 0.0,
prev_reset: 0.0,
rng: crate::rng::Rng::from_seed(42),
gate_out: 0.0,
trigger_countdown: 0,
sample_rate,
spec,
}
}
fn add_note(&mut self, note: f64) {
if self.num_notes >= 8 {
return;
}
let mut insert_pos = self.num_notes;
for i in 0..self.num_notes {
if note < self.held_notes[i] {
insert_pos = i;
break;
}
}
for i in (insert_pos..self.num_notes).rev() {
self.held_notes[i + 1] = self.held_notes[i];
}
self.held_notes[insert_pos] = note;
self.num_notes += 1;
}
pub fn remove_note(&mut self, note: f64) {
let mut found_idx = None;
for i in 0..self.num_notes {
if (self.held_notes[i] - note).abs() < 0.001 {
found_idx = Some(i);
break;
}
}
if let Some(idx) = found_idx {
for i in idx..self.num_notes - 1 {
self.held_notes[i] = self.held_notes[i + 1];
}
self.num_notes -= 1;
}
}
fn get_current_note(&mut self, pattern: ArpPattern, octaves: usize) -> f64 {
if self.num_notes == 0 {
return 0.0;
}
let total_steps = self.num_notes * octaves;
let step = self.current_step % total_steps;
let note_idx = match pattern {
ArpPattern::Up => step % self.num_notes,
ArpPattern::Down => (self.num_notes - 1) - (step % self.num_notes),
ArpPattern::UpDown => {
let cycle_len = if self.num_notes > 1 {
(self.num_notes - 1) * 2
} else {
1
};
let pos = step % cycle_len;
if pos < self.num_notes {
pos
} else {
(self.num_notes - 1) * 2 - pos
}
}
ArpPattern::Random => (self.rng.next_u64() as usize) % self.num_notes,
};
let octave = step / self.num_notes;
let base_note = self.held_notes[note_idx % self.num_notes];
base_note + octave as f64 }
}
impl Default for Arpeggiator {
fn default() -> Self {
Self::new(44100.0)
}
}
impl GraphModule for Arpeggiator {
fn port_spec(&self) -> &PortSpec {
&self.spec
}
fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
let v_oct = inputs.get_or(0, 0.0);
let gate = inputs.get_or(1, 0.0);
let clock = inputs.get_or(2, 0.0);
let pattern_cv = inputs.get_or(3, 0.0);
let octaves_cv = inputs.get_or(4, 0.0);
let reset = inputs.get_or(5, 0.0);
let pattern = ArpPattern::from_cv(pattern_cv);
let octaves = (1.0 + octaves_cv.clamp(0.0, 1.0) * 3.0) as usize;
if gate > GATE_THRESHOLD_V && self.prev_gate <= GATE_THRESHOLD_V {
self.add_note(v_oct);
self.captured_note = Some(v_oct);
} else if gate <= GATE_THRESHOLD_V && self.prev_gate > GATE_THRESHOLD_V {
if let Some(note) = self.captured_note.take() {
self.remove_note(note);
}
}
self.prev_gate = gate;
if reset > GATE_THRESHOLD_V && self.prev_reset <= GATE_THRESHOLD_V {
self.current_step = 0;
self.direction_up = true;
self.held_notes = [0.0; 8];
self.num_notes = 0;
self.captured_note = None;
}
self.prev_reset = reset;
let mut trigger_out = 0.0;
let clock_rising =
clock > GATE_THRESHOLD_V && self.prev_clock <= GATE_THRESHOLD_V && self.num_notes > 0;
if clock_rising {
self.gate_out = GATE_HIGH_V;
self.trigger_countdown = (Self::TRIGGER_MS * self.sample_rate / 1000.0) as usize;
trigger_out = GATE_HIGH_V;
}
self.prev_clock = clock;
if self.trigger_countdown > 0 {
self.trigger_countdown -= 1;
trigger_out = GATE_HIGH_V;
}
if clock <= GATE_THRESHOLD_V {
self.gate_out = 0.0;
}
let v_oct_out = if self.num_notes > 0 {
self.get_current_note(pattern, octaves)
} else {
0.0
};
if clock_rising {
self.current_step += 1;
}
outputs.set(10, v_oct_out);
outputs.set(
11,
if self.num_notes > 0 {
self.gate_out
} else {
0.0
},
);
outputs.set(12, trigger_out);
}
fn reset(&mut self) {
self.held_notes = [0.0; 8];
self.num_notes = 0;
self.captured_note = None;
self.current_step = 0;
self.direction_up = true;
self.prev_gate = 0.0;
self.prev_clock = 0.0;
self.prev_reset = 0.0;
self.gate_out = 0.0;
self.trigger_countdown = 0;
}
fn set_sample_rate(&mut self, sample_rate: f64) {
self.sample_rate = sample_rate;
}
fn type_id(&self) -> &'static str {
"arpeggiator"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::modules::common::SAFE_AUDIO_LIMIT;
#[test]
fn test_mixer() {
let mut mixer = Mixer::new(4);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 1.0);
inputs.set(1, 2.0);
inputs.set(2, 3.0);
inputs.set(3, 4.0);
mixer.tick(&inputs, &mut outputs);
let out = outputs.get(100).unwrap();
assert!((out - 10.0).abs() < 0.01);
}
#[test]
fn test_step_sequencer() {
let mut seq = StepSequencer::new();
seq.set_step(0, 0.0, true);
seq.set_step(1, 0.5, true);
seq.set_step(2, 1.0, true);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
seq.tick(&inputs, &mut outputs);
assert!((outputs.get(10).unwrap() - 0.0).abs() < 0.01);
inputs.set(0, 5.0);
seq.tick(&inputs, &mut outputs);
assert!((outputs.get(10).unwrap() - 0.5).abs() < 0.01);
inputs.set(0, 0.0);
seq.tick(&inputs, &mut outputs);
inputs.set(0, 5.0);
seq.tick(&inputs, &mut outputs);
assert!((outputs.get(10).unwrap() - 1.0).abs() < 0.01);
}
#[test]
fn test_sample_and_hold() {
let mut sh = SampleAndHold::new();
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 3.0);
inputs.set(1, 0.0);
sh.tick(&inputs, &mut outputs);
assert!((outputs.get(10).unwrap() - 0.0).abs() < 0.01);
inputs.set(1, 5.0);
sh.tick(&inputs, &mut outputs);
assert!((outputs.get(10).unwrap() - 3.0).abs() < 0.01);
inputs.set(0, 7.0);
sh.tick(&inputs, &mut outputs);
assert!((outputs.get(10).unwrap() - 3.0).abs() < 0.01);
inputs.set(1, 0.0);
sh.tick(&inputs, &mut outputs);
inputs.set(1, 5.0);
sh.tick(&inputs, &mut outputs);
assert!((outputs.get(10).unwrap() - 7.0).abs() < 0.01);
}
#[test]
fn test_slew_limiter() {
let mut slew = SlewLimiter::new(1000.0); let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(1, 0.5); inputs.set(2, 0.5);
inputs.set(0, 5.0);
slew.tick(&inputs, &mut outputs);
let first = outputs.get(10).unwrap();
assert!(first > 0.0);
assert!(first < 5.0);
for _ in 0..100 {
slew.tick(&inputs, &mut outputs);
}
let after_100 = outputs.get(10).unwrap();
assert!(after_100 > first);
}
#[test]
fn test_quantizer_chromatic() {
let mut quant = Quantizer::new(Scale::Chromatic);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 0.0); quant.tick(&inputs, &mut outputs);
assert!((outputs.get(10).unwrap() - 0.0).abs() < 0.01);
inputs.set(0, 0.04); quant.tick(&inputs, &mut outputs);
assert!((outputs.get(10).unwrap() - 0.0).abs() < 0.01);
inputs.set(0, 0.07);
quant.tick(&inputs, &mut outputs);
let expected_csharp = 1.0 / 12.0;
assert!((outputs.get(10).unwrap() - expected_csharp).abs() < 0.01);
}
#[test]
fn test_quantizer_major_scale() {
let mut quant = Quantizer::new(Scale::Major);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 1.0 / 12.0); quant.tick(&inputs, &mut outputs);
let out = outputs.get(10).unwrap();
assert!(out.abs() < 0.01 || (out - 2.0 / 12.0).abs() < 0.01);
}
#[test]
fn test_clock() {
let mut clock = Clock::new(1000.0); let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 10.0);
let mut trigger_count = 0;
let mut last_trigger = 0.0;
for _ in 0..1000 {
clock.tick(&inputs, &mut outputs);
let trigger = outputs.get(10).unwrap(); if trigger > 2.5 && last_trigger <= 2.5 {
trigger_count += 1;
}
last_trigger = trigger;
}
assert!(trigger_count >= 3);
}
#[test]
fn test_attenuverter() {
let mut att = Attenuverter::new();
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 5.0); inputs.set(1, 5.0); att.tick(&inputs, &mut outputs);
assert!((outputs.get(10).unwrap() - 5.0).abs() < 0.1);
inputs.set(1, 2.5);
att.tick(&inputs, &mut outputs);
assert!((outputs.get(10).unwrap() - 2.5).abs() < 0.1);
inputs.set(1, 0.0);
att.tick(&inputs, &mut outputs);
assert!((outputs.get(10).unwrap() - 0.0).abs() < 0.1);
}
#[test]
fn test_multiple() {
let mut mult = Multiple::new();
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 3.5);
mult.tick(&inputs, &mut outputs);
assert!((outputs.get(10).unwrap() - 3.5).abs() < 0.0001);
assert!((outputs.get(11).unwrap() - 3.5).abs() < 0.0001);
assert!((outputs.get(12).unwrap() - 3.5).abs() < 0.0001);
assert!((outputs.get(13).unwrap() - 3.5).abs() < 0.0001);
}
#[test]
fn test_crossfader() {
let mut xf = Crossfader::new();
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 5.0); inputs.set(1, -5.0);
inputs.set(2, -5.0);
xf.tick(&inputs, &mut outputs);
assert!((outputs.get(10).unwrap() - 5.0).abs() < 0.1);
inputs.set(2, 5.0);
xf.tick(&inputs, &mut outputs);
assert!((outputs.get(10).unwrap() - (-5.0)).abs() < 0.1);
inputs.set(2, 0.0);
xf.tick(&inputs, &mut outputs);
let out = outputs.get(10).unwrap();
assert!(out.abs() < 1.0); }
#[test]
fn test_logic_and() {
let mut gate = LogicAnd::new();
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 0.0);
inputs.set(1, 0.0);
gate.tick(&inputs, &mut outputs);
assert!(outputs.get(10).unwrap() < 2.5);
inputs.set(0, 5.0);
inputs.set(1, 0.0);
gate.tick(&inputs, &mut outputs);
assert!(outputs.get(10).unwrap() < 2.5);
inputs.set(0, 5.0);
inputs.set(1, 5.0);
gate.tick(&inputs, &mut outputs);
assert!(outputs.get(10).unwrap() > 2.5);
}
#[test]
fn test_logic_or() {
let mut gate = LogicOr::new();
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 0.0);
inputs.set(1, 0.0);
gate.tick(&inputs, &mut outputs);
assert!(outputs.get(10).unwrap() < 2.5);
inputs.set(0, 5.0);
inputs.set(1, 0.0);
gate.tick(&inputs, &mut outputs);
assert!(outputs.get(10).unwrap() > 2.5);
inputs.set(0, 5.0);
inputs.set(1, 5.0);
gate.tick(&inputs, &mut outputs);
assert!(outputs.get(10).unwrap() > 2.5);
}
#[test]
fn test_logic_xor() {
let mut gate = LogicXor::new();
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 0.0);
inputs.set(1, 0.0);
gate.tick(&inputs, &mut outputs);
assert!(outputs.get(10).unwrap() < 2.5);
inputs.set(0, 5.0);
inputs.set(1, 0.0);
gate.tick(&inputs, &mut outputs);
assert!(outputs.get(10).unwrap() > 2.5);
inputs.set(0, 5.0);
inputs.set(1, 5.0);
gate.tick(&inputs, &mut outputs);
assert!(outputs.get(10).unwrap() < 2.5);
}
#[test]
fn test_logic_not() {
let mut gate = LogicNot::new();
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 0.0);
gate.tick(&inputs, &mut outputs);
assert!(outputs.get(10).unwrap() > 2.5);
inputs.set(0, 5.0);
gate.tick(&inputs, &mut outputs);
assert!(outputs.get(10).unwrap() < 2.5);
}
#[test]
fn test_comparator() {
let mut cmp = Comparator::new();
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 3.0);
inputs.set(1, 1.0);
cmp.tick(&inputs, &mut outputs);
assert!(outputs.get(10).unwrap() > 2.5); assert!(outputs.get(11).unwrap() < 2.5); assert!(outputs.get(12).unwrap() < 2.5);
inputs.set(0, 1.0);
inputs.set(1, 3.0);
cmp.tick(&inputs, &mut outputs);
assert!(outputs.get(10).unwrap() < 2.5); assert!(outputs.get(11).unwrap() > 2.5); assert!(outputs.get(12).unwrap() < 2.5);
inputs.set(0, 2.0);
inputs.set(1, 2.0);
cmp.tick(&inputs, &mut outputs);
assert!(outputs.get(10).unwrap() < 2.5); assert!(outputs.get(11).unwrap() < 2.5); assert!(outputs.get(12).unwrap() > 2.5); }
#[test]
fn test_rectifier() {
let mut rect = Rectifier::new();
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 3.0);
rect.tick(&inputs, &mut outputs);
assert!((outputs.get(10).unwrap() - 3.0).abs() < 0.01); assert!((outputs.get(11).unwrap() - 3.0).abs() < 0.01); assert!((outputs.get(12).unwrap()).abs() < 0.01);
inputs.set(0, -3.0);
rect.tick(&inputs, &mut outputs);
assert!((outputs.get(10).unwrap() - 3.0).abs() < 0.01); assert!((outputs.get(11).unwrap()).abs() < 0.01); assert!((outputs.get(12).unwrap() - 3.0).abs() < 0.01); }
#[test]
fn test_precision_adder() {
let mut adder = PrecisionAdder::new();
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 1.0);
inputs.set(1, 2.0);
inputs.set(2, 0.5);
inputs.set(3, -0.5);
adder.tick(&inputs, &mut outputs);
assert!((outputs.get(10).unwrap() - 3.0).abs() < 0.01); assert!((outputs.get(11).unwrap() - (-3.0)).abs() < 0.01); }
#[test]
fn test_vc_switch() {
let mut sw = VcSwitch::new();
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 3.0); inputs.set(1, 7.0);
inputs.set(2, 0.0);
sw.tick(&inputs, &mut outputs);
assert!((outputs.get(10).unwrap() - 3.0).abs() < 0.01);
assert!((outputs.get(11).unwrap() - 3.0).abs() < 0.01);
assert!((outputs.get(12).unwrap()).abs() < 0.01);
inputs.set(2, 5.0);
sw.tick(&inputs, &mut outputs);
assert!((outputs.get(10).unwrap() - 7.0).abs() < 0.01);
assert!((outputs.get(11).unwrap()).abs() < 0.01);
assert!((outputs.get(12).unwrap() - 7.0).abs() < 0.01);
}
#[test]
fn test_bernoulli_gate() {
let mut bg = BernoulliGate::new();
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(1, 10.0);
inputs.set(0, 0.0);
bg.tick(&inputs, &mut outputs);
inputs.set(0, 5.0);
bg.tick(&inputs, &mut outputs);
assert!(outputs.get(10).unwrap() > 2.5); assert!(outputs.get(11).unwrap() < 2.5);
bg.reset();
inputs.set(1, 0.0);
inputs.set(0, 0.0);
bg.tick(&inputs, &mut outputs);
inputs.set(0, 5.0);
bg.tick(&inputs, &mut outputs);
assert!(outputs.get(10).unwrap() < 2.5); assert!(outputs.get(11).unwrap() > 2.5); }
#[test]
fn test_min() {
let mut m = Min::new();
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 3.0);
inputs.set(1, 5.0);
m.tick(&inputs, &mut outputs);
assert!((outputs.get(10).unwrap() - 3.0).abs() < 0.01);
inputs.set(0, 7.0);
inputs.set(1, 2.0);
m.tick(&inputs, &mut outputs);
assert!((outputs.get(10).unwrap() - 2.0).abs() < 0.01);
}
#[test]
fn test_max() {
let mut m = Max::new();
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 3.0);
inputs.set(1, 5.0);
m.tick(&inputs, &mut outputs);
assert!((outputs.get(10).unwrap() - 5.0).abs() < 0.01);
inputs.set(0, 7.0);
inputs.set(1, 2.0);
m.tick(&inputs, &mut outputs);
assert!((outputs.get(10).unwrap() - 7.0).abs() < 0.01);
}
#[test]
fn test_mixer_default_reset_sample_rate() {
let mut mixer = Mixer::default();
mixer.reset();
mixer.set_sample_rate(48000.0);
assert_eq!(mixer.type_id(), "mixer");
}
#[test]
fn test_stereo_output_default_reset_sample_rate() {
let mut stereo = StereoOutput::default();
stereo.reset();
stereo.set_sample_rate(48000.0);
assert_eq!(stereo.type_id(), "stereo_output");
}
#[test]
fn test_offset_default_reset_sample_rate() {
let mut offset = Offset::default();
offset.reset();
offset.set_sample_rate(48000.0);
assert_eq!(offset.type_id(), "offset");
}
#[test]
fn test_scale_enum_semitones() {
let scale = Scale::Chromatic;
assert!(scale.semitones().len() == 12);
let scale = Scale::Major;
assert!(scale.semitones().len() == 7);
let scale = Scale::PentatonicMajor;
assert!(scale.semitones().len() == 5);
}
#[test]
fn test_step_sequencer_default_reset_sample_rate() {
let mut seq = StepSequencer::default();
seq.set_step(0, 1.0, true);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 5.0);
seq.tick(&inputs, &mut outputs);
seq.reset();
assert!(seq.current == 0);
assert!(seq.prev_clock == 0.0);
seq.set_sample_rate(48000.0);
assert_eq!(seq.type_id(), "step_sequencer");
}
#[test]
fn test_sample_and_hold_default_reset_sample_rate() {
let mut sh = SampleAndHold::default();
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 5.0);
inputs.set(1, 5.0);
sh.tick(&inputs, &mut outputs);
sh.reset();
assert!(sh.held_value == 0.0);
sh.set_sample_rate(48000.0);
assert_eq!(sh.type_id(), "sample_hold");
}
#[test]
fn test_slew_limiter_default_reset_sample_rate() {
let mut slew = SlewLimiter::default();
assert!(slew.sample_rate == 44100.0);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 5.0);
slew.tick(&inputs, &mut outputs);
slew.reset();
assert!(slew.current == 0.0);
slew.set_sample_rate(48000.0);
assert!(slew.sample_rate == 48000.0);
assert_eq!(slew.type_id(), "slew_limiter");
}
#[test]
fn test_quantizer_default_reset_sample_rate() {
let mut quant = Quantizer::default();
quant.reset();
quant.set_sample_rate(48000.0);
assert_eq!(quant.type_id(), "quantizer");
}
#[test]
fn test_clock_default_reset_sample_rate() {
let mut clock = Clock::default();
assert!(clock.sample_rate == 44100.0);
let inputs = PortValues::new();
let mut outputs = PortValues::new();
for _ in 0..100 {
clock.tick(&inputs, &mut outputs);
}
clock.reset();
assert!(clock.phase == 0.0);
clock.set_sample_rate(48000.0);
assert!(clock.sample_rate == 48000.0);
assert_eq!(clock.type_id(), "clock");
}
#[test]
fn test_attenuverter_default_reset_sample_rate() {
let mut att = Attenuverter::default();
att.reset();
att.set_sample_rate(48000.0);
assert_eq!(att.type_id(), "attenuverter");
}
#[test]
fn test_multiple_default_reset_sample_rate() {
let mut mult = Multiple::default();
mult.reset();
mult.set_sample_rate(48000.0);
assert_eq!(mult.type_id(), "multiple");
}
#[test]
fn test_crossfader_default_reset_sample_rate() {
let mut xf = Crossfader::default();
xf.reset();
xf.set_sample_rate(48000.0);
assert_eq!(xf.type_id(), "crossfader");
}
#[test]
fn test_logic_and_default_reset_sample_rate() {
let mut gate = LogicAnd::default();
gate.reset();
gate.set_sample_rate(48000.0);
assert_eq!(gate.type_id(), "logic_and");
}
#[test]
fn test_logic_or_default_reset_sample_rate() {
let mut gate = LogicOr::default();
gate.reset();
gate.set_sample_rate(48000.0);
assert_eq!(gate.type_id(), "logic_or");
}
#[test]
fn test_logic_xor_default_reset_sample_rate() {
let mut gate = LogicXor::default();
gate.reset();
gate.set_sample_rate(48000.0);
assert_eq!(gate.type_id(), "logic_xor");
}
#[test]
fn test_logic_not_default_reset_sample_rate() {
let mut gate = LogicNot::default();
gate.reset();
gate.set_sample_rate(48000.0);
assert_eq!(gate.type_id(), "logic_not");
}
#[test]
fn test_comparator_default_reset_sample_rate() {
let mut cmp = Comparator::default();
cmp.reset();
cmp.set_sample_rate(48000.0);
assert_eq!(cmp.type_id(), "comparator");
}
#[test]
fn test_rectifier_default_reset_sample_rate() {
let mut rect = Rectifier::default();
rect.reset();
rect.set_sample_rate(48000.0);
assert_eq!(rect.type_id(), "rectifier");
}
#[test]
fn test_precision_adder_default_reset_sample_rate() {
let mut adder = PrecisionAdder::default();
adder.reset();
adder.set_sample_rate(48000.0);
assert_eq!(adder.type_id(), "precision_adder");
}
#[test]
fn test_vc_switch_default_reset_sample_rate() {
let mut sw = VcSwitch::default();
sw.reset();
sw.set_sample_rate(48000.0);
assert_eq!(sw.type_id(), "vc_switch");
}
#[test]
fn test_bernoulli_gate_default_reset_sample_rate() {
let mut bg = BernoulliGate::default();
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 5.0);
bg.tick(&inputs, &mut outputs);
bg.reset();
assert!(bg.prev_trigger == 0.0);
bg.set_sample_rate(48000.0);
assert_eq!(bg.type_id(), "bernoulli_gate");
}
#[test]
fn test_min_default_reset_sample_rate() {
let mut m = Min::default();
m.reset();
m.set_sample_rate(48000.0);
assert_eq!(m.type_id(), "min");
}
#[test]
fn test_max_default_reset_sample_rate() {
let mut m = Max::default();
m.reset();
m.set_sample_rate(48000.0);
assert_eq!(m.type_id(), "max");
}
#[test]
fn test_step_sequencer_skip_disabled() {
let mut seq = StepSequencer::new();
seq.set_step(0, 1.0, true);
seq.set_step(1, 2.0, false); seq.set_step(2, 3.0, true);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
seq.tick(&inputs, &mut outputs);
let _out = outputs.get(10).unwrap_or(0.0);
inputs.set(0, 5.0);
seq.tick(&inputs, &mut outputs);
}
#[test]
fn test_quantizer_pentatonic_scale() {
let mut quant = Quantizer::new(Scale::PentatonicMajor);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 0.0);
quant.tick(&inputs, &mut outputs);
assert!(outputs.get(10).unwrap().abs() < 0.01);
}
#[test]
fn test_quantizer_blues_scale() {
let mut quant = Quantizer::new(Scale::Blues);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 0.0);
quant.tick(&inputs, &mut outputs);
assert!(outputs.get(10).is_some());
}
#[test]
fn test_slew_limiter_falling() {
let mut slew = SlewLimiter::new(1000.0);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 5.0);
inputs.set(1, 10.0); inputs.set(2, 0.5); for _ in 0..1000 {
slew.tick(&inputs, &mut outputs);
}
inputs.set(0, 0.0);
slew.tick(&inputs, &mut outputs);
let falling = outputs.get(10).unwrap();
assert!(falling < 5.0);
assert!(falling > 0.0);
}
#[test]
fn test_scale_dorian_and_mixolydian() {
let scale = Scale::Dorian;
assert!(scale.semitones().len() == 7);
let scale = Scale::Mixolydian;
assert!(scale.semitones().len() == 7);
}
#[test]
fn test_clock_subdivisions() {
let mut clock = Clock::new(1000.0);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 5.0);
for _ in 0..1000 {
clock.tick(&inputs, &mut outputs);
}
assert!(outputs.get(10).is_some()); assert!(outputs.get(11).is_some()); assert!(outputs.get(12).is_some()); }
#[test]
fn test_chord_memory_major() {
let mut cm = ChordMemory::new();
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 0.0);
inputs.set(1, 0.0); inputs.set(2, 0.0); inputs.set(3, 0.0);
cm.tick(&inputs, &mut outputs);
let voice1 = outputs.get(10).unwrap();
let voice2 = outputs.get(11).unwrap();
let voice3 = outputs.get(12).unwrap();
let voice4 = outputs.get(13).unwrap();
assert!((voice1 - 0.0).abs() < 0.01); assert!((voice2 - 4.0 / 12.0).abs() < 0.01); assert!((voice3 - 7.0 / 12.0).abs() < 0.01); assert!((voice4 - 1.0).abs() < 0.01); }
#[test]
fn test_chord_memory_minor() {
let mut cm = ChordMemory::new();
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 0.0);
inputs.set(1, 0.15);
cm.tick(&inputs, &mut outputs);
let voice2 = outputs.get(11).unwrap();
assert!((voice2 - 3.0 / 12.0).abs() < 0.01); }
#[test]
fn test_chord_memory_seventh() {
let mut cm = ChordMemory::new();
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 0.0);
inputs.set(1, 0.26);
cm.tick(&inputs, &mut outputs);
let voice4 = outputs.get(13).unwrap();
assert!((voice4 - 10.0 / 12.0).abs() < 0.01); }
#[test]
fn test_chord_memory_inversion() {
let mut cm = ChordMemory::new();
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 0.0);
inputs.set(1, 0.0); inputs.set(2, 0.4);
cm.tick(&inputs, &mut outputs);
let voice1 = outputs.get(10).unwrap();
let voice2 = outputs.get(11).unwrap();
let voice3 = outputs.get(12).unwrap();
assert!((voice1 - 4.0 / 12.0).abs() < 0.01);
assert!((voice2 - 7.0 / 12.0).abs() < 0.01);
assert!((voice3 - 1.0).abs() < 0.01);
}
#[test]
fn test_chord_memory_spread() {
let mut cm = ChordMemory::new();
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 0.0);
inputs.set(1, 0.0); inputs.set(2, 0.0); inputs.set(3, 1.0);
cm.tick(&inputs, &mut outputs);
let voice1 = outputs.get(10).unwrap();
let voice2 = outputs.get(11).unwrap();
let voice3 = outputs.get(12).unwrap();
let voice4 = outputs.get(13).unwrap();
assert!(voice1 < voice2);
assert!(voice2 < voice3);
assert!(voice3 < voice4);
}
#[test]
fn test_chord_memory_all_chord_types() {
let mut cm = ChordMemory::new();
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
for i in 0..9 {
let chord_cv = i as f64 / 9.0;
inputs.set(0, 0.0);
inputs.set(1, chord_cv);
cm.tick(&inputs, &mut outputs);
assert!(outputs.get(10).is_some());
assert!(outputs.get(11).is_some());
assert!(outputs.get(12).is_some());
assert!(outputs.get(13).is_some());
}
}
#[test]
fn test_chord_memory_default_reset_sample_rate() {
let mut cm = ChordMemory::default();
cm.reset();
cm.set_sample_rate(48000.0);
assert_eq!(cm.type_id(), "chord_memory");
assert_eq!(cm.port_spec().inputs.len(), 4);
assert_eq!(cm.port_spec().outputs.len(), 4);
}
#[test]
fn test_chord_type_intervals() {
assert_eq!(ChordType::Major.intervals(), &[0, 4, 7]);
assert_eq!(ChordType::Minor.intervals(), &[0, 3, 7]);
assert_eq!(ChordType::Seventh.intervals(), &[0, 4, 7, 10]);
assert_eq!(ChordType::MajorSeventh.intervals(), &[0, 4, 7, 11]);
assert_eq!(ChordType::MinorSeventh.intervals(), &[0, 3, 7, 10]);
assert_eq!(ChordType::Diminished.intervals(), &[0, 3, 6]);
assert_eq!(ChordType::Augmented.intervals(), &[0, 4, 8]);
assert_eq!(ChordType::Sus2.intervals(), &[0, 2, 7]);
assert_eq!(ChordType::Sus4.intervals(), &[0, 5, 7]);
}
#[test]
fn test_chord_type_from_cv() {
assert_eq!(ChordType::from_cv(0.0), ChordType::Major);
assert_eq!(ChordType::from_cv(0.12), ChordType::Minor);
assert_eq!(ChordType::from_cv(0.23), ChordType::Seventh);
assert_eq!(ChordType::from_cv(1.0), ChordType::Sus4);
}
#[test]
fn test_arp_pattern_from_cv() {
assert_eq!(ArpPattern::from_cv(0.0), ArpPattern::Up);
assert_eq!(ArpPattern::from_cv(0.1), ArpPattern::Up);
assert_eq!(ArpPattern::from_cv(0.3), ArpPattern::Down);
assert_eq!(ArpPattern::from_cv(0.6), ArpPattern::UpDown);
assert_eq!(ArpPattern::from_cv(0.9), ArpPattern::Random);
assert_eq!(ArpPattern::from_cv(1.0), ArpPattern::Random);
}
#[test]
fn test_arpeggiator_default_reset_sample_rate() {
let mut arp = Arpeggiator::default();
assert_eq!(arp.sample_rate, 44100.0);
arp.add_note(0.0);
assert_eq!(arp.num_notes, 1);
arp.reset();
assert_eq!(arp.num_notes, 0);
assert_eq!(arp.current_step, 0);
arp.set_sample_rate(48000.0);
assert_eq!(arp.sample_rate, 48000.0);
assert_eq!(arp.type_id(), "arpeggiator");
assert_eq!(arp.port_spec().inputs.len(), 6);
assert_eq!(arp.port_spec().outputs.len(), 3);
}
#[test]
fn test_arpeggiator_add_remove_notes() {
let mut arp = Arpeggiator::new(44100.0);
arp.add_note(0.0); arp.add_note(0.5); arp.add_note(0.25);
assert_eq!(arp.num_notes, 3);
assert_eq!(arp.held_notes[0], 0.0);
assert_eq!(arp.held_notes[1], 0.25);
assert_eq!(arp.held_notes[2], 0.5);
arp.remove_note(0.25);
assert_eq!(arp.num_notes, 2);
assert_eq!(arp.held_notes[0], 0.0);
assert_eq!(arp.held_notes[1], 0.5);
}
#[test]
fn test_arpeggiator_up_pattern() {
let mut arp = Arpeggiator::new(44100.0);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
arp.add_note(0.0); arp.add_note(0.333); arp.add_note(0.583); inputs.set(1, 0.0);
assert_eq!(arp.num_notes, 3);
inputs.set(3, 0.0); let mut notes_out = Vec::new();
for _ in 0..6 {
inputs.set(2, 5.0); arp.tick(&inputs, &mut outputs);
notes_out.push(outputs.get(10).unwrap());
inputs.set(2, 0.0); arp.tick(&inputs, &mut outputs);
}
assert!(notes_out[0] < notes_out[1]);
assert!(notes_out[1] < notes_out[2]);
assert!((notes_out[3] - notes_out[0]).abs() < 0.01);
}
#[test]
fn test_arpeggiator_trigger_output() {
let mut arp = Arpeggiator::new(44100.0);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 0.0);
inputs.set(1, 5.0);
arp.tick(&inputs, &mut outputs);
inputs.set(2, 5.0);
arp.tick(&inputs, &mut outputs);
let trigger = outputs.get(12).unwrap();
assert!(trigger > 0.0, "Should output trigger on clock");
inputs.set(2, 0.0);
arp.tick(&inputs, &mut outputs);
let trigger2 = outputs.get(12).unwrap();
assert!(trigger2 > 0.0, "Trigger should persist briefly");
}
#[test]
fn test_arpeggiator_reset_input() {
let mut arp = Arpeggiator::new(44100.0);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 0.0);
inputs.set(1, 5.0);
arp.tick(&inputs, &mut outputs);
for _ in 0..5 {
inputs.set(2, 5.0);
arp.tick(&inputs, &mut outputs);
inputs.set(2, 0.0);
arp.tick(&inputs, &mut outputs);
}
let step_before = arp.current_step;
assert!(step_before > 0);
inputs.set(5, 5.0);
arp.tick(&inputs, &mut outputs);
assert_eq!(arp.current_step, 0, "Reset should clear step");
}
#[test]
fn test_arpeggiator_octaves() {
let mut arp = Arpeggiator::new(44100.0);
arp.add_note(0.0);
let note1 = arp.get_current_note(ArpPattern::Up, 2);
arp.current_step = 1;
let note2 = arp.get_current_note(ArpPattern::Up, 2);
assert!(
(note2 - note1 - 1.0).abs() < 0.01,
"Second note should be 1 octave higher"
);
}
#[test]
fn test_mixer_summation_bounded() {
let mut mixer = Mixer::new(4);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
for i in 0..4 {
inputs.set(i as u32, 5.0);
}
mixer.tick(&inputs, &mut outputs);
let out = outputs.get(100).unwrap_or(0.0);
assert!(
out.abs() <= SAFE_AUDIO_LIMIT * 2.0,
"Mixer output {} is very high - consider adding limiting",
out
);
}
#[test]
fn test_scale_quantizer_octave_wrap_minor_11() {
assert_eq!(
ScaleQuantizer::quantize_to_scale(11, &ScaleQuantizer::MINOR),
12
);
}
#[test]
fn test_scale_quantizer_monotonic_sweep() {
for scale in [
&ScaleQuantizer::MINOR[..],
&ScaleQuantizer::PENT_MAJOR[..],
&ScaleQuantizer::BLUES[..],
] {
let mut prev = i32::MIN;
for note in 0..=24 {
let q = ScaleQuantizer::quantize_to_scale(note, scale);
assert!(
q >= prev,
"non-monotonic: note {} -> {} after {}",
note,
q,
prev
);
prev = q;
}
}
}
#[test]
fn test_quantizer_hysteresis_no_chatter() {
let mut quant = Quantizer::new(Scale::Chromatic);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
let n = 4000;
let mut changes = 0;
let mut prev: Option<f64> = None;
for i in 0..=n {
let base = 2.0 * i as f64 / n as f64; let dither = if i % 2 == 0 { 0.1 } else { -0.1 };
inputs.set(0, (base + dither) / 12.0);
quant.tick(&inputs, &mut outputs);
let out = outputs.get(10).unwrap();
if let Some(p) = prev {
if (out - p).abs() > 1e-9 {
changes += 1;
}
}
prev = Some(out);
}
assert_eq!(changes, 2, "expected exactly two boundary crossings");
}
#[test]
fn test_scale_quantizer_trigger_once_per_boundary() {
let mut sq = ScaleQuantizer::new(44100.0);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(2, 0.0);
let n = 4000;
let mut triggers = 0;
for i in 0..=n {
let base = 2.0 * i as f64 / n as f64;
let dither = if i % 2 == 0 { 0.1 } else { -0.1 };
inputs.set(0, (base + dither) / 12.0);
sq.tick(&inputs, &mut outputs);
if outputs.get(11).unwrap() > 2.5 {
triggers += 1;
}
}
assert_eq!(triggers, 2, "trigger should fire once per boundary");
}
#[test]
fn test_comparator_hysteresis_no_chatter() {
let mut cmp = Comparator::new();
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(1, 0.0);
inputs.set(0, 0.0);
cmp.tick(&inputs, &mut outputs);
let mut gt_high = 0;
let mut lt_high = 0;
for i in 0..200 {
let a = if i % 2 == 0 { 0.02 } else { -0.02 };
inputs.set(0, a);
cmp.tick(&inputs, &mut outputs);
if outputs.get(10).unwrap() > 2.5 {
gt_high += 1;
}
if outputs.get(11).unwrap() > 2.5 {
lt_high += 1;
}
assert!(outputs.get(12).unwrap() > 2.5, "should stay equal");
}
assert_eq!(gt_high, 0, "gt should never fire on sub-band dither");
assert_eq!(lt_high, 0, "lt should never fire on sub-band dither");
}
#[test]
fn test_comparator_still_compares() {
let mut cmp = Comparator::new();
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 3.0);
inputs.set(1, 1.0);
cmp.tick(&inputs, &mut outputs);
assert!(outputs.get(10).unwrap() > 2.5);
inputs.set(0, 1.0);
inputs.set(1, 3.0);
cmp.tick(&inputs, &mut outputs);
assert!(outputs.get(11).unwrap() > 2.5);
inputs.set(0, 2.0);
inputs.set(1, 2.0);
cmp.tick(&inputs, &mut outputs);
assert!(outputs.get(12).unwrap() > 2.5);
}
#[test]
fn test_clock_divided_outputs() {
let mut clock = Clock::new(1000.0);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 5.0);
let (mut main_c, mut div2_c, mut div4_c) = (0, 0, 0);
let (mut pm, mut p2, mut p4) = (0.0, 0.0, 0.0);
for _ in 0..100_000 {
clock.tick(&inputs, &mut outputs);
let m = outputs.get(10).unwrap();
let d2 = outputs.get(11).unwrap();
let d4 = outputs.get(12).unwrap();
let m_rise = m > 2.5 && pm <= 2.5;
if m_rise {
main_c += 1;
}
if d2 > 2.5 && p2 <= 2.5 {
div2_c += 1;
}
if d4 > 2.5 && p4 <= 2.5 {
div4_c += 1;
}
pm = m;
p2 = d2;
p4 = d4;
if m_rise && main_c == 8 {
break;
}
}
assert_eq!(main_c, 8, "main should pulse every cycle");
assert_eq!(div2_c, 4, "div2 should pulse at half rate");
assert_eq!(div4_c, 2, "div4 should pulse at quarter rate");
}
#[test]
fn test_clock_default_tempo_120_bpm() {
let mut clock = Clock::default();
let inputs = PortValues::new(); let mut outputs = PortValues::new();
let mut edges = Vec::new();
let mut prev = 0.0;
for i in 0..100_000 {
clock.tick(&inputs, &mut outputs);
let m = outputs.get(10).unwrap();
if m > 2.5 && prev <= 2.5 {
edges.push(i);
}
prev = m;
if edges.len() >= 2 {
break;
}
}
assert!(edges.len() >= 2, "expected at least two clock pulses");
let period = (edges[1] - edges[0]) as f64;
let bpm = 60.0 * 44100.0 / period;
assert!(
(bpm - 120.0).abs() < 1.0,
"default tempo {} BPM should be ~120",
bpm
);
}
#[test]
fn test_bernoulli_gate_latches() {
let mut bg = BernoulliGate::new();
let mut inputs = PortValues::new();
inputs.set(1, 10.0);
let tick = |bg: &mut BernoulliGate, inputs: &PortValues| {
let mut o = PortValues::new();
bg.tick(inputs, &mut o);
o
};
inputs.set(0, 0.0);
tick(&mut bg, &inputs);
inputs.set(0, 5.0);
let o = tick(&mut bg, &inputs); assert!(o.get(12).unwrap() > 2.5, "gate_a should latch high");
assert!(o.get(13).unwrap() < 2.5);
inputs.set(0, 0.0);
for _ in 0..20 {
let o = tick(&mut bg, &inputs);
assert!(o.get(12).unwrap() > 2.5, "gate_a must stay latched");
assert!(o.get(13).unwrap() < 2.5);
}
inputs.set(1, 0.0);
inputs.set(0, 0.0);
tick(&mut bg, &inputs);
inputs.set(0, 5.0);
let o = tick(&mut bg, &inputs); assert!(o.get(12).unwrap() < 2.5, "gate_a should release");
assert!(o.get(13).unwrap() > 2.5, "gate_b should latch high");
}
#[test]
fn test_euclidean_pulses_control_live() {
let mut euc = Euclidean::new(44100.0);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(1, 0.5);
inputs.set(2, 0.25); euc.tick(&inputs, &mut outputs);
let active_low = euc.pattern.iter().filter(|&&x| x).count();
assert_eq!(active_low, 2);
inputs.set(2, 0.75); euc.tick(&inputs, &mut outputs);
let active_high = euc.pattern.iter().filter(|&&x| x).count();
assert_eq!(active_high, 6);
assert_ne!(active_low, active_high, "pulses control must be live");
}
#[test]
fn test_euclidean_accent_on_rotated_pulse() {
let mut euc = Euclidean::new(44100.0);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(1, 0.4003); inputs.set(2, 0.5); inputs.set(3, 0.3);
let mut accents = 0;
for _ in 0..8 {
inputs.set(0, 5.0); euc.tick(&inputs, &mut outputs);
let out = outputs.get(10).unwrap();
let accent = outputs.get(11).unwrap();
if accent > 2.5 {
accents += 1;
assert!(out > 2.5, "accent must coincide with a pulse");
}
inputs.set(0, 0.0); euc.tick(&inputs, &mut outputs);
}
assert_eq!(accents, 1, "exactly one accent per cycle");
}
#[test]
fn test_euclidean_gate_threshold() {
let mut euc = Euclidean::new(44100.0);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(1, 0.4003); inputs.set(2, 1.0);
let mut low_pulses = 0;
for _ in 0..8 {
inputs.set(0, 1.0);
euc.tick(&inputs, &mut outputs);
if outputs.get(10).unwrap() > 2.5 {
low_pulses += 1;
}
inputs.set(0, 0.0);
euc.tick(&inputs, &mut outputs);
}
assert_eq!(low_pulses, 0, "1.0V clock must not trigger");
let mut high_pulses = 0;
for _ in 0..8 {
inputs.set(0, 5.0);
euc.tick(&inputs, &mut outputs);
if outputs.get(10).unwrap() > 2.5 {
high_pulses += 1;
}
inputs.set(0, 0.0);
euc.tick(&inputs, &mut outputs);
}
assert!(high_pulses > 0, "5V clock must trigger");
}
#[test]
fn test_arpeggiator_releases_notes() {
let mut arp = Arpeggiator::new(44100.0);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 0.25);
inputs.set(1, 5.0);
arp.tick(&inputs, &mut outputs);
assert_eq!(arp.num_notes, 1);
for _ in 0..5 {
arp.tick(&inputs, &mut outputs);
}
assert_eq!(arp.num_notes, 1);
inputs.set(1, 0.0);
arp.tick(&inputs, &mut outputs);
assert_eq!(arp.num_notes, 0, "release must remove the held note");
inputs.set(0, 0.5);
inputs.set(1, 5.0);
arp.tick(&inputs, &mut outputs);
assert_eq!(arp.num_notes, 1);
inputs.set(1, 0.0);
arp.tick(&inputs, &mut outputs);
assert_eq!(arp.num_notes, 0);
}
#[test]
fn test_arpeggiator_reset_clears_held_notes() {
let mut arp = Arpeggiator::new(44100.0);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 0.0);
inputs.set(1, 5.0);
arp.tick(&inputs, &mut outputs);
assert_eq!(arp.num_notes, 1);
inputs.set(5, 5.0);
arp.tick(&inputs, &mut outputs);
assert_eq!(arp.num_notes, 0, "reset must clear held notes");
}
#[cfg(feature = "alloc")]
#[test]
fn test_custom_scale_snaps_to_degrees() {
let mut sq = ScaleQuantizer::new(44100.0);
assert!(!sq.has_custom_scale());
sq.set_custom_scale(&[0.0, 200.0, 400.0, 600.0, 800.0, 1000.0]);
assert!(sq.has_custom_scale());
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 0.175);
for _ in 0..4 {
sq.tick(&inputs, &mut outputs);
}
let out_v = outputs.get(10).unwrap();
assert!(
(out_v - 200.0 / 1200.0).abs() < 1e-6,
"custom scale should snap to 200 cents, got {} cents",
out_v * 1200.0
);
}
#[cfg(feature = "alloc")]
#[test]
fn test_load_scala_and_quantize() {
let scl = "\
whole tone
6
200.0
400.0
600.0
800.0
1000.0
1200.0
";
let mut sq = ScaleQuantizer::new(44100.0);
sq.load_scala(scl).unwrap();
assert!(sq.has_custom_scale());
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 390.0 / 1200.0);
for _ in 0..4 {
sq.tick(&inputs, &mut outputs);
}
let out_v = outputs.get(10).unwrap();
assert!(
(out_v - 400.0 / 1200.0).abs() < 1e-6,
"expected 400 cents, got {} cents",
out_v * 1200.0
);
}
#[cfg(feature = "alloc")]
#[test]
fn test_clear_custom_scale_restores_enum() {
let mut sq = ScaleQuantizer::new(44100.0);
sq.set_custom_scale(&[0.0, 200.0, 400.0]);
assert!(sq.has_custom_scale());
sq.clear_custom_scale();
assert!(!sq.has_custom_scale());
}
#[cfg(feature = "alloc")]
#[test]
fn test_load_scala_malformed_leaves_scale_unchanged() {
let mut sq = ScaleQuantizer::new(44100.0);
sq.set_custom_scale(&[0.0, 500.0]);
let err = sq.load_scala("bad\n3\n100.0\n");
assert!(err.is_err());
assert!(sq.has_custom_scale());
}
#[test]
fn test_quantizer_negative_voct_chromatic() {
let cases = [
(-0.5, -0.5), (-1.0, -1.0), (-13.0 / 12.0, -13.0 / 12.0), (-1.0 / 24.0, 0.0), ];
for (input, expected) in cases {
let mut q = Quantizer::new(Scale::Chromatic);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, input);
q.tick(&inputs, &mut outputs);
let out = outputs.get(10).unwrap();
assert!(
(out - expected).abs() < 1e-9,
"chromatic {input}V -> {out}V, expected {expected}V"
);
}
}
#[test]
fn test_quantizer_negative_voct_major_scale() {
let mut q = Quantizer::new(Scale::Major);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, -0.5);
q.tick(&inputs, &mut outputs);
let out = outputs.get(10).unwrap();
assert!(
(out - (-7.0 / 12.0)).abs() < 1e-9,
"major -0.5V should snap to F3 (-7/12 V), got {out}V"
);
let mut q2 = Quantizer::new(Scale::Major);
inputs.set(0, -1.0);
q2.tick(&inputs, &mut outputs);
assert!((outputs.get(10).unwrap() - (-1.0)).abs() < 1e-9);
}
#[test]
fn test_scale_quantizer_negative_voct() {
let cases = [(-1.0, -1.0), (-13.0 / 12.0, -13.0 / 12.0)];
for (input, expected) in cases {
let mut sq = ScaleQuantizer::new(44100.0);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, input);
inputs.set(2, 0.0); sq.tick(&inputs, &mut outputs);
let out = outputs.get(10).unwrap();
assert!(
(out - expected).abs() < 1e-9,
"scale-quantizer chromatic {input}V -> {out}V, expected {expected}V"
);
}
let mut sq = ScaleQuantizer::new(44100.0);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, -0.5);
inputs.set(2, 0.3); sq.tick(&inputs, &mut outputs);
let out = outputs.get(10).unwrap();
assert!(
(out - (-7.0 / 12.0)).abs() < 1e-9,
"minor -0.5V should snap to F3 (-7/12 V), got {out}V"
);
}
#[test]
fn test_euclidean_reset_and_sample_rate() {
let mut euc = Euclidean::new(44100.0);
assert_eq!(euc.type_id(), "euclidean");
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
for _ in 0..3 {
inputs.set(0, 5.0);
euc.tick(&inputs, &mut outputs);
inputs.set(0, 0.0);
euc.tick(&inputs, &mut outputs);
}
assert!(euc.step != 0, "clock pulses should advance the step");
euc.reset();
assert_eq!(euc.step, 0);
assert!(!euc.cycle_accented);
euc.set_sample_rate(48000.0);
inputs.set(0, 5.0);
euc.tick(&inputs, &mut outputs);
assert!(outputs.get(10).unwrap().is_finite());
}
#[test]
fn test_scale_quantizer_reset_and_sample_rate() {
let mut sq = ScaleQuantizer::new(44100.0);
assert_eq!(sq.type_id(), "scale_quantizer");
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 0.25); sq.tick(&inputs, &mut outputs);
assert!(sq.last_output.is_some());
sq.reset();
assert!(sq.last_output.is_none());
sq.set_sample_rate(48000.0);
sq.tick(&inputs, &mut outputs);
assert!(outputs.get(10).unwrap().is_finite());
}
}