Skip to main content

proof_engine/audio/
synthesis.rs

1//! Audio synthesis: oscillators, envelopes, LFOs, filters, mod-matrix, voices,
2//! polyphony, arpeggiator, step sequencer, drum machine, and synth patches.
3
4use std::f32::consts::{PI, TAU};
5
6// ---------------------------------------------------------------------------
7// Helpers
8// ---------------------------------------------------------------------------
9
10#[inline]
11fn lerp(a: f32, b: f32, t: f32) -> f32 { a + (b - a) * t }
12
13#[inline]
14fn note_to_freq(note: u8, concert_a: f32) -> f32 {
15    concert_a * 2.0f32.powf((note as f32 - 69.0) / 12.0)
16}
17
18#[inline]
19fn semitones_to_ratio(semi: f32) -> f32 {
20    2.0f32.powf(semi / 12.0)
21}
22
23// ---------------------------------------------------------------------------
24// Oscillator
25// ---------------------------------------------------------------------------
26
27#[derive(Clone, Copy, Debug, PartialEq)]
28pub enum OscWaveform {
29    Sine,
30    Square,
31    Triangle,
32    Sawtooth,
33    SawtoothBlit,
34    WhiteNoise,
35    PinkNoise,
36    BrownNoise,
37    Wavetable,
38}
39
40#[derive(Clone, Debug)]
41pub struct UnisonVoice {
42    pub phase: f32,
43    pub detune_semitones: f32,
44    pub pan: f32,
45}
46
47/// Wavetable: a single cycle of audio, interpolated at playback.
48#[derive(Clone, Debug)]
49pub struct Wavetable {
50    pub samples: Vec<f32>,
51}
52impl Wavetable {
53    pub fn sine(size: usize) -> Self {
54        let samples = (0..size).map(|i| (TAU * i as f32 / size as f32).sin()).collect();
55        Self { samples }
56    }
57    pub fn read(&self, phase: f32) -> f32 {
58        if self.samples.is_empty() { return 0.0; }
59        let n = self.samples.len();
60        let pos = phase.fract() * n as f32;
61        let i0 = pos as usize % n;
62        let i1 = (i0 + 1) % n;
63        let frac = pos.fract();
64        lerp(self.samples[i0], self.samples[i1], frac)
65    }
66}
67
68/// Multi-voice oscillator with unison, tuning, and multiple waveforms.
69#[derive(Clone, Debug)]
70pub struct Oscillator {
71    pub waveform: OscWaveform,
72    pub coarse_semitones: f32,
73    pub fine_cents: f32,
74    pub unison_voices: usize,
75    pub unison_detune: f32,
76    pub unison_spread: f32,
77    pub wavetable: Option<Wavetable>,
78    pub pwm: f32,
79
80    // Per-unison-voice state
81    phases: Vec<f32>,
82    // Pink noise state
83    pink_b: [f32; 7],
84    // Brown noise state
85    brown_last: f32,
86    blit_n: usize,
87}
88
89impl Oscillator {
90    pub fn new(waveform: OscWaveform) -> Self {
91        let mut osc = Self {
92            waveform,
93            coarse_semitones: 0.0,
94            fine_cents: 0.0,
95            unison_voices: 1,
96            unison_detune: 0.1,
97            unison_spread: 0.5,
98            wavetable: Some(Wavetable::sine(2048)),
99            pwm: 0.5,
100            phases: vec![0.0],
101            pink_b: [0.0; 7],
102            brown_last: 0.0,
103            blit_n: 0,
104        };
105        osc.set_unison_voices(1);
106        osc
107    }
108
109    pub fn set_unison_voices(&mut self, count: usize) {
110        let count = count.clamp(1, 8);
111        self.unison_voices = count;
112        self.phases = (0..count).map(|i| i as f32 / count as f32).collect();
113    }
114
115    /// Render one sample at the given base frequency.
116    pub fn render_sample(&mut self, base_freq: f32, sample_rate: f32) -> f32 {
117        let freq_mod = semitones_to_ratio(self.coarse_semitones + self.fine_cents / 100.0);
118        let base = base_freq * freq_mod;
119        let n = self.unison_voices;
120        let mut out = 0.0f32;
121
122        for i in 0..n {
123            let detune_ratio = if n > 1 {
124                let t = if n == 1 { 0.0 } else { (i as f32 / (n - 1) as f32) * 2.0 - 1.0 };
125                semitones_to_ratio(t * self.unison_detune)
126            } else { 1.0 };
127            let freq = base * detune_ratio;
128            let phase_inc = freq / sample_rate;
129            let phase = self.phases[i];
130
131            let sample = match self.waveform {
132                OscWaveform::Sine => (phase * TAU).sin(),
133                OscWaveform::Square => {
134                    if phase < self.pwm { 1.0 } else { -1.0 }
135                }
136                OscWaveform::Triangle => {
137                    if phase < 0.5 { 4.0 * phase - 1.0 } else { 3.0 - 4.0 * phase }
138                }
139                OscWaveform::Sawtooth => 2.0 * phase - 1.0,
140                OscWaveform::SawtoothBlit => {
141                    // BLIT: band-limited impulse train approximation
142                    let m = (sample_rate / (2.0 * freq.max(1.0))).floor() as usize * 2 + 1;
143                    let m = m.max(1);
144                    let x = PI * freq / sample_rate;
145                    let blit = if x.abs() < 1e-6 {
146                        1.0
147                    } else {
148                        (m as f32 * x).sin() / (m as f32 * x.sin())
149                    };
150                    2.0 * blit - 1.0
151                }
152                OscWaveform::WhiteNoise => {
153                    // LCG random
154                    let r = (self.blit_n.wrapping_mul(1664525).wrapping_add(1013904223)) as f32
155                        / u32::MAX as f32 * 2.0 - 1.0;
156                    self.blit_n = self.blit_n.wrapping_add(1);
157                    r
158                }
159                OscWaveform::PinkNoise => {
160                    let white = (self.blit_n.wrapping_mul(1664525).wrapping_add(1013904223)) as f32
161                        / u32::MAX as f32 * 2.0 - 1.0;
162                    self.blit_n = self.blit_n.wrapping_add(1);
163                    // Paul Kellet's pink noise filter
164                    self.pink_b[0] = 0.99886 * self.pink_b[0] + white * 0.0555179;
165                    self.pink_b[1] = 0.99332 * self.pink_b[1] + white * 0.0750759;
166                    self.pink_b[2] = 0.96900 * self.pink_b[2] + white * 0.1538520;
167                    self.pink_b[3] = 0.86650 * self.pink_b[3] + white * 0.3104856;
168                    self.pink_b[4] = 0.55000 * self.pink_b[4] + white * 0.5329522;
169                    self.pink_b[5] = -0.7616 * self.pink_b[5] - white * 0.0168980;
170                    let pink = self.pink_b[0] + self.pink_b[1] + self.pink_b[2]
171                        + self.pink_b[3] + self.pink_b[4] + self.pink_b[5]
172                        + self.pink_b[6] + white * 0.5362;
173                    self.pink_b[6] = white * 0.115926;
174                    pink * 0.11
175                }
176                OscWaveform::BrownNoise => {
177                    let white = (self.blit_n.wrapping_mul(1664525).wrapping_add(1013904223)) as f32
178                        / u32::MAX as f32 * 2.0 - 1.0;
179                    self.blit_n = self.blit_n.wrapping_add(1);
180                    self.brown_last = (self.brown_last + white * 0.02).clamp(-1.0, 1.0);
181                    self.brown_last
182                }
183                OscWaveform::Wavetable => {
184                    if let Some(ref wt) = self.wavetable { wt.read(phase) } else { 0.0 }
185                }
186            };
187
188            out += sample;
189            self.phases[i] = (phase + phase_inc) % 1.0;
190        }
191
192        out / n as f32
193    }
194
195    pub fn reset(&mut self) {
196        for p in self.phases.iter_mut() { *p = 0.0; }
197        self.pink_b = [0.0; 7];
198        self.brown_last = 0.0;
199        self.blit_n = 0;
200    }
201}
202
203// ---------------------------------------------------------------------------
204// Envelope
205// ---------------------------------------------------------------------------
206
207#[derive(Clone, Copy, Debug, PartialEq)]
208pub enum EnvStage {
209    Idle,
210    Attack,
211    Hold,
212    Decay,
213    Sustain,
214    SustainSlope,
215    Release,
216}
217
218/// Full ADSR envelope with optional hold and sustain-slope stages.
219#[derive(Clone, Debug)]
220pub struct Envelope {
221    pub attack_ms: f32,
222    pub hold_ms: f32,
223    pub decay_ms: f32,
224    pub sustain_level: f32,
225    /// Sustain slope: change in level per second while in sustain (0 = flat).
226    pub sustain_slope: f32,
227    pub release_ms: f32,
228    pub velocity_scale: f32,
229
230    stage: EnvStage,
231    level: f32,
232    velocity: f32,
233    hold_samples: usize,
234    hold_counter: usize,
235    attack_rate: f32,
236    decay_rate: f32,
237    release_rate: f32,
238}
239
240impl Envelope {
241    pub fn new(attack_ms: f32, hold_ms: f32, decay_ms: f32, sustain_level: f32, release_ms: f32) -> Self {
242        Self {
243            attack_ms, hold_ms, decay_ms, sustain_level,
244            sustain_slope: 0.0,
245            release_ms,
246            velocity_scale: 1.0,
247            stage: EnvStage::Idle,
248            level: 0.0,
249            velocity: 1.0,
250            hold_samples: 0,
251            hold_counter: 0,
252            attack_rate: 0.0,
253            decay_rate: 0.0,
254            release_rate: 0.0,
255        }
256    }
257
258    pub fn note_on(&mut self, velocity: f32, sample_rate: f32) {
259        self.velocity = velocity * self.velocity_scale + (1.0 - self.velocity_scale);
260        self.stage = EnvStage::Attack;
261        self.attack_rate = 1.0 / (self.attack_ms * 0.001 * sample_rate).max(1.0);
262        self.decay_rate = (1.0 - self.sustain_level) / (self.decay_ms * 0.001 * sample_rate).max(1.0);
263        self.hold_samples = (self.hold_ms * 0.001 * sample_rate) as usize;
264        self.hold_counter = 0;
265        self.release_rate = self.level / (self.release_ms * 0.001 * sample_rate).max(1.0);
266    }
267
268    pub fn note_off(&mut self, sample_rate: f32) {
269        self.stage = EnvStage::Release;
270        self.release_rate = self.level / (self.release_ms * 0.001 * sample_rate).max(1.0);
271    }
272
273    pub fn next_sample(&mut self) -> f32 {
274        match self.stage {
275            EnvStage::Idle => 0.0,
276            EnvStage::Attack => {
277                self.level += self.attack_rate;
278                if self.level >= 1.0 {
279                    self.level = 1.0;
280                    if self.hold_ms > 0.0 {
281                        self.stage = EnvStage::Hold;
282                        self.hold_counter = self.hold_samples;
283                    } else {
284                        self.stage = EnvStage::Decay;
285                    }
286                }
287                self.level * self.velocity
288            }
289            EnvStage::Hold => {
290                if self.hold_counter == 0 {
291                    self.stage = EnvStage::Decay;
292                } else {
293                    self.hold_counter -= 1;
294                }
295                self.level * self.velocity
296            }
297            EnvStage::Decay => {
298                self.level -= self.decay_rate;
299                if self.level <= self.sustain_level {
300                    self.level = self.sustain_level;
301                    if self.sustain_slope.abs() > 1e-6 {
302                        self.stage = EnvStage::SustainSlope;
303                    } else {
304                        self.stage = EnvStage::Sustain;
305                    }
306                }
307                self.level * self.velocity
308            }
309            EnvStage::Sustain => self.sustain_level * self.velocity,
310            EnvStage::SustainSlope => {
311                self.level = (self.level + self.sustain_slope * 0.001).clamp(0.0, 1.0);
312                if self.level <= 0.0 { self.stage = EnvStage::Idle; }
313                self.level * self.velocity
314            }
315            EnvStage::Release => {
316                self.level -= self.release_rate;
317                if self.level <= 0.0 {
318                    self.level = 0.0;
319                    self.stage = EnvStage::Idle;
320                }
321                self.level * self.velocity
322            }
323        }
324    }
325
326    pub fn is_active(&self) -> bool { self.stage != EnvStage::Idle }
327
328    pub fn reset(&mut self) {
329        self.stage = EnvStage::Idle;
330        self.level = 0.0;
331    }
332}
333
334// ---------------------------------------------------------------------------
335// LFO
336// ---------------------------------------------------------------------------
337
338#[derive(Clone, Copy, Debug, PartialEq)]
339pub enum LfoWaveform { Sine, Square, Triangle, Sawtooth, SampleAndHold }
340
341#[derive(Clone, Copy, Debug, PartialEq)]
342pub enum LfoRetrigger { Free, Gate, Note }
343
344/// Low-frequency oscillator with tempo sync, fade-in, and modulate helper.
345#[derive(Clone, Debug)]
346pub struct Lfo {
347    pub waveform: LfoWaveform,
348    pub rate_hz: f32,
349    pub phase_offset: f32,
350    pub fade_in_ms: f32,
351    pub retrigger: LfoRetrigger,
352    pub bipolar: bool,
353    pub tempo_bpm: f32,
354    pub beat_division: f32,
355
356    phase: f32,
357    fade_counter: f32,
358    sh_hold: f32,
359    sh_counter: usize,
360    rng_state: u32,
361}
362
363impl Lfo {
364    pub fn new(waveform: LfoWaveform, rate_hz: f32) -> Self {
365        Self {
366            waveform, rate_hz,
367            phase_offset: 0.0,
368            fade_in_ms: 0.0,
369            retrigger: LfoRetrigger::Free,
370            bipolar: true,
371            tempo_bpm: 0.0,
372            beat_division: 1.0,
373            phase: 0.0,
374            fade_counter: 0.0,
375            sh_hold: 0.0,
376            sh_counter: 0,
377            rng_state: 12345,
378        }
379    }
380
381    pub fn retrigger_lfo(&mut self) {
382        self.phase = self.phase_offset;
383        self.fade_counter = 0.0;
384    }
385
386    fn effective_rate(&self) -> f32 {
387        if self.tempo_bpm > 0.0 {
388            self.tempo_bpm / 60.0 * self.beat_division
389        } else {
390            self.rate_hz
391        }
392    }
393
394    fn next_random(&mut self) -> f32 {
395        self.rng_state = self.rng_state.wrapping_mul(1664525).wrapping_add(1013904223);
396        (self.rng_state as f32 / u32::MAX as f32) * 2.0 - 1.0
397    }
398
399    pub fn next_sample(&mut self, sample_rate: f32) -> f32 {
400        let rate = self.effective_rate();
401        let phase_inc = rate / sample_rate;
402        let p = (self.phase + self.phase_offset) % 1.0;
403
404        let raw = match self.waveform {
405            LfoWaveform::Sine => (p * TAU).sin(),
406            LfoWaveform::Square => if p < 0.5 { 1.0 } else { -1.0 },
407            LfoWaveform::Triangle => {
408                if p < 0.5 { 4.0 * p - 1.0 } else { 3.0 - 4.0 * p }
409            }
410            LfoWaveform::Sawtooth => 2.0 * p - 1.0,
411            LfoWaveform::SampleAndHold => {
412                let period_samp = (sample_rate / rate.max(0.001)) as usize;
413                if self.sh_counter == 0 {
414                    self.sh_hold = self.next_random();
415                    self.sh_counter = period_samp;
416                }
417                if self.sh_counter > 0 { self.sh_counter -= 1; }
418                self.sh_hold
419            }
420        };
421
422        self.phase = (self.phase + phase_inc) % 1.0;
423
424        // Fade in
425        let fade = if self.fade_in_ms > 0.0 {
426            let fade_samp = self.fade_in_ms * 0.001 * sample_rate;
427            let f = (self.fade_counter / fade_samp).min(1.0);
428            self.fade_counter += 1.0;
429            f
430        } else { 1.0 };
431
432        let out = raw * fade;
433        if self.bipolar { out } else { out * 0.5 + 0.5 }
434    }
435
436    /// Apply this LFO to a destination parameter.
437    pub fn modulate(&mut self, destination: &mut f32, amount: f32, sample_rate: f32) {
438        let val = self.next_sample(sample_rate);
439        *destination += val * amount;
440    }
441}
442
443// ---------------------------------------------------------------------------
444// Filter (State-Variable TPT)
445// ---------------------------------------------------------------------------
446
447#[derive(Clone, Copy, Debug, PartialEq)]
448pub enum FilterMode { LowPass, HighPass, BandPass, Notch }
449
450/// TPT state-variable filter (Cytomic/Andrew Simper design).
451/// Self-oscillates at resonance ≈ 1.0.
452#[derive(Clone, Debug)]
453pub struct Filter {
454    pub mode: FilterMode,
455    pub cutoff_hz: f32,
456    pub resonance: f32,
457    pub keytrack: f32,
458    pub env_amount: f32,
459    pub vel_amount: f32,
460
461    // TPT state
462    ic1eq: f32,
463    ic2eq: f32,
464}
465
466impl Filter {
467    pub fn new(mode: FilterMode, cutoff_hz: f32, resonance: f32) -> Self {
468        Self {
469            mode, cutoff_hz,
470            resonance: resonance.clamp(0.0, 1.0),
471            keytrack: 0.0,
472            env_amount: 0.0,
473            vel_amount: 0.0,
474            ic1eq: 0.0,
475            ic2eq: 0.0,
476        }
477    }
478
479    pub fn process_sample(&mut self, x: f32, cutoff_override: f32, sample_rate: f32) -> f32 {
480        let cutoff = cutoff_override.clamp(20.0, sample_rate * 0.49);
481        let g = (PI * cutoff / sample_rate).tan();
482        // k = 2 - 2*resonance; at resonance=1.0, k→0 → self-oscillation
483        let k = 2.0 * (1.0 - self.resonance.clamp(0.0, 0.9999));
484        let a1 = 1.0 / (1.0 + g * (g + k));
485        let a2 = g * a1;
486        let a3 = g * a2;
487
488        let v3 = x - self.ic2eq;
489        let v1 = a1 * self.ic1eq + a2 * v3;
490        let v2 = self.ic2eq + a2 * self.ic1eq + a3 * v3;
491        self.ic1eq = 2.0 * v1 - self.ic1eq;
492        self.ic2eq = 2.0 * v2 - self.ic2eq;
493
494        match self.mode {
495            FilterMode::LowPass  => v2,
496            FilterMode::HighPass => x - k * v1 - v2,
497            FilterMode::BandPass => v1,
498            FilterMode::Notch    => x - k * v1,
499        }
500    }
501
502    pub fn reset(&mut self) { self.ic1eq = 0.0; self.ic2eq = 0.0; }
503}
504
505// ---------------------------------------------------------------------------
506// ModMatrix
507// ---------------------------------------------------------------------------
508
509#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
510pub enum ModSource {
511    Lfo1, Lfo2,
512    Env1, Env2,
513    Velocity,
514    Aftertouch,
515    ModWheel,
516    Random,
517    Constant,
518}
519
520#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
521pub enum ModDestination {
522    OscPitch,
523    OscVolume,
524    FilterCutoff,
525    FilterResonance,
526    ReverbMix,
527    LfoRate,
528    EnvAttack,
529    Pan,
530    Gain,
531}
532
533#[derive(Clone, Copy, Debug, PartialEq)]
534pub enum ModCurve { Linear, Exponential, SCurve }
535
536/// A single modulation routing.
537#[derive(Clone, Debug)]
538pub struct ModRoute {
539    pub source: ModSource,
540    pub dest: ModDestination,
541    pub amount: f32,
542    pub curve: ModCurve,
543}
544
545impl ModRoute {
546    pub fn new(source: ModSource, dest: ModDestination, amount: f32) -> Self {
547        Self { source, dest, amount, curve: ModCurve::Linear }
548    }
549
550    fn apply_curve(&self, x: f32) -> f32 {
551        match self.curve {
552            ModCurve::Linear => x,
553            ModCurve::Exponential => x.signum() * x.abs().powi(2),
554            ModCurve::SCurve => {
555                let t = x * 0.5 + 0.5;
556                let s = t * t * (3.0 - 2.0 * t);
557                s * 2.0 - 1.0
558            }
559        }
560    }
561}
562
563/// Modulation matrix: maps sources to destinations with amounts.
564pub struct ModMatrix {
565    pub routes: Vec<ModRoute>,
566    /// Current source values (set each block by the synth voice).
567    pub source_values: std::collections::HashMap<ModSource, f32>,
568}
569
570impl ModMatrix {
571    pub fn new() -> Self {
572        Self {
573            routes: Vec::new(),
574            source_values: std::collections::HashMap::new(),
575        }
576    }
577
578    pub fn add_route(&mut self, route: ModRoute) { self.routes.push(route); }
579
580    pub fn set_source(&mut self, source: ModSource, value: f32) {
581        self.source_values.insert(source, value);
582    }
583
584    pub fn get_mod_value(&self, dest: ModDestination) -> f32 {
585        let mut total = 0.0f32;
586        for route in &self.routes {
587            if route.dest == dest {
588                let src_val = self.source_values.get(&route.source).copied().unwrap_or(0.0);
589                total += route.apply_curve(src_val) * route.amount;
590            }
591        }
592        total
593    }
594
595    pub fn apply_all(&self, params: &mut SynthParams) {
596        params.osc_pitch_mod     += self.get_mod_value(ModDestination::OscPitch);
597        params.osc_volume_mod    += self.get_mod_value(ModDestination::OscVolume);
598        params.filter_cutoff_mod += self.get_mod_value(ModDestination::FilterCutoff);
599        params.filter_res_mod    += self.get_mod_value(ModDestination::FilterResonance);
600        params.reverb_mix_mod    += self.get_mod_value(ModDestination::ReverbMix);
601        params.pan_mod           += self.get_mod_value(ModDestination::Pan);
602        params.gain_mod          += self.get_mod_value(ModDestination::Gain);
603    }
604}
605
606impl Default for ModMatrix {
607    fn default() -> Self { Self::new() }
608}
609
610/// Transient modulated parameter snapshot (reset each block).
611#[derive(Clone, Debug, Default)]
612pub struct SynthParams {
613    pub osc_pitch_mod: f32,
614    pub osc_volume_mod: f32,
615    pub filter_cutoff_mod: f32,
616    pub filter_res_mod: f32,
617    pub reverb_mix_mod: f32,
618    pub pan_mod: f32,
619    pub gain_mod: f32,
620}
621
622// ---------------------------------------------------------------------------
623// Voice
624// ---------------------------------------------------------------------------
625
626/// A complete synth voice: oscillator + filter + amp envelope.
627pub struct Voice {
628    pub oscillator: Oscillator,
629    pub filter: Filter,
630    pub amp_env: Envelope,
631    pub filter_env: Envelope,
632    pub lfo1: Lfo,
633    pub lfo2: Lfo,
634    pub mod_matrix: ModMatrix,
635
636    note: u8,
637    velocity: f32,
638    base_freq: f32,
639    active: bool,
640    portamento_rate: f32,
641    current_freq: f32,
642}
643
644impl Voice {
645    pub fn new() -> Self {
646        Self {
647            oscillator: Oscillator::new(OscWaveform::Sawtooth),
648            filter: Filter::new(FilterMode::LowPass, 2000.0, 0.5),
649            amp_env: Envelope::new(10.0, 0.0, 100.0, 0.7, 200.0),
650            filter_env: Envelope::new(5.0, 0.0, 80.0, 0.3, 150.0),
651            lfo1: Lfo::new(LfoWaveform::Sine, 3.0),
652            lfo2: Lfo::new(LfoWaveform::Triangle, 0.5),
653            mod_matrix: ModMatrix::new(),
654            note: 60,
655            velocity: 1.0,
656            base_freq: 440.0,
657            active: false,
658            portamento_rate: 0.0,
659            current_freq: 440.0,
660        }
661    }
662
663    pub fn note_on(&mut self, note: u8, vel: u8, sample_rate: f32) {
664        self.note = note;
665        self.velocity = vel as f32 / 127.0;
666        self.base_freq = note_to_freq(note, 440.0);
667        if self.portamento_rate <= 0.0 { self.current_freq = self.base_freq; }
668        self.active = true;
669        self.amp_env.note_on(self.velocity, sample_rate);
670        self.filter_env.note_on(self.velocity, sample_rate);
671        if self.lfo1.retrigger == LfoRetrigger::Note { self.lfo1.retrigger_lfo(); }
672        if self.lfo2.retrigger == LfoRetrigger::Note { self.lfo2.retrigger_lfo(); }
673    }
674
675    pub fn note_off(&mut self, sample_rate: f32) {
676        self.amp_env.note_off(sample_rate);
677        self.filter_env.note_off(sample_rate);
678    }
679
680    pub fn is_active(&self) -> bool { self.active && self.amp_env.is_active() }
681
682    pub fn render(&mut self, buffer: &mut [f32], sample_rate: f32) {
683        if !self.is_active() {
684            for s in buffer.iter_mut() { *s = 0.0; }
685            return;
686        }
687
688        // Update mod matrix sources
689        let lfo1_val = self.lfo1.next_sample(sample_rate);
690        let lfo2_val = self.lfo2.next_sample(sample_rate);
691        self.mod_matrix.set_source(ModSource::Lfo1, lfo1_val);
692        self.mod_matrix.set_source(ModSource::Lfo2, lfo2_val);
693        self.mod_matrix.set_source(ModSource::Velocity, self.velocity);
694
695        for s in buffer.iter_mut() {
696            // Portamento
697            if self.portamento_rate > 0.0 {
698                let diff = self.base_freq - self.current_freq;
699                self.current_freq += diff * self.portamento_rate / sample_rate;
700            } else {
701                self.current_freq = self.base_freq;
702            }
703
704            let amp = self.amp_env.next_sample();
705            let fenv = self.filter_env.next_sample();
706
707            // Mod matrix application
708            let mut params = SynthParams::default();
709            self.mod_matrix.apply_all(&mut params);
710
711            let pitch_ratio = semitones_to_ratio(params.osc_pitch_mod);
712            let osc_out = self.oscillator.render_sample(self.current_freq * pitch_ratio, sample_rate);
713
714            let cutoff = (self.filter.cutoff_hz + fenv * self.filter.env_amount + params.filter_cutoff_mod)
715                .clamp(20.0, sample_rate * 0.49);
716            let filtered = self.filter.process_sample(osc_out, cutoff, sample_rate);
717
718            *s = filtered * amp * (1.0 + params.osc_volume_mod);
719        }
720
721        if !self.amp_env.is_active() { self.active = false; }
722    }
723
724    pub fn note(&self) -> u8 { self.note }
725
726    pub fn reset(&mut self) {
727        self.amp_env.reset();
728        self.filter_env.reset();
729        self.oscillator.reset();
730        self.filter.reset();
731        self.active = false;
732    }
733}
734
735impl Default for Voice {
736    fn default() -> Self { Self::new() }
737}
738
739// ---------------------------------------------------------------------------
740// Polyphony
741// ---------------------------------------------------------------------------
742
743#[derive(Clone, Copy, Debug, PartialEq)]
744pub enum VoiceStealPolicy { Oldest, Quietest, SameNote }
745
746/// Voice pool managing up to 32 simultaneous voices.
747pub struct Polyphony {
748    pub voices: Vec<Voice>,
749    pub max_voices: usize,
750    pub steal_policy: VoiceStealPolicy,
751    pub mono_mode: bool,
752    pub portamento_ms: f32,
753    // Track note-on order for oldest-stealing
754    voice_age: Vec<u64>,
755    age_counter: u64,
756}
757
758impl Polyphony {
759    pub fn new(max_voices: usize) -> Self {
760        let max_voices = max_voices.clamp(1, 32);
761        Self {
762            voices: (0..max_voices).map(|_| Voice::new()).collect(),
763            max_voices,
764            steal_policy: VoiceStealPolicy::Oldest,
765            mono_mode: false,
766            portamento_ms: 0.0,
767            voice_age: vec![0u64; max_voices],
768            age_counter: 0,
769        }
770    }
771
772    fn find_free_voice(&self) -> Option<usize> {
773        self.voices.iter().position(|v| !v.is_active())
774    }
775
776    fn steal_voice(&self) -> usize {
777        match self.steal_policy {
778            VoiceStealPolicy::Oldest => {
779                self.voice_age.iter().enumerate()
780                    .min_by_key(|(_, &age)| age)
781                    .map(|(i, _)| i)
782                    .unwrap_or(0)
783            }
784            VoiceStealPolicy::Quietest => {
785                // Approximate: use voice age as proxy (older = more decayed)
786                self.voice_age.iter().enumerate()
787                    .min_by_key(|(_, &age)| age)
788                    .map(|(i, _)| i)
789                    .unwrap_or(0)
790            }
791            VoiceStealPolicy::SameNote => {
792                self.voice_age.iter().enumerate()
793                    .min_by_key(|(_, &age)| age)
794                    .map(|(i, _)| i)
795                    .unwrap_or(0)
796            }
797        }
798    }
799
800    pub fn note_on(&mut self, note: u8, velocity: u8, sample_rate: f32) {
801        if self.mono_mode {
802            // In mono mode, all voices play on voice 0
803            let porta_rate = if self.portamento_ms > 0.0 {
804                1000.0 / (self.portamento_ms * sample_rate)
805            } else { 0.0 };
806            self.voices[0].portamento_rate = porta_rate;
807            self.voices[0].note_on(note, velocity, sample_rate);
808            self.voice_age[0] = self.age_counter;
809            self.age_counter += 1;
810            return;
811        }
812
813        // Check for same note already playing (SameNote steal)
814        if self.steal_policy == VoiceStealPolicy::SameNote {
815            if let Some(idx) = self.voices.iter().position(|v| v.is_active() && v.note() == note) {
816                self.voices[idx].note_on(note, velocity, sample_rate);
817                self.voice_age[idx] = self.age_counter;
818                self.age_counter += 1;
819                return;
820            }
821        }
822
823        let idx = self.find_free_voice().unwrap_or_else(|| self.steal_voice());
824        self.voices[idx].note_on(note, velocity, sample_rate);
825        self.voice_age[idx] = self.age_counter;
826        self.age_counter += 1;
827    }
828
829    pub fn note_off(&mut self, note: u8, sample_rate: f32) {
830        for v in self.voices.iter_mut() {
831            if v.is_active() && v.note() == note {
832                v.note_off(sample_rate);
833            }
834        }
835    }
836
837    pub fn render(&mut self, buffer: &mut [f32], sample_rate: f32) {
838        let n = buffer.len();
839        for s in buffer.iter_mut() { *s = 0.0; }
840        let mut tmp = vec![0.0f32; n];
841        for v in self.voices.iter_mut() {
842            if v.is_active() {
843                v.render(&mut tmp, sample_rate);
844                for i in 0..n { buffer[i] += tmp[i]; }
845            }
846        }
847        // Soft normalize to avoid clipping with many voices
848        let scale = 1.0 / (self.max_voices as f32).sqrt();
849        for s in buffer.iter_mut() { *s *= scale; }
850    }
851}
852
853// ---------------------------------------------------------------------------
854// Arpeggiator
855// ---------------------------------------------------------------------------
856
857#[derive(Clone, Copy, Debug, PartialEq)]
858pub enum ArpPattern { Up, Down, UpDown, Random, Chord }
859
860/// MIDI arpeggiator with note pattern, rate, octave range, gate, and latch.
861pub struct Arpeggiator {
862    pub pattern: ArpPattern,
863    pub rate_hz: f32,
864    pub octave_range: u8,
865    pub gate_fraction: f32,
866    pub latch: bool,
867
868    held_notes: Vec<u8>,
869    latched_notes: Vec<u8>,
870    step: usize,
871    direction: i32,
872    phase: f32,
873    note_on: bool,
874    rng_state: u32,
875}
876
877impl Arpeggiator {
878    pub fn new(pattern: ArpPattern, rate_hz: f32, octave_range: u8) -> Self {
879        Self {
880            pattern,
881            rate_hz,
882            octave_range: octave_range.clamp(1, 4),
883            gate_fraction: 0.8,
884            latch: false,
885            held_notes: Vec::new(),
886            latched_notes: Vec::new(),
887            step: 0,
888            direction: 1,
889            phase: 0.0,
890            note_on: false,
891            rng_state: 42,
892        }
893    }
894
895    pub fn press(&mut self, note: u8) {
896        if !self.held_notes.contains(&note) {
897            self.held_notes.push(note);
898            self.held_notes.sort_unstable();
899        }
900    }
901
902    pub fn release(&mut self, note: u8) {
903        self.held_notes.retain(|&n| n != note);
904    }
905
906    pub fn latch_current(&mut self) {
907        if self.latch {
908            self.latched_notes = self.held_notes.clone();
909        }
910    }
911
912    fn active_notes(&self) -> &[u8] {
913        if self.latch && !self.latched_notes.is_empty() {
914            &self.latched_notes
915        } else {
916            &self.held_notes
917        }
918    }
919
920    fn next_rng(&mut self) -> u32 {
921        self.rng_state = self.rng_state.wrapping_mul(1664525).wrapping_add(1013904223);
922        self.rng_state
923    }
924
925    /// Tick once per sample; returns Some((note, velocity)) when a note should trigger.
926    pub fn tick(&mut self, sample_rate: f32) -> Option<(u8, u8)> {
927        // Collect notes into a local Vec to avoid borrow issues
928        let notes: Vec<u8> = self.active_notes().to_vec();
929        if notes.is_empty() { return None; }
930
931        let total_steps = notes.len() * self.octave_range as usize;
932        self.phase += self.rate_hz / sample_rate;
933
934        let mut result = None;
935        if self.phase >= 1.0 {
936            self.phase -= 1.0;
937
938            match self.pattern {
939                ArpPattern::Up => {
940                    self.step = (self.step + 1) % total_steps;
941                }
942                ArpPattern::Down => {
943                    self.step = if self.step == 0 { total_steps - 1 } else { self.step - 1 };
944                }
945                ArpPattern::UpDown => {
946                    self.step = (self.step as i32 + self.direction) as usize;
947                    if self.step == 0 || self.step >= total_steps - 1 {
948                        self.direction = -self.direction;
949                    }
950                    self.step = self.step.min(total_steps - 1);
951                }
952                ArpPattern::Random => {
953                    self.step = (self.next_rng() as usize) % total_steps;
954                }
955                ArpPattern::Chord => {
956                    // All notes simultaneously — return first, others handled externally
957                    self.step = (self.step + 1) % notes.len();
958                }
959            }
960
961            let note_idx = self.step % notes.len();
962            let octave = (self.step / notes.len()) as u8;
963            let base_note = notes[note_idx];
964            let final_note = base_note.saturating_add(octave * 12).min(127);
965            result = Some((final_note, 100u8));
966        }
967        result
968    }
969}
970
971// ---------------------------------------------------------------------------
972// StepSequencer
973// ---------------------------------------------------------------------------
974
975/// One step in a step sequencer.
976#[derive(Clone, Debug)]
977pub struct Step {
978    pub note: u8,
979    pub velocity: u8,
980    pub gate: bool,
981    pub probability: f32,
982}
983
984impl Default for Step {
985    fn default() -> Self {
986        Self { note: 60, velocity: 100, gate: true, probability: 1.0 }
987    }
988}
989
990/// 32-step MIDI step sequencer with swing, transpose, and per-step probability.
991pub struct StepSequencer {
992    pub steps: Vec<Step>,
993    pub num_steps: usize,
994    pub rate_hz: f32,
995    pub swing: f32,
996    pub transpose: i32,
997
998    current_step: usize,
999    phase: f32,
1000    rng_state: u32,
1001}
1002
1003impl StepSequencer {
1004    pub fn new(num_steps: usize, rate_hz: f32) -> Self {
1005        let num_steps = num_steps.clamp(1, 32);
1006        Self {
1007            steps: (0..32).map(|_| Step::default()).collect(),
1008            num_steps,
1009            rate_hz,
1010            swing: 0.0,
1011            transpose: 0,
1012            current_step: 0,
1013            phase: 0.0,
1014            rng_state: 99,
1015        }
1016    }
1017
1018    fn next_rng(&mut self) -> f32 {
1019        self.rng_state = self.rng_state.wrapping_mul(1664525).wrapping_add(1013904223);
1020        self.rng_state as f32 / u32::MAX as f32
1021    }
1022
1023    /// Advance by one sample. Returns Some((note, velocity)) when a step triggers.
1024    pub fn tick(&mut self, sample_rate: f32) -> Option<(u8, u8)> {
1025        // Apply swing: even steps are delayed by swing amount
1026        let swing_offset = if self.current_step % 2 == 1 { self.swing * 0.5 } else { 0.0 };
1027        let effective_rate = self.rate_hz / (1.0 + swing_offset);
1028
1029        self.phase += effective_rate / sample_rate;
1030
1031        if self.phase >= 1.0 {
1032            self.phase -= 1.0;
1033            // Extract step data before borrowing self mutably for next_rng
1034            let (gate, note, velocity, probability) = {
1035                let step = &self.steps[self.current_step];
1036                (step.gate, step.note, step.velocity, step.probability)
1037            };
1038            self.current_step = (self.current_step + 1) % self.num_steps;
1039
1040            if gate && self.next_rng() < probability {
1041                let transposed = (note as i32 + self.transpose).clamp(0, 127) as u8;
1042                Some((transposed, velocity))
1043            } else {
1044                None
1045            }
1046        } else {
1047            None
1048        }
1049    }
1050}
1051
1052// ---------------------------------------------------------------------------
1053// SynthPatch
1054// ---------------------------------------------------------------------------
1055
1056/// Serializable synth parameter snapshot.
1057#[derive(Clone, Debug)]
1058pub struct SynthPatch {
1059    pub name: String,
1060    pub osc_waveform: OscWaveform,
1061    pub osc_coarse: f32,
1062    pub osc_fine: f32,
1063    pub osc_unison: usize,
1064    pub osc_detune: f32,
1065    pub filter_mode: FilterMode,
1066    pub filter_cutoff: f32,
1067    pub filter_resonance: f32,
1068    pub filter_env_amount: f32,
1069    pub amp_attack_ms: f32,
1070    pub amp_hold_ms: f32,
1071    pub amp_decay_ms: f32,
1072    pub amp_sustain: f32,
1073    pub amp_release_ms: f32,
1074    pub filter_attack_ms: f32,
1075    pub filter_decay_ms: f32,
1076    pub filter_sustain: f32,
1077    pub filter_release_ms: f32,
1078    pub lfo1_rate: f32,
1079    pub lfo1_waveform: LfoWaveform,
1080    pub lfo1_amount: f32,
1081    pub lfo1_dest: ModDestination,
1082    pub reverb_mix: f32,
1083    pub delay_mix: f32,
1084    pub volume: f32,
1085}
1086
1087impl SynthPatch {
1088    pub fn load(&self, poly: &mut Polyphony) {
1089        for v in poly.voices.iter_mut() {
1090            v.oscillator.waveform = self.osc_waveform;
1091            v.oscillator.coarse_semitones = self.osc_coarse;
1092            v.oscillator.fine_cents = self.osc_fine;
1093            v.oscillator.set_unison_voices(self.osc_unison);
1094            v.oscillator.unison_detune = self.osc_detune;
1095            v.filter.mode = self.filter_mode;
1096            v.filter.cutoff_hz = self.filter_cutoff;
1097            v.filter.resonance = self.filter_resonance;
1098            v.filter.env_amount = self.filter_env_amount;
1099            v.amp_env.attack_ms = self.amp_attack_ms;
1100            v.amp_env.hold_ms = self.amp_hold_ms;
1101            v.amp_env.decay_ms = self.amp_decay_ms;
1102            v.amp_env.sustain_level = self.amp_sustain;
1103            v.amp_env.release_ms = self.amp_release_ms;
1104            v.filter_env.attack_ms = self.filter_attack_ms;
1105            v.filter_env.decay_ms = self.filter_decay_ms;
1106            v.filter_env.sustain_level = self.filter_sustain;
1107            v.filter_env.release_ms = self.filter_release_ms;
1108            v.lfo1.rate_hz = self.lfo1_rate;
1109            v.lfo1.waveform = self.lfo1_waveform;
1110            // Set mod route for lfo1
1111            v.mod_matrix.routes.retain(|r| r.source != ModSource::Lfo1);
1112            if self.lfo1_amount.abs() > 1e-6 {
1113                v.mod_matrix.add_route(ModRoute::new(ModSource::Lfo1, self.lfo1_dest, self.lfo1_amount));
1114            }
1115        }
1116    }
1117
1118    pub fn save(poly: &Polyphony) -> Self {
1119        let v = &poly.voices[0];
1120        let lfo1_route = v.mod_matrix.routes.iter().find(|r| r.source == ModSource::Lfo1);
1121        Self {
1122            name: "Current".to_string(),
1123            osc_waveform: v.oscillator.waveform,
1124            osc_coarse: v.oscillator.coarse_semitones,
1125            osc_fine: v.oscillator.fine_cents,
1126            osc_unison: v.oscillator.unison_voices,
1127            osc_detune: v.oscillator.unison_detune,
1128            filter_mode: v.filter.mode,
1129            filter_cutoff: v.filter.cutoff_hz,
1130            filter_resonance: v.filter.resonance,
1131            filter_env_amount: v.filter.env_amount,
1132            amp_attack_ms: v.amp_env.attack_ms,
1133            amp_hold_ms: v.amp_env.hold_ms,
1134            amp_decay_ms: v.amp_env.decay_ms,
1135            amp_sustain: v.amp_env.sustain_level,
1136            amp_release_ms: v.amp_env.release_ms,
1137            filter_attack_ms: v.filter_env.attack_ms,
1138            filter_decay_ms: v.filter_env.decay_ms,
1139            filter_sustain: v.filter_env.sustain_level,
1140            filter_release_ms: v.filter_env.release_ms,
1141            lfo1_rate: v.lfo1.rate_hz,
1142            lfo1_waveform: v.lfo1.waveform,
1143            lfo1_amount: lfo1_route.map(|r| r.amount).unwrap_or(0.0),
1144            lfo1_dest: lfo1_route.map(|r| r.dest).unwrap_or(ModDestination::FilterCutoff),
1145            reverb_mix: 0.0,
1146            delay_mix: 0.0,
1147            volume: 1.0,
1148        }
1149    }
1150
1151    /// 8 factory presets.
1152    pub fn factory_presets() -> Vec<SynthPatch> {
1153        vec![
1154            // Pad
1155            SynthPatch {
1156                name: "Pad".into(), osc_waveform: OscWaveform::Sawtooth,
1157                osc_coarse: 0.0, osc_fine: 0.0, osc_unison: 4, osc_detune: 0.3,
1158                filter_mode: FilterMode::LowPass, filter_cutoff: 800.0, filter_resonance: 0.4,
1159                filter_env_amount: 400.0,
1160                amp_attack_ms: 400.0, amp_hold_ms: 0.0, amp_decay_ms: 200.0, amp_sustain: 0.8, amp_release_ms: 600.0,
1161                filter_attack_ms: 300.0, filter_decay_ms: 200.0, filter_sustain: 0.5, filter_release_ms: 500.0,
1162                lfo1_rate: 0.3, lfo1_waveform: LfoWaveform::Sine, lfo1_amount: 0.1, lfo1_dest: ModDestination::OscPitch,
1163                reverb_mix: 0.4, delay_mix: 0.0, volume: 0.8,
1164            },
1165            // Lead
1166            SynthPatch {
1167                name: "Lead".into(), osc_waveform: OscWaveform::Square,
1168                osc_coarse: 0.0, osc_fine: 0.0, osc_unison: 1, osc_detune: 0.0,
1169                filter_mode: FilterMode::LowPass, filter_cutoff: 3000.0, filter_resonance: 0.6,
1170                filter_env_amount: 2000.0,
1171                amp_attack_ms: 5.0, amp_hold_ms: 0.0, amp_decay_ms: 100.0, amp_sustain: 0.9, amp_release_ms: 80.0,
1172                filter_attack_ms: 5.0, filter_decay_ms: 100.0, filter_sustain: 0.3, filter_release_ms: 80.0,
1173                lfo1_rate: 5.0, lfo1_waveform: LfoWaveform::Sine, lfo1_amount: 0.15, lfo1_dest: ModDestination::OscPitch,
1174                reverb_mix: 0.1, delay_mix: 0.2, volume: 0.9,
1175            },
1176            // Bass
1177            SynthPatch {
1178                name: "Bass".into(), osc_waveform: OscWaveform::Sawtooth,
1179                osc_coarse: -12.0, osc_fine: 0.0, osc_unison: 1, osc_detune: 0.0,
1180                filter_mode: FilterMode::LowPass, filter_cutoff: 400.0, filter_resonance: 0.5,
1181                filter_env_amount: 1500.0,
1182                amp_attack_ms: 3.0, amp_hold_ms: 0.0, amp_decay_ms: 80.0, amp_sustain: 0.7, amp_release_ms: 60.0,
1183                filter_attack_ms: 2.0, filter_decay_ms: 60.0, filter_sustain: 0.0, filter_release_ms: 50.0,
1184                lfo1_rate: 0.0, lfo1_waveform: LfoWaveform::Sine, lfo1_amount: 0.0, lfo1_dest: ModDestination::OscPitch,
1185                reverb_mix: 0.0, delay_mix: 0.0, volume: 1.0,
1186            },
1187            // Pluck
1188            SynthPatch {
1189                name: "Pluck".into(), osc_waveform: OscWaveform::Triangle,
1190                osc_coarse: 0.0, osc_fine: 0.0, osc_unison: 1, osc_detune: 0.0,
1191                filter_mode: FilterMode::LowPass, filter_cutoff: 2000.0, filter_resonance: 0.2,
1192                filter_env_amount: 3000.0,
1193                amp_attack_ms: 1.0, amp_hold_ms: 0.0, amp_decay_ms: 300.0, amp_sustain: 0.0, amp_release_ms: 100.0,
1194                filter_attack_ms: 1.0, filter_decay_ms: 200.0, filter_sustain: 0.0, filter_release_ms: 100.0,
1195                lfo1_rate: 0.0, lfo1_waveform: LfoWaveform::Sine, lfo1_amount: 0.0, lfo1_dest: ModDestination::OscPitch,
1196                reverb_mix: 0.2, delay_mix: 0.1, volume: 0.9,
1197            },
1198            // Organ
1199            SynthPatch {
1200                name: "Organ".into(), osc_waveform: OscWaveform::Sine,
1201                osc_coarse: 0.0, osc_fine: 0.0, osc_unison: 1, osc_detune: 0.0,
1202                filter_mode: FilterMode::LowPass, filter_cutoff: 10000.0, filter_resonance: 0.1,
1203                filter_env_amount: 0.0,
1204                amp_attack_ms: 5.0, amp_hold_ms: 0.0, amp_decay_ms: 0.0, amp_sustain: 1.0, amp_release_ms: 20.0,
1205                filter_attack_ms: 0.0, filter_decay_ms: 0.0, filter_sustain: 1.0, filter_release_ms: 0.0,
1206                lfo1_rate: 6.0, lfo1_waveform: LfoWaveform::Sine, lfo1_amount: 0.05, lfo1_dest: ModDestination::OscPitch,
1207                reverb_mix: 0.3, delay_mix: 0.0, volume: 0.8,
1208            },
1209            // Bell
1210            SynthPatch {
1211                name: "Bell".into(), osc_waveform: OscWaveform::Sine,
1212                osc_coarse: 12.0, osc_fine: 0.0, osc_unison: 2, osc_detune: 0.15,
1213                filter_mode: FilterMode::LowPass, filter_cutoff: 8000.0, filter_resonance: 0.1,
1214                filter_env_amount: 0.0,
1215                amp_attack_ms: 2.0, amp_hold_ms: 0.0, amp_decay_ms: 800.0, amp_sustain: 0.0, amp_release_ms: 400.0,
1216                filter_attack_ms: 0.0, filter_decay_ms: 0.0, filter_sustain: 1.0, filter_release_ms: 0.0,
1217                lfo1_rate: 0.0, lfo1_waveform: LfoWaveform::Sine, lfo1_amount: 0.0, lfo1_dest: ModDestination::OscPitch,
1218                reverb_mix: 0.5, delay_mix: 0.2, volume: 0.7,
1219            },
1220            // Arp
1221            SynthPatch {
1222                name: "Arp".into(), osc_waveform: OscWaveform::Square,
1223                osc_coarse: 0.0, osc_fine: 5.0, osc_unison: 2, osc_detune: 0.2,
1224                filter_mode: FilterMode::BandPass, filter_cutoff: 1500.0, filter_resonance: 0.7,
1225                filter_env_amount: 1000.0,
1226                amp_attack_ms: 2.0, amp_hold_ms: 0.0, amp_decay_ms: 150.0, amp_sustain: 0.4, amp_release_ms: 100.0,
1227                filter_attack_ms: 2.0, filter_decay_ms: 100.0, filter_sustain: 0.2, filter_release_ms: 80.0,
1228                lfo1_rate: 4.0, lfo1_waveform: LfoWaveform::Square, lfo1_amount: 0.2, lfo1_dest: ModDestination::FilterCutoff,
1229                reverb_mix: 0.15, delay_mix: 0.3, volume: 0.85,
1230            },
1231            // Noise
1232            SynthPatch {
1233                name: "Noise".into(), osc_waveform: OscWaveform::WhiteNoise,
1234                osc_coarse: 0.0, osc_fine: 0.0, osc_unison: 1, osc_detune: 0.0,
1235                filter_mode: FilterMode::BandPass, filter_cutoff: 2000.0, filter_resonance: 0.8,
1236                filter_env_amount: 3000.0,
1237                amp_attack_ms: 10.0, amp_hold_ms: 0.0, amp_decay_ms: 500.0, amp_sustain: 0.0, amp_release_ms: 200.0,
1238                filter_attack_ms: 5.0, filter_decay_ms: 300.0, filter_sustain: 0.0, filter_release_ms: 150.0,
1239                lfo1_rate: 2.0, lfo1_waveform: LfoWaveform::Sine, lfo1_amount: 500.0, lfo1_dest: ModDestination::FilterCutoff,
1240                reverb_mix: 0.25, delay_mix: 0.0, volume: 0.7,
1241            },
1242        ]
1243    }
1244}
1245
1246// ---------------------------------------------------------------------------
1247// DrumMachine
1248// ---------------------------------------------------------------------------
1249
1250/// One pad in the drum machine.
1251#[derive(Clone, Debug)]
1252pub struct DrumPad {
1253    pub sample: Vec<f32>,
1254    pub pitch: f32,
1255    pub volume: f32,
1256    pub pan: f32,
1257    pub reverb_send: f32,
1258    /// 32-step grid: true = step active.
1259    pub pattern: [bool; 32],
1260    pub velocities: [u8; 32],
1261
1262    // Playback state
1263    playback_pos: f32,
1264    playing: bool,
1265    current_velocity: f32,
1266}
1267
1268impl DrumPad {
1269    pub fn new() -> Self {
1270        Self {
1271            sample: Vec::new(),
1272            pitch: 1.0,
1273            volume: 1.0,
1274            pan: 0.0,
1275            reverb_send: 0.0,
1276            pattern: [false; 32],
1277            velocities: [100u8; 32],
1278            playback_pos: 0.0,
1279            playing: false,
1280            current_velocity: 1.0,
1281        }
1282    }
1283
1284    /// Trigger this pad with a given velocity.
1285    pub fn trigger(&mut self, velocity: u8) {
1286        self.playing = true;
1287        self.playback_pos = 0.0;
1288        self.current_velocity = velocity as f32 / 127.0;
1289    }
1290
1291    /// Render one sample from the pad's sample buffer.
1292    pub fn render_sample(&mut self) -> f32 {
1293        if !self.playing || self.sample.is_empty() { return 0.0; }
1294        let idx = self.playback_pos as usize;
1295        if idx >= self.sample.len() {
1296            self.playing = false;
1297            return 0.0;
1298        }
1299        // Linear interpolation
1300        let frac = self.playback_pos.fract();
1301        let s0 = self.sample[idx];
1302        let s1 = if idx + 1 < self.sample.len() { self.sample[idx + 1] } else { 0.0 };
1303        let s = s0 + (s1 - s0) * frac;
1304        self.playback_pos += self.pitch;
1305        s * self.current_velocity * self.volume
1306    }
1307}
1308
1309impl Default for DrumPad {
1310    fn default() -> Self { Self::new() }
1311}
1312
1313/// 16-pad × 32-step drum machine with swing and pattern chaining.
1314pub struct DrumMachine {
1315    pub pads: Vec<DrumPad>,
1316    pub num_steps: usize,
1317    pub rate_hz: f32,
1318    pub swing: f32,
1319    /// Number of patterns to chain (each pattern = 32 steps).
1320    pub chain_length: usize,
1321
1322    current_step: usize,
1323    current_pattern: usize,
1324    phase: f32,
1325    rng_state: u32,
1326}
1327
1328impl DrumMachine {
1329    pub fn new(rate_hz: f32) -> Self {
1330        Self {
1331            pads: (0..16).map(|_| DrumPad::new()).collect(),
1332            num_steps: 16,
1333            rate_hz,
1334            swing: 0.0,
1335            chain_length: 1,
1336            current_step: 0,
1337            current_pattern: 0,
1338            phase: 0.0,
1339            rng_state: 777,
1340        }
1341    }
1342
1343    /// Advance by one sample; returns list of (pad_index, velocity) that triggered.
1344    pub fn tick(&mut self, sample_rate: f32) -> Vec<(usize, u8)> {
1345        let swing_offset = if self.current_step % 2 == 1 { self.swing * 0.5 } else { 0.0 };
1346        let effective_rate = self.rate_hz / (1.0 + swing_offset).max(0.01);
1347        self.phase += effective_rate / sample_rate;
1348
1349        let mut triggered = Vec::new();
1350
1351        if self.phase >= 1.0 {
1352            self.phase -= 1.0;
1353            let step = self.current_step % 32;
1354
1355            for (pad_idx, pad) in self.pads.iter_mut().enumerate() {
1356                if pad.pattern[step] {
1357                    let vel = pad.velocities[step];
1358                    pad.trigger(vel);
1359                    triggered.push((pad_idx, vel));
1360                }
1361            }
1362
1363            self.current_step += 1;
1364            if self.current_step >= self.num_steps {
1365                self.current_step = 0;
1366                self.current_pattern = (self.current_pattern + 1) % self.chain_length;
1367            }
1368        }
1369
1370        triggered
1371    }
1372
1373    /// Render all pads to a buffer (mixed mono).
1374    pub fn render(&mut self, buffer: &mut [f32], sample_rate: f32) {
1375        for s in buffer.iter_mut() { *s = 0.0; }
1376        for i in 0..buffer.len() {
1377            let triggers = self.tick(sample_rate);
1378            // Apply triggers (pad state updated inside tick)
1379            for (pad_idx, _vel) in triggers {
1380                // Already triggered via tick
1381                let _ = pad_idx;
1382            }
1383            // Render all active pads
1384            for pad in self.pads.iter_mut() {
1385                buffer[i] += pad.render_sample();
1386            }
1387        }
1388    }
1389}
1390
1391// ---------------------------------------------------------------------------
1392// Tests
1393// ---------------------------------------------------------------------------
1394
1395#[cfg(test)]
1396mod tests {
1397    use super::*;
1398
1399    #[test]
1400    fn test_oscillator_sine_bounded() {
1401        let mut osc = Oscillator::new(OscWaveform::Sine);
1402        for _ in 0..1024 {
1403            let s = osc.render_sample(440.0, 44100.0);
1404            assert!(s.abs() <= 1.0 + 1e-5, "sine out of bounds: {}", s);
1405        }
1406    }
1407
1408    #[test]
1409    fn test_oscillator_square_bounded() {
1410        let mut osc = Oscillator::new(OscWaveform::Square);
1411        for _ in 0..1024 {
1412            let s = osc.render_sample(440.0, 44100.0);
1413            assert!(s.abs() <= 1.0 + 1e-5, "square out of bounds: {}", s);
1414        }
1415    }
1416
1417    #[test]
1418    fn test_oscillator_white_noise_range() {
1419        let mut osc = Oscillator::new(OscWaveform::WhiteNoise);
1420        for _ in 0..1024 {
1421            let s = osc.render_sample(440.0, 44100.0);
1422            assert!(s.abs() <= 1.01, "noise out of range: {}", s);
1423        }
1424    }
1425
1426    #[test]
1427    fn test_envelope_adsr_full_cycle() {
1428        let mut env = Envelope::new(10.0, 0.0, 50.0, 0.5, 100.0);
1429        env.note_on(1.0, 44100.0);
1430        // Consume through attack
1431        for _ in 0..1000 { env.next_sample(); }
1432        // Should be in sustain around 0.5
1433        let v = env.next_sample();
1434        assert!(v > 0.3 && v < 0.7, "sustain value unexpected: {}", v);
1435        env.note_off(44100.0);
1436        // Consume through release
1437        for _ in 0..10000 { env.next_sample(); }
1438        assert!(!env.is_active(), "envelope should be idle after release");
1439    }
1440
1441    #[test]
1442    fn test_envelope_velocity_scaling() {
1443        let mut env = Envelope::new(1.0, 0.0, 0.0, 1.0, 1.0);
1444        env.velocity_scale = 1.0;
1445        env.note_on(0.5, 44100.0); // half velocity
1446        let mut max_v = 0.0f32;
1447        for _ in 0..1000 { max_v = max_v.max(env.next_sample()); }
1448        // With velocity_scale=1, half velocity → peak ~0.5
1449        assert!(max_v < 0.6, "velocity scaling: expected <0.6, got {}", max_v);
1450    }
1451
1452    #[test]
1453    fn test_lfo_sine_bipolar_range() {
1454        let mut lfo = Lfo::new(LfoWaveform::Sine, 5.0);
1455        let mut min = f32::MAX;
1456        let mut max = f32::MIN;
1457        for _ in 0..44100 {
1458            let v = lfo.next_sample(44100.0);
1459            min = min.min(v);
1460            max = max.max(v);
1461        }
1462        assert!(min < -0.9, "LFO min should approach -1: {}", min);
1463        assert!(max > 0.9, "LFO max should approach 1: {}", max);
1464    }
1465
1466    #[test]
1467    fn test_lfo_unipolar() {
1468        let mut lfo = Lfo::new(LfoWaveform::Sine, 5.0);
1469        lfo.bipolar = false;
1470        for _ in 0..44100 {
1471            let v = lfo.next_sample(44100.0);
1472            assert!(v >= 0.0 && v <= 1.0 + 1e-5, "unipolar LFO out of range: {}", v);
1473        }
1474    }
1475
1476    #[test]
1477    fn test_filter_lowpass_attenuates_high_freq() {
1478        let mut f = Filter::new(FilterMode::LowPass, 500.0, 0.5);
1479        // High frequency input: 4kHz at 44100 sr
1480        let freq = 4000.0f32;
1481        let sr = 44100.0f32;
1482        let mut energy_out = 0.0f32;
1483        for i in 0..1024 {
1484            let x = (TAU * freq * i as f32 / sr).sin();
1485            let y = f.process_sample(x, 500.0, sr);
1486            energy_out += y * y;
1487        }
1488        // Direct energy of input
1489        let energy_in = 512.0f32; // ~512 for unit sine
1490        // Filter should significantly attenuate
1491        assert!(energy_out < energy_in * 0.2, "LPF should attenuate 4kHz: {} vs {}", energy_out, energy_in);
1492    }
1493
1494    #[test]
1495    fn test_voice_renders_nonzero() {
1496        let mut voice = Voice::new();
1497        voice.note_on(60, 100, 44100.0);
1498        let mut buf = vec![0.0f32; 256];
1499        voice.render(&mut buf, 44100.0);
1500        let energy: f32 = buf.iter().map(|s| s * s).sum();
1501        assert!(energy > 0.0, "voice should produce audio");
1502    }
1503
1504    #[test]
1505    fn test_polyphony_voice_stealing() {
1506        let mut poly = Polyphony::new(2); // only 2 voices
1507        poly.note_on(60, 100, 44100.0);
1508        poly.note_on(62, 100, 44100.0);
1509        poly.note_on(64, 100, 44100.0); // should steal
1510        let active_count = poly.voices.iter().filter(|v| v.is_active()).count();
1511        assert_eq!(active_count, 2, "should have exactly 2 active voices after steal");
1512    }
1513
1514    #[test]
1515    fn test_arpeggiator_up_pattern() {
1516        let mut arp = Arpeggiator::new(ArpPattern::Up, 10.0, 1);
1517        arp.press(60);
1518        arp.press(64);
1519        arp.press(67);
1520        let sr = 44100.0f32;
1521        let mut notes = Vec::new();
1522        for _ in 0..sr as usize {
1523            if let Some((note, _)) = arp.tick(sr) {
1524                notes.push(note);
1525            }
1526        }
1527        assert!(!notes.is_empty(), "arpeggiator should trigger notes");
1528    }
1529
1530    #[test]
1531    fn test_step_sequencer_triggers() {
1532        let mut seq = StepSequencer::new(4, 10.0);
1533        seq.steps[0].gate = true;
1534        seq.steps[0].probability = 1.0;
1535        let sr = 44100.0f32;
1536        let mut triggered = false;
1537        for _ in 0..sr as usize {
1538            if seq.tick(sr).is_some() { triggered = true; break; }
1539        }
1540        assert!(triggered, "step sequencer should trigger at least one note");
1541    }
1542
1543    #[test]
1544    fn test_synth_patch_load_save() {
1545        let presets = SynthPatch::factory_presets();
1546        assert_eq!(presets.len(), 8, "should have 8 factory presets");
1547        let mut poly = Polyphony::new(4);
1548        presets[0].load(&mut poly);
1549        let saved = SynthPatch::save(&poly);
1550        assert_eq!(saved.osc_waveform, presets[0].osc_waveform);
1551    }
1552
1553    #[test]
1554    fn test_drum_machine_tick() {
1555        let mut dm = DrumMachine::new(4.0);
1556        dm.pads[0].pattern[0] = true;
1557        dm.pads[0].sample = vec![0.5f32; 100];
1558        let sr = 44100.0f32;
1559        let mut triggered = false;
1560        for _ in 0..sr as usize {
1561            let t = dm.tick(sr);
1562            if !t.is_empty() { triggered = true; break; }
1563        }
1564        assert!(triggered, "drum machine should trigger pad 0");
1565    }
1566
1567    #[test]
1568    fn test_wavetable_interpolation() {
1569        let wt = Wavetable::sine(1024);
1570        let v0 = wt.read(0.0);
1571        let v25 = wt.read(0.25);
1572        // At phase=0: sin(0)=0, at phase=0.25: sin(π/2)≈1
1573        assert!(v0.abs() < 0.01, "wavetable at 0: {}", v0);
1574        assert!(v25 > 0.99, "wavetable at 0.25: {}", v25);
1575    }
1576
1577    #[test]
1578    fn test_mod_matrix_routes() {
1579        let mut mm = ModMatrix::new();
1580        mm.add_route(ModRoute::new(ModSource::Lfo1, ModDestination::FilterCutoff, 100.0));
1581        mm.set_source(ModSource::Lfo1, 0.5);
1582        let val = mm.get_mod_value(ModDestination::FilterCutoff);
1583        assert!((val - 50.0).abs() < 1e-4, "mod matrix value: expected 50, got {}", val);
1584    }
1585}