1use super::common::{polyblamp, polyblep, voct_to_hz, EdgeDetector, GATE_THRESHOLD_V};
4use crate::port::{GraphModule, PortDef, PortSpec, PortValues, SignalKind};
5use crate::rng;
6use alloc::vec;
7use alloc::vec::Vec;
8use core::f64::consts::TAU;
9use libm::Libm;
10
11pub struct Vco {
29 phase: f64,
30 sample_rate: f64,
31 sync_edge: EdgeDetector,
32 spec: PortSpec,
33}
34
35impl Vco {
36 pub fn new(sample_rate: f64) -> Self {
37 Self {
38 phase: 0.0,
39 sample_rate,
40 sync_edge: EdgeDetector::new(),
41 spec: PortSpec {
42 inputs: vec![
43 PortDef::new(0, "voct", SignalKind::VoltPerOctave),
44 PortDef::new(1, "fm", SignalKind::CvBipolar).with_attenuverter(),
46 PortDef::new(2, "pw", SignalKind::CvUnipolar)
47 .with_default(0.5)
48 .with_attenuverter(),
49 PortDef::new(3, "sync", SignalKind::Gate),
50 PortDef::new(4, "fm_lin", SignalKind::CvBipolar).with_attenuverter(),
52 ],
53 outputs: vec![
54 PortDef::new(10, "sin", SignalKind::Audio),
55 PortDef::new(11, "tri", SignalKind::Audio),
56 PortDef::new(12, "saw", SignalKind::Audio),
57 PortDef::new(13, "sqr", SignalKind::Audio),
58 ],
59 },
60 }
61 }
62}
63
64impl Default for Vco {
65 fn default() -> Self {
66 Self::new(44100.0)
67 }
68}
69
70impl GraphModule for Vco {
71 fn port_spec(&self) -> &PortSpec {
72 &self.spec
73 }
74
75 fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
76 let voct = inputs.get_or(0, 0.0);
77 let fm = inputs.get_or(1, 0.0);
78 let pw = inputs.get_or(2, 0.5).clamp(0.05, 0.95);
79 let sync = inputs.get_or(3, 0.0);
80 let fm_lin = inputs.get_or(4, 0.0);
81
82 let base_freq = voct_to_hz(voct);
84 let mut freq = base_freq * Libm::<f64>::pow(2.0, fm);
86 freq += (fm_lin / 5.0) * base_freq;
89
90 let dt = freq / self.sample_rate;
92 let dt_abs = Libm::<f64>::fabs(dt);
94
95 let mut sync_reset: Option<(f64, f64)> = None;
98 if let Some(frac) = self.sync_edge.rising_frac(sync) {
99 sync_reset = Some((self.phase, frac));
100 self.phase = 0.0;
101 }
102
103 let phase = self.phase;
104
105 let sin = Libm::<f64>::sin(phase * TAU) * 5.0;
107
108 let mut saw = 2.0 * phase - 1.0;
110 saw -= polyblep(phase, dt_abs);
111
112 let mut sqr = if phase < pw { 1.0 } else { -1.0 };
115 sqr += polyblep(phase, dt_abs);
116 let pw_edge = {
117 let x = phase + (1.0 - pw);
118 x - Libm::<f64>::floor(x)
119 };
120 sqr -= polyblep(pw_edge, dt_abs);
121
122 let mut tri = 1.0 - 4.0 * Libm::<f64>::fabs(phase - 0.5);
125 let corner_half = {
126 let x = phase - 0.5;
127 if x < 0.0 {
128 x + 1.0
129 } else {
130 x
131 }
132 };
133 tri += 4.0 * dt_abs * polyblamp(phase, dt_abs);
134 tri -= 4.0 * dt_abs * polyblamp(corner_half, dt_abs);
135
136 if let Some((p_old, frac)) = sync_reset {
144 let equiv = (1.0 - frac) * dt_abs;
146 let blep = polyblep(equiv, dt_abs);
147 let saw_step = -2.0 * p_old;
149 saw += (saw_step / 2.0) * blep;
150 let old_sqr = if p_old < pw { 1.0 } else { -1.0 };
152 let sqr_step = 1.0 - old_sqr;
153 sqr += (sqr_step / 2.0) * blep;
154 }
155
156 outputs.set(10, sin);
158 outputs.set(11, tri * 5.0);
159 outputs.set(12, saw * 5.0);
160 outputs.set(13, sqr * 5.0);
161
162 let new_phase = self.phase + dt;
164 self.phase = new_phase - Libm::<f64>::floor(new_phase);
165 if self.phase < 0.0 {
166 self.phase += 1.0;
167 }
168 }
169
170 fn reset(&mut self) {
171 self.phase = 0.0;
172 self.sync_edge.reset();
173 }
174
175 fn set_sample_rate(&mut self, sample_rate: f64) {
176 self.sample_rate = sample_rate;
177 }
178
179 fn type_id(&self) -> &'static str {
180 "vco"
181 }
182}
183
184pub struct Lfo {
189 phase: f64,
190 sample_rate: f64,
191 reset_edge: EdgeDetector,
192 spec: PortSpec,
193}
194
195impl Lfo {
196 pub fn new(sample_rate: f64) -> Self {
197 Self {
198 phase: 0.0,
199 sample_rate,
200 reset_edge: EdgeDetector::new(),
201 spec: PortSpec {
202 inputs: vec![
203 PortDef::new(0, "rate", SignalKind::CvUnipolar)
204 .with_default(0.5)
205 .with_attenuverter(),
206 PortDef::new(1, "depth", SignalKind::CvUnipolar).with_default(10.0),
207 PortDef::new(2, "reset", SignalKind::Trigger),
208 ],
209 outputs: vec![
210 PortDef::new(10, "sin", SignalKind::CvBipolar),
211 PortDef::new(11, "tri", SignalKind::CvBipolar),
212 PortDef::new(12, "saw", SignalKind::CvBipolar),
213 PortDef::new(13, "sqr", SignalKind::CvBipolar),
214 PortDef::new(14, "sin_uni", SignalKind::CvUnipolar),
215 ],
216 },
217 }
218 }
219}
220
221impl Default for Lfo {
222 fn default() -> Self {
223 Self::new(44100.0)
224 }
225}
226
227impl GraphModule for Lfo {
228 fn port_spec(&self) -> &PortSpec {
229 &self.spec
230 }
231
232 fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
233 let rate_cv = inputs.get_or(0, 0.5);
234 let depth = inputs.get_or(1, 10.0) / 10.0; let reset = inputs.get_or(2, 0.0);
236
237 let freq = 0.01 * Libm::<f64>::pow(3000.0, rate_cv.clamp(0.0, 1.0));
239
240 if self.reset_edge.rising(reset) {
242 self.phase = 0.0;
243 }
244
245 let scale = 5.0 * depth;
247 let sin = Libm::<f64>::sin(self.phase * TAU) * scale;
248 let tri = (1.0 - 4.0 * Libm::<f64>::fabs(self.phase - 0.5)) * scale;
249 let saw = (2.0 * self.phase - 1.0) * scale;
250 let sqr = if self.phase < 0.5 { scale } else { -scale };
251 let sin_uni = (Libm::<f64>::sin(self.phase * TAU) * 0.5 + 0.5) * depth * 10.0;
252
253 outputs.set(10, sin);
254 outputs.set(11, tri);
255 outputs.set(12, saw);
256 outputs.set(13, sqr);
257 outputs.set(14, sin_uni);
258
259 let new_phase = self.phase + freq / self.sample_rate;
260 self.phase = new_phase - Libm::<f64>::floor(new_phase);
261 }
262
263 fn reset(&mut self) {
264 self.phase = 0.0;
265 self.reset_edge.reset();
266 }
267
268 fn set_sample_rate(&mut self, sample_rate: f64) {
269 self.sample_rate = sample_rate;
270 }
271
272 fn type_id(&self) -> &'static str {
273 "lfo"
274 }
275}
276
277pub struct Supersaw {
282 phases: [f64; 7],
283 sub_phase: f64,
286 sample_rate: f64,
287 spec: PortSpec,
288}
289
290impl Supersaw {
291 const DETUNE_RATIOS: [f64; 7] = [
294 -0.11002313, -0.06288439, -0.01952356, 0.0, 0.01991221, 0.06216538, 0.10745242, ];
302
303 const MIX_LEVELS: [f64; 7] = [0.5, 0.7, 0.9, 1.0, 0.9, 0.7, 0.5];
305
306 pub fn new(sample_rate: f64) -> Self {
307 let mut phases = [0.0; 7];
309 for (i, phase) in phases.iter_mut().enumerate() {
310 *phase = (i as f64) / 7.0;
311 }
312
313 Self {
314 phases,
315 sub_phase: 0.0,
316 sample_rate,
317 spec: PortSpec {
318 inputs: vec![
319 PortDef::new(0, "voct", SignalKind::VoltPerOctave).with_default(0.0),
320 PortDef::new(1, "detune", SignalKind::CvUnipolar)
321 .with_default(0.5)
322 .with_attenuverter(),
323 PortDef::new(2, "mix", SignalKind::CvUnipolar)
324 .with_default(0.5)
325 .with_attenuverter(),
326 ],
327 outputs: vec![
328 PortDef::new(10, "out", SignalKind::Audio),
329 PortDef::new(11, "sub", SignalKind::Audio),
330 ],
331 },
332 }
333 }
334
335 }
337
338impl Default for Supersaw {
339 fn default() -> Self {
340 Self::new(44100.0)
341 }
342}
343
344impl GraphModule for Supersaw {
345 fn port_spec(&self) -> &PortSpec {
346 &self.spec
347 }
348
349 fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
350 let voct = inputs.get_or(0, 0.0);
351 let detune = inputs.get_or(1, 0.5).clamp(0.0, 1.0);
352 let mix = inputs.get_or(2, 0.5).clamp(0.0, 1.0);
353
354 let base_freq = voct_to_hz(voct); let mut sum = 0.0;
358 let mut total_mix = 0.0;
359 let mut center_saw = 0.0;
361
362 for i in 0..7 {
363 let detune_amount = Self::DETUNE_RATIOS[i] * detune;
365 let freq = base_freq * (1.0 + detune_amount);
366 let dt = freq / self.sample_rate;
367
368 let raw_saw = 2.0 * self.phases[i] - 1.0;
370 let blep = polyblep(self.phases[i], dt);
371 let saw = raw_saw - blep;
372
373 if i == 3 {
376 center_saw = saw;
377 }
378
379 sum += saw * Self::MIX_LEVELS[i];
381 total_mix += Self::MIX_LEVELS[i];
382
383 self.phases[i] += dt;
385 if self.phases[i] >= 1.0 {
386 self.phases[i] -= 1.0;
387 }
388 }
389
390 let normalized = sum / total_mix;
392 let output = center_saw * (1.0 - mix) + normalized * mix;
393
394 let sub_dt = base_freq / (2.0 * self.sample_rate);
397 let sub = (2.0 * self.sub_phase - 1.0) - polyblep(self.sub_phase, sub_dt);
398 self.sub_phase += sub_dt;
399 if self.sub_phase >= 1.0 {
400 self.sub_phase -= 1.0;
401 }
402
403 outputs.set(10, output);
404 outputs.set(11, sub);
405 }
406
407 fn reset(&mut self) {
408 for (i, phase) in self.phases.iter_mut().enumerate() {
409 *phase = (i as f64) / 7.0;
410 }
411 self.sub_phase = 0.0;
412 }
413
414 fn set_sample_rate(&mut self, sample_rate: f64) {
415 self.sample_rate = sample_rate;
416 }
417
418 fn type_id(&self) -> &'static str {
419 "supersaw"
420 }
421}
422
423pub struct KarplusStrong {
428 buffer: Vec<f64>,
429 max_len: usize,
434 write_pos: usize,
435 sample_rate: f64,
436 last_output: f64,
437 trigger_edge: EdgeDetector,
439 spec: PortSpec,
440}
441
442impl KarplusStrong {
443 const LOOP_LEAK: f64 = 0.9995;
446
447 pub fn new(sample_rate: f64) -> Self {
448 let buffer_size = (sample_rate / 20.0) as usize + 10;
450 Self {
451 buffer: vec![0.0; buffer_size],
452 max_len: buffer_size,
453 write_pos: 0,
454 sample_rate,
455 last_output: 0.0,
456 trigger_edge: EdgeDetector::new(),
457 spec: PortSpec {
458 inputs: vec![
459 PortDef::new(0, "voct", SignalKind::VoltPerOctave).with_default(0.0),
460 PortDef::new(1, "trigger", SignalKind::Trigger),
461 PortDef::new(2, "damping", SignalKind::CvUnipolar)
462 .with_default(0.5)
463 .with_attenuverter(),
464 PortDef::new(3, "brightness", SignalKind::CvUnipolar)
465 .with_default(0.5)
466 .with_attenuverter(),
467 PortDef::new(4, "stretch", SignalKind::CvBipolar)
468 .with_default(0.0)
469 .with_attenuverter(),
470 ],
471 outputs: vec![PortDef::new(10, "out", SignalKind::Audio)],
472 },
473 }
474 }
475
476 fn excite(&mut self, brightness: f64) {
477 let period = self.buffer.len();
479 for i in 0..period {
480 let noise = rng::random_bipolar();
482 let impulse = if i < period / 4 { 1.0 } else { 0.0 };
483 self.buffer[i] = noise * brightness + impulse * (1.0 - brightness);
484 }
485 let mean: f64 = self.buffer.iter().sum::<f64>() / period as f64;
490 for sample in self.buffer.iter_mut() {
491 *sample -= mean;
492 }
493 }
494}
495
496impl Default for KarplusStrong {
497 fn default() -> Self {
498 Self::new(44100.0)
499 }
500}
501
502impl GraphModule for KarplusStrong {
503 fn port_spec(&self) -> &PortSpec {
504 &self.spec
505 }
506
507 fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
508 let voct = inputs.get_or(0, 0.0);
509 let trigger = inputs.get_or(1, 0.0);
510 let damping = inputs.get_or(2, 0.5).clamp(0.0, 1.0);
511 let brightness = inputs.get_or(3, 0.5).clamp(0.0, 1.0);
512 let stretch = inputs.get_or(4, 0.0).clamp(-1.0, 1.0);
513
514 let freq = voct_to_hz(voct);
519 let period = (self.sample_rate / freq).clamp(2.0, self.max_len as f64 - 1.0);
520 let period_int = period as usize;
521
522 if self.trigger_edge.rising(trigger) {
527 self.buffer.truncate(period_int + 2);
529 self.buffer.resize(period_int + 2, 0.0);
530 self.excite(brightness);
531 self.write_pos = 0;
532 }
533
534 let filter_coef = 0.5 + damping * 0.49; let filter_gd = (1.0 - filter_coef) / filter_coef;
542 let target_delay = (period - filter_gd).max(1.0);
543 let delay_int = target_delay as usize;
544 let delay_frac = target_delay - delay_int as f64;
545
546 let len = self.buffer.len();
548 let off1 = len.saturating_sub(delay_int); let off2 = off1.saturating_sub(1); let read_pos = (self.write_pos + off1) % len;
551 let read_pos2 = (self.write_pos + off2) % len;
552 let sample =
553 self.buffer[read_pos] * (1.0 - delay_frac) + self.buffer[read_pos2] * delay_frac;
554
555 let filtered = sample * filter_coef + self.last_output * (1.0 - filter_coef);
557
558 let stretch_coef = stretch * 0.5;
560 let stretched = filtered + stretch_coef * (filtered - self.last_output);
561
562 let leaked = stretched * Self::LOOP_LEAK;
565
566 self.last_output = leaked;
567
568 self.buffer[self.write_pos] = leaked;
570 self.write_pos = (self.write_pos + 1) % len;
571
572 outputs.set(10, leaked);
573 }
574
575 fn reset(&mut self) {
576 self.buffer.fill(0.0);
577 self.write_pos = 0;
578 self.last_output = 0.0;
579 self.trigger_edge.reset();
580 }
581
582 fn set_sample_rate(&mut self, sample_rate: f64) {
583 self.sample_rate = sample_rate;
584 let buffer_size = (sample_rate / 20.0) as usize + 10;
585 self.max_len = buffer_size;
586 self.buffer.resize(buffer_size, 0.0);
587 }
588
589 fn type_id(&self) -> &'static str {
590 "karplus_strong"
591 }
592}
593
594struct PinkNoiseState {
600 rows: [f64; 16],
601 running_sum: f64,
602 index: u32,
603}
604
605impl PinkNoiseState {
606 fn new() -> Self {
607 Self {
608 rows: [0.0; 16],
609 running_sum: 0.0,
610 index: 0,
611 }
612 }
613
614 fn sample(&mut self) -> f64 {
615 self.index = self.index.wrapping_add(1);
616 let changed_bits = (self.index ^ (self.index.wrapping_sub(1))).trailing_ones() as usize;
617
618 for i in 0..changed_bits.min(16) {
619 self.running_sum -= self.rows[i];
620 self.rows[i] = rng::random_bipolar();
621 self.running_sum += self.rows[i];
622 }
623
624 self.running_sum / 16.0
625 }
626}
627
628pub struct NoiseGenerator {
635 pink: PinkNoiseState,
636 pink2: PinkNoiseState,
638 pub(crate) correlation: f64,
640 last_white: f64,
642 spec: PortSpec,
643}
644
645impl NoiseGenerator {
646 pub fn new() -> Self {
647 Self {
648 pink: PinkNoiseState::new(),
649 pink2: PinkNoiseState::new(),
650 correlation: 0.3, last_white: 0.0,
652 spec: PortSpec {
653 inputs: vec![
654 PortDef::new(0, "correlation", SignalKind::CvUnipolar).with_default(0.3),
656 ],
657 outputs: vec![
658 PortDef::new(10, "white", SignalKind::Audio),
659 PortDef::new(11, "pink", SignalKind::Audio),
660 PortDef::new(12, "white2", SignalKind::Audio),
662 PortDef::new(13, "pink2", SignalKind::Audio),
663 ],
664 },
665 }
666 }
667
668 pub fn with_correlation(correlation: f64) -> Self {
670 let mut gen = Self::new();
671 gen.correlation = correlation.clamp(0.0, 1.0);
672 gen
673 }
674}
675
676impl Default for NoiseGenerator {
677 fn default() -> Self {
678 Self::new()
679 }
680}
681
682impl GraphModule for NoiseGenerator {
683 fn port_spec(&self) -> &PortSpec {
684 &self.spec
685 }
686
687 fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
688 let correlation = inputs.get_or(0, self.correlation).clamp(0.0, 1.0);
690
691 let white1 = rng::random_bipolar();
693
694 let independent = rng::random_bipolar();
697 let white2 = white1 * correlation + independent * (1.0 - correlation);
698
699 let pink1 = self.pink.sample();
701
702 let pink2_independent = self.pink2.sample();
704 let pink2 = pink1 * correlation + pink2_independent * (1.0 - correlation);
705
706 self.last_white = white1;
707
708 outputs.set(10, white1 * 5.0);
709 outputs.set(11, pink1 * 5.0);
710 outputs.set(12, white2 * 5.0);
711 outputs.set(13, pink2 * 5.0);
712 }
713
714 fn reset(&mut self) {
715 self.pink = PinkNoiseState::new();
716 self.pink2 = PinkNoiseState::new();
717 self.last_white = 0.0;
718 }
719
720 fn set_sample_rate(&mut self, _: f64) {}
721
722 fn type_id(&self) -> &'static str {
723 "noise"
724 }
725}
726
727#[derive(Debug, Clone, Copy, PartialEq)]
729pub enum WavetableType {
730 Sine,
732 Triangle,
734 Saw,
736 Square,
738 Pulse25,
740 Pulse12,
742 FormantA,
744 FormantO,
746}
747
748impl WavetableType {
749 pub fn index(self) -> usize {
751 match self {
752 WavetableType::Sine => 0,
753 WavetableType::Triangle => 1,
754 WavetableType::Saw => 2,
755 WavetableType::Square => 3,
756 WavetableType::Pulse25 => 4,
757 WavetableType::Pulse12 => 5,
758 WavetableType::FormantA => 6,
759 WavetableType::FormantO => 7,
760 }
761 }
762
763 pub fn from_index(idx: usize) -> Self {
765 match idx % 8 {
766 0 => WavetableType::Sine,
767 1 => WavetableType::Triangle,
768 2 => WavetableType::Saw,
769 3 => WavetableType::Square,
770 4 => WavetableType::Pulse25,
771 5 => WavetableType::Pulse12,
772 6 => WavetableType::FormantA,
773 _ => WavetableType::FormantO,
774 }
775 }
776}
777
778pub struct Wavetable {
790 tables: [[[f64; 256]; 8]; 8],
794 phase: f64,
796 prev_sync: f64,
798 sample_rate: f64,
799 spec: PortSpec,
800}
801
802impl Wavetable {
803 const TABLE_SIZE: usize = 256;
805 const NUM_TABLES: usize = 8;
807 const NUM_MIPS: usize = 8;
810 const BASE_HARMONICS: [usize; 8] = [1, 31, 64, 63, 64, 64, 10, 10];
814
815 pub fn new(sample_rate: f64) -> Self {
816 let spec = PortSpec {
817 inputs: vec![
818 PortDef::new(0, "v_oct", SignalKind::VoltPerOctave).with_default(0.0),
819 PortDef::new(1, "table", SignalKind::CvUnipolar).with_default(0.0),
820 PortDef::new(2, "morph", SignalKind::CvUnipolar).with_default(0.0),
821 PortDef::new(3, "sync", SignalKind::Gate).with_default(0.0),
822 ],
823 outputs: vec![PortDef::new(10, "out", SignalKind::Audio)],
824 };
825
826 let mut osc = Self {
827 tables: [[[0.0; 256]; 8]; 8],
828 phase: 0.0,
829 prev_sync: 0.0,
830 sample_rate,
831 spec,
832 };
833 osc.generate_tables();
834 osc
835 }
836
837 fn max_harmonic(table: usize, level: usize) -> usize {
840 (Self::BASE_HARMONICS[table] >> level).max(1)
841 }
842
843 fn generate_tables(&mut self) {
845 let n = Self::TABLE_SIZE;
846 let pi = core::f64::consts::PI;
847
848 for level in 0..Self::NUM_MIPS {
849 for i in 0..n {
850 let phase = (i as f64) / (n as f64);
851 let partial = |harmonic: f64| Libm::<f64>::sin(phase * harmonic * 2.0 * pi);
852
853 self.tables[0][level][i] = partial(1.0);
855
856 let mut tri = 0.0;
858 let mut h = 1usize;
859 let mh = Self::max_harmonic(1, level);
860 let mut sign = 1.0; while h <= mh {
862 let hf = h as f64;
863 tri += sign * partial(hf) / (hf * hf);
864 sign = -sign;
865 h += 2;
866 }
867 self.tables[1][level][i] = tri * (8.0 / (pi * pi));
868
869 let mut saw = 0.0;
871 let mh = Self::max_harmonic(2, level);
872 let mut sign = -1.0; for h in 1..=mh {
874 let hf = h as f64;
875 saw += sign * partial(hf) / hf;
876 sign = -sign;
877 }
878 self.tables[2][level][i] = saw * (2.0 / pi);
879
880 let mut sqr = 0.0;
882 let mut h = 1usize;
883 let mh = Self::max_harmonic(3, level);
884 while h <= mh {
885 let hf = h as f64;
886 sqr += partial(hf) / hf;
887 h += 2;
888 }
889 self.tables[3][level][i] = sqr * (4.0 / pi);
890
891 for (table_idx, duty) in [(4usize, 0.25f64), (5usize, 0.125f64)] {
893 let mut pulse = 0.0;
894 let mh = Self::max_harmonic(table_idx, level);
895 for h in 1..=mh {
896 let hf = h as f64;
897 let coef = Libm::<f64>::sin(pi * hf * duty) / hf;
898 pulse += coef * partial(hf);
899 }
900 self.tables[table_idx][level][i] = pulse * 2.0;
901 }
902
903 let mh_a = Self::max_harmonic(6, level) as f64;
906 let formant_a = [(1.0, 1.0), (2.7, 0.5), (4.6, 0.3), (9.6, 0.15)]
907 .iter()
908 .filter(|(mult, _)| *mult <= mh_a)
909 .map(|(mult, amp)| partial(*mult) * amp)
910 .sum::<f64>();
911 self.tables[6][level][i] = formant_a * 0.5;
912
913 let mh_o = Self::max_harmonic(7, level) as f64;
914 let formant_o = [(1.0, 1.0), (1.5, 0.6), (3.0, 0.4), (10.0, 0.15)]
915 .iter()
916 .filter(|(mult, _)| *mult <= mh_o)
917 .map(|(mult, amp)| partial(*mult) * amp)
918 .sum::<f64>();
919 self.tables[7][level][i] = formant_o * 0.5;
920 }
921 }
922 }
923
924 fn select_level(table: usize, phase_inc: f64) -> usize {
927 let inc = Libm::<f64>::fabs(phase_inc).max(1e-9);
928 let allowed = Libm::<f64>::floor(0.5 / inc);
930 for level in 0..Self::NUM_MIPS {
931 if (Self::max_harmonic(table, level) as f64) <= allowed {
932 return level;
933 }
934 }
935 Self::NUM_MIPS - 1
936 }
937
938 fn read_table(&self, table_idx: usize, level: usize, phase: f64) -> f64 {
940 let table = &self.tables[table_idx % Self::NUM_TABLES][level.min(Self::NUM_MIPS - 1)];
941 let pos = phase * (Self::TABLE_SIZE as f64);
942 let idx0 = (pos as usize) % Self::TABLE_SIZE;
943 let idx1 = (idx0 + 1) % Self::TABLE_SIZE;
944 let frac = pos - Libm::<f64>::floor(pos);
945
946 table[idx0] * (1.0 - frac) + table[idx1] * frac
948 }
949}
950
951impl Default for Wavetable {
952 fn default() -> Self {
953 Self::new(44100.0)
954 }
955}
956
957impl GraphModule for Wavetable {
958 fn port_spec(&self) -> &PortSpec {
959 &self.spec
960 }
961
962 fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
963 let v_oct = inputs.get_or(0, 0.0);
965 let table_cv = inputs.get_or(1, 0.0).clamp(0.0, 1.0);
966 let morph = inputs.get_or(2, 0.0).clamp(0.0, 1.0);
967 let sync = inputs.get_or(3, 0.0);
968
969 if sync > GATE_THRESHOLD_V && self.prev_sync <= GATE_THRESHOLD_V {
971 self.phase = 0.0;
972 }
973 self.prev_sync = sync;
974
975 let frequency = voct_to_hz(v_oct);
977 let phase_inc = frequency / self.sample_rate;
978
979 let table_pos = table_cv * ((Self::NUM_TABLES - 1) as f64);
982 let table_idx = (table_pos as usize).min(Self::NUM_TABLES - 2);
983 let table_frac = table_pos - (table_idx as f64);
984
985 let blend = (table_frac + morph).min(1.0);
987
988 let level0 = Self::select_level(table_idx, phase_inc);
991 let level1 = Self::select_level(table_idx + 1, phase_inc);
992
993 let sample0 = self.read_table(table_idx, level0, self.phase);
995 let sample1 = self.read_table(table_idx + 1, level1, self.phase);
996 let sample = sample0 * (1.0 - blend) + sample1 * blend;
997
998 self.phase += phase_inc;
1000 while self.phase >= 1.0 {
1001 self.phase -= 1.0;
1002 }
1003
1004 outputs.set(10, sample * 5.0);
1006 }
1007
1008 fn reset(&mut self) {
1009 self.phase = 0.0;
1010 self.prev_sync = 0.0;
1011 }
1012
1013 fn set_sample_rate(&mut self, sample_rate: f64) {
1014 self.sample_rate = sample_rate;
1015 }
1016
1017 fn type_id(&self) -> &'static str {
1018 "wavetable"
1019 }
1020}
1021
1022pub struct FormantOsc {
1034 phase: f64,
1036 vibrato_phase: f64,
1038 resonator_state: [[f64; 2]; 5],
1040 sample_rate: f64,
1041 spec: PortSpec,
1042}
1043
1044impl FormantOsc {
1045 const FORMANTS: [[f64; 5]; 5] = [
1048 [700.0, 1220.0, 2600.0, 3500.0, 4500.0],
1050 [530.0, 1840.0, 2480.0, 3500.0, 4500.0],
1052 [280.0, 2250.0, 2890.0, 3500.0, 4500.0],
1054 [500.0, 700.0, 2350.0, 3500.0, 4500.0],
1056 [300.0, 870.0, 2250.0, 3500.0, 4500.0],
1058 ];
1059
1060 const BANDWIDTHS: [f64; 5] = [80.0, 90.0, 120.0, 150.0, 200.0];
1062
1063 const AMPLITUDES: [f64; 5] = [1.0, 0.5, 0.25, 0.1, 0.05];
1065
1066 const VIBRATO_RATE: f64 = 5.5;
1068
1069 pub fn new(sample_rate: f64) -> Self {
1070 let spec = PortSpec {
1071 inputs: vec![
1072 PortDef::new(0, "v_oct", SignalKind::VoltPerOctave).with_default(0.0),
1073 PortDef::new(1, "vowel", SignalKind::CvUnipolar).with_default(0.0),
1074 PortDef::new(2, "formant_shift", SignalKind::CvBipolar).with_default(0.0),
1075 PortDef::new(3, "vibrato", SignalKind::CvUnipolar).with_default(0.0),
1076 ],
1077 outputs: vec![PortDef::new(10, "out", SignalKind::Audio)],
1078 };
1079
1080 Self {
1081 phase: 0.0,
1082 vibrato_phase: 0.0,
1083 resonator_state: [[0.0; 2]; 5],
1084 sample_rate,
1085 spec,
1086 }
1087 }
1088
1089 fn get_formants(&self, vowel: f64, shift: f64) -> [f64; 5] {
1091 let vowel = vowel.clamp(0.0, 1.0);
1092 let idx = vowel * 4.0;
1093 let idx0 = (idx as usize).min(3);
1094 let idx1 = idx0 + 1;
1095 let frac = idx - (idx0 as f64);
1096
1097 let shift_mult = Libm::<f64>::pow(2.0, shift / 5.0);
1099
1100 let mut result = [0.0; 5];
1101 for (i, value) in result.iter_mut().enumerate() {
1102 let f0 = Self::FORMANTS[idx0][i];
1103 let f1 = Self::FORMANTS[idx1][i];
1104 *value = (f0 * (1.0 - frac) + f1 * frac) * shift_mult;
1105 }
1106 result
1107 }
1108
1109 fn process_resonator(
1111 &mut self,
1112 input: f64,
1113 freq: f64,
1114 bandwidth: f64,
1115 formant_idx: usize,
1116 ) -> f64 {
1117 let omega = 2.0 * core::f64::consts::PI * freq / self.sample_rate;
1118 let omega = omega.clamp(0.01, core::f64::consts::PI * 0.45);
1119
1120 let q = freq / bandwidth;
1121 let alpha = Libm::<f64>::sin(omega) / (2.0 * q);
1122
1123 let cos_omega = Libm::<f64>::cos(omega);
1125 let b0 = alpha;
1126 let a1 = -2.0 * cos_omega;
1127 let a2 = 1.0 - alpha;
1128 let norm = 1.0 + alpha;
1129
1130 let state = &mut self.resonator_state[formant_idx];
1131
1132 let output = b0 / norm * input + state[0];
1134 state[0] = -a1 / norm * output + state[1];
1135 state[1] = -b0 / norm * input - a2 / norm * output;
1136
1137 output
1138 }
1139
1140 fn glottal_pulse(phase: f64) -> f64 {
1142 if phase < 0.4 {
1145 let t = phase / 0.4;
1147 Libm::<f64>::sin(t * core::f64::consts::PI * 0.5)
1148 } else if phase < 0.8 {
1149 let t = (phase - 0.4) / 0.4;
1151 Libm::<f64>::cos(t * core::f64::consts::PI * 0.5)
1152 } else {
1153 0.0
1155 }
1156 }
1157}
1158
1159impl Default for FormantOsc {
1160 fn default() -> Self {
1161 Self::new(44100.0)
1162 }
1163}
1164
1165impl GraphModule for FormantOsc {
1166 fn port_spec(&self) -> &PortSpec {
1167 &self.spec
1168 }
1169
1170 fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
1171 let v_oct = inputs.get_or(0, 0.0);
1173 let vowel = inputs.get_or(1, 0.0).clamp(0.0, 1.0);
1174 let formant_shift = inputs.get_or(2, 0.0);
1175 let vibrato_depth = inputs.get_or(3, 0.0).clamp(0.0, 1.0);
1176
1177 let vibrato = Libm::<f64>::sin(self.vibrato_phase * 2.0 * core::f64::consts::PI);
1179 let vibrato_semitones = vibrato * vibrato_depth * 0.5; let v_oct_with_vibrato = v_oct + vibrato_semitones / 12.0;
1181
1182 let frequency = voct_to_hz(v_oct_with_vibrato);
1184 let phase_inc = frequency / self.sample_rate;
1185
1186 let excitation = Self::glottal_pulse(self.phase);
1188
1189 let formants = self.get_formants(vowel, formant_shift);
1191
1192 let mut output = 0.0;
1194 for (i, &freq) in formants.iter().enumerate() {
1195 let formant_out = self.process_resonator(excitation, freq, Self::BANDWIDTHS[i], i);
1196 output += formant_out * Self::AMPLITUDES[i];
1197 }
1198
1199 self.phase += phase_inc;
1201 while self.phase >= 1.0 {
1202 self.phase -= 1.0;
1203 }
1204
1205 self.vibrato_phase += Self::VIBRATO_RATE / self.sample_rate;
1206 while self.vibrato_phase >= 1.0 {
1207 self.vibrato_phase -= 1.0;
1208 }
1209
1210 outputs.set(10, output.clamp(-1.0, 1.0) * 5.0);
1212 }
1213
1214 fn reset(&mut self) {
1215 self.phase = 0.0;
1216 self.vibrato_phase = 0.0;
1217 self.resonator_state = [[0.0; 2]; 5];
1218 }
1219
1220 fn set_sample_rate(&mut self, sample_rate: f64) {
1221 self.sample_rate = sample_rate;
1222 }
1223
1224 fn type_id(&self) -> &'static str {
1225 "formant_osc"
1226 }
1227}
1228
1229#[cfg(test)]
1230mod tests {
1231 use super::*;
1232 use crate::modules::common::measure_max_output;
1233
1234 #[test]
1235 fn test_vco_frequency() {
1236 let mut vco = Vco::new(44100.0);
1237 let mut inputs = PortValues::new();
1238 let mut outputs = PortValues::new();
1239
1240 inputs.set(0, 0.0);
1242
1243 let period_samples = (44100.0 / 261.63) as usize;
1245 let mut samples = Vec::new();
1246
1247 for _ in 0..period_samples * 10 {
1248 vco.tick(&inputs, &mut outputs);
1249 samples.push(outputs.get(12).unwrap()); }
1251
1252 let crossings: Vec<_> = samples
1254 .windows(2)
1255 .filter(|w| w[0] <= 0.0 && w[1] > 0.0)
1256 .collect();
1257
1258 assert!(crossings.len() >= 8 && crossings.len() <= 12);
1260 }
1261 #[test]
1262 fn test_lfo_rate() {
1263 let mut lfo = Lfo::new(1000.0); let mut inputs = PortValues::new();
1265 let mut outputs = PortValues::new();
1266
1267 inputs.set(0, 0.5); for _ in 0..1000 {
1271 lfo.tick(&inputs, &mut outputs);
1272 }
1273
1274 let out = outputs.get(10).unwrap();
1276 assert!(out.abs() <= 5.0);
1277 }
1278 #[test]
1279 fn test_noise_generator() {
1280 let mut noise = NoiseGenerator::new();
1281 let inputs = PortValues::new();
1282 let mut outputs = PortValues::new();
1283
1284 noise.tick(&inputs, &mut outputs);
1285
1286 assert!(outputs.get(10).is_some());
1288 assert!(outputs.get(11).is_some());
1289 }
1290 #[test]
1291 fn test_vco_default_reset_sample_rate() {
1292 let mut vco = Vco::default();
1293 assert!(vco.sample_rate == 44100.0);
1294
1295 vco.set_sample_rate(48000.0);
1296 assert!(vco.sample_rate == 48000.0);
1297
1298 let mut inputs = PortValues::new();
1299 let mut outputs = PortValues::new();
1300 inputs.set(0, 0.0);
1301 for _ in 0..100 {
1302 vco.tick(&inputs, &mut outputs);
1303 }
1304
1305 vco.reset();
1306 assert!(vco.phase == 0.0);
1307
1308 assert_eq!(vco.type_id(), "vco");
1309 }
1310 #[test]
1311 fn test_lfo_default_reset_sample_rate() {
1312 let mut lfo = Lfo::default();
1313 assert!(lfo.sample_rate == 44100.0);
1314
1315 lfo.set_sample_rate(48000.0);
1316 assert!(lfo.sample_rate == 48000.0);
1317
1318 let inputs = PortValues::new();
1319 let mut outputs = PortValues::new();
1320 for _ in 0..100 {
1321 lfo.tick(&inputs, &mut outputs);
1322 }
1323
1324 lfo.reset();
1325 assert!(lfo.phase == 0.0);
1326
1327 assert_eq!(lfo.type_id(), "lfo");
1328 }
1329 #[test]
1330 fn test_noise_generator_default_reset_sample_rate() {
1331 let mut noise = NoiseGenerator::default();
1332 noise.reset();
1333 noise.set_sample_rate(48000.0);
1334 assert_eq!(noise.type_id(), "noise");
1335 }
1336 #[test]
1337 fn test_lfo_shapes() {
1338 let mut lfo = Lfo::new(1000.0);
1339 let mut inputs = PortValues::new();
1340 let mut outputs = PortValues::new();
1341
1342 inputs.set(0, 5.0); for _ in 0..1000 {
1346 lfo.tick(&inputs, &mut outputs);
1347 }
1348
1349 assert!(outputs.get(10).is_some()); assert!(outputs.get(11).is_some()); assert!(outputs.get(12).is_some()); assert!(outputs.get(13).is_some()); }
1355 #[test]
1356 fn test_vco_pwm() {
1357 let mut vco = Vco::new(44100.0);
1358 let mut inputs = PortValues::new();
1359 let mut outputs = PortValues::new();
1360
1361 inputs.set(0, 0.0); inputs.set(2, 7.5); for _ in 0..1000 {
1365 vco.tick(&inputs, &mut outputs);
1366 }
1367
1368 assert!(outputs.get(13).is_some());
1370 }
1371 #[test]
1372 fn test_wavetable_type_index() {
1373 assert_eq!(WavetableType::Sine.index(), 0);
1374 assert_eq!(WavetableType::Triangle.index(), 1);
1375 assert_eq!(WavetableType::Saw.index(), 2);
1376 assert_eq!(WavetableType::Square.index(), 3);
1377 assert_eq!(WavetableType::Pulse25.index(), 4);
1378 assert_eq!(WavetableType::Pulse12.index(), 5);
1379 assert_eq!(WavetableType::FormantA.index(), 6);
1380 assert_eq!(WavetableType::FormantO.index(), 7);
1381 }
1382 #[test]
1383 fn test_wavetable_type_from_index() {
1384 assert_eq!(WavetableType::from_index(0), WavetableType::Sine);
1385 assert_eq!(WavetableType::from_index(1), WavetableType::Triangle);
1386 assert_eq!(WavetableType::from_index(7), WavetableType::FormantO);
1387 assert_eq!(WavetableType::from_index(8), WavetableType::Sine); }
1389 #[test]
1390 fn test_wavetable_default_reset_sample_rate() {
1391 let mut wt = Wavetable::default();
1392 assert_eq!(wt.sample_rate, 44100.0);
1393
1394 let inputs = PortValues::new();
1396 let mut outputs = PortValues::new();
1397 for _ in 0..100 {
1398 wt.tick(&inputs, &mut outputs);
1399 }
1400
1401 assert!(wt.phase > 0.0);
1403
1404 wt.reset();
1406 assert_eq!(wt.phase, 0.0);
1407 assert_eq!(wt.prev_sync, 0.0);
1408
1409 wt.set_sample_rate(48000.0);
1411 assert_eq!(wt.sample_rate, 48000.0);
1412
1413 assert_eq!(wt.type_id(), "wavetable");
1414 assert_eq!(wt.port_spec().inputs.len(), 4);
1415 assert_eq!(wt.port_spec().outputs.len(), 1);
1416 }
1417 #[test]
1418 fn test_wavetable_sine_output() {
1419 let mut wt = Wavetable::new(44100.0);
1420 let mut inputs = PortValues::new();
1421 let mut outputs = PortValues::new();
1422
1423 inputs.set(0, 0.0); inputs.set(1, 0.0); let samples_per_cycle = (44100.0 / 261.63) as usize;
1429 let mut max_val = 0.0f64;
1430 let mut min_val = 0.0f64;
1431
1432 for _ in 0..samples_per_cycle {
1433 wt.tick(&inputs, &mut outputs);
1434 let out = outputs.get(10).unwrap();
1435 max_val = max_val.max(out);
1436 min_val = min_val.min(out);
1437 }
1438
1439 assert!(max_val > 4.0, "max should be near 5V: {}", max_val);
1441 assert!(min_val < -4.0, "min should be near -5V: {}", min_val);
1442 }
1443 #[test]
1444 fn test_wavetable_table_selection() {
1445 let mut wt = Wavetable::new(44100.0);
1446 let mut inputs = PortValues::new();
1447 let mut outputs = PortValues::new();
1448
1449 inputs.set(0, 2.0); let mut outputs_by_table = Vec::new();
1453 for table_cv in [0.0, 0.5, 1.0] {
1454 wt.reset();
1455 inputs.set(1, table_cv);
1456 inputs.set(2, 0.0); let mut sum = 0.0;
1459 for _ in 0..100 {
1460 wt.tick(&inputs, &mut outputs);
1461 sum += outputs.get(10).unwrap().abs();
1462 }
1463 outputs_by_table.push(sum);
1464 }
1465
1466 assert!((outputs_by_table[0] - outputs_by_table[1]).abs() > 1.0);
1468 assert!((outputs_by_table[1] - outputs_by_table[2]).abs() > 1.0);
1469 }
1470 #[test]
1471 fn test_wavetable_morph() {
1472 let mut wt = Wavetable::new(44100.0);
1473 let mut inputs = PortValues::new();
1474 let mut outputs = PortValues::new();
1475
1476 inputs.set(0, 1.0);
1477 inputs.set(1, 0.0); wt.reset();
1481 inputs.set(2, 0.0);
1482 let mut sum_no_morph = 0.0;
1483 for _ in 0..100 {
1484 wt.tick(&inputs, &mut outputs);
1485 sum_no_morph += outputs.get(10).unwrap();
1486 }
1487
1488 wt.reset();
1490 inputs.set(2, 1.0);
1491 let mut sum_full_morph = 0.0;
1492 for _ in 0..100 {
1493 wt.tick(&inputs, &mut outputs);
1494 sum_full_morph += outputs.get(10).unwrap();
1495 }
1496
1497 assert!((sum_no_morph - sum_full_morph).abs() > 0.1);
1499 }
1500 #[test]
1501 fn test_wavetable_hard_sync() {
1502 let mut wt = Wavetable::new(44100.0);
1503 let mut inputs = PortValues::new();
1504 let mut outputs = PortValues::new();
1505
1506 inputs.set(0, 0.0);
1507 inputs.set(1, 0.0);
1508
1509 for _ in 0..50 {
1511 wt.tick(&inputs, &mut outputs);
1512 }
1513 let phase_before = wt.phase;
1514 assert!(phase_before > 0.0);
1515
1516 inputs.set(3, 0.0);
1518 wt.tick(&inputs, &mut outputs);
1519 inputs.set(3, 5.0); wt.tick(&inputs, &mut outputs);
1521
1522 assert!(wt.phase < 0.1, "Phase should reset on sync: {}", wt.phase);
1524 }
1525 #[test]
1526 fn test_wavetable_frequency_tracking() {
1527 let mut wt = Wavetable::new(44100.0);
1528
1529 let count_zero_crossings = |wt: &mut Wavetable, v_oct: f64| -> usize {
1532 let mut inputs = PortValues::new();
1533 let mut outputs = PortValues::new();
1534 inputs.set(0, v_oct);
1535 inputs.set(1, 0.0);
1536 wt.reset();
1537
1538 let mut crossings = 0;
1539 let mut prev_out = 0.0;
1540 for _ in 0..1000 {
1541 wt.tick(&inputs, &mut outputs);
1542 let out = outputs.get(10).unwrap();
1543 if prev_out <= 0.0 && out > 0.0 {
1544 crossings += 1;
1545 }
1546 prev_out = out;
1547 }
1548 crossings
1549 };
1550
1551 let crossings_c4 = count_zero_crossings(&mut wt, 0.0); let crossings_c5 = count_zero_crossings(&mut wt, 1.0); let ratio = crossings_c5 as f64 / crossings_c4 as f64;
1556 assert!(
1557 ratio > 1.8 && ratio < 2.2,
1558 "Octave ratio should be ~2: {}",
1559 ratio
1560 );
1561 }
1562 #[test]
1563 fn test_formant_osc_default_reset_sample_rate() {
1564 let mut osc = FormantOsc::default();
1565 assert_eq!(osc.sample_rate, 44100.0);
1566
1567 let inputs = PortValues::new();
1569 let mut outputs = PortValues::new();
1570 for _ in 0..100 {
1571 osc.tick(&inputs, &mut outputs);
1572 }
1573
1574 assert!(osc.phase > 0.0);
1576
1577 osc.reset();
1579 assert_eq!(osc.phase, 0.0);
1580 assert_eq!(osc.vibrato_phase, 0.0);
1581 assert_eq!(osc.resonator_state, [[0.0; 2]; 5]);
1582
1583 osc.set_sample_rate(48000.0);
1585 assert_eq!(osc.sample_rate, 48000.0);
1586
1587 assert_eq!(osc.type_id(), "formant_osc");
1588 assert_eq!(osc.port_spec().inputs.len(), 4);
1589 assert_eq!(osc.port_spec().outputs.len(), 1);
1590 }
1591 #[test]
1592 fn test_formant_osc_output() {
1593 let mut osc = FormantOsc::new(44100.0);
1594 let mut inputs = PortValues::new();
1595 let mut outputs = PortValues::new();
1596
1597 inputs.set(0, 0.0); inputs.set(1, 0.0); let mut max_val = 0.0f64;
1602 let mut min_val = 0.0f64;
1603
1604 for _ in 0..1000 {
1605 osc.tick(&inputs, &mut outputs);
1606 let out = outputs.get(10).unwrap();
1607 max_val = max_val.max(out);
1608 min_val = min_val.min(out);
1609 }
1610
1611 assert!(max_val > 0.0, "Should have positive output: {}", max_val);
1613 assert!(min_val < 0.0 || max_val > 0.0, "Should have some signal");
1614 }
1615 #[test]
1616 fn test_formant_osc_vowel_selection() {
1617 let mut osc = FormantOsc::new(44100.0);
1618 let mut inputs = PortValues::new();
1619 let mut outputs = PortValues::new();
1620
1621 inputs.set(0, 1.0); let mut sums_by_vowel = Vec::new();
1625 for vowel_cv in [0.0, 0.25, 0.5, 0.75, 1.0] {
1626 osc.reset();
1627 inputs.set(1, vowel_cv);
1628
1629 let mut sum = 0.0;
1630 for _ in 0..500 {
1631 osc.tick(&inputs, &mut outputs);
1632 sum += outputs.get(10).unwrap().abs();
1633 }
1634 sums_by_vowel.push(sum);
1635 }
1636
1637 let mut any_different = false;
1640 for i in 0..sums_by_vowel.len() - 1 {
1641 if (sums_by_vowel[i] - sums_by_vowel[i + 1]).abs() > 10.0 {
1642 any_different = true;
1643 break;
1644 }
1645 }
1646 assert!(any_different, "Vowels should produce different timbres");
1647 }
1648 #[test]
1649 fn test_formant_osc_formant_shift() {
1650 let mut osc = FormantOsc::new(44100.0);
1651 let mut inputs = PortValues::new();
1652 let mut outputs = PortValues::new();
1653
1654 inputs.set(0, 0.0);
1655 inputs.set(1, 0.5); osc.reset();
1659 inputs.set(2, 0.0);
1660 let mut sum_no_shift = 0.0;
1661 for _ in 0..500 {
1662 osc.tick(&inputs, &mut outputs);
1663 sum_no_shift += outputs.get(10).unwrap();
1664 }
1665
1666 osc.reset();
1668 inputs.set(2, 2.5);
1669 let mut sum_high_shift = 0.0;
1670 for _ in 0..500 {
1671 osc.tick(&inputs, &mut outputs);
1672 sum_high_shift += outputs.get(10).unwrap();
1673 }
1674
1675 assert!(
1677 (sum_no_shift - sum_high_shift).abs() > 0.1,
1678 "Shift should affect output"
1679 );
1680 }
1681 #[test]
1682 fn test_formant_osc_vibrato() {
1683 let mut osc = FormantOsc::new(44100.0);
1684 let mut inputs = PortValues::new();
1685 let mut outputs = PortValues::new();
1686
1687 inputs.set(0, 0.0);
1688 inputs.set(1, 0.0);
1689
1690 inputs.set(3, 1.0); for _ in 0..1000 {
1694 osc.tick(&inputs, &mut outputs);
1695 }
1696
1697 assert!(osc.vibrato_phase > 0.0);
1699 }
1700 #[test]
1701 fn test_formant_osc_glottal_pulse() {
1702 let opening = FormantOsc::glottal_pulse(0.0);
1704 let peak = FormantOsc::glottal_pulse(0.4);
1705 let closing = FormantOsc::glottal_pulse(0.6);
1706 let closed = FormantOsc::glottal_pulse(0.9);
1707
1708 assert_eq!(opening, 0.0, "Should start at zero");
1709 assert!(peak > 0.9, "Peak should be near 1.0: {}", peak);
1710 assert!(
1711 closing > 0.0 && closing < peak,
1712 "Closing phase should be declining"
1713 );
1714 assert_eq!(closed, 0.0, "Closed phase should be zero");
1715 }
1716 #[test]
1717 fn test_formant_osc_frequency_tracking() {
1718 let mut osc = FormantOsc::new(44100.0);
1719
1720 let count_crossings = |osc: &mut FormantOsc, v_oct: f64| -> usize {
1722 let mut inputs = PortValues::new();
1723 let mut outputs = PortValues::new();
1724 inputs.set(0, v_oct);
1725 osc.reset();
1726
1727 let mut crossings = 0;
1728 let mut prev_phase = 0.0;
1729 for _ in 0..1000 {
1730 osc.tick(&inputs, &mut outputs);
1731 if osc.phase < prev_phase {
1733 crossings += 1;
1734 }
1735 prev_phase = osc.phase;
1736 }
1737 crossings
1738 };
1739
1740 let crossings_c4 = count_crossings(&mut osc, 0.0);
1741 let crossings_c5 = count_crossings(&mut osc, 1.0);
1742
1743 let ratio = crossings_c5 as f64 / crossings_c4 as f64;
1744 assert!(
1745 ratio > 1.7 && ratio < 2.3,
1746 "Octave ratio should be ~2: {}",
1747 ratio
1748 );
1749 }
1750 #[test]
1751 fn test_vco_output_bounded() {
1752 let mut vco = Vco::new(44100.0);
1754 let mut inputs = PortValues::new();
1755 let mut outputs = PortValues::new();
1756
1757 for voct in [-2.0, 0.0, 2.0, 4.0] {
1759 inputs.set(0, voct);
1760
1761 let max = measure_max_output(1000, || {
1762 vco.tick(&inputs, &mut outputs);
1763 let sin = outputs.get(10).unwrap_or(0.0).abs();
1764 let tri = outputs.get(11).unwrap_or(0.0).abs();
1765 let saw = outputs.get(12).unwrap_or(0.0).abs();
1766 let sqr = outputs.get(13).unwrap_or(0.0).abs();
1767 sin.max(tri).max(saw).max(sqr)
1768 });
1769
1770 assert!(
1771 max <= 5.5, "VCO output {} exceeds expected range at voct={}",
1773 max,
1774 voct
1775 );
1776 }
1777 }
1778 #[test]
1779 fn test_lfo_output_bounded() {
1780 let mut lfo = Lfo::new(44100.0);
1781 let mut inputs = PortValues::new();
1782 let mut outputs = PortValues::new();
1783
1784 inputs.set(0, 1.0); let max = measure_max_output(50000, || {
1787 lfo.tick(&inputs, &mut outputs);
1788 outputs.get(10).unwrap_or(0.0).abs()
1789 });
1790
1791 assert!(max <= 5.5, "LFO output {} exceeds expected ±5V range", max);
1792 }
1793 #[test]
1794 fn test_noise_output_bounded() {
1795 let mut noise = NoiseGenerator::new();
1796 let inputs = PortValues::new();
1797 let mut outputs = PortValues::new();
1798
1799 let max = measure_max_output(10000, || {
1800 noise.tick(&inputs, &mut outputs);
1801 let white = outputs.get(10).unwrap_or(0.0).abs();
1802 let pink = outputs.get(11).unwrap_or(0.0).abs();
1803 white.max(pink)
1804 });
1805
1806 assert!(
1807 max <= 5.5,
1808 "Noise output {} exceeds expected ±5V range",
1809 max
1810 );
1811 }
1812
1813 fn dft_mag(sig: &[f64], k: usize) -> f64 {
1819 let n = sig.len();
1820 let mut re = 0.0;
1821 let mut im = 0.0;
1822 for (i, &s) in sig.iter().enumerate() {
1823 let ang = -TAU * (k as f64) * (i as f64) / (n as f64);
1824 re += s * Libm::<f64>::cos(ang);
1825 im += s * Libm::<f64>::sin(ang);
1826 }
1827 Libm::<f64>::sqrt(re * re + im * im) / (n as f64)
1828 }
1829
1830 fn alias_energy(sig: &[f64], fund: usize) -> f64 {
1833 let n = sig.len();
1834 let mut total = 0.0;
1835 for k in 1..(n / 2) {
1836 if k % fund != 0 {
1837 total += dft_mag(sig, k);
1838 }
1839 }
1840 total
1841 }
1842
1843 fn measure_period(seg: &[f64], expected: f64) -> f64 {
1846 let autocorr = |lag: usize| -> f64 {
1847 let mut acc = 0.0;
1848 for i in 0..(seg.len() - lag) {
1849 acc += seg[i] * seg[i + lag];
1850 }
1851 acc
1852 };
1853 let lo = ((expected * 0.6) as usize).max(2);
1854 let hi = ((expected * 1.6) as usize).min(seg.len() / 2);
1855 let mut best_lag = lo;
1856 let mut best = f64::MIN;
1857 for lag in lo..hi {
1858 let a = autocorr(lag);
1859 if a > best {
1860 best = a;
1861 best_lag = lag;
1862 }
1863 }
1864 let y0 = autocorr(best_lag - 1);
1865 let y1 = autocorr(best_lag);
1866 let y2 = autocorr(best_lag + 1);
1867 let denom = y0 - 2.0 * y1 + y2;
1868 let delta = if denom.abs() > 1e-12 {
1869 0.5 * (y0 - y2) / denom
1870 } else {
1871 0.0
1872 };
1873 best_lag as f64 + delta
1874 }
1875
1876 fn vco_capture(voct: f64, port: u32, n: usize) -> Vec<f64> {
1878 let mut vco = Vco::new(44100.0);
1879 let mut inputs = PortValues::new();
1880 let mut outputs = PortValues::new();
1881 inputs.set(0, voct);
1882 let mut out = Vec::with_capacity(n);
1883 for _ in 0..n {
1884 vco.tick(&inputs, &mut outputs);
1885 out.push(outputs.get(port).unwrap());
1886 }
1887 out
1888 }
1889
1890 #[test]
1893 fn test_vco_saw_frequency_preserved() {
1894 let saw = vco_capture(0.0, 12, (44100.0 / 261.63) as usize * 10);
1896 let crossings = saw.windows(2).filter(|w| w[0] <= 0.0 && w[1] > 0.0).count();
1897 assert!(
1898 (8..=12).contains(&crossings),
1899 "expected ~10 zero crossings, got {}",
1900 crossings
1901 );
1902 }
1903
1904 #[test]
1905 fn test_vco_saw_aliasing_reduced() {
1906 let voct = Libm::<f64>::log2(4200.0 / voct_to_hz(0.0));
1908 let n = 441;
1909 let fund = 42;
1910 let dt = voct_to_hz(voct) / 44100.0;
1911 let saw_bl = vco_capture(voct, 12, n);
1912 let mut ph = 0.0;
1914 let mut saw_naive = Vec::with_capacity(n);
1915 for _ in 0..n {
1916 saw_naive.push((2.0 * ph - 1.0) * 5.0);
1917 ph += dt;
1918 ph -= Libm::<f64>::floor(ph);
1919 }
1920 let a_bl = alias_energy(&saw_bl, fund);
1921 let a_naive = alias_energy(&saw_naive, fund);
1922 assert!(
1923 a_bl < 0.3 * a_naive,
1924 "saw alias energy not reduced: bl={} naive={}",
1925 a_bl,
1926 a_naive
1927 );
1928 }
1929
1930 #[test]
1931 fn test_vco_square_aliasing_reduced() {
1932 let voct = Libm::<f64>::log2(4200.0 / voct_to_hz(0.0));
1933 let n = 441;
1934 let fund = 42;
1935 let dt = voct_to_hz(voct) / 44100.0;
1936 let sqr_bl = vco_capture(voct, 13, n);
1937 let mut ph = 0.0;
1938 let mut sqr_naive = Vec::with_capacity(n);
1939 for _ in 0..n {
1940 sqr_naive.push(if ph < 0.5 { 5.0 } else { -5.0 });
1941 ph += dt;
1942 ph -= Libm::<f64>::floor(ph);
1943 }
1944 let a_bl = alias_energy(&sqr_bl, fund);
1945 let a_naive = alias_energy(&sqr_naive, fund);
1946 assert!(
1947 a_bl < 0.3 * a_naive,
1948 "square alias energy not reduced: bl={} naive={}",
1949 a_bl,
1950 a_naive
1951 );
1952 let max_delta = |v: &[f64]| {
1954 v.windows(2)
1955 .map(|w| (w[1] - w[0]).abs())
1956 .fold(0.0, f64::max)
1957 };
1958 assert!(max_delta(&sqr_bl) < max_delta(&sqr_naive));
1959 }
1960
1961 #[test]
1962 fn test_vco_triangle_aliasing_reduced() {
1963 let voct = Libm::<f64>::log2(4200.0 / voct_to_hz(0.0));
1965 let n = 441;
1966 let fund = 42;
1967 let dt = voct_to_hz(voct) / 44100.0;
1968 let tri_bl = vco_capture(voct, 11, n);
1969 let mut ph = 0.0;
1970 let mut tri_naive = Vec::with_capacity(n);
1971 for _ in 0..n {
1972 tri_naive.push((1.0 - 4.0 * Libm::<f64>::fabs(ph - 0.5)) * 5.0);
1973 ph += dt;
1974 ph -= Libm::<f64>::floor(ph);
1975 }
1976 let a_bl = alias_energy(&tri_bl, fund);
1977 let a_naive = alias_energy(&tri_naive, fund);
1978 assert!(
1979 a_bl < 0.5 * a_naive,
1980 "triangle alias energy not reduced: bl={} naive={}",
1981 a_bl,
1982 a_naive
1983 );
1984 }
1985
1986 #[test]
1987 fn test_vco_hard_sync_bounded_and_reduces_step() {
1988 let mut vco = Vco::new(44100.0);
1991 let mut inputs = PortValues::new();
1992 let mut outputs = PortValues::new();
1993 inputs.set(0, 2.0); let master_dt = 110.0 / 44100.0;
1995 let mut mp = 0.0;
1996 let mut out = Vec::new();
1997 let mut max_abs = 0.0f64;
1998 for _ in 0..4000 {
1999 let sync = if mp < 0.5 { 5.0 } else { 0.0 };
2000 inputs.set(3, sync);
2001 vco.tick(&inputs, &mut outputs);
2002 let saw = outputs.get(12).unwrap();
2003 max_abs = max_abs.max(saw.abs());
2004 out.push(saw);
2005 mp += master_dt;
2006 if mp >= 1.0 {
2007 mp -= 1.0;
2008 }
2009 }
2010 assert!(max_abs <= 5.5, "hard-sync saw exceeded ±5V: {}", max_abs);
2011 let rms = (out.iter().map(|x| x * x).sum::<f64>() / out.len() as f64).sqrt();
2013 assert!(rms > 1.0, "hard-sync output too quiet: rms={}", rms);
2014 }
2015
2016 #[test]
2019 fn test_vco_has_fm_lin_port() {
2020 let vco = Vco::new(44100.0);
2021 assert_eq!(vco.port_spec().inputs.len(), 5);
2022 let fm_lin = vco.port_spec().inputs.iter().find(|p| p.name == "fm_lin");
2023 assert!(fm_lin.is_some(), "fm_lin input port missing");
2024 assert_eq!(fm_lin.unwrap().id, 4);
2025 }
2026
2027 #[test]
2028 fn test_vco_fm_lin_zero_is_noop() {
2029 let mut a = Vco::new(44100.0);
2031 let mut b = Vco::new(44100.0);
2032 let mut ia = PortValues::new();
2033 let mut ib = PortValues::new();
2034 let mut oa = PortValues::new();
2035 let mut ob = PortValues::new();
2036 ia.set(0, 1.0);
2037 ib.set(0, 1.0);
2038 ib.set(4, 0.0); for _ in 0..500 {
2040 a.tick(&ia, &mut oa);
2041 b.tick(&ib, &mut ob);
2042 assert_eq!(oa.get(12).unwrap(), ob.get(12).unwrap());
2043 }
2044 }
2045
2046 #[test]
2047 fn test_vco_fm_lin_through_zero_symmetric() {
2048 let mut vco = Vco::new(44100.0);
2051 let mut inputs = PortValues::new();
2052 let mut outputs = PortValues::new();
2053 inputs.set(0, 0.0); let mod_dt = 200.0 / 44100.0; let mut mphase = 0.0;
2056 let mut sum = 0.0;
2057 let mut max_abs = 0.0f64;
2058 let n = 44100;
2059 for _ in 0..n {
2060 let m = Libm::<f64>::sin(mphase * TAU) * 5.0; inputs.set(4, m);
2062 vco.tick(&inputs, &mut outputs);
2063 let sine = outputs.get(10).unwrap();
2064 sum += sine;
2065 max_abs = max_abs.max(sine.abs());
2066 mphase += mod_dt;
2067 if mphase >= 1.0 {
2068 mphase -= 1.0;
2069 }
2070 }
2071 let mean = sum / n as f64;
2072 assert!(
2073 mean.abs() < 0.2,
2074 "FM sidebands not symmetric: mean={}",
2075 mean
2076 );
2077 assert!(max_abs <= 5.5, "FM output exceeded range: {}", max_abs);
2078 }
2079
2080 #[test]
2083 fn test_supersaw_sub_is_octave_down_zero_mean() {
2084 let mut ss = Supersaw::new(44100.0);
2085 let mut inputs = PortValues::new();
2086 let mut outputs = PortValues::new();
2087 inputs.set(0, 0.0); let base_freq = voct_to_hz(0.0);
2089 let n = 44100 * 2;
2090 let mut sub = Vec::with_capacity(n);
2091 for _ in 0..n {
2092 ss.tick(&inputs, &mut outputs);
2093 sub.push(outputs.get(11).unwrap());
2094 }
2095 let cross = |v: &[f64]| v.windows(2).filter(|w| w[0] <= 0.0 && w[1] > 0.0).count();
2099 let sub_rate = cross(&sub) as f64 / (n as f64 / 44100.0);
2100 let expected = base_freq / 2.0;
2101 assert!(
2102 (sub_rate - expected).abs() < 0.05 * expected,
2103 "sub should ring an octave down (~{} Hz), measured {} Hz",
2104 expected,
2105 sub_rate
2106 );
2107 let mean = sub.iter().sum::<f64>() / sub.len() as f64;
2108 assert!(mean.abs() < 0.05, "sub should be zero-mean: mean={}", mean);
2109 }
2110
2111 #[test]
2112 fn test_supersaw_mix_zero_equals_blepped_center() {
2113 let mut ss = Supersaw::new(44100.0);
2116 let mut inputs = PortValues::new();
2117 let mut outputs = PortValues::new();
2118 inputs.set(0, 0.5); inputs.set(2, 0.0); let base_freq = voct_to_hz(0.5);
2121 let dt = base_freq / 44100.0; let mut ph = 3.0 / 7.0; for _ in 0..500 {
2124 ss.tick(&inputs, &mut outputs);
2125 let expected = (2.0 * ph - 1.0) - polyblep(ph, dt);
2126 let got = outputs.get(10).unwrap();
2127 assert!(
2128 (got - expected).abs() < 1e-9,
2129 "mix=0 output {} != blepped center {}",
2130 got,
2131 expected
2132 );
2133 ph += dt;
2134 if ph >= 1.0 {
2135 ph -= 1.0;
2136 }
2137 }
2138 }
2139
2140 #[test]
2143 fn test_ks_excites_once_per_gate() {
2144 let mut ks = KarplusStrong::new(44100.0);
2145 let mut inputs = PortValues::new();
2146 let mut outputs = PortValues::new();
2147 inputs.set(0, 0.0); inputs.set(2, 0.95); inputs.set(3, 0.5); let mut ring = Vec::new();
2153 for i in 0..100 {
2154 inputs.set(1, 5.0);
2155 ks.tick(&inputs, &mut outputs);
2156 if i >= 10 {
2157 ring.push(outputs.get(10).unwrap());
2158 }
2159 }
2160 assert_eq!(
2164 ks.write_pos, 100,
2165 "gate should excite once; write_pos={}",
2166 ks.write_pos
2167 );
2168 let rms = (ring.iter().map(|x| x * x).sum::<f64>() / ring.len() as f64).sqrt();
2170 assert!(rms > 0.05, "string did not ring during gate: rms={}", rms);
2171 }
2172
2173 #[test]
2174 fn test_ks_gate_high_threshold() {
2175 let mut ks = KarplusStrong::new(44100.0);
2177 let mut inputs = PortValues::new();
2178 let mut outputs = PortValues::new();
2179 inputs.set(0, 0.0);
2180 inputs.set(1, 1.0); for _ in 0..200 {
2182 ks.tick(&inputs, &mut outputs);
2183 }
2184 let out = outputs.get(10).unwrap();
2185 assert_eq!(out, 0.0, "sub-threshold trigger should not excite: {}", out);
2186 }
2187
2188 #[test]
2191 fn test_ks_tuning_accuracy() {
2192 for &(voct, target_hz) in &[(-1.0, 130.81), (0.0, 261.63), (1.0, 523.25), (2.0, 1046.5)] {
2193 let mut ks = KarplusStrong::new(44100.0);
2194 let mut inputs = PortValues::new();
2195 let mut outputs = PortValues::new();
2196 inputs.set(0, voct);
2197 inputs.set(2, 0.95); inputs.set(3, 0.5);
2199 inputs.set(1, 5.0);
2201 ks.tick(&inputs, &mut outputs);
2202 inputs.set(1, 0.0);
2203 let mut out = Vec::with_capacity(12000);
2204 for _ in 0..12000 {
2205 ks.tick(&inputs, &mut outputs);
2206 out.push(outputs.get(10).unwrap());
2207 }
2208 let seg = &out[2000..10000];
2209 let expected_period = 44100.0 / target_hz;
2210 let period = measure_period(seg, expected_period);
2211 let measured_hz = 44100.0 / period;
2212 let cents = 1200.0 * Libm::<f64>::log2(measured_hz / target_hz);
2213 assert!(
2214 cents.abs() < 20.0,
2215 "KS pitch off at {} Hz: measured {} Hz ({:+.1} cents)",
2216 target_hz,
2217 measured_hz,
2218 cents
2219 );
2220 }
2221 }
2222
2223 #[test]
2224 fn test_ks_high_then_low_pitch_same_instance() {
2225 let mut ks = KarplusStrong::new(44100.0);
2231 let mut inputs = PortValues::new();
2232 let mut outputs = PortValues::new();
2233 inputs.set(2, 0.95); inputs.set(3, 0.5);
2235
2236 inputs.set(0, 2.0);
2238 inputs.set(1, 5.0);
2239 ks.tick(&inputs, &mut outputs);
2240 inputs.set(1, 0.0);
2241 for _ in 0..4000 {
2242 ks.tick(&inputs, &mut outputs);
2243 }
2244
2245 let target_hz = 65.41;
2247 inputs.set(0, -2.0);
2248 inputs.set(1, 5.0);
2249 ks.tick(&inputs, &mut outputs);
2250 inputs.set(1, 0.0);
2251 let mut out = Vec::with_capacity(12000);
2252 for _ in 0..12000 {
2253 ks.tick(&inputs, &mut outputs);
2254 out.push(outputs.get(10).unwrap());
2255 }
2256 let seg = &out[2000..10000];
2257 let expected_period = 44100.0 / target_hz;
2258 let period = measure_period(seg, expected_period);
2259 let measured_hz = 44100.0 / period;
2260 let cents = 1200.0 * Libm::<f64>::log2(measured_hz / target_hz);
2261 assert!(
2262 cents.abs() < 50.0,
2263 "KS low note after high pluck mistuned: measured {} Hz \
2264 (target {} Hz, {:+.1} cents)",
2265 measured_hz,
2266 target_hz,
2267 cents
2268 );
2269 }
2270
2271 #[test]
2274 fn test_ks_dc_decays() {
2275 let mut ks = KarplusStrong::new(44100.0);
2278 let mut inputs = PortValues::new();
2279 let mut outputs = PortValues::new();
2280 inputs.set(0, 0.0);
2281 inputs.set(2, 0.7);
2282 inputs.set(3, 0.0); inputs.set(1, 5.0);
2284 ks.tick(&inputs, &mut outputs);
2285 inputs.set(1, 0.0);
2286 let mut out = Vec::with_capacity(20000);
2287 for _ in 0..20000 {
2288 ks.tick(&inputs, &mut outputs);
2289 out.push(outputs.get(10).unwrap());
2290 }
2291 let mean_window = |s: &[f64]| s.iter().sum::<f64>() / s.len() as f64;
2292 let late = mean_window(&out[10000..20000]);
2293 assert!(
2294 late.abs() < 0.02,
2295 "KS output retains DC offset: late mean = {}",
2296 late
2297 );
2298 }
2299
2300 #[test]
2303 fn test_wavetable_mip_keeps_harmonics_below_nyquist() {
2304 let fs = 44100.0;
2305 for &freq in &[1000.0, 2000.0, 3000.0, 6000.0] {
2308 let phase_inc = freq / fs;
2309 let level = Wavetable::select_level(2, phase_inc); let harmonics = Wavetable::max_harmonic(2, level);
2311 let top = harmonics as f64 * freq;
2312 assert!(
2313 top < fs / 2.0,
2314 "saw at {} Hz: level {} keeps {} harmonics, top partial {} >= Nyquist",
2315 freq,
2316 level,
2317 harmonics,
2318 top
2319 );
2320 }
2321 }
2322
2323 #[test]
2324 fn test_wavetable_mip_selects_higher_level_for_higher_pitch() {
2325 let fs = 44100.0;
2327 let l_low = Wavetable::select_level(2, 100.0 / fs);
2328 let l_mid = Wavetable::select_level(2, 1000.0 / fs);
2329 let l_high = Wavetable::select_level(2, 5000.0 / fs);
2330 assert!(l_low <= l_mid && l_mid <= l_high);
2331 assert!(l_high > l_low, "expected higher pitch to raise mip level");
2332 }
2333
2334 #[test]
2335 fn test_wavetable_high_pitch_bounded() {
2336 let mut wt = Wavetable::new(44100.0);
2338 let mut inputs = PortValues::new();
2339 let mut outputs = PortValues::new();
2340 inputs.set(0, 3.5); inputs.set(1, 2.0 / 7.0); let mut max_abs = 0.0f64;
2343 let mut sumsq = 0.0;
2344 let n = 4000;
2345 for _ in 0..n {
2346 wt.tick(&inputs, &mut outputs);
2347 let v = outputs.get(10).unwrap();
2348 max_abs = max_abs.max(v.abs());
2349 sumsq += v * v;
2350 }
2351 assert!(
2352 max_abs <= 5.5,
2353 "wavetable high-pitch exceeded range: {}",
2354 max_abs
2355 );
2356 assert!(
2357 (sumsq / n as f64).sqrt() > 0.5,
2358 "wavetable high-pitch silent"
2359 );
2360 }
2361
2362 fn block_rms_ptp(sig: &[f64], block: usize) -> f64 {
2368 let mut lo = f64::INFINITY;
2369 let mut hi = f64::NEG_INFINITY;
2370 for chunk in sig.chunks(block) {
2371 let rms = (chunk.iter().map(|x| x * x).sum::<f64>() / chunk.len() as f64).sqrt();
2372 lo = lo.min(rms);
2373 hi = hi.max(rms);
2374 }
2375 hi - lo
2376 }
2377
2378 #[test]
2379 fn test_supersaw_detune_spread() {
2380 let run = |detune: f64| -> Vec<f64> {
2381 let mut ss = Supersaw::new(44100.0);
2382 let mut inputs = PortValues::new();
2383 let mut outputs = PortValues::new();
2384 inputs.set(0, 0.0); inputs.set(1, detune);
2386 inputs.set(2, 1.0); let mut out = Vec::with_capacity(20_000);
2388 for _ in 0..20_000 {
2389 ss.tick(&inputs, &mut outputs);
2390 out.push(outputs.get(10).unwrap());
2391 }
2392 out
2393 };
2394
2395 let ptp_off = block_rms_ptp(&run(0.0), 500);
2396 let ptp_on = block_rms_ptp(&run(1.0), 500);
2397 assert!(
2400 ptp_off < 0.02,
2401 "no-detune supersaw should not beat: ptp={ptp_off}"
2402 );
2403 assert!(
2405 ptp_on > ptp_off + 0.03,
2406 "detuned supersaw must beat (spread the voices): on={ptp_on} off={ptp_off}"
2407 );
2408 }
2409
2410 #[test]
2411 fn test_supersaw_reset_and_sample_rate() {
2412 let mut ss = Supersaw::default();
2413 assert_eq!(ss.type_id(), "supersaw");
2414 let mut inputs = PortValues::new();
2415 let mut outputs = PortValues::new();
2416 inputs.set(0, 0.0);
2417 for _ in 0..500 {
2418 ss.tick(&inputs, &mut outputs);
2419 }
2420 assert!(ss.sub_phase != 0.0 || ss.phases[3] != 3.0 / 7.0);
2421 ss.reset();
2422 assert_eq!(ss.sub_phase, 0.0);
2423 for (i, &p) in ss.phases.iter().enumerate() {
2424 assert_eq!(p, i as f64 / 7.0);
2425 }
2426 ss.set_sample_rate(48000.0);
2427 assert_eq!(ss.sample_rate, 48000.0);
2428 ss.tick(&inputs, &mut outputs);
2429 assert!(outputs.get(10).unwrap().is_finite());
2430 }
2431
2432 #[test]
2435 fn test_karplus_strong_reset_and_sample_rate() {
2436 let mut ks = KarplusStrong::default();
2437 assert_eq!(ks.type_id(), "karplus_strong");
2438 assert_eq!(ks.sample_rate, 44100.0);
2439 let mut inputs = PortValues::new();
2440 let mut outputs = PortValues::new();
2441 inputs.set(0, 0.0);
2442 inputs.set(1, 5.0); for _ in 0..500 {
2444 ks.tick(&inputs, &mut outputs);
2445 }
2446 assert!(ks.write_pos != 0);
2447 ks.reset();
2448 assert_eq!(ks.write_pos, 0);
2449 assert_eq!(ks.last_output, 0.0);
2450 assert!(ks.buffer.iter().all(|&x| x == 0.0));
2451 ks.set_sample_rate(48000.0);
2453 assert_eq!(ks.sample_rate, 48000.0);
2454 for _ in 0..100 {
2455 ks.tick(&inputs, &mut outputs);
2456 assert!(outputs.get(10).unwrap().is_finite());
2457 }
2458 }
2459}