1use super::common::{env_coef, sanitize_audio, GATE_THRESHOLD_V};
4use super::oversample::{Oversample, Oversampler};
5use crate::analog::saturation;
6use crate::port::{GraphModule, PortDef, PortSpec, PortValues, SignalKind};
7use alloc::vec;
8use alloc::vec::Vec;
9use libm::Libm;
10
11fn rem_euclid_f64(x: f64, n: f64) -> f64 {
14 let r = Libm::<f64>::fmod(x, n);
15 if r < 0.0 {
16 r + Libm::<f64>::fabs(n)
17 } else {
18 r
19 }
20}
21
22pub struct Bitcrusher {
26 hold_sample: f64,
27 hold_counter: f64,
28 spec: PortSpec,
29}
30
31impl Bitcrusher {
32 pub fn new() -> Self {
33 Self {
34 hold_sample: 0.0,
35 hold_counter: 0.0,
36 spec: PortSpec {
37 inputs: vec![
38 PortDef::new(0, "in", SignalKind::Audio),
39 PortDef::new(1, "bits", SignalKind::CvUnipolar)
40 .with_default(0.5)
41 .with_attenuverter(),
42 PortDef::new(2, "downsample", SignalKind::CvUnipolar)
43 .with_default(0.0)
44 .with_attenuverter(),
45 ],
46 outputs: vec![PortDef::new(10, "out", SignalKind::Audio)],
47 },
48 }
49 }
50}
51
52impl Default for Bitcrusher {
53 fn default() -> Self {
54 Self::new()
55 }
56}
57
58impl GraphModule for Bitcrusher {
59 fn port_spec(&self) -> &PortSpec {
60 &self.spec
61 }
62
63 fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
64 let input = inputs.get_or(0, 0.0);
65 let bits_cv = inputs.get_or(1, 0.5).clamp(0.0, 1.0);
66 let downsample_cv = inputs.get_or(2, 0.0).clamp(0.0, 1.0);
67
68 let bits = 1.0 + bits_cv * 15.0;
69 let downsample_factor = 1.0 + downsample_cv * 63.0;
70
71 self.hold_counter += 1.0;
76 if self.hold_counter >= downsample_factor {
77 self.hold_counter -= downsample_factor;
78 self.hold_sample = input;
79 }
80
81 let levels = Libm::<f64>::round(Libm::<f64>::pow(2.0, bits)).max(2.0);
86 let steps = levels - 1.0;
87 let normalized = ((self.hold_sample / 5.0 + 1.0) * 0.5).clamp(0.0, 1.0);
88 let quantized = Libm::<f64>::round(normalized * steps) / steps;
89 outputs.set(10, (quantized * 2.0 - 1.0) * 5.0);
90 }
91
92 fn reset(&mut self) {
93 self.hold_sample = 0.0;
94 self.hold_counter = 0.0;
95 }
96
97 fn set_sample_rate(&mut self, _: f64) {}
98
99 fn type_id(&self) -> &'static str {
100 "bitcrusher"
101 }
102}
103
104const DISTORTION_TONE_MIN_HZ: f64 = 500.0;
106const DISTORTION_TONE_MAX_HZ: f64 = 18_000.0;
108
109pub struct Distortion {
123 tone_lp: f64,
125 sample_rate: f64,
126 oversampler: Oversampler,
129 spec: PortSpec,
130}
131
132impl Distortion {
133 pub fn new(sample_rate: f64) -> Self {
134 let sample_rate = if sample_rate > 0.0 {
135 sample_rate
136 } else {
137 44100.0
138 };
139 Self {
140 tone_lp: 0.0,
141 sample_rate,
142 oversampler: Oversampler::new(Oversample::Off),
143 spec: PortSpec {
144 inputs: vec![
145 PortDef::new(0, "in", SignalKind::Audio),
146 PortDef::new(1, "drive", SignalKind::CvUnipolar)
147 .with_default(0.5)
148 .with_attenuverter(),
149 PortDef::new(2, "tone", SignalKind::CvUnipolar)
150 .with_default(0.5)
151 .with_attenuverter(),
152 PortDef::new(3, "mode", SignalKind::CvUnipolar)
153 .with_default(0.0)
154 .with_attenuverter(),
155 PortDef::new(4, "mix", SignalKind::CvUnipolar)
156 .with_default(1.0)
157 .with_attenuverter(),
158 ],
159 outputs: vec![PortDef::new(10, "out", SignalKind::Audio)],
160 },
161 }
162 }
163
164 fn soft_clip(x: f64, drive: f64) -> f64 {
167 let gained = (x / 5.0) * (1.0 + drive * 10.0);
168 Libm::<f64>::tanh(gained) * 5.0
169 }
170
171 fn hard_clip(x: f64, drive: f64) -> f64 {
174 let gained = (x / 5.0) * (1.0 + drive * 10.0);
175 gained.clamp(-1.0, 1.0) * 5.0
176 }
177
178 fn foldback(x: f64, drive: f64) -> f64 {
180 let gained = (x / 5.0) * (1.0 + drive * 5.0);
181 Self::triangle_fold(gained, 1.0) * 5.0
182 }
183
184 fn triangle_fold(x: f64, threshold: f64) -> f64 {
189 let period = 4.0 * threshold;
190 threshold - Libm::<f64>::fabs(rem_euclid_f64(x + threshold, period) - 2.0 * threshold)
191 }
192
193 fn asymmetric(x: f64, drive: f64) -> f64 {
195 let gained = (x / 5.0) * (1.0 + drive * 8.0);
196 let shaped = if gained >= 0.0 {
197 1.0 - Libm::<f64>::exp(-gained)
199 } else {
200 Libm::<f64>::tanh(gained)
202 };
203 shaped * 5.0
204 }
205
206 pub fn set_oversample(&mut self, mode: Oversample) {
214 self.oversampler = Oversampler::new(mode);
215 }
216
217 pub fn oversample_factor(&self) -> usize {
219 self.oversampler.factor()
220 }
221}
222
223impl Default for Distortion {
224 fn default() -> Self {
225 Self::new(44100.0)
226 }
227}
228
229impl GraphModule for Distortion {
230 fn port_spec(&self) -> &PortSpec {
231 &self.spec
232 }
233
234 fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
235 let input = sanitize_audio(inputs.get_or(0, 0.0));
236 let drive = inputs.get_or(1, 0.5).clamp(0.0, 1.0);
237 let tone = inputs.get_or(2, 0.5).clamp(0.0, 1.0);
238 let mode = inputs.get_or(3, 0.0).clamp(0.0, 1.0);
239 let mix = inputs.get_or(4, 1.0).clamp(0.0, 1.0);
240
241 let mode_idx = (mode * 3.99) as u8;
243 let distorted = self.oversampler.process(input, |x| match mode_idx {
247 0 => Self::soft_clip(x, drive),
248 1 => Self::hard_clip(x, drive),
249 2 => Self::foldback(x, drive),
250 _ => Self::asymmetric(x, drive),
251 });
252
253 let cutoff = DISTORTION_TONE_MIN_HZ
258 * Libm::<f64>::pow(DISTORTION_TONE_MAX_HZ / DISTORTION_TONE_MIN_HZ, tone);
259 let alpha =
260 1.0 - Libm::<f64>::exp(-2.0 * core::f64::consts::PI * cutoff / self.sample_rate);
261 self.tone_lp += alpha * (distorted - self.tone_lp);
262 let filtered = self.tone_lp;
263
264 outputs.set(10, input * (1.0 - mix) + filtered * mix);
265 }
266
267 fn reset(&mut self) {
268 self.tone_lp = 0.0;
269 self.oversampler.reset();
270 }
271
272 fn set_sample_rate(&mut self, sample_rate: f64) {
273 if sample_rate > 0.0 {
274 self.sample_rate = sample_rate;
275 }
276 self.tone_lp = 0.0;
277 self.oversampler.reset();
278 }
279
280 fn type_id(&self) -> &'static str {
281 "distortion"
282 }
283
284 crate::impl_introspect!();
286}
287
288pub struct RingModulator {
297 spec: PortSpec,
298}
299
300impl RingModulator {
301 pub fn new() -> Self {
302 Self {
303 spec: PortSpec {
304 inputs: vec![
305 PortDef::new(0, "carrier", SignalKind::Audio),
306 PortDef::new(1, "modulator", SignalKind::Audio),
307 ],
308 outputs: vec![PortDef::new(10, "out", SignalKind::Audio)],
309 },
310 }
311 }
312}
313
314impl Default for RingModulator {
315 fn default() -> Self {
316 Self::new()
317 }
318}
319
320impl GraphModule for RingModulator {
321 fn port_spec(&self) -> &PortSpec {
322 &self.spec
323 }
324
325 fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
326 let carrier = inputs.get_or(0, 0.0);
327 let modulator = inputs.get_or(1, 0.0);
328
329 let out = (carrier * modulator) / 5.0;
332 outputs.set(10, out);
333 }
334
335 fn reset(&mut self) {}
336
337 fn set_sample_rate(&mut self, _: f64) {}
338
339 fn type_id(&self) -> &'static str {
340 "ring_mod"
341 }
342}
343
344pub struct PitchShifter {
366 buffer: [f64; 4800],
368 write_pos: usize,
370 grain_pos: [f64; 2],
372 grain_phase: [f64; 2],
374 sample_rate: f64,
375 spec: PortSpec,
376}
377
378impl PitchShifter {
379 const BUFFER_SIZE: usize = 4800;
381
382 pub fn new(sample_rate: f64) -> Self {
383 let spec = PortSpec {
384 inputs: vec![
385 PortDef::new(0, "in", SignalKind::Audio),
386 PortDef::new(1, "shift", SignalKind::CvBipolar).with_default(0.0),
387 PortDef::new(2, "window", SignalKind::CvUnipolar).with_default(0.5),
388 PortDef::new(3, "mix", SignalKind::CvUnipolar).with_default(1.0),
389 ],
390 outputs: vec![PortDef::new(10, "out", SignalKind::Audio)],
391 };
392
393 Self {
394 buffer: [0.0; Self::BUFFER_SIZE],
395 write_pos: 0,
396 grain_pos: [0.0, 0.5 * Self::BUFFER_SIZE as f64], grain_phase: [0.0, 0.5], sample_rate,
399 spec,
400 }
401 }
402
403 fn hann_window(phase: f64) -> f64 {
405 0.5 * (1.0 - Libm::<f64>::cos(phase * 2.0 * core::f64::consts::PI))
406 }
407
408 fn read_buffer(&self, pos: f64) -> f64 {
410 let pos = rem_euclid_f64(pos, Self::BUFFER_SIZE as f64);
411 let idx0 = pos as usize;
412 let idx1 = (idx0 + 1) % Self::BUFFER_SIZE;
413 let frac = pos - Libm::<f64>::floor(pos);
414
415 self.buffer[idx0] * (1.0 - frac) + self.buffer[idx1] * frac
416 }
417}
418
419impl Default for PitchShifter {
420 fn default() -> Self {
421 Self::new(44100.0)
422 }
423}
424
425impl GraphModule for PitchShifter {
426 fn port_spec(&self) -> &PortSpec {
427 &self.spec
428 }
429
430 fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
431 let input = inputs.get_or(0, 0.0);
432
433 let shift_semitones = (inputs.get_or(1, 0.0) / 5.0) * 24.0;
436 let shift_semitones = shift_semitones.clamp(-24.0, 24.0);
437
438 let window_cv = inputs.get_or(2, 0.5).clamp(0.0, 1.0);
440 let window_ms = 10.0 + window_cv * 90.0;
441 let mut window_samples = (window_ms * self.sample_rate / 1000.0) as usize;
442 window_samples = window_samples.min(Self::BUFFER_SIZE / 2);
443
444 let mix = inputs.get_or(3, 1.0).clamp(0.0, 1.0);
446
447 self.buffer[self.write_pos] = input / 5.0; self.write_pos = (self.write_pos + 1) % Self::BUFFER_SIZE;
450
451 let rate = Libm::<f64>::pow(2.0, shift_semitones / 12.0);
453
454 if rate > 1.0 {
460 let max_lead = Self::BUFFER_SIZE as f64 * 0.4;
461 let window_cap = (max_lead / (rate - 1.0)) as usize;
462 window_samples = window_samples.min(window_cap);
463 }
464 window_samples = window_samples.max(1);
465 let read_margin =
466 (rate - 1.0).max(0.0) * window_samples as f64 + window_samples as f64 * 0.5;
467
468 let phase_inc = 1.0 / window_samples as f64;
469
470 let mut wet_output = 0.0;
472
473 for i in 0..2 {
474 let sample = self.read_buffer(self.grain_pos[i]);
476
477 let window = Self::hann_window(self.grain_phase[i]);
479 wet_output += sample * window;
480
481 self.grain_pos[i] += rate;
485
486 if self.grain_pos[i] >= Self::BUFFER_SIZE as f64 {
488 self.grain_pos[i] -= Self::BUFFER_SIZE as f64;
489 } else if self.grain_pos[i] < 0.0 {
490 self.grain_pos[i] += Self::BUFFER_SIZE as f64;
491 }
492
493 self.grain_phase[i] += phase_inc;
495
496 if self.grain_phase[i] >= 1.0 {
498 self.grain_phase[i] -= 1.0;
499 self.grain_pos[i] = rem_euclid_f64(
503 self.write_pos as f64 - read_margin,
504 Self::BUFFER_SIZE as f64,
505 );
506 }
507 }
508
509 let dry = input / 5.0;
511 let output = dry * (1.0 - mix) + wet_output * mix;
512
513 outputs.set(10, output * 5.0); }
515
516 fn reset(&mut self) {
517 self.buffer = [0.0; Self::BUFFER_SIZE];
518 self.write_pos = 0;
519 self.grain_pos = [0.0, Self::BUFFER_SIZE as f64 * 0.5];
520 self.grain_phase = [0.0, 0.5];
521 }
522
523 fn set_sample_rate(&mut self, sample_rate: f64) {
524 self.sample_rate = sample_rate;
525 self.reset();
526 }
527
528 fn type_id(&self) -> &'static str {
529 "pitch_shifter"
530 }
531}
532
533const MAX_VOCODER_BANDS: usize = 16;
535
536const VOCODER_FREQ_MIN: f64 = 100.0;
538
539const VOCODER_FREQ_MAX: f64 = 8000.0;
541
542const VOCODER_MAX_SVF_COEF: f64 = 0.95;
548
549pub struct Vocoder {
563 analysis_state: [[f64; 2]; MAX_VOCODER_BANDS],
565 synthesis_state: [[f64; 2]; MAX_VOCODER_BANDS],
567 envelopes: [f64; MAX_VOCODER_BANDS],
569
570 band_freqs: [f64; MAX_VOCODER_BANDS],
572
573 sample_rate: f64,
574 spec: PortSpec,
575}
576
577impl Vocoder {
578 pub fn new(sample_rate: f64) -> Self {
580 let mut vocoder = Self {
581 analysis_state: [[0.0; 2]; MAX_VOCODER_BANDS],
582 synthesis_state: [[0.0; 2]; MAX_VOCODER_BANDS],
583 envelopes: [0.0; MAX_VOCODER_BANDS],
584 band_freqs: [0.0; MAX_VOCODER_BANDS],
585 sample_rate,
586 spec: PortSpec {
587 inputs: vec![
588 PortDef::new(0, "carrier", SignalKind::Audio),
589 PortDef::new(1, "modulator", SignalKind::Audio),
590 PortDef::new(2, "bands", SignalKind::CvUnipolar).with_default(1.0),
591 PortDef::new(3, "attack", SignalKind::CvUnipolar).with_default(0.3),
592 PortDef::new(4, "release", SignalKind::CvUnipolar).with_default(0.3),
593 ],
594 outputs: vec![PortDef::new(10, "out", SignalKind::Audio)],
595 },
596 };
597 vocoder.compute_band_freqs();
598 vocoder
599 }
600
601 fn compute_band_freqs(&mut self) {
609 let coef_limit_freq = Libm::<f64>::asin(VOCODER_MAX_SVF_COEF / 2.0) * self.sample_rate
610 / core::f64::consts::PI;
611 let freq_max = VOCODER_FREQ_MAX
612 .min(coef_limit_freq)
613 .max(VOCODER_FREQ_MIN * 2.0);
614
615 let log_min = Libm::<f64>::log2(VOCODER_FREQ_MIN);
616 let log_max = Libm::<f64>::log2(freq_max);
617
618 for i in 0..MAX_VOCODER_BANDS {
619 let t = i as f64 / (MAX_VOCODER_BANDS - 1) as f64;
620 let log_freq = log_min + t * (log_max - log_min);
621 self.band_freqs[i] = Libm::<f64>::exp2(log_freq);
622 }
623 }
624
625 #[inline]
628 fn process_svf_bandpass(
629 state: &mut [f64; 2],
630 input: f64,
631 freq: f64,
632 q: f64,
633 sample_rate: f64,
634 ) -> f64 {
635 let f = 2.0 * Libm::<f64>::sin(core::f64::consts::PI * freq / sample_rate);
637 let f = f.min(0.99); let q_inv = 1.0 / q;
641
642 let low = state[0];
644 let high = input - low - q_inv * state[1];
645 let band = f * high + state[1];
646 let new_low = f * band + low;
647
648 state[0] = new_low;
649 state[1] = band;
650
651 band
652 }
653}
654
655impl Default for Vocoder {
656 fn default() -> Self {
657 Self::new(44100.0)
658 }
659}
660
661impl GraphModule for Vocoder {
662 fn port_spec(&self) -> &PortSpec {
663 &self.spec
664 }
665
666 fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
667 let carrier = sanitize_audio(inputs.get_or(0, 0.0));
668 let modulator = sanitize_audio(inputs.get_or(1, 0.0));
669 let bands_cv = inputs.get_or(2, 1.0).clamp(0.0, 1.0);
670 let attack_cv = inputs.get_or(3, 0.3).clamp(0.0, 1.0);
671 let release_cv = inputs.get_or(4, 0.3).clamp(0.0, 1.0);
672
673 let num_bands = Libm::<f64>::round(4.0 + bands_cv * 12.0) as usize;
675 let num_bands = num_bands.min(MAX_VOCODER_BANDS);
676
677 let attack_time = 0.01 + attack_cv * 0.19;
679 let release_time = 0.01 + release_cv * 0.19;
680 let attack_coef = env_coef(attack_time, self.sample_rate);
681 let release_coef = env_coef(release_time, self.sample_rate);
682
683 let q = 2.0;
685
686 let mut output = 0.0;
687
688 for i in 0..num_bands {
689 let freq = self.band_freqs[i * MAX_VOCODER_BANDS / num_bands];
690
691 let analysis_band = Self::process_svf_bandpass(
693 &mut self.analysis_state[i],
694 modulator,
695 freq,
696 q,
697 self.sample_rate,
698 );
699
700 let rectified = analysis_band.abs();
702 if rectified > self.envelopes[i] {
703 self.envelopes[i] =
704 attack_coef * self.envelopes[i] + (1.0 - attack_coef) * rectified;
705 } else {
706 self.envelopes[i] =
707 release_coef * self.envelopes[i] + (1.0 - release_coef) * rectified;
708 }
709
710 let synthesis_band = Self::process_svf_bandpass(
712 &mut self.synthesis_state[i],
713 carrier,
714 freq,
715 q,
716 self.sample_rate,
717 );
718
719 output += synthesis_band * self.envelopes[i];
721 }
722
723 output /= num_bands as f64;
725
726 outputs.set(10, output * 4.0);
728 }
729
730 fn reset(&mut self) {
731 self.analysis_state = [[0.0; 2]; MAX_VOCODER_BANDS];
732 self.synthesis_state = [[0.0; 2]; MAX_VOCODER_BANDS];
733 self.envelopes = [0.0; MAX_VOCODER_BANDS];
734 }
735
736 fn set_sample_rate(&mut self, sample_rate: f64) {
737 self.sample_rate = sample_rate;
738 self.compute_band_freqs();
739 self.reset();
740 }
741
742 fn type_id(&self) -> &'static str {
743 "vocoder"
744 }
745}
746
747const MAX_GRAINS: usize = 16;
753
754const GRANULAR_BUFFER_SIZE: usize = 96000;
756
757#[derive(Clone, Copy)]
759struct Grain {
760 active: bool,
762 start_pos: usize,
764 phase: f64,
766 size: usize,
768 speed: f64,
770}
771
772impl Default for Grain {
773 fn default() -> Self {
774 Self {
775 active: false,
776 start_pos: 0,
777 phase: 0.0,
778 size: 4410, speed: 1.0,
780 }
781 }
782}
783
784pub struct Granular {
801 buffer: Vec<f64>,
803 write_pos: usize,
805
806 grains: [Grain; MAX_GRAINS],
808
809 spawn_timer: usize,
811
812 rng: crate::rng::Rng,
814
815 norm_smooth: f64,
819
820 sample_rate: f64,
821 spec: PortSpec,
822}
823
824impl Granular {
825 pub fn new(sample_rate: f64) -> Self {
827 Self {
828 buffer: vec![0.0; GRANULAR_BUFFER_SIZE],
829 write_pos: 0,
830 grains: [Grain::default(); MAX_GRAINS],
831 spawn_timer: 0,
832 rng: crate::rng::Rng::from_seed(42),
833 norm_smooth: 1.0,
834 sample_rate,
835 spec: PortSpec {
836 inputs: vec![
837 PortDef::new(0, "in", SignalKind::Audio),
838 PortDef::new(1, "position", SignalKind::CvUnipolar).with_default(0.5),
839 PortDef::new(2, "size", SignalKind::CvUnipolar).with_default(0.3),
840 PortDef::new(3, "density", SignalKind::CvUnipolar).with_default(0.5),
841 PortDef::new(4, "pitch", SignalKind::CvBipolar).with_default(0.0),
842 PortDef::new(5, "spray", SignalKind::CvUnipolar).with_default(0.1),
843 PortDef::new(6, "freeze", SignalKind::Gate).with_default(0.0),
844 ],
845 outputs: vec![PortDef::new(10, "out", SignalKind::Audio)],
846 },
847 }
848 }
849
850 #[inline]
852 fn hann_window(phase: f64) -> f64 {
853 0.5 * (1.0 - Libm::<f64>::cos(2.0 * core::f64::consts::PI * phase))
854 }
855
856 #[inline]
858 pub fn read_buffer(&self, pos: f64) -> f64 {
859 let pos = pos % GRANULAR_BUFFER_SIZE as f64;
860 let index = pos as usize;
861 let frac = pos - index as f64;
862
863 let s0 = self.buffer[index % GRANULAR_BUFFER_SIZE];
864 let s1 = self.buffer[(index + 1) % GRANULAR_BUFFER_SIZE];
865
866 s0 + frac * (s1 - s0)
867 }
868
869 fn spawn_grain(&mut self, position: f64, size: usize, speed: f64, spray: f64) {
871 for grain in &mut self.grains {
873 if !grain.active {
874 let spray_offset = if spray > 0.0 {
876 (self.rng.next_f64() - 0.5) * spray * GRANULAR_BUFFER_SIZE as f64 * 0.5
877 } else {
878 0.0
879 };
880
881 let base_pos = position * GRANULAR_BUFFER_SIZE as f64;
882 let pos = (base_pos + spray_offset) as usize % GRANULAR_BUFFER_SIZE;
883
884 grain.active = true;
885 grain.start_pos = pos;
886 grain.phase = 0.0;
887 grain.size = size.max(100); grain.speed = speed;
889 break;
890 }
891 }
892 }
893}
894
895impl Default for Granular {
896 fn default() -> Self {
897 Self::new(44100.0)
898 }
899}
900
901impl GraphModule for Granular {
902 fn port_spec(&self) -> &PortSpec {
903 &self.spec
904 }
905
906 fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
907 let input = inputs.get_or(0, 0.0);
908 let position = inputs.get_or(1, 0.5).clamp(0.0, 1.0);
909 let size_cv = inputs.get_or(2, 0.3).clamp(0.0, 1.0);
910 let density_cv = inputs.get_or(3, 0.5).clamp(0.0, 1.0);
911 let pitch_cv = inputs.get_or(4, 0.0).clamp(-5.0, 5.0);
912 let spray = inputs.get_or(5, 0.1).clamp(0.0, 1.0);
913 let freeze = inputs.get_or(6, 0.0);
914
915 let grains_per_sec = 1.0 + density_cv * 19.0;
917 let spawn_interval = (self.sample_rate / grains_per_sec) as usize;
918
919 let semitones = (pitch_cv * 4.8).clamp(-24.0, 24.0);
922 let speed = Libm::<f64>::exp2(semitones / 12.0);
923
924 let max_size = (GRANULAR_BUFFER_SIZE as f64 / speed) as usize;
929 let size_samples = (((0.01 + size_cv * 0.49) * self.sample_rate) as usize).min(max_size);
930
931 if freeze <= GATE_THRESHOLD_V {
933 self.buffer[self.write_pos] = input;
934 self.write_pos = (self.write_pos + 1) % GRANULAR_BUFFER_SIZE;
935 }
936
937 if self.spawn_timer == 0 {
939 self.spawn_grain(position, size_samples, speed, spray);
940
941 let jitter = 1.0 + (self.rng.next_f64() - 0.5) * 0.4;
943 self.spawn_timer = ((spawn_interval as f64) * jitter) as usize;
944 } else {
945 self.spawn_timer -= 1;
946 }
947
948 let mut output = 0.0;
950
951 for i in 0..MAX_GRAINS {
952 if self.grains[i].active {
953 let grain = &self.grains[i];
954
955 let read_offset = grain.phase * grain.size as f64 * grain.speed;
957 let read_pos = grain.start_pos as f64 + read_offset;
958
959 let envelope = Self::hann_window(grain.phase);
961
962 let pos = read_pos % GRANULAR_BUFFER_SIZE as f64;
964 let index = pos as usize;
965 let frac = pos - index as f64;
966 let s0 = self.buffer[index % GRANULAR_BUFFER_SIZE];
967 let s1 = self.buffer[(index + 1) % GRANULAR_BUFFER_SIZE];
968 let sample = s0 + frac * (s1 - s0);
969
970 output += sample * envelope;
971
972 let new_phase = self.grains[i].phase + 1.0 / self.grains[i].size as f64;
974 self.grains[i].phase = new_phase;
975
976 if new_phase >= 1.0 {
977 self.grains[i].active = false;
978 }
979 }
980 }
981
982 let grain_seconds = size_samples as f64 / self.sample_rate;
989 let expected_overlap = grains_per_sec * grain_seconds;
990 let target_norm = Libm::<f64>::sqrt(expected_overlap).max(1.0);
992 let smooth = env_coef(0.05, self.sample_rate); self.norm_smooth = smooth * self.norm_smooth + (1.0 - smooth) * target_norm;
994 output /= self.norm_smooth.max(1.0);
995
996 outputs.set(10, output);
997 }
998
999 fn reset(&mut self) {
1000 self.buffer.iter_mut().for_each(|x| *x = 0.0);
1001 self.write_pos = 0;
1002 self.grains = [Grain::default(); MAX_GRAINS];
1003 self.spawn_timer = 0;
1004 self.rng = crate::rng::Rng::from_seed(42);
1005 self.norm_smooth = 1.0;
1006 }
1007
1008 fn set_sample_rate(&mut self, sample_rate: f64) {
1009 self.sample_rate = sample_rate;
1010 self.reset();
1011 }
1012
1013 fn type_id(&self) -> &'static str {
1014 "granular"
1015 }
1016}
1017
1018pub struct Wavefolder {
1026 pub(crate) threshold: f64,
1027 oversampler: Oversampler,
1030 spec: PortSpec,
1031}
1032
1033impl Wavefolder {
1034 pub fn new(threshold: f64) -> Self {
1035 Self {
1036 threshold: threshold.max(0.1),
1037 oversampler: Oversampler::new(Oversample::Off),
1038 spec: PortSpec {
1039 inputs: vec![
1040 PortDef::new(0, "in", SignalKind::Audio),
1041 PortDef::new(1, "threshold", SignalKind::CvUnipolar)
1042 .with_default(threshold)
1043 .with_attenuverter(),
1044 ],
1045 outputs: vec![PortDef::new(10, "out", SignalKind::Audio)],
1046 },
1047 }
1048 }
1049
1050 pub fn set_oversample(&mut self, mode: Oversample) {
1056 self.oversampler = Oversampler::new(mode);
1057 }
1058
1059 pub fn oversample_factor(&self) -> usize {
1061 self.oversampler.factor()
1062 }
1063}
1064
1065impl Default for Wavefolder {
1066 fn default() -> Self {
1067 Self::new(1.0)
1068 }
1069}
1070
1071impl GraphModule for Wavefolder {
1072 fn port_spec(&self) -> &PortSpec {
1073 &self.spec
1074 }
1075
1076 fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
1077 let input = inputs.get_or(0, 0.0);
1078 let threshold = inputs.get_or(1, self.threshold).max(0.1);
1079
1080 let folded = self
1083 .oversampler
1084 .process(input, |x| saturation::fold(x / 5.0, threshold) * 5.0);
1085 outputs.set(10, folded);
1086 }
1087
1088 fn reset(&mut self) {
1089 self.oversampler.reset();
1090 }
1091
1092 fn set_sample_rate(&mut self, _: f64) {}
1093
1094 fn type_id(&self) -> &'static str {
1095 "wavefolder"
1096 }
1097
1098 crate::impl_introspect!();
1100}
1101
1102#[cfg(test)]
1103mod tests {
1104 use super::*;
1105
1106 #[test]
1107 fn test_bitcrusher() {
1108 let mut bc = Bitcrusher::new();
1109 let mut inputs = PortValues::new();
1110 let mut outputs = PortValues::new();
1111
1112 inputs.set(0, 2.5);
1113 inputs.set(1, 0.3); inputs.set(2, 0.5); bc.tick(&inputs, &mut outputs);
1116
1117 let out = outputs.get(10).unwrap();
1118 assert!(out.is_finite());
1119 }
1120 #[test]
1121 fn test_bitcrusher_default() {
1122 let bc = Bitcrusher::default();
1123 assert_eq!(bc.type_id(), "bitcrusher");
1124 }
1125 #[test]
1126 fn test_ring_modulator() {
1127 let mut rm = RingModulator::new();
1128 let mut inputs = PortValues::new();
1129 let mut outputs = PortValues::new();
1130
1131 inputs.set(0, 5.0); inputs.set(1, 5.0); rm.tick(&inputs, &mut outputs);
1135 assert!((outputs.get(10).unwrap() - 5.0).abs() < 0.1);
1136
1137 inputs.set(0, 5.0);
1139 inputs.set(1, -5.0);
1140 rm.tick(&inputs, &mut outputs);
1141 assert!((outputs.get(10).unwrap() - (-5.0)).abs() < 0.1);
1142
1143 inputs.set(0, 5.0);
1145 inputs.set(1, 0.0);
1146 rm.tick(&inputs, &mut outputs);
1147 assert!((outputs.get(10).unwrap()).abs() < 0.01);
1148 }
1149 #[test]
1150 fn test_ring_modulator_default_reset_sample_rate() {
1151 let mut rm = RingModulator::default();
1152 rm.reset();
1153 rm.set_sample_rate(48000.0);
1154 assert_eq!(rm.type_id(), "ring_mod");
1155 }
1156 #[test]
1157 fn test_pitch_shifter_default_reset_sample_rate() {
1158 let mut ps = PitchShifter::default();
1159 assert_eq!(ps.sample_rate, 44100.0);
1160
1161 let mut inputs = PortValues::new();
1163 let mut outputs = PortValues::new();
1164 inputs.set(0, 2.5); for _ in 0..100 {
1166 ps.tick(&inputs, &mut outputs);
1167 }
1168
1169 assert!(ps.write_pos > 0);
1171
1172 ps.reset();
1174 assert_eq!(ps.write_pos, 0);
1175 assert_eq!(ps.grain_phase, [0.0, 0.5]);
1176
1177 ps.set_sample_rate(48000.0);
1179 assert_eq!(ps.sample_rate, 48000.0);
1180
1181 assert_eq!(ps.type_id(), "pitch_shifter");
1182 assert_eq!(ps.port_spec().inputs.len(), 4);
1183 assert_eq!(ps.port_spec().outputs.len(), 1);
1184 }
1185 #[test]
1186 fn test_pitch_shifter_hann_window() {
1187 let start = PitchShifter::hann_window(0.0);
1189 let peak = PitchShifter::hann_window(0.5);
1190 let end = PitchShifter::hann_window(1.0);
1191
1192 assert!(start.abs() < 0.01, "Window should start at 0: {}", start);
1193 assert!(
1194 (peak - 1.0).abs() < 0.01,
1195 "Window should peak at 1: {}",
1196 peak
1197 );
1198 assert!(end.abs() < 0.01, "Window should end at 0: {}", end);
1199 }
1200 #[test]
1201 fn test_pitch_shifter_passthrough() {
1202 let mut ps = PitchShifter::new(44100.0);
1203 let mut inputs = PortValues::new();
1204 let mut outputs = PortValues::new();
1205
1206 inputs.set(1, 0.0); inputs.set(3, 1.0); let mut sum_out = 0.0;
1212 for i in 0..1000 {
1213 let input = Libm::<f64>::sin(i as f64 * 0.1) * 5.0;
1214 inputs.set(0, input);
1215 ps.tick(&inputs, &mut outputs);
1216 sum_out += outputs.get(10).unwrap().abs();
1217 }
1218
1219 assert!(sum_out > 100.0, "Should have output signal: {}", sum_out);
1221 }
1222 #[test]
1223 fn test_pitch_shifter_dry_wet_mix() {
1224 let mut ps = PitchShifter::new(44100.0);
1225 let mut inputs = PortValues::new();
1226 let mut outputs = PortValues::new();
1227
1228 inputs.set(1, 0.0);
1230 inputs.set(3, 0.0); let input_val = 2.5; inputs.set(0, input_val);
1234
1235 ps.tick(&inputs, &mut outputs);
1236 let dry_out = outputs.get(10).unwrap();
1237
1238 assert!(
1240 (dry_out - input_val).abs() < 0.1,
1241 "Dry output should match input: {} vs {}",
1242 dry_out,
1243 input_val
1244 );
1245 }
1246 #[test]
1247 fn test_pitch_shifter_shift_changes_output() {
1248 let mut ps = PitchShifter::new(44100.0);
1249
1250 let collect_output = |ps: &mut PitchShifter, shift_cv: f64| -> f64 {
1252 let mut inputs = PortValues::new();
1253 let mut outputs = PortValues::new();
1254 inputs.set(1, shift_cv);
1255 inputs.set(3, 1.0);
1256 ps.reset();
1257
1258 let mut sum = 0.0;
1259 for i in 0..2000 {
1260 let input = Libm::<f64>::sin(i as f64 * 0.05) * 5.0;
1261 inputs.set(0, input);
1262 ps.tick(&inputs, &mut outputs);
1263 sum += outputs.get(10).unwrap();
1264 }
1265 sum
1266 };
1267
1268 let sum_no_shift = collect_output(&mut ps, 0.0);
1269 let sum_up_octave = collect_output(&mut ps, 2.5); let sum_down_octave = collect_output(&mut ps, -2.5); assert!(
1274 (sum_no_shift - sum_up_octave).abs() > 1.0,
1275 "Up shift should differ"
1276 );
1277 assert!(
1278 (sum_no_shift - sum_down_octave).abs() > 1.0,
1279 "Down shift should differ"
1280 );
1281 }
1282 #[test]
1283 fn test_pitch_shifter_buffer_wraparound() {
1284 let mut ps = PitchShifter::new(44100.0);
1285 let mut inputs = PortValues::new();
1286 let mut outputs = PortValues::new();
1287
1288 inputs.set(0, 2.5);
1289 inputs.set(1, 0.0);
1290 inputs.set(3, 1.0);
1291
1292 for _ in 0..10000 {
1294 ps.tick(&inputs, &mut outputs);
1295 let out = outputs.get(10).unwrap();
1296 assert!(out.is_finite(), "Output should be finite");
1297 }
1298
1299 assert!(ps.write_pos < PitchShifter::BUFFER_SIZE);
1301 }
1302 #[test]
1303 fn test_vocoder_default_reset_sample_rate() {
1304 let mut vocoder = Vocoder::default();
1305 assert_eq!(vocoder.sample_rate, 44100.0);
1306
1307 let mut inputs = PortValues::new();
1309 let mut outputs = PortValues::new();
1310 inputs.set(0, 0.5); inputs.set(1, 0.5); vocoder.tick(&inputs, &mut outputs);
1313
1314 vocoder.reset();
1316 assert_eq!(vocoder.envelopes, [0.0; MAX_VOCODER_BANDS]);
1317
1318 vocoder.set_sample_rate(48000.0);
1320 assert_eq!(vocoder.sample_rate, 48000.0);
1321
1322 assert_eq!(vocoder.type_id(), "vocoder");
1323 assert_eq!(vocoder.port_spec().inputs.len(), 5);
1324 assert_eq!(vocoder.port_spec().outputs.len(), 1);
1325 }
1326 #[test]
1327 fn test_vocoder_band_frequencies() {
1328 let vocoder = Vocoder::new(44100.0);
1329
1330 assert!(vocoder.band_freqs[0] >= VOCODER_FREQ_MIN - 1.0);
1332 assert!(vocoder.band_freqs[MAX_VOCODER_BANDS - 1] <= VOCODER_FREQ_MAX + 1.0);
1333
1334 for i in 1..MAX_VOCODER_BANDS {
1336 assert!(
1337 vocoder.band_freqs[i] > vocoder.band_freqs[i - 1],
1338 "Band frequencies should be ascending"
1339 );
1340 }
1341 }
1342 #[test]
1343 fn test_vocoder_silent_when_no_modulator() {
1344 let mut vocoder = Vocoder::new(44100.0);
1345 let mut inputs = PortValues::new();
1346 let mut outputs = PortValues::new();
1347
1348 inputs.set(0, 0.8);
1350 inputs.set(1, 0.0);
1351
1352 for _ in 0..1000 {
1354 vocoder.tick(&inputs, &mut outputs);
1355 }
1356
1357 let out = outputs.get(10).unwrap();
1358 assert!(
1360 out.abs() < 0.1,
1361 "Output should be near zero without modulator, got {}",
1362 out
1363 );
1364 }
1365 #[test]
1366 fn test_vocoder_output_when_both_active() {
1367 let mut vocoder = Vocoder::new(44100.0);
1368 let mut inputs = PortValues::new();
1369 let mut outputs = PortValues::new();
1370
1371 let mut total_output = 0.0;
1373 for i in 0..2000 {
1374 let phase = i as f64 * 0.05;
1375 inputs.set(0, Libm::<f64>::sin(phase)); inputs.set(1, Libm::<f64>::sin(phase * 0.1)); vocoder.tick(&inputs, &mut outputs);
1378 total_output += outputs.get(10).unwrap().abs();
1379 }
1380
1381 assert!(
1382 total_output > 1.0,
1383 "Should produce output when both signals active, got {}",
1384 total_output
1385 );
1386 }
1387 #[test]
1388 fn test_vocoder_band_count() {
1389 let mut vocoder_few = Vocoder::new(44100.0);
1390 let mut vocoder_many = Vocoder::new(44100.0);
1391 let mut inputs_few = PortValues::new();
1392 let mut inputs_many = PortValues::new();
1393 let mut outputs_few = PortValues::new();
1394 let mut outputs_many = PortValues::new();
1395
1396 inputs_few.set(2, 0.0); inputs_many.set(2, 1.0); let mut total_few = 0.0;
1402 let mut total_many = 0.0;
1403
1404 for i in 0..1000 {
1405 let phase = i as f64 * 0.05;
1406 let carrier = Libm::<f64>::sin(phase);
1407 let modulator = Libm::<f64>::sin(phase * 0.2);
1408
1409 inputs_few.set(0, carrier);
1410 inputs_few.set(1, modulator);
1411 inputs_many.set(0, carrier);
1412 inputs_many.set(1, modulator);
1413
1414 vocoder_few.tick(&inputs_few, &mut outputs_few);
1415 vocoder_many.tick(&inputs_many, &mut outputs_many);
1416
1417 total_few += outputs_few.get(10).unwrap().abs();
1418 total_many += outputs_many.get(10).unwrap().abs();
1419 }
1420
1421 assert!(total_few > 0.5, "Few bands should produce output");
1423 assert!(total_many > 0.5, "Many bands should produce output");
1424 }
1425 #[test]
1426 fn test_vocoder_envelope_attack_release() {
1427 let mut vocoder = Vocoder::new(44100.0);
1428 let mut inputs = PortValues::new();
1429 let mut outputs = PortValues::new();
1430
1431 inputs.set(0, 1.0); inputs.set(1, 1.0); inputs.set(3, 0.0); inputs.set(4, 0.0); for _ in 0..100 {
1439 vocoder.tick(&inputs, &mut outputs);
1440 }
1441 let fast_envelope = vocoder.envelopes[0];
1442
1443 vocoder.reset();
1444 inputs.set(3, 1.0); for _ in 0..100 {
1447 vocoder.tick(&inputs, &mut outputs);
1448 }
1449 let slow_envelope = vocoder.envelopes[0];
1450
1451 assert!(
1453 fast_envelope > slow_envelope,
1454 "Fast attack should build envelope faster"
1455 );
1456 }
1457 #[test]
1458 fn test_granular_default_reset_sample_rate() {
1459 let mut granular = Granular::default();
1460 assert_eq!(granular.sample_rate, 44100.0);
1461
1462 let mut inputs = PortValues::new();
1464 let mut outputs = PortValues::new();
1465 inputs.set(0, 0.5);
1466 granular.tick(&inputs, &mut outputs);
1467
1468 assert_eq!(granular.write_pos, 1);
1470
1471 granular.reset();
1473 assert_eq!(granular.write_pos, 0);
1474 assert!(granular.grains.iter().all(|g| !g.active));
1475
1476 granular.set_sample_rate(48000.0);
1478 assert_eq!(granular.sample_rate, 48000.0);
1479
1480 assert_eq!(granular.type_id(), "granular");
1481 assert_eq!(granular.port_spec().inputs.len(), 7);
1482 assert_eq!(granular.port_spec().outputs.len(), 1);
1483 }
1484 #[test]
1485 fn test_granular_hann_window() {
1486 assert!(Granular::hann_window(0.0).abs() < 0.001);
1488 assert!((Granular::hann_window(0.5) - 1.0).abs() < 0.001);
1489 assert!(Granular::hann_window(1.0).abs() < 0.001);
1490 }
1491 #[test]
1492 fn test_granular_records_to_buffer() {
1493 let mut granular = Granular::new(44100.0);
1494 let mut inputs = PortValues::new();
1495 let mut outputs = PortValues::new();
1496
1497 for i in 0..100 {
1499 inputs.set(0, i as f64 * 0.01);
1500 granular.tick(&inputs, &mut outputs);
1501 }
1502
1503 assert!((granular.buffer[50] - 0.5).abs() < 0.01);
1505 }
1506 #[test]
1507 fn test_granular_freeze_stops_recording() {
1508 let mut granular = Granular::new(44100.0);
1509 let mut inputs = PortValues::new();
1510 let mut outputs = PortValues::new();
1511
1512 inputs.set(0, 1.0);
1514 for _ in 0..100 {
1515 granular.tick(&inputs, &mut outputs);
1516 }
1517 let pos_before = granular.write_pos;
1518
1519 inputs.set(6, 5.0); for _ in 0..100 {
1524 granular.tick(&inputs, &mut outputs);
1525 }
1526
1527 assert_eq!(granular.write_pos, pos_before);
1528 }
1529 #[test]
1530 fn test_granular_produces_output() {
1531 let mut granular = Granular::new(44100.0);
1532 let mut inputs = PortValues::new();
1533 let mut outputs = PortValues::new();
1534
1535 inputs.set(1, 0.05); for i in 0..10000 {
1540 let phase = i as f64 * 0.01;
1541 inputs.set(0, Libm::<f64>::sin(phase));
1542 granular.tick(&inputs, &mut outputs);
1543 }
1544
1545 let mut total_output = 0.0;
1547 for _ in 0..5000 {
1548 inputs.set(0, 0.0);
1549 granular.tick(&inputs, &mut outputs);
1550 total_output += outputs.get(10).unwrap().abs();
1551 }
1552
1553 assert!(
1554 total_output > 1.0,
1555 "Granular should produce output, got {}",
1556 total_output
1557 );
1558 }
1559 #[test]
1560 fn test_granular_density_affects_grain_count() {
1561 let mut granular_low = Granular::new(44100.0);
1562 let mut granular_high = Granular::new(44100.0);
1563 let mut inputs_low = PortValues::new();
1564 let mut inputs_high = PortValues::new();
1565 let mut outputs = PortValues::new();
1566
1567 inputs_low.set(3, 0.0); inputs_high.set(3, 1.0); for i in 0..5000 {
1572 let sample = Libm::<f64>::sin(i as f64 * 0.05);
1573 inputs_low.set(0, sample);
1574 inputs_high.set(0, sample);
1575 granular_low.tick(&inputs_low, &mut outputs);
1576 granular_high.tick(&inputs_high, &mut outputs);
1577 }
1578
1579 let active_low = granular_low.grains.iter().filter(|g| g.active).count();
1581 let active_high = granular_high.grains.iter().filter(|g| g.active).count();
1582
1583 assert!(
1586 active_high >= active_low || (active_low == 0 && active_high == 0),
1587 "Higher density should produce more concurrent grains"
1588 );
1589 }
1590 #[test]
1591 fn test_granular_buffer_interpolation() {
1592 let granular = Granular::new(44100.0);
1593
1594 let mut granular = granular;
1596 granular.buffer[0] = 0.0;
1597 granular.buffer[1] = 1.0;
1598
1599 let val = granular.read_buffer(0.5);
1601 assert!(
1602 (val - 0.5).abs() < 0.01,
1603 "Interpolation should give 0.5, got {}",
1604 val
1605 );
1606 }
1607 #[test]
1608 fn test_grain_default() {
1609 let grain = Grain::default();
1610 assert!(!grain.active);
1611 assert_eq!(grain.phase, 0.0);
1612 assert_eq!(grain.speed, 1.0);
1613 }
1614
1615 #[test]
1623 fn test_distortion_tone_is_real_filter() {
1624 let sr = 44100.0;
1625 let rms = |freq: f64, tone: f64| -> f64 {
1628 let mut d = Distortion::new(sr);
1629 let mut inputs = PortValues::new();
1630 let mut outputs = PortValues::new();
1631 inputs.set(1, 0.0); inputs.set(2, tone); inputs.set(3, 0.0); inputs.set(4, 1.0); let n = 8000usize;
1636 let mut sumsq = 0.0;
1637 for i in 0..n {
1638 let x = Libm::<f64>::sin(2.0 * core::f64::consts::PI * freq * i as f64 / sr);
1639 inputs.set(0, x); d.tick(&inputs, &mut outputs);
1641 let out = outputs.get(10).unwrap();
1642 if i >= n / 2 {
1643 sumsq += out * out;
1644 }
1645 }
1646 Libm::<f64>::sqrt(sumsq / (n / 2) as f64)
1647 };
1648
1649 let input_rms = 1.0 / Libm::<f64>::sqrt(2.0); let high_at_min = rms(5000.0, 0.0);
1653 let low_at_min = rms(200.0, 0.0);
1654 assert!(
1655 high_at_min < 0.5 * low_at_min,
1656 "tone min should attenuate highs more than lows: high={high_at_min} low={low_at_min}"
1657 );
1658
1659 let high_at_max = rms(5000.0, 1.0);
1661 assert!(
1662 high_at_max > 0.8 * input_rms,
1663 "tone max should be ~transparent: out_rms={high_at_max} in_rms={input_rms}"
1664 );
1665 assert!(
1666 high_at_max > 3.0 * high_at_min,
1667 "tone max should pass highs that tone min blocks: max={high_at_max} min={high_at_min}"
1668 );
1669 }
1670
1671 #[test]
1674 fn test_distortion_all_algorithms_bounded() {
1675 for drive in [0.0, 0.5, 1.0] {
1677 let mut x = -12.0;
1678 while x <= 12.0 {
1679 for out in [
1680 Distortion::soft_clip(x, drive),
1681 Distortion::hard_clip(x, drive),
1682 Distortion::foldback(x, drive),
1683 Distortion::asymmetric(x, drive),
1684 ] {
1685 assert!(
1686 out.is_finite() && out.abs() <= 5.05,
1687 "shaper out {out} exceeds ±5.05 at x={x} drive={drive}"
1688 );
1689 }
1690 x += 0.05;
1691 }
1692 }
1693
1694 for mode_cv in [0.0f64, 0.34, 0.67, 1.0] {
1696 for &v in &[5.0f64, -5.0] {
1697 let mut d = Distortion::new(44100.0);
1698 let mut inputs = PortValues::new();
1699 let mut outputs = PortValues::new();
1700 inputs.set(1, 1.0); inputs.set(2, 1.0); inputs.set(3, mode_cv);
1703 inputs.set(4, 1.0); inputs.set(0, v);
1705 let mut out = 0.0;
1706 for _ in 0..500 {
1707 d.tick(&inputs, &mut outputs);
1708 out = outputs.get(10).unwrap();
1709 }
1710 assert!(
1711 out.abs() <= 5.05,
1712 "mode {mode_cv} at {v}V max drive should stay ≤5.05V, got {out}"
1713 );
1714 }
1715 }
1716 }
1717
1718 #[test]
1721 fn test_distortion_unity_at_low_drive() {
1722 assert!((Distortion::hard_clip(0.5, 0.0) - 0.5).abs() < 1e-9);
1724 let out = Distortion::soft_clip(1.0, 0.0);
1726 assert!((out - 1.0).abs() < 0.05, "soft_clip near unity, got {out}");
1727 }
1728
1729 #[test]
1732 fn test_triangle_fold_matches_reference_loop() {
1733 fn reference(gained: f64, threshold: f64) -> f64 {
1734 let mut folded = gained;
1735 while folded > threshold || folded < -threshold {
1736 if folded > threshold {
1737 folded = 2.0 * threshold - folded;
1738 } else if folded < -threshold {
1739 folded = -2.0 * threshold - folded;
1740 }
1741 }
1742 folded
1743 }
1744 let threshold = 1.0;
1745 let mut x = -1000.0;
1746 while x <= 1000.0 {
1747 let a = Distortion::triangle_fold(x, threshold);
1748 let b = reference(x, threshold);
1749 assert!((a - b).abs() < 1e-6, "fold mismatch at {x}: {a} vs {b}");
1750 x += 0.05;
1751 }
1752 for &x in &[1000.0, -1000.0, 5.0, -5.0, 3.0, -3.0, 1.0, -1.0, 0.0] {
1753 let a = Distortion::triangle_fold(x, threshold);
1754 let b = reference(x, threshold);
1755 assert!(
1756 (a - b).abs() < 1e-6,
1757 "fold mismatch at extreme {x}: {a} vs {b}"
1758 );
1759 }
1760 }
1761
1762 #[test]
1765 fn test_vocoder_band_coefficients_strictly_increasing() {
1766 for &sr in &[44100.0, 22050.0, 32000.0] {
1767 let v = Vocoder::new(sr);
1768 let mut prev = -1.0;
1769 for i in 0..MAX_VOCODER_BANDS {
1770 let coef = (2.0 * Libm::<f64>::sin(core::f64::consts::PI * v.band_freqs[i] / sr))
1771 .min(0.99);
1772 assert!(
1773 coef > prev + 1e-9,
1774 "band {i} coef {coef} not strictly greater than {prev} at sr {sr}"
1775 );
1776 prev = coef;
1777 }
1778 }
1779 }
1780
1781 #[test]
1784 fn test_granular_no_amplitude_zipper() {
1785 let mut g = Granular::new(44100.0);
1786 let mut inputs = PortValues::new();
1787 let mut outputs = PortValues::new();
1788 inputs.set(0, 1.0); inputs.set(1, 0.5); inputs.set(2, 0.3); inputs.set(3, 1.0); inputs.set(5, 0.0); for _ in 0..(GRANULAR_BUFFER_SIZE + 20000) {
1796 g.tick(&inputs, &mut outputs);
1797 }
1798
1799 let mut prev = outputs.get(10).unwrap();
1800 let mut max_delta = 0.0f64;
1801 for _ in 0..30000 {
1802 g.tick(&inputs, &mut outputs);
1803 let out = outputs.get(10).unwrap();
1804 max_delta = max_delta.max((out - prev).abs());
1805 prev = out;
1806 }
1807 assert!(
1808 max_delta < 0.05,
1809 "granular output should have no zipper jumps, max delta {max_delta}"
1810 );
1811 }
1812
1813 #[test]
1816 fn test_bitcrusher_fractional_downsample_period() {
1817 let mut bc = Bitcrusher::new();
1818 let mut inputs = PortValues::new();
1819 let mut outputs = PortValues::new();
1820 inputs.set(2, 0.5 / 63.0);
1822 inputs.set(1, 1.0); let n = 3000usize;
1825 let mut transitions = 0usize;
1826 let mut prev = f64::NAN;
1827 for i in 0..n {
1828 inputs.set(0, i as f64 * 0.001); bc.tick(&inputs, &mut outputs);
1830 let out = outputs.get(10).unwrap();
1831 if i > 0 && (out - prev).abs() > 1e-9 {
1832 transitions += 1;
1833 }
1834 prev = out;
1835 }
1836 let avg_period = n as f64 / transitions as f64;
1837 assert!(
1838 (avg_period - 1.5).abs() < 0.1,
1839 "fractional downsample average period should be ~1.5, got {avg_period}"
1840 );
1841 }
1842
1843 #[test]
1846 fn test_bitcrusher_no_dc_bias() {
1847 let mut bc = Bitcrusher::new();
1848 let mut inputs = PortValues::new();
1849 let mut outputs = PortValues::new();
1850 inputs.set(1, 2.0 / 15.0); inputs.set(2, 0.0); let n = 20000usize;
1853 let mut sum = 0.0;
1854 for i in 0..n {
1855 let v = Libm::<f64>::sin(i as f64 * 0.01) * 4.0; inputs.set(0, v);
1857 bc.tick(&inputs, &mut outputs);
1858 sum += outputs.get(10).unwrap();
1859 }
1860 let mean = sum / n as f64;
1861 assert!(
1862 mean.abs() < 0.1,
1863 "quantizer DC bias should be ~0, got {mean}"
1864 );
1865 }
1866
1867 #[test]
1868 fn test_bitcrusher_full_scale_maps_in_range() {
1869 let mut bc = Bitcrusher::new();
1870 let mut inputs = PortValues::new();
1871 let mut outputs = PortValues::new();
1872 inputs.set(1, 0.3);
1873 inputs.set(2, 0.0); for &(v, expected) in &[(5.0, 5.0), (-5.0, -5.0)] {
1875 inputs.set(0, v);
1876 bc.tick(&inputs, &mut outputs);
1877 let out = outputs.get(10).unwrap();
1878 assert!(
1879 out.abs() <= 5.0 + 1e-9 && (out - expected).abs() < 1e-9,
1880 "full-scale {v}V should map to {expected}V in range, got {out}"
1881 );
1882 }
1883 }
1884
1885 #[test]
1888 fn test_granular_pitch_clamped_and_bounded() {
1889 let mut g = Granular::new(44100.0);
1890 let mut inputs = PortValues::new();
1891 let mut outputs = PortValues::new();
1892 inputs.set(1, 0.1); inputs.set(2, 1.0); inputs.set(4, 5.0); inputs.set(5, 0.0); inputs.set(0, 1.0);
1897 g.tick(&inputs, &mut outputs); let grain = g
1900 .grains
1901 .iter()
1902 .find(|gr| gr.active)
1903 .expect("a grain should be active after the first tick");
1904 assert!(
1905 (grain.speed - 4.0).abs() < 1e-6,
1906 "pitch +5V should be +24 st (speed 4), got speed {}",
1907 grain.speed
1908 );
1909 assert!(
1910 grain.size as f64 * grain.speed <= GRANULAR_BUFFER_SIZE as f64,
1911 "grain read span {} must not exceed buffer {}",
1912 grain.size as f64 * grain.speed,
1913 GRANULAR_BUFFER_SIZE
1914 );
1915
1916 for &pitch in &[5.0f64, -5.0] {
1918 let mut g = Granular::new(44100.0);
1919 inputs.set(4, pitch);
1920 let mut total = 0.0;
1921 let mut max_abs = 0.0f64;
1922 for i in 0..20000 {
1923 inputs.set(0, Libm::<f64>::sin(i as f64 * 0.05) * 5.0);
1924 g.tick(&inputs, &mut outputs);
1925 let out = outputs.get(10).unwrap();
1926 assert!(out.is_finite(), "granular output must be finite");
1927 max_abs = max_abs.max(out.abs());
1928 total += out.abs();
1929 }
1930 assert!(max_abs < 50.0, "output should stay bounded, got {max_abs}");
1931 assert!(total > 1.0, "output should be non-silent, got {total}");
1932 }
1933 }
1934
1935 #[test]
1938 fn test_pitch_shifter_max_pitch_up_bounded() {
1939 let mut ps = PitchShifter::new(44100.0);
1940 let mut inputs = PortValues::new();
1941 let mut outputs = PortValues::new();
1942 inputs.set(1, 5.0); inputs.set(3, 1.0); let mut total = 0.0;
1946 let mut max_abs = 0.0f64;
1947 for i in 0..10000 {
1948 inputs.set(0, Libm::<f64>::sin(i as f64 * 0.1) * 5.0);
1949 ps.tick(&inputs, &mut outputs);
1950 let out = outputs.get(10).unwrap();
1951 assert!(out.is_finite(), "pitch-up output must be finite");
1952 max_abs = max_abs.max(out.abs());
1953 total += out.abs();
1954 }
1955 assert!(
1956 max_abs <= 5.5,
1957 "wet output should stay near ±5V (COLA), got {max_abs}"
1958 );
1959 assert!(
1960 total > 10.0,
1961 "pitch-up output should be non-silent, got {total}"
1962 );
1963 }
1964
1965 fn dft_mag(sig: &[f64], k: usize) -> f64 {
1971 let n = sig.len();
1972 let mut re = 0.0;
1973 let mut im = 0.0;
1974 for (i, &s) in sig.iter().enumerate() {
1975 let ang = -core::f64::consts::TAU * (k as f64) * (i as f64) / (n as f64);
1976 re += s * Libm::<f64>::cos(ang);
1977 im += s * Libm::<f64>::sin(ang);
1978 }
1979 Libm::<f64>::sqrt(re * re + im * im) / (n as f64)
1980 }
1981
1982 fn alias_energy(sig: &[f64], fund: usize) -> f64 {
1985 let n = sig.len();
1986 let mut total = 0.0;
1987 for k in 1..(n / 2) {
1988 if k % fund != 0 {
1989 total += dft_mag(sig, k);
1990 }
1991 }
1992 total
1993 }
1994
1995 fn distortion_hardclip_capture(mode: Oversample, n: usize) -> Vec<f64> {
1998 let sr = 44100.0;
1999 let mut d = Distortion::new(sr);
2000 d.set_oversample(mode);
2001 let mut inputs = PortValues::new();
2002 let mut outputs = PortValues::new();
2003 inputs.set(1, 1.0); inputs.set(2, 1.0); inputs.set(3, 0.4); inputs.set(4, 1.0); let freq = 4200.0;
2010 let mut out = Vec::with_capacity(n);
2011 for i in 0..(n * 3) {
2013 let t = i as f64 / sr;
2014 let x = Libm::<f64>::sin(core::f64::consts::TAU * freq * t) * 5.0;
2015 inputs.set(0, x);
2016 d.tick(&inputs, &mut outputs);
2017 if i >= n * 2 {
2018 out.push(outputs.get(10).unwrap());
2019 }
2020 }
2021 out
2022 }
2023
2024 #[test]
2025 fn test_distortion_oversampling_reduces_aliasing() {
2026 let n = 441;
2027 let fund = 42;
2028 let off = distortion_hardclip_capture(Oversample::Off, n);
2029 let x4 = distortion_hardclip_capture(Oversample::X4, n);
2030
2031 let a_off = alias_energy(&off, fund);
2032 let a_x4 = alias_energy(&x4, fund);
2033
2034 assert!(
2035 a_x4 < 0.7 * a_off,
2036 "4x oversampling should materially reduce alias energy: off={a_off} x4={a_x4}"
2037 );
2038 }
2039
2040 #[test]
2041 fn test_distortion_oversample_off_is_default_and_transparent() {
2042 let sr = 44100.0;
2044 let mut a = Distortion::new(sr);
2045 let mut b = Distortion::new(sr);
2046 b.set_oversample(Oversample::Off);
2047 let mut ia = PortValues::new();
2048 let mut oa = PortValues::new();
2049 let mut ib = PortValues::new();
2050 let mut ob = PortValues::new();
2051 for i in 0..500 {
2052 let x = Libm::<f64>::sin(i as f64 * 0.3) * 5.0;
2053 ia.set(0, x);
2054 ib.set(0, x);
2055 a.tick(&ia, &mut oa);
2056 b.tick(&ib, &mut ob);
2057 assert!((oa.get(10).unwrap() - ob.get(10).unwrap()).abs() < 1e-12);
2058 }
2059 }
2060
2061 #[test]
2062 fn test_wavefolder_oversampling_reduces_aliasing() {
2063 let sr = 44100.0;
2064 let n = 441;
2065 let fund = 42;
2066 let freq = 4200.0;
2067
2068 let capture = |mode: Oversample| -> Vec<f64> {
2069 let mut wf = Wavefolder::new(0.3);
2070 wf.set_oversample(mode);
2071 let mut inputs = PortValues::new();
2072 let mut outputs = PortValues::new();
2073 let mut out = Vec::with_capacity(n);
2074 for i in 0..(n * 3) {
2075 let t = i as f64 / sr;
2076 inputs.set(0, Libm::<f64>::sin(core::f64::consts::TAU * freq * t) * 5.0);
2077 wf.tick(&inputs, &mut outputs);
2078 if i >= n * 2 {
2079 out.push(outputs.get(10).unwrap());
2080 }
2081 }
2082 out
2083 };
2084
2085 let a_off = alias_energy(&capture(Oversample::Off), fund);
2086 let a_x4 = alias_energy(&capture(Oversample::X4), fund);
2087 assert!(
2088 a_x4 < 0.7 * a_off,
2089 "wavefolder 4x oversampling should reduce alias energy: off={a_off} x4={a_x4}"
2090 );
2091 }
2092
2093 #[test]
2096 fn test_distortion_reset_and_sample_rate() {
2097 let mut dist = Distortion::default();
2098 assert_eq!(dist.type_id(), "distortion");
2099 assert_eq!(dist.sample_rate, 44100.0);
2100 let mut inputs = PortValues::new();
2101 let mut outputs = PortValues::new();
2102 inputs.set(0, 3.0);
2103 inputs.set(1, 0.8); inputs.set(2, 0.2); for _ in 0..500 {
2106 dist.tick(&inputs, &mut outputs);
2107 }
2108 assert!(dist.tone_lp != 0.0, "tone low-pass should hold state");
2109 dist.reset();
2110 assert_eq!(dist.tone_lp, 0.0);
2111 dist.set_sample_rate(48000.0);
2112 assert_eq!(dist.sample_rate, 48000.0);
2113 for _ in 0..100 {
2114 dist.tick(&inputs, &mut outputs);
2115 assert!(outputs.get(10).unwrap().is_finite());
2116 }
2117 }
2118}