1use std::f32::consts::{PI, TAU};
5
6#[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#[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#[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#[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 phases: Vec<f32>,
82 pink_b: [f32; 7],
84 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 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 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 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 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#[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#[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 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#[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#[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 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 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#[derive(Clone, Copy, Debug, PartialEq)]
448pub enum FilterMode { LowPass, HighPass, BandPass, Notch }
449
450#[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 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 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#[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#[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
563pub struct ModMatrix {
565 pub routes: Vec<ModRoute>,
566 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#[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
622pub 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 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 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 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#[derive(Clone, Copy, Debug, PartialEq)]
744pub enum VoiceStealPolicy { Oldest, Quietest, SameNote }
745
746pub 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 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 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 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 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 let scale = 1.0 / (self.max_voices as f32).sqrt();
849 for s in buffer.iter_mut() { *s *= scale; }
850 }
851}
852
853#[derive(Clone, Copy, Debug, PartialEq)]
858pub enum ArpPattern { Up, Down, UpDown, Random, Chord }
859
860pub 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(¬e) {
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 pub fn tick(&mut self, sample_rate: f32) -> Option<(u8, u8)> {
927 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 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#[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
990pub 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 pub fn tick(&mut self, sample_rate: f32) -> Option<(u8, u8)> {
1025 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 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#[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 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 pub fn factory_presets() -> Vec<SynthPatch> {
1153 vec![
1154 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 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 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 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 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 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 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 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#[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 pub pattern: [bool; 32],
1260 pub velocities: [u8; 32],
1261
1262 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 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 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 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
1313pub struct DrumMachine {
1315 pub pads: Vec<DrumPad>,
1316 pub num_steps: usize,
1317 pub rate_hz: f32,
1318 pub swing: f32,
1319 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 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 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 for (pad_idx, _vel) in triggers {
1380 let _ = pad_idx;
1382 }
1383 for pad in self.pads.iter_mut() {
1385 buffer[i] += pad.render_sample();
1386 }
1387 }
1388 }
1389}
1390
1391#[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 for _ in 0..1000 { env.next_sample(); }
1432 let v = env.next_sample();
1434 assert!(v > 0.3 && v < 0.7, "sustain value unexpected: {}", v);
1435 env.note_off(44100.0);
1436 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); let mut max_v = 0.0f32;
1447 for _ in 0..1000 { max_v = max_v.max(env.next_sample()); }
1448 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 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 let energy_in = 512.0f32; 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); poly.note_on(60, 100, 44100.0);
1508 poly.note_on(62, 100, 44100.0);
1509 poly.note_on(64, 100, 44100.0); 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 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}