1use super::common::{
4 polyblamp, polyblep, voct_to_hz, wrap_phase, EdgeDetector, Memo, GATE_THRESHOLD_V,
5};
6use crate::port::{GraphModule, PortDef, PortSpec, PortValues, SignalKind};
7use crate::rng;
8use alloc::vec;
9use alloc::vec::Vec;
10use core::f64::consts::TAU;
11use libm::Libm;
12
13pub struct Vco {
31 phase: f64,
32 sample_rate: f64,
33 sync_edge: EdgeDetector,
34 freq_memo: Memo<3, f64>,
37 spec: PortSpec,
38}
39
40impl Vco {
41 pub fn new(sample_rate: f64) -> Self {
42 Self {
43 phase: 0.0,
44 sample_rate,
45 sync_edge: EdgeDetector::new(),
46 freq_memo: Memo::new(0.0),
47 spec: PortSpec {
48 inputs: vec![
49 PortDef::new(0, "voct", SignalKind::VoltPerOctave),
50 PortDef::new(1, "fm", SignalKind::CvBipolar).with_attenuverter(),
52 PortDef::new(2, "pw", SignalKind::CvUnipolar)
53 .with_default(0.5)
54 .with_attenuverter(),
55 PortDef::new(3, "sync", SignalKind::Gate),
56 PortDef::new(4, "fm_lin", SignalKind::CvBipolar).with_attenuverter(),
58 ],
59 outputs: vec![
60 PortDef::new(10, "sin", SignalKind::Audio),
61 PortDef::new(11, "tri", SignalKind::Audio),
62 PortDef::new(12, "saw", SignalKind::Audio),
63 PortDef::new(13, "sqr", SignalKind::Audio),
64 ],
65 },
66 }
67 }
68}
69
70impl Default for Vco {
71 fn default() -> Self {
72 Self::new(44100.0)
73 }
74}
75
76impl Vco {
77 const WANT_SIN: u32 = 1 << 0;
79 const WANT_TRI: u32 = 1 << 1;
80 const WANT_SAW: u32 = 1 << 2;
81 const WANT_SQR: u32 = 1 << 3;
82
83 fn tick_wanted(&mut self, inputs: &PortValues, outputs: &mut PortValues, wanted: u32) {
91 let voct = inputs.get_or(0, 0.0);
92 let fm = inputs.get_or(1, 0.0);
93 let pw = inputs.get_or(2, 0.5).clamp(0.05, 0.95);
94 let sync = inputs.get_or(3, 0.0);
95 let fm_lin = inputs.get_or(4, 0.0);
96
97 let freq = self.freq_memo.get_or_compute([voct, fm, fm_lin], || {
100 let base_freq = voct_to_hz(voct);
102 let mut freq = base_freq * Libm::<f64>::pow(2.0, fm);
104 freq += (fm_lin / 5.0) * base_freq;
107 freq
108 });
109
110 let dt = freq / self.sample_rate;
112 let dt_abs = Libm::<f64>::fabs(dt);
114
115 let mut sync_reset: Option<(f64, f64)> = None;
118 if let Some(frac) = self.sync_edge.rising_frac(sync) {
119 sync_reset = Some((self.phase, frac));
120 self.phase = 0.0;
121 }
122
123 let phase = self.phase;
124
125 let sin = if wanted & Self::WANT_SIN != 0 {
127 Libm::<f64>::sin(phase * TAU) * 5.0
128 } else {
129 0.0
130 };
131
132 let mut saw = 0.0;
134 if wanted & Self::WANT_SAW != 0 {
135 saw = 2.0 * phase - 1.0;
136 saw -= polyblep(phase, dt_abs);
137 }
138
139 let mut sqr = 0.0;
142 if wanted & Self::WANT_SQR != 0 {
143 sqr = if phase < pw { 1.0 } else { -1.0 };
144 sqr += polyblep(phase, dt_abs);
145 let pw_edge = {
146 let x = phase + (1.0 - pw);
147 x - Libm::<f64>::floor(x)
148 };
149 sqr -= polyblep(pw_edge, dt_abs);
150 }
151
152 let mut tri = 0.0;
155 if wanted & Self::WANT_TRI != 0 {
156 tri = 1.0 - 4.0 * Libm::<f64>::fabs(phase - 0.5);
157 let corner_half = {
158 let x = phase - 0.5;
159 if x < 0.0 {
160 x + 1.0
161 } else {
162 x
163 }
164 };
165 tri += 4.0 * dt_abs * polyblamp(phase, dt_abs);
166 tri -= 4.0 * dt_abs * polyblamp(corner_half, dt_abs);
167 }
168
169 if let Some((p_old, frac)) = sync_reset {
177 let equiv = (1.0 - frac) * dt_abs;
179 let blep = polyblep(equiv, dt_abs);
180 if wanted & Self::WANT_SAW != 0 {
181 let saw_step = -2.0 * p_old;
183 saw += (saw_step / 2.0) * blep;
184 }
185 if wanted & Self::WANT_SQR != 0 {
186 let old_sqr = if p_old < pw { 1.0 } else { -1.0 };
188 let sqr_step = 1.0 - old_sqr;
189 sqr += (sqr_step / 2.0) * blep;
190 }
191 }
192
193 if wanted & Self::WANT_SIN != 0 {
196 outputs.set(10, sin);
197 }
198 if wanted & Self::WANT_TRI != 0 {
199 outputs.set(11, tri * 5.0);
200 }
201 if wanted & Self::WANT_SAW != 0 {
202 outputs.set(12, saw * 5.0);
203 }
204 if wanted & Self::WANT_SQR != 0 {
205 outputs.set(13, sqr * 5.0);
206 }
207
208 self.phase = wrap_phase(self.phase + dt);
212 }
213}
214
215impl GraphModule for Vco {
216 fn port_spec(&self) -> &PortSpec {
217 &self.spec
218 }
219
220 fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
221 self.tick_wanted(inputs, outputs, u32::MAX);
222 }
223
224 fn tick_masked(&mut self, inputs: &PortValues, outputs: &mut PortValues, wanted: u32) {
225 self.tick_wanted(inputs, outputs, wanted);
226 }
227
228 fn reset(&mut self) {
229 self.phase = 0.0;
230 self.sync_edge.reset();
231 }
232
233 fn set_sample_rate(&mut self, sample_rate: f64) {
234 self.sample_rate = sample_rate;
235 }
236
237 fn type_id(&self) -> &'static str {
238 "vco"
239 }
240}
241
242pub struct Lfo {
247 phase: f64,
248 sample_rate: f64,
249 reset_edge: EdgeDetector,
250 freq_memo: Memo<1, f64>,
252 spec: PortSpec,
253}
254
255impl Lfo {
256 pub fn new(sample_rate: f64) -> Self {
257 Self {
258 phase: 0.0,
259 sample_rate,
260 reset_edge: EdgeDetector::new(),
261 freq_memo: Memo::new(0.0),
262 spec: PortSpec {
263 inputs: vec![
264 PortDef::new(0, "rate", SignalKind::CvUnipolar)
265 .with_default(0.5)
266 .with_attenuverter(),
267 PortDef::new(1, "depth", SignalKind::CvUnipolar).with_default(10.0),
268 PortDef::new(2, "reset", SignalKind::Trigger),
269 ],
270 outputs: vec![
271 PortDef::new(10, "sin", SignalKind::CvBipolar),
272 PortDef::new(11, "tri", SignalKind::CvBipolar),
273 PortDef::new(12, "saw", SignalKind::CvBipolar),
274 PortDef::new(13, "sqr", SignalKind::CvBipolar),
275 PortDef::new(14, "sin_uni", SignalKind::CvUnipolar),
276 ],
277 },
278 }
279 }
280}
281
282impl Default for Lfo {
283 fn default() -> Self {
284 Self::new(44100.0)
285 }
286}
287
288impl Lfo {
289 const WANT_SIN: u32 = 1 << 0;
291 const WANT_TRI: u32 = 1 << 1;
292 const WANT_SAW: u32 = 1 << 2;
293 const WANT_SQR: u32 = 1 << 3;
294 const WANT_SIN_UNI: u32 = 1 << 4;
295
296 fn tick_wanted(&mut self, inputs: &PortValues, outputs: &mut PortValues, wanted: u32) {
302 let rate_cv = inputs.get_or(0, 0.5);
303 let depth = inputs.get_or(1, 10.0) / 10.0; let reset = inputs.get_or(2, 0.0);
305
306 let freq = self.freq_memo.get_or_compute([rate_cv], || {
309 0.01 * Libm::<f64>::pow(3000.0, rate_cv.clamp(0.0, 1.0))
310 });
311
312 if self.reset_edge.rising(reset) {
314 self.phase = 0.0;
315 }
316
317 let scale = 5.0 * depth;
321 if wanted & Self::WANT_SIN != 0 {
322 outputs.set(10, Libm::<f64>::sin(self.phase * TAU) * scale);
323 }
324 if wanted & Self::WANT_TRI != 0 {
325 outputs.set(
326 11,
327 (1.0 - 4.0 * Libm::<f64>::fabs(self.phase - 0.5)) * scale,
328 );
329 }
330 if wanted & Self::WANT_SAW != 0 {
331 outputs.set(12, (2.0 * self.phase - 1.0) * scale);
332 }
333 if wanted & Self::WANT_SQR != 0 {
334 outputs.set(13, if self.phase < 0.5 { scale } else { -scale });
335 }
336 if wanted & Self::WANT_SIN_UNI != 0 {
337 outputs.set(
338 14,
339 (Libm::<f64>::sin(self.phase * TAU) * 0.5 + 0.5) * depth * 10.0,
340 );
341 }
342
343 self.phase = wrap_phase(self.phase + freq / self.sample_rate);
345 }
346}
347
348impl GraphModule for Lfo {
349 fn port_spec(&self) -> &PortSpec {
350 &self.spec
351 }
352
353 fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
354 self.tick_wanted(inputs, outputs, u32::MAX);
355 }
356
357 fn tick_masked(&mut self, inputs: &PortValues, outputs: &mut PortValues, wanted: u32) {
358 self.tick_wanted(inputs, outputs, wanted);
359 }
360
361 fn reset(&mut self) {
362 self.phase = 0.0;
363 self.reset_edge.reset();
364 }
365
366 fn set_sample_rate(&mut self, sample_rate: f64) {
367 self.sample_rate = sample_rate;
368 }
369
370 fn type_id(&self) -> &'static str {
371 "lfo"
372 }
373}
374
375pub struct Supersaw {
380 phases: [f64; 7],
381 sub_phase: f64,
384 sample_rate: f64,
385 freq_memo: Memo<1, f64>,
387 spec: PortSpec,
388}
389
390impl Supersaw {
391 const DETUNE_RATIOS: [f64; 7] = [
394 -0.11002313, -0.06288439, -0.01952356, 0.0, 0.01991221, 0.06216538, 0.10745242, ];
402
403 const MIX_LEVELS: [f64; 7] = [0.5, 0.7, 0.9, 1.0, 0.9, 0.7, 0.5];
405
406 pub fn new(sample_rate: f64) -> Self {
407 let mut phases = [0.0; 7];
409 for (i, phase) in phases.iter_mut().enumerate() {
410 *phase = (i as f64) / 7.0;
411 }
412
413 Self {
414 phases,
415 sub_phase: 0.0,
416 sample_rate,
417 freq_memo: Memo::new(0.0),
418 spec: PortSpec {
419 inputs: vec![
420 PortDef::new(0, "voct", SignalKind::VoltPerOctave).with_default(0.0),
421 PortDef::new(1, "detune", SignalKind::CvUnipolar)
422 .with_default(0.5)
423 .with_attenuverter(),
424 PortDef::new(2, "mix", SignalKind::CvUnipolar)
425 .with_default(0.5)
426 .with_attenuverter(),
427 ],
428 outputs: vec![
429 PortDef::new(10, "out", SignalKind::Audio),
430 PortDef::new(11, "sub", SignalKind::Audio),
431 ],
432 },
433 }
434 }
435
436 }
438
439impl Default for Supersaw {
440 fn default() -> Self {
441 Self::new(44100.0)
442 }
443}
444
445impl GraphModule for Supersaw {
446 fn port_spec(&self) -> &PortSpec {
447 &self.spec
448 }
449
450 fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
451 let voct = inputs.get_or(0, 0.0);
452 let detune = inputs.get_or(1, 0.5).clamp(0.0, 1.0);
453 let mix = inputs.get_or(2, 0.5).clamp(0.0, 1.0);
454
455 let base_freq = self.freq_memo.get_or_compute([voct], || voct_to_hz(voct));
457
458 let mut sum = 0.0;
459 let mut total_mix = 0.0;
460 let mut center_saw = 0.0;
462
463 for i in 0..7 {
464 let detune_amount = Self::DETUNE_RATIOS[i] * detune;
466 let freq = base_freq * (1.0 + detune_amount);
467 let dt = freq / self.sample_rate;
468
469 let raw_saw = 2.0 * self.phases[i] - 1.0;
471 let blep = polyblep(self.phases[i], dt);
472 let saw = raw_saw - blep;
473
474 if i == 3 {
477 center_saw = saw;
478 }
479
480 sum += saw * Self::MIX_LEVELS[i];
482 total_mix += Self::MIX_LEVELS[i];
483
484 self.phases[i] = wrap_phase(self.phases[i] + dt);
487 }
488
489 let normalized = sum / total_mix;
491 let output = center_saw * (1.0 - mix) + normalized * mix;
492
493 let sub_dt = base_freq / (2.0 * self.sample_rate);
496 let sub = (2.0 * self.sub_phase - 1.0) - polyblep(self.sub_phase, sub_dt);
497 self.sub_phase = wrap_phase(self.sub_phase + sub_dt); outputs.set(10, output);
500 outputs.set(11, sub);
501 }
502
503 fn reset(&mut self) {
504 for (i, phase) in self.phases.iter_mut().enumerate() {
505 *phase = (i as f64) / 7.0;
506 }
507 self.sub_phase = 0.0;
508 }
509
510 fn set_sample_rate(&mut self, sample_rate: f64) {
511 self.sample_rate = sample_rate;
512 }
513
514 fn type_id(&self) -> &'static str {
515 "supersaw"
516 }
517}
518
519pub struct KarplusStrong {
524 buffer: Vec<f64>,
525 max_len: usize,
530 write_pos: usize,
531 sample_rate: f64,
532 last_output: f64,
533 trigger_edge: EdgeDetector,
535 freq_memo: Memo<1, f64>,
537 spec: PortSpec,
538}
539
540impl KarplusStrong {
541 const LOOP_LEAK: f64 = 0.9995;
544
545 pub fn new(sample_rate: f64) -> Self {
546 let buffer_size = (sample_rate / 20.0) as usize + 10;
548 Self {
549 buffer: vec![0.0; buffer_size],
550 max_len: buffer_size,
551 write_pos: 0,
552 sample_rate,
553 last_output: 0.0,
554 trigger_edge: EdgeDetector::new(),
555 freq_memo: Memo::new(0.0),
556 spec: PortSpec {
557 inputs: vec![
558 PortDef::new(0, "voct", SignalKind::VoltPerOctave).with_default(0.0),
559 PortDef::new(1, "trigger", SignalKind::Trigger),
560 PortDef::new(2, "damping", SignalKind::CvUnipolar)
561 .with_default(0.5)
562 .with_attenuverter(),
563 PortDef::new(3, "brightness", SignalKind::CvUnipolar)
564 .with_default(0.5)
565 .with_attenuverter(),
566 PortDef::new(4, "stretch", SignalKind::CvBipolar)
567 .with_default(0.0)
568 .with_attenuverter(),
569 ],
570 outputs: vec![PortDef::new(10, "out", SignalKind::Audio)],
571 },
572 }
573 }
574
575 fn excite(&mut self, brightness: f64) {
576 let period = self.buffer.len();
578 for i in 0..period {
579 let noise = rng::random_bipolar();
581 let impulse = if i < period / 4 { 1.0 } else { 0.0 };
582 self.buffer[i] = noise * brightness + impulse * (1.0 - brightness);
583 }
584 let mean: f64 = self.buffer.iter().sum::<f64>() / period as f64;
589 for sample in self.buffer.iter_mut() {
590 *sample -= mean;
591 }
592 }
593}
594
595impl Default for KarplusStrong {
596 fn default() -> Self {
597 Self::new(44100.0)
598 }
599}
600
601impl GraphModule for KarplusStrong {
602 fn port_spec(&self) -> &PortSpec {
603 &self.spec
604 }
605
606 fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
607 let voct = inputs.get_or(0, 0.0);
608 let trigger = inputs.get_or(1, 0.0);
609 let damping = inputs.get_or(2, 0.5).clamp(0.0, 1.0);
610 let brightness = inputs.get_or(3, 0.5).clamp(0.0, 1.0);
611 let stretch = inputs.get_or(4, 0.0).clamp(-1.0, 1.0);
612
613 let freq = self.freq_memo.get_or_compute([voct], || voct_to_hz(voct));
619 let period = (self.sample_rate / freq).clamp(2.0, self.max_len as f64 - 1.0);
620 let period_int = period as usize;
621
622 if self.trigger_edge.rising(trigger) {
627 self.buffer.truncate(period_int + 2);
629 self.buffer.resize(period_int + 2, 0.0);
630 self.excite(brightness);
631 self.write_pos = 0;
632 }
633
634 let filter_coef = 0.5 + damping * 0.49; let filter_gd = (1.0 - filter_coef) / filter_coef;
642 let target_delay = (period - filter_gd).max(1.0);
643 let delay_int = target_delay as usize;
644 let delay_frac = target_delay - delay_int as f64;
645
646 let len = self.buffer.len();
648 let off1 = len.saturating_sub(delay_int); let off2 = off1.saturating_sub(1); let read_pos = (self.write_pos + off1) % len;
651 let read_pos2 = (self.write_pos + off2) % len;
652 let sample =
653 self.buffer[read_pos] * (1.0 - delay_frac) + self.buffer[read_pos2] * delay_frac;
654
655 let filtered = sample * filter_coef + self.last_output * (1.0 - filter_coef);
657
658 let stretch_coef = stretch * 0.5;
660 let stretched = filtered + stretch_coef * (filtered - self.last_output);
661
662 let leaked = stretched * Self::LOOP_LEAK;
665
666 self.last_output = leaked;
667
668 self.buffer[self.write_pos] = leaked;
670 self.write_pos = (self.write_pos + 1) % len;
671
672 outputs.set(10, leaked);
673 }
674
675 fn reset(&mut self) {
676 self.buffer.fill(0.0);
677 self.write_pos = 0;
678 self.last_output = 0.0;
679 self.trigger_edge.reset();
680 }
681
682 fn set_sample_rate(&mut self, sample_rate: f64) {
683 self.sample_rate = sample_rate;
684 let buffer_size = (sample_rate / 20.0) as usize + 10;
685 self.max_len = buffer_size;
686 self.buffer.resize(buffer_size, 0.0);
687 }
688
689 fn type_id(&self) -> &'static str {
690 "karplus_strong"
691 }
692}
693
694struct PinkNoiseState {
700 rows: [f64; 16],
701 running_sum: f64,
702 index: u32,
703}
704
705impl PinkNoiseState {
706 fn new() -> Self {
707 Self {
708 rows: [0.0; 16],
709 running_sum: 0.0,
710 index: 0,
711 }
712 }
713
714 fn sample(&mut self) -> f64 {
715 self.index = self.index.wrapping_add(1);
716 let changed_bits = (self.index ^ (self.index.wrapping_sub(1))).trailing_ones() as usize;
717
718 for i in 0..changed_bits.min(16) {
719 self.running_sum -= self.rows[i];
720 self.rows[i] = rng::random_bipolar();
721 self.running_sum += self.rows[i];
722 }
723
724 self.running_sum / 16.0
725 }
726}
727
728pub struct NoiseGenerator {
735 pink: PinkNoiseState,
736 pink2: PinkNoiseState,
738 pub(crate) correlation: f64,
740 last_white: f64,
742 spec: PortSpec,
743}
744
745impl NoiseGenerator {
746 pub fn new() -> Self {
747 Self {
748 pink: PinkNoiseState::new(),
749 pink2: PinkNoiseState::new(),
750 correlation: 0.3, last_white: 0.0,
752 spec: PortSpec {
753 inputs: vec![
754 PortDef::new(0, "correlation", SignalKind::CvUnipolar).with_default(0.3),
756 ],
757 outputs: vec![
758 PortDef::new(10, "white", SignalKind::Audio),
759 PortDef::new(11, "pink", SignalKind::Audio),
760 PortDef::new(12, "white2", SignalKind::Audio),
762 PortDef::new(13, "pink2", SignalKind::Audio),
763 ],
764 },
765 }
766 }
767
768 pub fn with_correlation(correlation: f64) -> Self {
770 let mut gen = Self::new();
771 gen.correlation = correlation.clamp(0.0, 1.0);
772 gen
773 }
774}
775
776impl Default for NoiseGenerator {
777 fn default() -> Self {
778 Self::new()
779 }
780}
781
782impl NoiseGenerator {
783 const WANT_WHITE: u32 = 1 << 0;
785 const WANT_PINK: u32 = 1 << 1;
786 const WANT_WHITE2: u32 = 1 << 2;
787 const WANT_PINK2: u32 = 1 << 3;
788
789 fn tick_wanted(&mut self, inputs: &PortValues, outputs: &mut PortValues, wanted: u32) {
798 let correlation = inputs.get_or(0, self.correlation).clamp(0.0, 1.0);
800
801 let white1 = rng::random_bipolar();
803
804 let independent = rng::random_bipolar();
807
808 let pink1 = self.pink.sample();
810
811 let pink2_independent = self.pink2.sample();
814
815 self.last_white = white1;
816
817 if wanted & Self::WANT_WHITE != 0 {
820 outputs.set(10, white1 * 5.0);
821 }
822 if wanted & Self::WANT_PINK != 0 {
823 outputs.set(11, pink1 * 5.0);
824 }
825 if wanted & Self::WANT_WHITE2 != 0 {
826 let white2 = white1 * correlation + independent * (1.0 - correlation);
827 outputs.set(12, white2 * 5.0);
828 }
829 if wanted & Self::WANT_PINK2 != 0 {
830 let pink2 = pink1 * correlation + pink2_independent * (1.0 - correlation);
831 outputs.set(13, pink2 * 5.0);
832 }
833 }
834}
835
836impl GraphModule for NoiseGenerator {
837 fn port_spec(&self) -> &PortSpec {
838 &self.spec
839 }
840
841 fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
842 self.tick_wanted(inputs, outputs, u32::MAX);
843 }
844
845 fn tick_masked(&mut self, inputs: &PortValues, outputs: &mut PortValues, wanted: u32) {
846 self.tick_wanted(inputs, outputs, wanted);
847 }
848
849 fn reset(&mut self) {
850 self.pink = PinkNoiseState::new();
851 self.pink2 = PinkNoiseState::new();
852 self.last_white = 0.0;
853 }
854
855 fn set_sample_rate(&mut self, _: f64) {}
856
857 fn type_id(&self) -> &'static str {
858 "noise"
859 }
860}
861
862#[derive(Debug, Clone, Copy, PartialEq)]
864pub enum WavetableType {
865 Sine,
867 Triangle,
869 Saw,
871 Square,
873 Pulse25,
875 Pulse12,
877 FormantA,
879 FormantO,
881}
882
883impl WavetableType {
884 pub fn index(self) -> usize {
886 match self {
887 WavetableType::Sine => 0,
888 WavetableType::Triangle => 1,
889 WavetableType::Saw => 2,
890 WavetableType::Square => 3,
891 WavetableType::Pulse25 => 4,
892 WavetableType::Pulse12 => 5,
893 WavetableType::FormantA => 6,
894 WavetableType::FormantO => 7,
895 }
896 }
897
898 pub fn from_index(idx: usize) -> Self {
900 match idx % 8 {
901 0 => WavetableType::Sine,
902 1 => WavetableType::Triangle,
903 2 => WavetableType::Saw,
904 3 => WavetableType::Square,
905 4 => WavetableType::Pulse25,
906 5 => WavetableType::Pulse12,
907 6 => WavetableType::FormantA,
908 _ => WavetableType::FormantO,
909 }
910 }
911}
912
913pub struct Wavetable {
925 tables: [[[f64; 256]; 8]; 8],
929 phase: f64,
931 prev_sync: f64,
933 sample_rate: f64,
934 freq_memo: Memo<1, f64>,
936 spec: PortSpec,
937}
938
939impl Wavetable {
940 const TABLE_SIZE: usize = 256;
942 const NUM_TABLES: usize = 8;
944 const NUM_MIPS: usize = 8;
947 const BASE_HARMONICS: [usize; 8] = [1, 31, 64, 63, 64, 64, 10, 10];
951
952 pub fn new(sample_rate: f64) -> Self {
953 let spec = PortSpec {
954 inputs: vec![
955 PortDef::new(0, "v_oct", SignalKind::VoltPerOctave).with_default(0.0),
956 PortDef::new(1, "table", SignalKind::CvUnipolar).with_default(0.0),
957 PortDef::new(2, "morph", SignalKind::CvUnipolar).with_default(0.0),
958 PortDef::new(3, "sync", SignalKind::Gate).with_default(0.0),
959 ],
960 outputs: vec![PortDef::new(10, "out", SignalKind::Audio)],
961 };
962
963 let mut osc = Self {
964 tables: [[[0.0; 256]; 8]; 8],
965 phase: 0.0,
966 prev_sync: 0.0,
967 sample_rate,
968 freq_memo: Memo::new(0.0),
969 spec,
970 };
971 osc.generate_tables();
972 osc
973 }
974
975 fn max_harmonic(table: usize, level: usize) -> usize {
978 (Self::BASE_HARMONICS[table] >> level).max(1)
979 }
980
981 fn generate_tables(&mut self) {
983 let n = Self::TABLE_SIZE;
984 let pi = core::f64::consts::PI;
985
986 for level in 0..Self::NUM_MIPS {
987 for i in 0..n {
988 let phase = (i as f64) / (n as f64);
989 let partial = |harmonic: f64| Libm::<f64>::sin(phase * harmonic * 2.0 * pi);
990
991 self.tables[0][level][i] = partial(1.0);
993
994 let mut tri = 0.0;
996 let mut h = 1usize;
997 let mh = Self::max_harmonic(1, level);
998 let mut sign = 1.0; while h <= mh {
1000 let hf = h as f64;
1001 tri += sign * partial(hf) / (hf * hf);
1002 sign = -sign;
1003 h += 2;
1004 }
1005 self.tables[1][level][i] = tri * (8.0 / (pi * pi));
1006
1007 let mut saw = 0.0;
1009 let mh = Self::max_harmonic(2, level);
1010 let mut sign = -1.0; for h in 1..=mh {
1012 let hf = h as f64;
1013 saw += sign * partial(hf) / hf;
1014 sign = -sign;
1015 }
1016 self.tables[2][level][i] = saw * (2.0 / pi);
1017
1018 let mut sqr = 0.0;
1020 let mut h = 1usize;
1021 let mh = Self::max_harmonic(3, level);
1022 while h <= mh {
1023 let hf = h as f64;
1024 sqr += partial(hf) / hf;
1025 h += 2;
1026 }
1027 self.tables[3][level][i] = sqr * (4.0 / pi);
1028
1029 for (table_idx, duty) in [(4usize, 0.25f64), (5usize, 0.125f64)] {
1031 let mut pulse = 0.0;
1032 let mh = Self::max_harmonic(table_idx, level);
1033 for h in 1..=mh {
1034 let hf = h as f64;
1035 let coef = Libm::<f64>::sin(pi * hf * duty) / hf;
1036 pulse += coef * partial(hf);
1037 }
1038 self.tables[table_idx][level][i] = pulse * 2.0;
1039 }
1040
1041 let mh_a = Self::max_harmonic(6, level) as f64;
1044 let formant_a = [(1.0, 1.0), (2.7, 0.5), (4.6, 0.3), (9.6, 0.15)]
1045 .iter()
1046 .filter(|(mult, _)| *mult <= mh_a)
1047 .map(|(mult, amp)| partial(*mult) * amp)
1048 .sum::<f64>();
1049 self.tables[6][level][i] = formant_a * 0.5;
1050
1051 let mh_o = Self::max_harmonic(7, level) as f64;
1052 let formant_o = [(1.0, 1.0), (1.5, 0.6), (3.0, 0.4), (10.0, 0.15)]
1053 .iter()
1054 .filter(|(mult, _)| *mult <= mh_o)
1055 .map(|(mult, amp)| partial(*mult) * amp)
1056 .sum::<f64>();
1057 self.tables[7][level][i] = formant_o * 0.5;
1058 }
1059 }
1060 }
1061
1062 fn select_level(table: usize, phase_inc: f64) -> usize {
1065 let inc = Libm::<f64>::fabs(phase_inc).max(1e-9);
1066 let allowed = Libm::<f64>::floor(0.5 / inc);
1068 for level in 0..Self::NUM_MIPS {
1069 if (Self::max_harmonic(table, level) as f64) <= allowed {
1070 return level;
1071 }
1072 }
1073 Self::NUM_MIPS - 1
1074 }
1075
1076 fn read_table(&self, table_idx: usize, level: usize, phase: f64) -> f64 {
1078 let table = &self.tables[table_idx % Self::NUM_TABLES][level.min(Self::NUM_MIPS - 1)];
1079 let pos = phase * (Self::TABLE_SIZE as f64);
1080 let idx0 = (pos as usize) % Self::TABLE_SIZE;
1081 let idx1 = (idx0 + 1) % Self::TABLE_SIZE;
1082 let frac = pos - Libm::<f64>::floor(pos);
1083
1084 table[idx0] * (1.0 - frac) + table[idx1] * frac
1086 }
1087}
1088
1089impl Default for Wavetable {
1090 fn default() -> Self {
1091 Self::new(44100.0)
1092 }
1093}
1094
1095impl GraphModule for Wavetable {
1096 fn port_spec(&self) -> &PortSpec {
1097 &self.spec
1098 }
1099
1100 fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
1101 let v_oct = inputs.get_or(0, 0.0);
1103 let table_cv = inputs.get_or(1, 0.0).clamp(0.0, 1.0);
1104 let morph = inputs.get_or(2, 0.0).clamp(0.0, 1.0);
1105 let sync = inputs.get_or(3, 0.0);
1106
1107 if sync > GATE_THRESHOLD_V && self.prev_sync <= GATE_THRESHOLD_V {
1109 self.phase = 0.0;
1110 }
1111 self.prev_sync = sync;
1112
1113 let frequency = self.freq_memo.get_or_compute([v_oct], || voct_to_hz(v_oct));
1116 let phase_inc = frequency / self.sample_rate;
1117
1118 let table_pos = table_cv * ((Self::NUM_TABLES - 1) as f64);
1121 let table_idx = (table_pos as usize).min(Self::NUM_TABLES - 2);
1122 let table_frac = table_pos - (table_idx as f64);
1123
1124 let blend = (table_frac + morph).min(1.0);
1126
1127 let level0 = Self::select_level(table_idx, phase_inc);
1130 let level1 = Self::select_level(table_idx + 1, phase_inc);
1131
1132 let sample0 = self.read_table(table_idx, level0, self.phase);
1134 let sample1 = self.read_table(table_idx + 1, level1, self.phase);
1135 let sample = sample0 * (1.0 - blend) + sample1 * blend;
1136
1137 self.phase = wrap_phase(self.phase + phase_inc);
1140
1141 outputs.set(10, sample * 5.0);
1143 }
1144
1145 fn reset(&mut self) {
1146 self.phase = 0.0;
1147 self.prev_sync = 0.0;
1148 }
1149
1150 fn set_sample_rate(&mut self, sample_rate: f64) {
1151 self.sample_rate = sample_rate;
1152 }
1153
1154 fn type_id(&self) -> &'static str {
1155 "wavetable"
1156 }
1157}
1158
1159pub struct FormantOsc {
1171 phase: f64,
1173 vibrato_phase: f64,
1175 resonator_state: [[f64; 2]; 5],
1177 sample_rate: f64,
1178 freq_memo: Memo<1, f64>,
1181 coef_memo: Memo<3, [[f64; 3]; 5]>,
1185 spec: PortSpec,
1186}
1187
1188impl FormantOsc {
1189 const FORMANTS: [[f64; 5]; 5] = [
1192 [700.0, 1220.0, 2600.0, 3500.0, 4500.0],
1194 [530.0, 1840.0, 2480.0, 3500.0, 4500.0],
1196 [280.0, 2250.0, 2890.0, 3500.0, 4500.0],
1198 [500.0, 700.0, 2350.0, 3500.0, 4500.0],
1200 [300.0, 870.0, 2250.0, 3500.0, 4500.0],
1202 ];
1203
1204 const BANDWIDTHS: [f64; 5] = [80.0, 90.0, 120.0, 150.0, 200.0];
1206
1207 const AMPLITUDES: [f64; 5] = [1.0, 0.5, 0.25, 0.1, 0.05];
1209
1210 const VIBRATO_RATE: f64 = 5.5;
1212
1213 pub fn new(sample_rate: f64) -> Self {
1214 let spec = PortSpec {
1215 inputs: vec![
1216 PortDef::new(0, "v_oct", SignalKind::VoltPerOctave).with_default(0.0),
1217 PortDef::new(1, "vowel", SignalKind::CvUnipolar).with_default(0.0),
1218 PortDef::new(2, "formant_shift", SignalKind::CvBipolar).with_default(0.0),
1219 PortDef::new(3, "vibrato", SignalKind::CvUnipolar).with_default(0.0),
1220 ],
1221 outputs: vec![PortDef::new(10, "out", SignalKind::Audio)],
1222 };
1223
1224 Self {
1225 phase: 0.0,
1226 vibrato_phase: 0.0,
1227 resonator_state: [[0.0; 2]; 5],
1228 sample_rate,
1229 freq_memo: Memo::new(0.0),
1230 coef_memo: Memo::new([[0.0; 3]; 5]),
1231 spec,
1232 }
1233 }
1234
1235 fn get_formants(vowel: f64, shift: f64) -> [f64; 5] {
1237 let vowel = vowel.clamp(0.0, 1.0);
1238 let idx = vowel * 4.0;
1239 let idx0 = (idx as usize).min(3);
1240 let idx1 = idx0 + 1;
1241 let frac = idx - (idx0 as f64);
1242
1243 let shift_mult = Libm::<f64>::pow(2.0, shift / 5.0);
1245
1246 let mut result = [0.0; 5];
1247 for (i, value) in result.iter_mut().enumerate() {
1248 let f0 = Self::FORMANTS[idx0][i];
1249 let f1 = Self::FORMANTS[idx1][i];
1250 *value = (f0 * (1.0 - frac) + f1 * frac) * shift_mult;
1251 }
1252 result
1253 }
1254
1255 fn resonator_coefs(vowel: f64, shift: f64, sample_rate: f64) -> [[f64; 3]; 5] {
1266 let formants = Self::get_formants(vowel, shift);
1267 let mut coefs = [[0.0; 3]; 5];
1268 for (i, coef) in coefs.iter_mut().enumerate() {
1269 let freq = formants[i];
1270 let bandwidth = Self::BANDWIDTHS[i];
1271
1272 let omega = 2.0 * core::f64::consts::PI * freq / sample_rate;
1273 let omega = omega.clamp(0.01, core::f64::consts::PI * 0.45);
1274
1275 let q = freq / bandwidth;
1276 let alpha = Libm::<f64>::sin(omega) / (2.0 * q);
1277
1278 let cos_omega = Libm::<f64>::cos(omega);
1280 let b0 = alpha;
1281 let a1 = -2.0 * cos_omega;
1282 let a2 = 1.0 - alpha;
1283 let norm = 1.0 + alpha;
1284
1285 *coef = [b0 / norm, a1 / norm, a2 / norm];
1286 }
1287 coefs
1288 }
1289
1290 fn glottal_pulse(phase: f64) -> f64 {
1292 if phase < 0.4 {
1295 let t = phase / 0.4;
1297 Libm::<f64>::sin(t * core::f64::consts::PI * 0.5)
1298 } else if phase < 0.8 {
1299 let t = (phase - 0.4) / 0.4;
1301 Libm::<f64>::cos(t * core::f64::consts::PI * 0.5)
1302 } else {
1303 0.0
1305 }
1306 }
1307}
1308
1309impl Default for FormantOsc {
1310 fn default() -> Self {
1311 Self::new(44100.0)
1312 }
1313}
1314
1315impl GraphModule for FormantOsc {
1316 fn port_spec(&self) -> &PortSpec {
1317 &self.spec
1318 }
1319
1320 fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
1321 let v_oct = inputs.get_or(0, 0.0);
1323 let vowel = inputs.get_or(1, 0.0).clamp(0.0, 1.0);
1324 let formant_shift = inputs.get_or(2, 0.0);
1325 let vibrato_depth = inputs.get_or(3, 0.0).clamp(0.0, 1.0);
1326
1327 let vibrato = Libm::<f64>::sin(self.vibrato_phase * 2.0 * core::f64::consts::PI);
1329 let vibrato_semitones = vibrato * vibrato_depth * 0.5; let v_oct_with_vibrato = v_oct + vibrato_semitones / 12.0;
1331
1332 let frequency = self
1335 .freq_memo
1336 .get_or_compute([v_oct_with_vibrato], || voct_to_hz(v_oct_with_vibrato));
1337 let phase_inc = frequency / self.sample_rate;
1338
1339 let excitation = Self::glottal_pulse(self.phase);
1341
1342 let sample_rate = self.sample_rate;
1345 let coefs = self
1346 .coef_memo
1347 .get_or_compute([vowel, formant_shift, sample_rate], || {
1348 Self::resonator_coefs(vowel, formant_shift, sample_rate)
1349 });
1350
1351 let mut output = 0.0;
1355 for (i, c) in coefs.iter().enumerate() {
1356 let state = &mut self.resonator_state[i];
1357 let formant_out = c[0] * excitation + state[0];
1358 state[0] = -c[1] * formant_out + state[1];
1359 state[1] = -c[0] * excitation - c[2] * formant_out;
1360 output += formant_out * Self::AMPLITUDES[i];
1361 }
1362
1363 self.phase = wrap_phase(self.phase + phase_inc);
1365 self.vibrato_phase = wrap_phase(self.vibrato_phase + Self::VIBRATO_RATE / self.sample_rate);
1366
1367 outputs.set(10, output.clamp(-1.0, 1.0) * 5.0);
1369 }
1370
1371 fn reset(&mut self) {
1372 self.phase = 0.0;
1373 self.vibrato_phase = 0.0;
1374 self.resonator_state = [[0.0; 2]; 5];
1375 }
1376
1377 fn set_sample_rate(&mut self, sample_rate: f64) {
1378 self.sample_rate = sample_rate;
1379 }
1380
1381 fn type_id(&self) -> &'static str {
1382 "formant_osc"
1383 }
1384}
1385
1386#[cfg(test)]
1387mod tests {
1388 use super::*;
1389 use crate::modules::common::measure_max_output;
1390
1391 #[test]
1395 fn test_vco_nan_pitch_recovery() {
1396 let mut vco = Vco::new(44100.0);
1397 let mut inputs = PortValues::new();
1398 let mut outputs = PortValues::new();
1399
1400 for &bad in &[f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
1401 inputs.set(0, bad);
1402 vco.tick(&inputs, &mut outputs);
1403 }
1404 inputs.set(0, 1100.0);
1411 vco.tick(&inputs, &mut outputs);
1412
1413 inputs.set(0, 0.0);
1415 let mut max_abs: f64 = 0.0;
1416 for _ in 0..4410 {
1417 vco.tick(&inputs, &mut outputs);
1418 let saw = outputs.get(12).unwrap();
1419 assert!(
1420 saw.is_finite(),
1421 "VCO output stayed non-finite after bad pitch input"
1422 );
1423 max_abs = max_abs.max(saw.abs());
1424 }
1425 assert!(
1426 max_abs > 1.0,
1427 "VCO failed to oscillate after bad pitch input (max |saw| = {max_abs})"
1428 );
1429 }
1430
1431 #[test]
1435 fn test_wavetable_extreme_pitch_no_hang() {
1436 let mut wt = Wavetable::new(44100.0);
1437 let mut inputs = PortValues::new();
1438 let mut outputs = PortValues::new();
1439
1440 for &voct in &[1100.0, f64::INFINITY, f64::NAN, -1100.0] {
1441 inputs.set(0, voct);
1442 wt.tick(&inputs, &mut outputs);
1443 }
1444
1445 inputs.set(0, 0.0);
1446 for _ in 0..64 {
1447 wt.tick(&inputs, &mut outputs);
1448 assert!(outputs.get(10).unwrap().is_finite());
1449 }
1450 }
1451
1452 #[test]
1453 fn test_vco_frequency() {
1454 let mut vco = Vco::new(44100.0);
1455 let mut inputs = PortValues::new();
1456 let mut outputs = PortValues::new();
1457
1458 inputs.set(0, 0.0);
1460
1461 let period_samples = (44100.0 / 261.63) as usize;
1463 let mut samples = Vec::new();
1464
1465 for _ in 0..period_samples * 10 {
1466 vco.tick(&inputs, &mut outputs);
1467 samples.push(outputs.get(12).unwrap()); }
1469
1470 let crossings: Vec<_> = samples
1472 .windows(2)
1473 .filter(|w| w[0] <= 0.0 && w[1] > 0.0)
1474 .collect();
1475
1476 assert!(crossings.len() >= 8 && crossings.len() <= 12);
1478 }
1479 #[test]
1480 fn test_lfo_rate() {
1481 let mut lfo = Lfo::new(1000.0); let mut inputs = PortValues::new();
1483 let mut outputs = PortValues::new();
1484
1485 inputs.set(0, 0.5); for _ in 0..1000 {
1489 lfo.tick(&inputs, &mut outputs);
1490 }
1491
1492 let out = outputs.get(10).unwrap();
1494 assert!(out.abs() <= 5.0);
1495 }
1496 #[test]
1497 fn test_noise_generator() {
1498 let mut noise = NoiseGenerator::new();
1499 let inputs = PortValues::new();
1500 let mut outputs = PortValues::new();
1501
1502 noise.tick(&inputs, &mut outputs);
1503
1504 assert!(outputs.get(10).is_some());
1506 assert!(outputs.get(11).is_some());
1507 }
1508 #[test]
1509 fn test_vco_default_reset_sample_rate() {
1510 let mut vco = Vco::default();
1511 assert!(vco.sample_rate == 44100.0);
1512
1513 vco.set_sample_rate(48000.0);
1514 assert!(vco.sample_rate == 48000.0);
1515
1516 let mut inputs = PortValues::new();
1517 let mut outputs = PortValues::new();
1518 inputs.set(0, 0.0);
1519 for _ in 0..100 {
1520 vco.tick(&inputs, &mut outputs);
1521 }
1522
1523 vco.reset();
1524 assert!(vco.phase == 0.0);
1525
1526 assert_eq!(vco.type_id(), "vco");
1527 }
1528 #[test]
1529 fn test_lfo_default_reset_sample_rate() {
1530 let mut lfo = Lfo::default();
1531 assert!(lfo.sample_rate == 44100.0);
1532
1533 lfo.set_sample_rate(48000.0);
1534 assert!(lfo.sample_rate == 48000.0);
1535
1536 let inputs = PortValues::new();
1537 let mut outputs = PortValues::new();
1538 for _ in 0..100 {
1539 lfo.tick(&inputs, &mut outputs);
1540 }
1541
1542 lfo.reset();
1543 assert!(lfo.phase == 0.0);
1544
1545 assert_eq!(lfo.type_id(), "lfo");
1546 }
1547 #[test]
1548 fn test_noise_generator_default_reset_sample_rate() {
1549 let mut noise = NoiseGenerator::default();
1550 noise.reset();
1551 noise.set_sample_rate(48000.0);
1552 assert_eq!(noise.type_id(), "noise");
1553 }
1554 #[test]
1555 fn test_lfo_shapes() {
1556 let mut lfo = Lfo::new(1000.0);
1557 let mut inputs = PortValues::new();
1558 let mut outputs = PortValues::new();
1559
1560 inputs.set(0, 5.0); for _ in 0..1000 {
1564 lfo.tick(&inputs, &mut outputs);
1565 }
1566
1567 assert!(outputs.get(10).is_some()); assert!(outputs.get(11).is_some()); assert!(outputs.get(12).is_some()); assert!(outputs.get(13).is_some()); }
1573 #[test]
1574 fn test_vco_pwm() {
1575 let mut vco = Vco::new(44100.0);
1576 let mut inputs = PortValues::new();
1577 let mut outputs = PortValues::new();
1578
1579 inputs.set(0, 0.0); inputs.set(2, 7.5); for _ in 0..1000 {
1583 vco.tick(&inputs, &mut outputs);
1584 }
1585
1586 assert!(outputs.get(13).is_some());
1588 }
1589 #[test]
1590 fn test_wavetable_type_index() {
1591 assert_eq!(WavetableType::Sine.index(), 0);
1592 assert_eq!(WavetableType::Triangle.index(), 1);
1593 assert_eq!(WavetableType::Saw.index(), 2);
1594 assert_eq!(WavetableType::Square.index(), 3);
1595 assert_eq!(WavetableType::Pulse25.index(), 4);
1596 assert_eq!(WavetableType::Pulse12.index(), 5);
1597 assert_eq!(WavetableType::FormantA.index(), 6);
1598 assert_eq!(WavetableType::FormantO.index(), 7);
1599 }
1600 #[test]
1601 fn test_wavetable_type_from_index() {
1602 assert_eq!(WavetableType::from_index(0), WavetableType::Sine);
1603 assert_eq!(WavetableType::from_index(1), WavetableType::Triangle);
1604 assert_eq!(WavetableType::from_index(7), WavetableType::FormantO);
1605 assert_eq!(WavetableType::from_index(8), WavetableType::Sine); }
1607 #[test]
1608 fn test_wavetable_default_reset_sample_rate() {
1609 let mut wt = Wavetable::default();
1610 assert_eq!(wt.sample_rate, 44100.0);
1611
1612 let inputs = PortValues::new();
1614 let mut outputs = PortValues::new();
1615 for _ in 0..100 {
1616 wt.tick(&inputs, &mut outputs);
1617 }
1618
1619 assert!(wt.phase > 0.0);
1621
1622 wt.reset();
1624 assert_eq!(wt.phase, 0.0);
1625 assert_eq!(wt.prev_sync, 0.0);
1626
1627 wt.set_sample_rate(48000.0);
1629 assert_eq!(wt.sample_rate, 48000.0);
1630
1631 assert_eq!(wt.type_id(), "wavetable");
1632 assert_eq!(wt.port_spec().inputs.len(), 4);
1633 assert_eq!(wt.port_spec().outputs.len(), 1);
1634 }
1635 #[test]
1636 fn test_wavetable_sine_output() {
1637 let mut wt = Wavetable::new(44100.0);
1638 let mut inputs = PortValues::new();
1639 let mut outputs = PortValues::new();
1640
1641 inputs.set(0, 0.0); inputs.set(1, 0.0); let samples_per_cycle = (44100.0 / 261.63) as usize;
1647 let mut max_val = 0.0f64;
1648 let mut min_val = 0.0f64;
1649
1650 for _ in 0..samples_per_cycle {
1651 wt.tick(&inputs, &mut outputs);
1652 let out = outputs.get(10).unwrap();
1653 max_val = max_val.max(out);
1654 min_val = min_val.min(out);
1655 }
1656
1657 assert!(max_val > 4.0, "max should be near 5V: {}", max_val);
1659 assert!(min_val < -4.0, "min should be near -5V: {}", min_val);
1660 }
1661 #[test]
1662 fn test_wavetable_table_selection() {
1663 let mut wt = Wavetable::new(44100.0);
1664 let mut inputs = PortValues::new();
1665 let mut outputs = PortValues::new();
1666
1667 inputs.set(0, 2.0); let mut outputs_by_table = Vec::new();
1671 for table_cv in [0.0, 0.5, 1.0] {
1672 wt.reset();
1673 inputs.set(1, table_cv);
1674 inputs.set(2, 0.0); let mut sum = 0.0;
1677 for _ in 0..100 {
1678 wt.tick(&inputs, &mut outputs);
1679 sum += outputs.get(10).unwrap().abs();
1680 }
1681 outputs_by_table.push(sum);
1682 }
1683
1684 assert!((outputs_by_table[0] - outputs_by_table[1]).abs() > 1.0);
1686 assert!((outputs_by_table[1] - outputs_by_table[2]).abs() > 1.0);
1687 }
1688 #[test]
1689 fn test_wavetable_morph() {
1690 let mut wt = Wavetable::new(44100.0);
1691 let mut inputs = PortValues::new();
1692 let mut outputs = PortValues::new();
1693
1694 inputs.set(0, 1.0);
1695 inputs.set(1, 0.0); wt.reset();
1699 inputs.set(2, 0.0);
1700 let mut sum_no_morph = 0.0;
1701 for _ in 0..100 {
1702 wt.tick(&inputs, &mut outputs);
1703 sum_no_morph += outputs.get(10).unwrap();
1704 }
1705
1706 wt.reset();
1708 inputs.set(2, 1.0);
1709 let mut sum_full_morph = 0.0;
1710 for _ in 0..100 {
1711 wt.tick(&inputs, &mut outputs);
1712 sum_full_morph += outputs.get(10).unwrap();
1713 }
1714
1715 assert!((sum_no_morph - sum_full_morph).abs() > 0.1);
1717 }
1718 #[test]
1719 fn test_wavetable_hard_sync() {
1720 let mut wt = Wavetable::new(44100.0);
1721 let mut inputs = PortValues::new();
1722 let mut outputs = PortValues::new();
1723
1724 inputs.set(0, 0.0);
1725 inputs.set(1, 0.0);
1726
1727 for _ in 0..50 {
1729 wt.tick(&inputs, &mut outputs);
1730 }
1731 let phase_before = wt.phase;
1732 assert!(phase_before > 0.0);
1733
1734 inputs.set(3, 0.0);
1736 wt.tick(&inputs, &mut outputs);
1737 inputs.set(3, 5.0); wt.tick(&inputs, &mut outputs);
1739
1740 assert!(wt.phase < 0.1, "Phase should reset on sync: {}", wt.phase);
1742 }
1743 #[test]
1744 fn test_wavetable_frequency_tracking() {
1745 let mut wt = Wavetable::new(44100.0);
1746
1747 let count_zero_crossings = |wt: &mut Wavetable, v_oct: f64| -> usize {
1750 let mut inputs = PortValues::new();
1751 let mut outputs = PortValues::new();
1752 inputs.set(0, v_oct);
1753 inputs.set(1, 0.0);
1754 wt.reset();
1755
1756 let mut crossings = 0;
1757 let mut prev_out = 0.0;
1758 for _ in 0..1000 {
1759 wt.tick(&inputs, &mut outputs);
1760 let out = outputs.get(10).unwrap();
1761 if prev_out <= 0.0 && out > 0.0 {
1762 crossings += 1;
1763 }
1764 prev_out = out;
1765 }
1766 crossings
1767 };
1768
1769 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;
1774 assert!(
1775 ratio > 1.8 && ratio < 2.2,
1776 "Octave ratio should be ~2: {}",
1777 ratio
1778 );
1779 }
1780 #[test]
1781 fn test_formant_osc_default_reset_sample_rate() {
1782 let mut osc = FormantOsc::default();
1783 assert_eq!(osc.sample_rate, 44100.0);
1784
1785 let inputs = PortValues::new();
1787 let mut outputs = PortValues::new();
1788 for _ in 0..100 {
1789 osc.tick(&inputs, &mut outputs);
1790 }
1791
1792 assert!(osc.phase > 0.0);
1794
1795 osc.reset();
1797 assert_eq!(osc.phase, 0.0);
1798 assert_eq!(osc.vibrato_phase, 0.0);
1799 assert_eq!(osc.resonator_state, [[0.0; 2]; 5]);
1800
1801 osc.set_sample_rate(48000.0);
1803 assert_eq!(osc.sample_rate, 48000.0);
1804
1805 assert_eq!(osc.type_id(), "formant_osc");
1806 assert_eq!(osc.port_spec().inputs.len(), 4);
1807 assert_eq!(osc.port_spec().outputs.len(), 1);
1808 }
1809 #[test]
1810 fn test_formant_osc_output() {
1811 let mut osc = FormantOsc::new(44100.0);
1812 let mut inputs = PortValues::new();
1813 let mut outputs = PortValues::new();
1814
1815 inputs.set(0, 0.0); inputs.set(1, 0.0); let mut max_val = 0.0f64;
1820 let mut min_val = 0.0f64;
1821
1822 for _ in 0..1000 {
1823 osc.tick(&inputs, &mut outputs);
1824 let out = outputs.get(10).unwrap();
1825 max_val = max_val.max(out);
1826 min_val = min_val.min(out);
1827 }
1828
1829 assert!(max_val > 0.0, "Should have positive output: {}", max_val);
1831 assert!(min_val < 0.0 || max_val > 0.0, "Should have some signal");
1832 }
1833 #[test]
1834 fn test_formant_osc_vowel_selection() {
1835 let mut osc = FormantOsc::new(44100.0);
1836 let mut inputs = PortValues::new();
1837 let mut outputs = PortValues::new();
1838
1839 inputs.set(0, 1.0); let mut sums_by_vowel = Vec::new();
1843 for vowel_cv in [0.0, 0.25, 0.5, 0.75, 1.0] {
1844 osc.reset();
1845 inputs.set(1, vowel_cv);
1846
1847 let mut sum = 0.0;
1848 for _ in 0..500 {
1849 osc.tick(&inputs, &mut outputs);
1850 sum += outputs.get(10).unwrap().abs();
1851 }
1852 sums_by_vowel.push(sum);
1853 }
1854
1855 let mut any_different = false;
1858 for i in 0..sums_by_vowel.len() - 1 {
1859 if (sums_by_vowel[i] - sums_by_vowel[i + 1]).abs() > 10.0 {
1860 any_different = true;
1861 break;
1862 }
1863 }
1864 assert!(any_different, "Vowels should produce different timbres");
1865 }
1866 #[test]
1867 fn test_formant_osc_formant_shift() {
1868 let mut osc = FormantOsc::new(44100.0);
1869 let mut inputs = PortValues::new();
1870 let mut outputs = PortValues::new();
1871
1872 inputs.set(0, 0.0);
1873 inputs.set(1, 0.5); osc.reset();
1877 inputs.set(2, 0.0);
1878 let mut sum_no_shift = 0.0;
1879 for _ in 0..500 {
1880 osc.tick(&inputs, &mut outputs);
1881 sum_no_shift += outputs.get(10).unwrap();
1882 }
1883
1884 osc.reset();
1886 inputs.set(2, 2.5);
1887 let mut sum_high_shift = 0.0;
1888 for _ in 0..500 {
1889 osc.tick(&inputs, &mut outputs);
1890 sum_high_shift += outputs.get(10).unwrap();
1891 }
1892
1893 assert!(
1895 (sum_no_shift - sum_high_shift).abs() > 0.1,
1896 "Shift should affect output"
1897 );
1898 }
1899 #[test]
1900 fn test_formant_osc_vibrato() {
1901 let mut osc = FormantOsc::new(44100.0);
1902 let mut inputs = PortValues::new();
1903 let mut outputs = PortValues::new();
1904
1905 inputs.set(0, 0.0);
1906 inputs.set(1, 0.0);
1907
1908 inputs.set(3, 1.0); for _ in 0..1000 {
1912 osc.tick(&inputs, &mut outputs);
1913 }
1914
1915 assert!(osc.vibrato_phase > 0.0);
1917 }
1918 #[test]
1919 fn test_formant_osc_glottal_pulse() {
1920 let opening = FormantOsc::glottal_pulse(0.0);
1922 let peak = FormantOsc::glottal_pulse(0.4);
1923 let closing = FormantOsc::glottal_pulse(0.6);
1924 let closed = FormantOsc::glottal_pulse(0.9);
1925
1926 assert_eq!(opening, 0.0, "Should start at zero");
1927 assert!(peak > 0.9, "Peak should be near 1.0: {}", peak);
1928 assert!(
1929 closing > 0.0 && closing < peak,
1930 "Closing phase should be declining"
1931 );
1932 assert_eq!(closed, 0.0, "Closed phase should be zero");
1933 }
1934 #[test]
1935 fn test_formant_osc_frequency_tracking() {
1936 let mut osc = FormantOsc::new(44100.0);
1937
1938 let count_crossings = |osc: &mut FormantOsc, v_oct: f64| -> usize {
1940 let mut inputs = PortValues::new();
1941 let mut outputs = PortValues::new();
1942 inputs.set(0, v_oct);
1943 osc.reset();
1944
1945 let mut crossings = 0;
1946 let mut prev_phase = 0.0;
1947 for _ in 0..1000 {
1948 osc.tick(&inputs, &mut outputs);
1949 if osc.phase < prev_phase {
1951 crossings += 1;
1952 }
1953 prev_phase = osc.phase;
1954 }
1955 crossings
1956 };
1957
1958 let crossings_c4 = count_crossings(&mut osc, 0.0);
1959 let crossings_c5 = count_crossings(&mut osc, 1.0);
1960
1961 let ratio = crossings_c5 as f64 / crossings_c4 as f64;
1962 assert!(
1963 ratio > 1.7 && ratio < 2.3,
1964 "Octave ratio should be ~2: {}",
1965 ratio
1966 );
1967 }
1968 #[test]
1969 fn test_vco_output_bounded() {
1970 let mut vco = Vco::new(44100.0);
1972 let mut inputs = PortValues::new();
1973 let mut outputs = PortValues::new();
1974
1975 for voct in [-2.0, 0.0, 2.0, 4.0] {
1977 inputs.set(0, voct);
1978
1979 let max = measure_max_output(1000, || {
1980 vco.tick(&inputs, &mut outputs);
1981 let sin = outputs.get(10).unwrap_or(0.0).abs();
1982 let tri = outputs.get(11).unwrap_or(0.0).abs();
1983 let saw = outputs.get(12).unwrap_or(0.0).abs();
1984 let sqr = outputs.get(13).unwrap_or(0.0).abs();
1985 sin.max(tri).max(saw).max(sqr)
1986 });
1987
1988 assert!(
1989 max <= 5.5, "VCO output {} exceeds expected range at voct={}",
1991 max,
1992 voct
1993 );
1994 }
1995 }
1996 #[test]
1997 fn test_lfo_output_bounded() {
1998 let mut lfo = Lfo::new(44100.0);
1999 let mut inputs = PortValues::new();
2000 let mut outputs = PortValues::new();
2001
2002 inputs.set(0, 1.0); let max = measure_max_output(50000, || {
2005 lfo.tick(&inputs, &mut outputs);
2006 outputs.get(10).unwrap_or(0.0).abs()
2007 });
2008
2009 assert!(max <= 5.5, "LFO output {} exceeds expected ±5V range", max);
2010 }
2011 #[test]
2012 fn test_noise_output_bounded() {
2013 let mut noise = NoiseGenerator::new();
2014 let inputs = PortValues::new();
2015 let mut outputs = PortValues::new();
2016
2017 let max = measure_max_output(10000, || {
2018 noise.tick(&inputs, &mut outputs);
2019 let white = outputs.get(10).unwrap_or(0.0).abs();
2020 let pink = outputs.get(11).unwrap_or(0.0).abs();
2021 white.max(pink)
2022 });
2023
2024 assert!(
2025 max <= 5.5,
2026 "Noise output {} exceeds expected ±5V range",
2027 max
2028 );
2029 }
2030
2031 fn dft_mag(sig: &[f64], k: usize) -> f64 {
2037 let n = sig.len();
2038 let mut re = 0.0;
2039 let mut im = 0.0;
2040 for (i, &s) in sig.iter().enumerate() {
2041 let ang = -TAU * (k as f64) * (i as f64) / (n as f64);
2042 re += s * Libm::<f64>::cos(ang);
2043 im += s * Libm::<f64>::sin(ang);
2044 }
2045 Libm::<f64>::sqrt(re * re + im * im) / (n as f64)
2046 }
2047
2048 fn alias_energy(sig: &[f64], fund: usize) -> f64 {
2051 let n = sig.len();
2052 let mut total = 0.0;
2053 for k in 1..(n / 2) {
2054 if k % fund != 0 {
2055 total += dft_mag(sig, k);
2056 }
2057 }
2058 total
2059 }
2060
2061 fn measure_period(seg: &[f64], expected: f64) -> f64 {
2064 let autocorr = |lag: usize| -> f64 {
2065 let mut acc = 0.0;
2066 for i in 0..(seg.len() - lag) {
2067 acc += seg[i] * seg[i + lag];
2068 }
2069 acc
2070 };
2071 let lo = ((expected * 0.6) as usize).max(2);
2072 let hi = ((expected * 1.6) as usize).min(seg.len() / 2);
2073 let mut best_lag = lo;
2074 let mut best = f64::MIN;
2075 for lag in lo..hi {
2076 let a = autocorr(lag);
2077 if a > best {
2078 best = a;
2079 best_lag = lag;
2080 }
2081 }
2082 let y0 = autocorr(best_lag - 1);
2083 let y1 = autocorr(best_lag);
2084 let y2 = autocorr(best_lag + 1);
2085 let denom = y0 - 2.0 * y1 + y2;
2086 let delta = if denom.abs() > 1e-12 {
2087 0.5 * (y0 - y2) / denom
2088 } else {
2089 0.0
2090 };
2091 best_lag as f64 + delta
2092 }
2093
2094 fn vco_capture(voct: f64, port: u32, n: usize) -> Vec<f64> {
2096 let mut vco = Vco::new(44100.0);
2097 let mut inputs = PortValues::new();
2098 let mut outputs = PortValues::new();
2099 inputs.set(0, voct);
2100 let mut out = Vec::with_capacity(n);
2101 for _ in 0..n {
2102 vco.tick(&inputs, &mut outputs);
2103 out.push(outputs.get(port).unwrap());
2104 }
2105 out
2106 }
2107
2108 #[test]
2111 fn test_vco_saw_frequency_preserved() {
2112 let saw = vco_capture(0.0, 12, (44100.0 / 261.63) as usize * 10);
2114 let crossings = saw.windows(2).filter(|w| w[0] <= 0.0 && w[1] > 0.0).count();
2115 assert!(
2116 (8..=12).contains(&crossings),
2117 "expected ~10 zero crossings, got {}",
2118 crossings
2119 );
2120 }
2121
2122 #[test]
2123 fn test_vco_saw_aliasing_reduced() {
2124 let voct = Libm::<f64>::log2(4200.0 / voct_to_hz(0.0));
2126 let n = 441;
2127 let fund = 42;
2128 let dt = voct_to_hz(voct) / 44100.0;
2129 let saw_bl = vco_capture(voct, 12, n);
2130 let mut ph = 0.0;
2132 let mut saw_naive = Vec::with_capacity(n);
2133 for _ in 0..n {
2134 saw_naive.push((2.0 * ph - 1.0) * 5.0);
2135 ph += dt;
2136 ph -= Libm::<f64>::floor(ph);
2137 }
2138 let a_bl = alias_energy(&saw_bl, fund);
2139 let a_naive = alias_energy(&saw_naive, fund);
2140 assert!(
2141 a_bl < 0.3 * a_naive,
2142 "saw alias energy not reduced: bl={} naive={}",
2143 a_bl,
2144 a_naive
2145 );
2146 }
2147
2148 #[test]
2149 fn test_vco_square_aliasing_reduced() {
2150 let voct = Libm::<f64>::log2(4200.0 / voct_to_hz(0.0));
2151 let n = 441;
2152 let fund = 42;
2153 let dt = voct_to_hz(voct) / 44100.0;
2154 let sqr_bl = vco_capture(voct, 13, n);
2155 let mut ph = 0.0;
2156 let mut sqr_naive = Vec::with_capacity(n);
2157 for _ in 0..n {
2158 sqr_naive.push(if ph < 0.5 { 5.0 } else { -5.0 });
2159 ph += dt;
2160 ph -= Libm::<f64>::floor(ph);
2161 }
2162 let a_bl = alias_energy(&sqr_bl, fund);
2163 let a_naive = alias_energy(&sqr_naive, fund);
2164 assert!(
2165 a_bl < 0.3 * a_naive,
2166 "square alias energy not reduced: bl={} naive={}",
2167 a_bl,
2168 a_naive
2169 );
2170 let max_delta = |v: &[f64]| {
2172 v.windows(2)
2173 .map(|w| (w[1] - w[0]).abs())
2174 .fold(0.0, f64::max)
2175 };
2176 assert!(max_delta(&sqr_bl) < max_delta(&sqr_naive));
2177 }
2178
2179 #[test]
2180 fn test_vco_triangle_aliasing_reduced() {
2181 let voct = Libm::<f64>::log2(4200.0 / voct_to_hz(0.0));
2183 let n = 441;
2184 let fund = 42;
2185 let dt = voct_to_hz(voct) / 44100.0;
2186 let tri_bl = vco_capture(voct, 11, n);
2187 let mut ph = 0.0;
2188 let mut tri_naive = Vec::with_capacity(n);
2189 for _ in 0..n {
2190 tri_naive.push((1.0 - 4.0 * Libm::<f64>::fabs(ph - 0.5)) * 5.0);
2191 ph += dt;
2192 ph -= Libm::<f64>::floor(ph);
2193 }
2194 let a_bl = alias_energy(&tri_bl, fund);
2195 let a_naive = alias_energy(&tri_naive, fund);
2196 assert!(
2197 a_bl < 0.5 * a_naive,
2198 "triangle alias energy not reduced: bl={} naive={}",
2199 a_bl,
2200 a_naive
2201 );
2202 }
2203
2204 #[test]
2205 fn test_vco_hard_sync_bounded_and_reduces_step() {
2206 let mut vco = Vco::new(44100.0);
2209 let mut inputs = PortValues::new();
2210 let mut outputs = PortValues::new();
2211 inputs.set(0, 2.0); let master_dt = 110.0 / 44100.0;
2213 let mut mp = 0.0;
2214 let mut out = Vec::new();
2215 let mut max_abs = 0.0f64;
2216 for _ in 0..4000 {
2217 let sync = if mp < 0.5 { 5.0 } else { 0.0 };
2218 inputs.set(3, sync);
2219 vco.tick(&inputs, &mut outputs);
2220 let saw = outputs.get(12).unwrap();
2221 max_abs = max_abs.max(saw.abs());
2222 out.push(saw);
2223 mp += master_dt;
2224 if mp >= 1.0 {
2225 mp -= 1.0;
2226 }
2227 }
2228 assert!(max_abs <= 5.5, "hard-sync saw exceeded ±5V: {}", max_abs);
2229 let rms = (out.iter().map(|x| x * x).sum::<f64>() / out.len() as f64).sqrt();
2231 assert!(rms > 1.0, "hard-sync output too quiet: rms={}", rms);
2232 }
2233
2234 #[test]
2237 fn test_vco_has_fm_lin_port() {
2238 let vco = Vco::new(44100.0);
2239 assert_eq!(vco.port_spec().inputs.len(), 5);
2240 let fm_lin = vco.port_spec().inputs.iter().find(|p| p.name == "fm_lin");
2241 assert!(fm_lin.is_some(), "fm_lin input port missing");
2242 assert_eq!(fm_lin.unwrap().id, 4);
2243 }
2244
2245 #[test]
2246 fn test_vco_fm_lin_zero_is_noop() {
2247 let mut a = Vco::new(44100.0);
2249 let mut b = Vco::new(44100.0);
2250 let mut ia = PortValues::new();
2251 let mut ib = PortValues::new();
2252 let mut oa = PortValues::new();
2253 let mut ob = PortValues::new();
2254 ia.set(0, 1.0);
2255 ib.set(0, 1.0);
2256 ib.set(4, 0.0); for _ in 0..500 {
2258 a.tick(&ia, &mut oa);
2259 b.tick(&ib, &mut ob);
2260 assert_eq!(oa.get(12).unwrap(), ob.get(12).unwrap());
2261 }
2262 }
2263
2264 #[test]
2265 fn test_vco_fm_lin_through_zero_symmetric() {
2266 let mut vco = Vco::new(44100.0);
2269 let mut inputs = PortValues::new();
2270 let mut outputs = PortValues::new();
2271 inputs.set(0, 0.0); let mod_dt = 200.0 / 44100.0; let mut mphase = 0.0;
2274 let mut sum = 0.0;
2275 let mut max_abs = 0.0f64;
2276 let n = 44100;
2277 for _ in 0..n {
2278 let m = Libm::<f64>::sin(mphase * TAU) * 5.0; inputs.set(4, m);
2280 vco.tick(&inputs, &mut outputs);
2281 let sine = outputs.get(10).unwrap();
2282 sum += sine;
2283 max_abs = max_abs.max(sine.abs());
2284 mphase += mod_dt;
2285 if mphase >= 1.0 {
2286 mphase -= 1.0;
2287 }
2288 }
2289 let mean = sum / n as f64;
2290 assert!(
2291 mean.abs() < 0.2,
2292 "FM sidebands not symmetric: mean={}",
2293 mean
2294 );
2295 assert!(max_abs <= 5.5, "FM output exceeded range: {}", max_abs);
2296 }
2297
2298 #[test]
2301 fn test_supersaw_sub_is_octave_down_zero_mean() {
2302 let mut ss = Supersaw::new(44100.0);
2303 let mut inputs = PortValues::new();
2304 let mut outputs = PortValues::new();
2305 inputs.set(0, 0.0); let base_freq = voct_to_hz(0.0);
2307 let n = 44100 * 2;
2308 let mut sub = Vec::with_capacity(n);
2309 for _ in 0..n {
2310 ss.tick(&inputs, &mut outputs);
2311 sub.push(outputs.get(11).unwrap());
2312 }
2313 let cross = |v: &[f64]| v.windows(2).filter(|w| w[0] <= 0.0 && w[1] > 0.0).count();
2317 let sub_rate = cross(&sub) as f64 / (n as f64 / 44100.0);
2318 let expected = base_freq / 2.0;
2319 assert!(
2320 (sub_rate - expected).abs() < 0.05 * expected,
2321 "sub should ring an octave down (~{} Hz), measured {} Hz",
2322 expected,
2323 sub_rate
2324 );
2325 let mean = sub.iter().sum::<f64>() / sub.len() as f64;
2326 assert!(mean.abs() < 0.05, "sub should be zero-mean: mean={}", mean);
2327 }
2328
2329 #[test]
2330 fn test_supersaw_mix_zero_equals_blepped_center() {
2331 let mut ss = Supersaw::new(44100.0);
2334 let mut inputs = PortValues::new();
2335 let mut outputs = PortValues::new();
2336 inputs.set(0, 0.5); inputs.set(2, 0.0); let base_freq = voct_to_hz(0.5);
2339 let dt = base_freq / 44100.0; let mut ph = 3.0 / 7.0; for _ in 0..500 {
2342 ss.tick(&inputs, &mut outputs);
2343 let expected = (2.0 * ph - 1.0) - polyblep(ph, dt);
2344 let got = outputs.get(10).unwrap();
2345 assert!(
2346 (got - expected).abs() < 1e-9,
2347 "mix=0 output {} != blepped center {}",
2348 got,
2349 expected
2350 );
2351 ph += dt;
2352 if ph >= 1.0 {
2353 ph -= 1.0;
2354 }
2355 }
2356 }
2357
2358 #[test]
2361 fn test_ks_excites_once_per_gate() {
2362 let mut ks = KarplusStrong::new(44100.0);
2363 let mut inputs = PortValues::new();
2364 let mut outputs = PortValues::new();
2365 inputs.set(0, 0.0); inputs.set(2, 0.95); inputs.set(3, 0.5); let mut ring = Vec::new();
2371 for i in 0..100 {
2372 inputs.set(1, 5.0);
2373 ks.tick(&inputs, &mut outputs);
2374 if i >= 10 {
2375 ring.push(outputs.get(10).unwrap());
2376 }
2377 }
2378 assert_eq!(
2382 ks.write_pos, 100,
2383 "gate should excite once; write_pos={}",
2384 ks.write_pos
2385 );
2386 let rms = (ring.iter().map(|x| x * x).sum::<f64>() / ring.len() as f64).sqrt();
2388 assert!(rms > 0.05, "string did not ring during gate: rms={}", rms);
2389 }
2390
2391 #[test]
2392 fn test_ks_gate_high_threshold() {
2393 let mut ks = KarplusStrong::new(44100.0);
2395 let mut inputs = PortValues::new();
2396 let mut outputs = PortValues::new();
2397 inputs.set(0, 0.0);
2398 inputs.set(1, 1.0); for _ in 0..200 {
2400 ks.tick(&inputs, &mut outputs);
2401 }
2402 let out = outputs.get(10).unwrap();
2403 assert_eq!(out, 0.0, "sub-threshold trigger should not excite: {}", out);
2404 }
2405
2406 #[test]
2409 fn test_ks_tuning_accuracy() {
2410 for &(voct, target_hz) in &[(-1.0, 130.81), (0.0, 261.63), (1.0, 523.25), (2.0, 1046.5)] {
2411 let mut ks = KarplusStrong::new(44100.0);
2412 let mut inputs = PortValues::new();
2413 let mut outputs = PortValues::new();
2414 inputs.set(0, voct);
2415 inputs.set(2, 0.95); inputs.set(3, 0.5);
2417 inputs.set(1, 5.0);
2419 ks.tick(&inputs, &mut outputs);
2420 inputs.set(1, 0.0);
2421 let mut out = Vec::with_capacity(12000);
2422 for _ in 0..12000 {
2423 ks.tick(&inputs, &mut outputs);
2424 out.push(outputs.get(10).unwrap());
2425 }
2426 let seg = &out[2000..10000];
2427 let expected_period = 44100.0 / target_hz;
2428 let period = measure_period(seg, expected_period);
2429 let measured_hz = 44100.0 / period;
2430 let cents = 1200.0 * Libm::<f64>::log2(measured_hz / target_hz);
2431 assert!(
2432 cents.abs() < 20.0,
2433 "KS pitch off at {} Hz: measured {} Hz ({:+.1} cents)",
2434 target_hz,
2435 measured_hz,
2436 cents
2437 );
2438 }
2439 }
2440
2441 #[test]
2442 fn test_ks_high_then_low_pitch_same_instance() {
2443 let mut ks = KarplusStrong::new(44100.0);
2449 let mut inputs = PortValues::new();
2450 let mut outputs = PortValues::new();
2451 inputs.set(2, 0.95); inputs.set(3, 0.5);
2453
2454 inputs.set(0, 2.0);
2456 inputs.set(1, 5.0);
2457 ks.tick(&inputs, &mut outputs);
2458 inputs.set(1, 0.0);
2459 for _ in 0..4000 {
2460 ks.tick(&inputs, &mut outputs);
2461 }
2462
2463 let target_hz = 65.41;
2465 inputs.set(0, -2.0);
2466 inputs.set(1, 5.0);
2467 ks.tick(&inputs, &mut outputs);
2468 inputs.set(1, 0.0);
2469 let mut out = Vec::with_capacity(12000);
2470 for _ in 0..12000 {
2471 ks.tick(&inputs, &mut outputs);
2472 out.push(outputs.get(10).unwrap());
2473 }
2474 let seg = &out[2000..10000];
2475 let expected_period = 44100.0 / target_hz;
2476 let period = measure_period(seg, expected_period);
2477 let measured_hz = 44100.0 / period;
2478 let cents = 1200.0 * Libm::<f64>::log2(measured_hz / target_hz);
2479 assert!(
2480 cents.abs() < 50.0,
2481 "KS low note after high pluck mistuned: measured {} Hz \
2482 (target {} Hz, {:+.1} cents)",
2483 measured_hz,
2484 target_hz,
2485 cents
2486 );
2487 }
2488
2489 #[test]
2492 fn test_ks_dc_decays() {
2493 let mut ks = KarplusStrong::new(44100.0);
2496 let mut inputs = PortValues::new();
2497 let mut outputs = PortValues::new();
2498 inputs.set(0, 0.0);
2499 inputs.set(2, 0.7);
2500 inputs.set(3, 0.0); inputs.set(1, 5.0);
2502 ks.tick(&inputs, &mut outputs);
2503 inputs.set(1, 0.0);
2504 let mut out = Vec::with_capacity(20000);
2505 for _ in 0..20000 {
2506 ks.tick(&inputs, &mut outputs);
2507 out.push(outputs.get(10).unwrap());
2508 }
2509 let mean_window = |s: &[f64]| s.iter().sum::<f64>() / s.len() as f64;
2510 let late = mean_window(&out[10000..20000]);
2511 assert!(
2512 late.abs() < 0.02,
2513 "KS output retains DC offset: late mean = {}",
2514 late
2515 );
2516 }
2517
2518 #[test]
2521 fn test_wavetable_mip_keeps_harmonics_below_nyquist() {
2522 let fs = 44100.0;
2523 for &freq in &[1000.0, 2000.0, 3000.0, 6000.0] {
2526 let phase_inc = freq / fs;
2527 let level = Wavetable::select_level(2, phase_inc); let harmonics = Wavetable::max_harmonic(2, level);
2529 let top = harmonics as f64 * freq;
2530 assert!(
2531 top < fs / 2.0,
2532 "saw at {} Hz: level {} keeps {} harmonics, top partial {} >= Nyquist",
2533 freq,
2534 level,
2535 harmonics,
2536 top
2537 );
2538 }
2539 }
2540
2541 #[test]
2542 fn test_wavetable_mip_selects_higher_level_for_higher_pitch() {
2543 let fs = 44100.0;
2545 let l_low = Wavetable::select_level(2, 100.0 / fs);
2546 let l_mid = Wavetable::select_level(2, 1000.0 / fs);
2547 let l_high = Wavetable::select_level(2, 5000.0 / fs);
2548 assert!(l_low <= l_mid && l_mid <= l_high);
2549 assert!(l_high > l_low, "expected higher pitch to raise mip level");
2550 }
2551
2552 #[test]
2553 fn test_wavetable_high_pitch_bounded() {
2554 let mut wt = Wavetable::new(44100.0);
2556 let mut inputs = PortValues::new();
2557 let mut outputs = PortValues::new();
2558 inputs.set(0, 3.5); inputs.set(1, 2.0 / 7.0); let mut max_abs = 0.0f64;
2561 let mut sumsq = 0.0;
2562 let n = 4000;
2563 for _ in 0..n {
2564 wt.tick(&inputs, &mut outputs);
2565 let v = outputs.get(10).unwrap();
2566 max_abs = max_abs.max(v.abs());
2567 sumsq += v * v;
2568 }
2569 assert!(
2570 max_abs <= 5.5,
2571 "wavetable high-pitch exceeded range: {}",
2572 max_abs
2573 );
2574 assert!(
2575 (sumsq / n as f64).sqrt() > 0.5,
2576 "wavetable high-pitch silent"
2577 );
2578 }
2579
2580 fn block_rms_ptp(sig: &[f64], block: usize) -> f64 {
2586 let mut lo = f64::INFINITY;
2587 let mut hi = f64::NEG_INFINITY;
2588 for chunk in sig.chunks(block) {
2589 let rms = (chunk.iter().map(|x| x * x).sum::<f64>() / chunk.len() as f64).sqrt();
2590 lo = lo.min(rms);
2591 hi = hi.max(rms);
2592 }
2593 hi - lo
2594 }
2595
2596 #[test]
2597 fn test_supersaw_detune_spread() {
2598 let run = |detune: f64| -> Vec<f64> {
2599 let mut ss = Supersaw::new(44100.0);
2600 let mut inputs = PortValues::new();
2601 let mut outputs = PortValues::new();
2602 inputs.set(0, 0.0); inputs.set(1, detune);
2604 inputs.set(2, 1.0); let mut out = Vec::with_capacity(20_000);
2606 for _ in 0..20_000 {
2607 ss.tick(&inputs, &mut outputs);
2608 out.push(outputs.get(10).unwrap());
2609 }
2610 out
2611 };
2612
2613 let ptp_off = block_rms_ptp(&run(0.0), 500);
2614 let ptp_on = block_rms_ptp(&run(1.0), 500);
2615 assert!(
2618 ptp_off < 0.02,
2619 "no-detune supersaw should not beat: ptp={ptp_off}"
2620 );
2621 assert!(
2623 ptp_on > ptp_off + 0.03,
2624 "detuned supersaw must beat (spread the voices): on={ptp_on} off={ptp_off}"
2625 );
2626 }
2627
2628 #[test]
2629 fn test_supersaw_reset_and_sample_rate() {
2630 let mut ss = Supersaw::default();
2631 assert_eq!(ss.type_id(), "supersaw");
2632 let mut inputs = PortValues::new();
2633 let mut outputs = PortValues::new();
2634 inputs.set(0, 0.0);
2635 for _ in 0..500 {
2636 ss.tick(&inputs, &mut outputs);
2637 }
2638 assert!(ss.sub_phase != 0.0 || ss.phases[3] != 3.0 / 7.0);
2639 ss.reset();
2640 assert_eq!(ss.sub_phase, 0.0);
2641 for (i, &p) in ss.phases.iter().enumerate() {
2642 assert_eq!(p, i as f64 / 7.0);
2643 }
2644 ss.set_sample_rate(48000.0);
2645 assert_eq!(ss.sample_rate, 48000.0);
2646 ss.tick(&inputs, &mut outputs);
2647 assert!(outputs.get(10).unwrap().is_finite());
2648 }
2649
2650 #[test]
2653 fn test_karplus_strong_reset_and_sample_rate() {
2654 let mut ks = KarplusStrong::default();
2655 assert_eq!(ks.type_id(), "karplus_strong");
2656 assert_eq!(ks.sample_rate, 44100.0);
2657 let mut inputs = PortValues::new();
2658 let mut outputs = PortValues::new();
2659 inputs.set(0, 0.0);
2660 inputs.set(1, 5.0); for _ in 0..500 {
2662 ks.tick(&inputs, &mut outputs);
2663 }
2664 assert!(ks.write_pos != 0);
2665 ks.reset();
2666 assert_eq!(ks.write_pos, 0);
2667 assert_eq!(ks.last_output, 0.0);
2668 assert!(ks.buffer.iter().all(|&x| x == 0.0));
2669 ks.set_sample_rate(48000.0);
2671 assert_eq!(ks.sample_rate, 48000.0);
2672 for _ in 0..100 {
2673 ks.tick(&inputs, &mut outputs);
2674 assert!(outputs.get(10).unwrap().is_finite());
2675 }
2676 }
2677
2678 #[test]
2686 fn test_vco_memo_bit_identical() {
2687 let mut memoized = Vco::new(44100.0);
2688 let mut forced = Vco::new(44100.0);
2689 let mut inputs = PortValues::new();
2690 let mut out_m = PortValues::new();
2691 let mut out_f = PortValues::new();
2692
2693 for n in 0..20_000u32 {
2694 let t = n as f64;
2695 inputs.set(0, 0.25);
2696 inputs.set(2, 0.4);
2697 if n >= 10_000 {
2698 inputs.set(1, 2.0 * Libm::<f64>::sin(t * 0.09));
2700 inputs.set(4, 4.0 * Libm::<f64>::sin(t * 0.031));
2701 }
2702
2703 memoized.tick(&inputs, &mut out_m);
2704 forced.freq_memo.invalidate();
2705 forced.tick(&inputs, &mut out_f);
2706
2707 for &id in &[10u32, 11, 12, 13] {
2708 assert_eq!(
2709 out_m.get(id).unwrap().to_bits(),
2710 out_f.get(id).unwrap().to_bits(),
2711 "VCO output {id} diverged at sample {n}"
2712 );
2713 }
2714 }
2715 assert!(memoized.freq_memo.recompute_count() <= 10_001);
2716 assert_eq!(forced.freq_memo.recompute_count(), 20_000);
2717 }
2718
2719 #[test]
2727 fn test_formant_osc_matches_per_sample_reference() {
2728 let sample_rate = 44100.0;
2729 let mut osc = FormantOsc::new(sample_rate);
2730 let mut inputs = PortValues::new();
2731 let mut outputs = PortValues::new();
2732
2733 let mut phase = 0.0f64;
2735 let mut vibrato_phase = 0.0f64;
2736 let mut res_state = [[0.0f64; 2]; 5];
2737
2738 for n in 0..8_000u32 {
2739 let (v_oct, vowel_in, shift, depth_in) = if n < 4_000 {
2740 (0.25, 0.3, 1.0, 0.0)
2741 } else {
2742 (0.25, 0.3, 1.0, 0.8)
2743 };
2744 inputs.set(0, v_oct);
2745 inputs.set(1, vowel_in);
2746 inputs.set(2, shift);
2747 inputs.set(3, depth_in);
2748
2749 osc.tick(&inputs, &mut outputs);
2750 let got = outputs.get(10).unwrap();
2751
2752 let vowel = vowel_in.clamp(0.0, 1.0);
2754 let vibrato_depth: f64 = depth_in.clamp(0.0, 1.0);
2755 let vibrato = Libm::<f64>::sin(vibrato_phase * 2.0 * core::f64::consts::PI);
2756 let vibrato_semitones = vibrato * vibrato_depth * 0.5;
2757 let v_oct_with_vibrato = v_oct + vibrato_semitones / 12.0;
2758 let frequency = voct_to_hz(v_oct_with_vibrato);
2759 let phase_inc = frequency / sample_rate;
2760 let excitation = FormantOsc::glottal_pulse(phase);
2761 let formants = FormantOsc::get_formants(vowel, shift);
2762 let mut output = 0.0;
2763 for (i, &freq) in formants.iter().enumerate() {
2764 let omega = 2.0 * core::f64::consts::PI * freq / sample_rate;
2765 let omega = omega.clamp(0.01, core::f64::consts::PI * 0.45);
2766 let q = freq / FormantOsc::BANDWIDTHS[i];
2767 let alpha = Libm::<f64>::sin(omega) / (2.0 * q);
2768 let cos_omega = Libm::<f64>::cos(omega);
2769 let b0 = alpha;
2770 let a1 = -2.0 * cos_omega;
2771 let a2 = 1.0 - alpha;
2772 let norm = 1.0 + alpha;
2773 let state = &mut res_state[i];
2774 let formant_out = b0 / norm * excitation + state[0];
2775 state[0] = -a1 / norm * formant_out + state[1];
2776 state[1] = -b0 / norm * excitation - a2 / norm * formant_out;
2777 output += formant_out * FormantOsc::AMPLITUDES[i];
2778 }
2779 phase = wrap_phase(phase + phase_inc);
2780 vibrato_phase = wrap_phase(vibrato_phase + FormantOsc::VIBRATO_RATE / sample_rate);
2781 let want = output.clamp(-1.0, 1.0) * 5.0;
2782
2783 assert_eq!(
2784 got.to_bits(),
2785 want.to_bits(),
2786 "FormantOsc diverged from per-sample reference at sample {n}"
2787 );
2788 }
2789 assert!(osc.freq_memo.recompute_count() <= 4_002);
2792 assert_eq!(osc.coef_memo.recompute_count(), 1);
2793 }
2794}