1use super::common::{env_coef, sanitize_audio, Memo, 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 levels_memo: Memo<1, f64>,
31 spec: PortSpec,
32}
33
34impl Bitcrusher {
35 pub fn new() -> Self {
36 Self {
37 hold_sample: 0.0,
38 hold_counter: 0.0,
39 levels_memo: Memo::new(0.0),
40 spec: PortSpec {
41 inputs: vec![
42 PortDef::new(0, "in", SignalKind::Audio),
43 PortDef::new(1, "bits", SignalKind::CvUnipolar)
44 .with_default(0.5)
45 .with_attenuverter(),
46 PortDef::new(2, "downsample", SignalKind::CvUnipolar)
47 .with_default(0.0)
48 .with_attenuverter(),
49 ],
50 outputs: vec![PortDef::new(10, "out", SignalKind::Audio)],
51 },
52 }
53 }
54}
55
56impl Default for Bitcrusher {
57 fn default() -> Self {
58 Self::new()
59 }
60}
61
62impl GraphModule for Bitcrusher {
63 fn port_spec(&self) -> &PortSpec {
64 &self.spec
65 }
66
67 fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
68 let input = inputs.get_or(0, 0.0);
69 let bits_cv = inputs.get_or(1, 0.5).clamp(0.0, 1.0);
70 let downsample_cv = inputs.get_or(2, 0.0).clamp(0.0, 1.0);
71
72 let downsample_factor = 1.0 + downsample_cv * 63.0;
73
74 self.hold_counter += 1.0;
79 if self.hold_counter >= downsample_factor {
80 self.hold_counter -= downsample_factor;
81 self.hold_sample = input;
82 }
83
84 let levels = self.levels_memo.get_or_compute([bits_cv], || {
90 let bits = 1.0 + bits_cv * 15.0;
91 Libm::<f64>::round(Libm::<f64>::pow(2.0, bits)).max(2.0)
92 });
93 let steps = levels - 1.0;
94 let normalized = ((self.hold_sample / 5.0 + 1.0) * 0.5).clamp(0.0, 1.0);
95 let quantized = Libm::<f64>::round(normalized * steps) / steps;
96 outputs.set(10, (quantized * 2.0 - 1.0) * 5.0);
97 }
98
99 fn reset(&mut self) {
100 self.hold_sample = 0.0;
101 self.hold_counter = 0.0;
102 }
103
104 fn set_sample_rate(&mut self, _: f64) {}
105
106 fn type_id(&self) -> &'static str {
107 "bitcrusher"
108 }
109}
110
111const DISTORTION_TONE_MIN_HZ: f64 = 500.0;
113const DISTORTION_TONE_MAX_HZ: f64 = 18_000.0;
115
116pub struct Distortion {
130 tone_lp: f64,
132 sample_rate: f64,
133 alpha_memo: Memo<2, f64>,
136 oversampler: Oversampler,
139 spec: PortSpec,
140}
141
142impl Distortion {
143 pub fn new(sample_rate: f64) -> Self {
144 let sample_rate = if sample_rate > 0.0 {
145 sample_rate
146 } else {
147 44100.0
148 };
149 Self {
150 tone_lp: 0.0,
151 sample_rate,
152 alpha_memo: Memo::new(0.0),
153 oversampler: Oversampler::new(Oversample::Off),
154 spec: PortSpec {
155 inputs: vec![
156 PortDef::new(0, "in", SignalKind::Audio),
157 PortDef::new(1, "drive", SignalKind::CvUnipolar)
158 .with_default(0.5)
159 .with_attenuverter(),
160 PortDef::new(2, "tone", SignalKind::CvUnipolar)
161 .with_default(0.5)
162 .with_attenuverter(),
163 PortDef::new(3, "mode", SignalKind::CvUnipolar)
164 .with_default(0.0)
165 .with_attenuverter(),
166 PortDef::new(4, "mix", SignalKind::CvUnipolar)
167 .with_default(1.0)
168 .with_attenuverter(),
169 ],
170 outputs: vec![PortDef::new(10, "out", SignalKind::Audio)],
171 },
172 }
173 }
174
175 fn soft_clip(x: f64, drive: f64) -> f64 {
178 let gained = (x / 5.0) * (1.0 + drive * 10.0);
179 Libm::<f64>::tanh(gained) * 5.0
180 }
181
182 fn hard_clip(x: f64, drive: f64) -> f64 {
185 let gained = (x / 5.0) * (1.0 + drive * 10.0);
186 gained.clamp(-1.0, 1.0) * 5.0
187 }
188
189 fn foldback(x: f64, drive: f64) -> f64 {
191 let gained = (x / 5.0) * (1.0 + drive * 5.0);
192 Self::triangle_fold(gained, 1.0) * 5.0
193 }
194
195 fn triangle_fold(x: f64, threshold: f64) -> f64 {
200 let period = 4.0 * threshold;
201 threshold - Libm::<f64>::fabs(rem_euclid_f64(x + threshold, period) - 2.0 * threshold)
202 }
203
204 fn asymmetric(x: f64, drive: f64) -> f64 {
206 let gained = (x / 5.0) * (1.0 + drive * 8.0);
207 let shaped = if gained >= 0.0 {
208 1.0 - Libm::<f64>::exp(-gained)
210 } else {
211 Libm::<f64>::tanh(gained)
213 };
214 shaped * 5.0
215 }
216
217 pub fn set_oversample(&mut self, mode: Oversample) {
225 self.oversampler = Oversampler::new(mode);
226 }
227
228 pub fn oversample_factor(&self) -> usize {
230 self.oversampler.factor()
231 }
232}
233
234impl Default for Distortion {
235 fn default() -> Self {
236 Self::new(44100.0)
237 }
238}
239
240impl GraphModule for Distortion {
241 fn port_spec(&self) -> &PortSpec {
242 &self.spec
243 }
244
245 fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
246 let input = sanitize_audio(inputs.get_or(0, 0.0));
247 let drive = inputs.get_or(1, 0.5).clamp(0.0, 1.0);
248 let tone = inputs.get_or(2, 0.5).clamp(0.0, 1.0);
249 let mode = inputs.get_or(3, 0.0).clamp(0.0, 1.0);
250 let mix = inputs.get_or(4, 1.0).clamp(0.0, 1.0);
251
252 let mode_idx = (mode * 3.99) as u8;
254 let distorted = self.oversampler.process(input, |x| match mode_idx {
258 0 => Self::soft_clip(x, drive),
259 1 => Self::hard_clip(x, drive),
260 2 => Self::foldback(x, drive),
261 _ => Self::asymmetric(x, drive),
262 });
263
264 let sample_rate = self.sample_rate;
270 let alpha = self.alpha_memo.get_or_compute([tone, sample_rate], || {
271 let cutoff = DISTORTION_TONE_MIN_HZ
272 * Libm::<f64>::pow(DISTORTION_TONE_MAX_HZ / DISTORTION_TONE_MIN_HZ, tone);
273 1.0 - Libm::<f64>::exp(-2.0 * core::f64::consts::PI * cutoff / sample_rate)
274 });
275 self.tone_lp += alpha * (distorted - self.tone_lp);
276 let filtered = self.tone_lp;
277
278 outputs.set(10, input * (1.0 - mix) + filtered * mix);
279 }
280
281 fn reset(&mut self) {
282 self.tone_lp = 0.0;
283 self.oversampler.reset();
284 }
285
286 fn set_sample_rate(&mut self, sample_rate: f64) {
287 if sample_rate > 0.0 {
288 self.sample_rate = sample_rate;
289 }
290 self.tone_lp = 0.0;
291 self.oversampler.reset();
292 }
293
294 fn type_id(&self) -> &'static str {
295 "distortion"
296 }
297
298 crate::impl_introspect!();
300}
301
302pub struct RingModulator {
311 spec: PortSpec,
312}
313
314impl RingModulator {
315 pub fn new() -> Self {
316 Self {
317 spec: PortSpec {
318 inputs: vec![
319 PortDef::new(0, "carrier", SignalKind::Audio),
320 PortDef::new(1, "modulator", SignalKind::Audio),
321 ],
322 outputs: vec![PortDef::new(10, "out", SignalKind::Audio)],
323 },
324 }
325 }
326}
327
328impl Default for RingModulator {
329 fn default() -> Self {
330 Self::new()
331 }
332}
333
334impl GraphModule for RingModulator {
335 fn port_spec(&self) -> &PortSpec {
336 &self.spec
337 }
338
339 fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
340 let carrier = inputs.get_or(0, 0.0);
341 let modulator = inputs.get_or(1, 0.0);
342
343 let out = (carrier * modulator) / 5.0;
346 outputs.set(10, out);
347 }
348
349 fn reset(&mut self) {}
350
351 fn set_sample_rate(&mut self, _: f64) {}
352
353 fn type_id(&self) -> &'static str {
354 "ring_mod"
355 }
356}
357
358pub struct PitchShifter {
380 buffer: [f64; 4800],
382 write_pos: usize,
384 grain_pos: [f64; 2],
386 grain_phase: [f64; 2],
388 sample_rate: f64,
389 rate_memo: Memo<1, f64>,
392 spec: PortSpec,
393}
394
395impl PitchShifter {
396 const BUFFER_SIZE: usize = 4800;
398
399 pub fn new(sample_rate: f64) -> Self {
400 let spec = PortSpec {
401 inputs: vec![
402 PortDef::new(0, "in", SignalKind::Audio),
403 PortDef::new(1, "shift", SignalKind::CvBipolar).with_default(0.0),
404 PortDef::new(2, "window", SignalKind::CvUnipolar).with_default(0.5),
405 PortDef::new(3, "mix", SignalKind::CvUnipolar).with_default(1.0),
406 ],
407 outputs: vec![PortDef::new(10, "out", SignalKind::Audio)],
408 };
409
410 Self {
411 buffer: [0.0; Self::BUFFER_SIZE],
412 write_pos: 0,
413 grain_pos: [0.0, 0.5 * Self::BUFFER_SIZE as f64], grain_phase: [0.0, 0.5], sample_rate,
416 rate_memo: Memo::new(0.0),
417 spec,
418 }
419 }
420
421 fn hann_window(phase: f64) -> f64 {
423 0.5 * (1.0 - Libm::<f64>::cos(phase * 2.0 * core::f64::consts::PI))
424 }
425
426 fn read_buffer(&self, pos: f64) -> f64 {
428 let pos = rem_euclid_f64(pos, Self::BUFFER_SIZE as f64);
429 let idx0 = pos as usize;
430 let idx1 = (idx0 + 1) % Self::BUFFER_SIZE;
431 let frac = pos - Libm::<f64>::floor(pos);
432
433 self.buffer[idx0] * (1.0 - frac) + self.buffer[idx1] * frac
434 }
435}
436
437impl Default for PitchShifter {
438 fn default() -> Self {
439 Self::new(44100.0)
440 }
441}
442
443impl GraphModule for PitchShifter {
444 fn port_spec(&self) -> &PortSpec {
445 &self.spec
446 }
447
448 fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
449 let input = inputs.get_or(0, 0.0);
450
451 let shift_semitones = (inputs.get_or(1, 0.0) / 5.0) * 24.0;
454 let shift_semitones = shift_semitones.clamp(-24.0, 24.0);
455
456 let window_cv = inputs.get_or(2, 0.5).clamp(0.0, 1.0);
458 let window_ms = 10.0 + window_cv * 90.0;
459 let mut window_samples = (window_ms * self.sample_rate / 1000.0) as usize;
460 window_samples = window_samples.min(Self::BUFFER_SIZE / 2);
461
462 let mix = inputs.get_or(3, 1.0).clamp(0.0, 1.0);
464
465 self.buffer[self.write_pos] = input / 5.0; self.write_pos = (self.write_pos + 1) % Self::BUFFER_SIZE;
468
469 let rate = self.rate_memo.get_or_compute([shift_semitones], || {
472 Libm::<f64>::pow(2.0, shift_semitones / 12.0)
473 });
474
475 if rate > 1.0 {
481 let max_lead = Self::BUFFER_SIZE as f64 * 0.4;
482 let window_cap = (max_lead / (rate - 1.0)) as usize;
483 window_samples = window_samples.min(window_cap);
484 }
485 window_samples = window_samples.max(1);
486 let read_margin =
487 (rate - 1.0).max(0.0) * window_samples as f64 + window_samples as f64 * 0.5;
488
489 let phase_inc = 1.0 / window_samples as f64;
490
491 let mut wet_output = 0.0;
493
494 for i in 0..2 {
495 let sample = self.read_buffer(self.grain_pos[i]);
497
498 let window = Self::hann_window(self.grain_phase[i]);
500 wet_output += sample * window;
501
502 self.grain_pos[i] += rate;
506
507 if self.grain_pos[i] >= Self::BUFFER_SIZE as f64 {
509 self.grain_pos[i] -= Self::BUFFER_SIZE as f64;
510 } else if self.grain_pos[i] < 0.0 {
511 self.grain_pos[i] += Self::BUFFER_SIZE as f64;
512 }
513
514 self.grain_phase[i] += phase_inc;
516
517 if self.grain_phase[i] >= 1.0 {
519 self.grain_phase[i] -= 1.0;
520 self.grain_pos[i] = rem_euclid_f64(
524 self.write_pos as f64 - read_margin,
525 Self::BUFFER_SIZE as f64,
526 );
527 }
528 }
529
530 let dry = input / 5.0;
532 let output = dry * (1.0 - mix) + wet_output * mix;
533
534 outputs.set(10, output * 5.0); }
536
537 fn reset(&mut self) {
538 self.buffer = [0.0; Self::BUFFER_SIZE];
539 self.write_pos = 0;
540 self.grain_pos = [0.0, Self::BUFFER_SIZE as f64 * 0.5];
541 self.grain_phase = [0.0, 0.5];
542 }
543
544 fn set_sample_rate(&mut self, sample_rate: f64) {
545 self.sample_rate = sample_rate;
546 self.reset();
547 }
548
549 fn type_id(&self) -> &'static str {
550 "pitch_shifter"
551 }
552}
553
554const MAX_VOCODER_BANDS: usize = 16;
556
557const VOCODER_FREQ_MIN: f64 = 100.0;
559
560const VOCODER_FREQ_MAX: f64 = 8000.0;
562
563const VOCODER_MAX_SVF_COEF: f64 = 0.95;
569
570pub struct Vocoder {
584 analysis_state: [[f64; 2]; MAX_VOCODER_BANDS],
586 synthesis_state: [[f64; 2]; MAX_VOCODER_BANDS],
588 envelopes: [f64; MAX_VOCODER_BANDS],
590
591 band_freqs: [f64; MAX_VOCODER_BANDS],
593
594 band_f_memo: Memo<2, [f64; MAX_VOCODER_BANDS]>,
599
600 env_memo: Memo<3, [f64; 2]>,
602
603 sample_rate: f64,
604 spec: PortSpec,
605}
606
607impl Vocoder {
608 pub fn new(sample_rate: f64) -> Self {
610 let mut vocoder = Self {
611 analysis_state: [[0.0; 2]; MAX_VOCODER_BANDS],
612 synthesis_state: [[0.0; 2]; MAX_VOCODER_BANDS],
613 envelopes: [0.0; MAX_VOCODER_BANDS],
614 band_freqs: [0.0; MAX_VOCODER_BANDS],
615 band_f_memo: Memo::new([0.0; MAX_VOCODER_BANDS]),
616 env_memo: Memo::new([0.0; 2]),
617 sample_rate,
618 spec: PortSpec {
619 inputs: vec![
620 PortDef::new(0, "carrier", SignalKind::Audio),
621 PortDef::new(1, "modulator", SignalKind::Audio),
622 PortDef::new(2, "bands", SignalKind::CvUnipolar).with_default(1.0),
623 PortDef::new(3, "attack", SignalKind::CvUnipolar).with_default(0.3),
624 PortDef::new(4, "release", SignalKind::CvUnipolar).with_default(0.3),
625 ],
626 outputs: vec![PortDef::new(10, "out", SignalKind::Audio)],
627 },
628 };
629 vocoder.compute_band_freqs();
630 vocoder
631 }
632
633 fn compute_band_freqs(&mut self) {
641 let coef_limit_freq = Libm::<f64>::asin(VOCODER_MAX_SVF_COEF / 2.0) * self.sample_rate
642 / core::f64::consts::PI;
643 let freq_max = VOCODER_FREQ_MAX
644 .min(coef_limit_freq)
645 .max(VOCODER_FREQ_MIN * 2.0);
646
647 let log_min = Libm::<f64>::log2(VOCODER_FREQ_MIN);
648 let log_max = Libm::<f64>::log2(freq_max);
649
650 for i in 0..MAX_VOCODER_BANDS {
651 let t = i as f64 / (MAX_VOCODER_BANDS - 1) as f64;
652 let log_freq = log_min + t * (log_max - log_min);
653 self.band_freqs[i] = Libm::<f64>::exp2(log_freq);
654 }
655 }
656
657 #[inline]
663 fn process_svf_bandpass(state: &mut [f64; 2], input: f64, f: f64, q: f64) -> f64 {
664 let q_inv = 1.0 / q;
666
667 let low = state[0];
669 let high = input - low - q_inv * state[1];
670 let band = f * high + state[1];
671 let new_low = f * band + low;
672
673 state[0] = new_low;
674 state[1] = band;
675
676 band
677 }
678}
679
680impl Default for Vocoder {
681 fn default() -> Self {
682 Self::new(44100.0)
683 }
684}
685
686impl GraphModule for Vocoder {
687 fn port_spec(&self) -> &PortSpec {
688 &self.spec
689 }
690
691 fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
692 let carrier = sanitize_audio(inputs.get_or(0, 0.0));
693 let modulator = sanitize_audio(inputs.get_or(1, 0.0));
694 let bands_cv = inputs.get_or(2, 1.0).clamp(0.0, 1.0);
695 let attack_cv = inputs.get_or(3, 0.3).clamp(0.0, 1.0);
696 let release_cv = inputs.get_or(4, 0.3).clamp(0.0, 1.0);
697
698 let num_bands = Libm::<f64>::round(4.0 + bands_cv * 12.0) as usize;
700 let num_bands = num_bands.min(MAX_VOCODER_BANDS);
701
702 let sample_rate = self.sample_rate;
705 let [attack_coef, release_coef] =
706 self.env_memo
707 .get_or_compute([attack_cv, release_cv, sample_rate], || {
708 let attack_time = 0.01 + attack_cv * 0.19;
709 let release_time = 0.01 + release_cv * 0.19;
710 [
711 env_coef(attack_time, sample_rate),
712 env_coef(release_time, sample_rate),
713 ]
714 });
715
716 let band_freqs = &self.band_freqs;
720 let band_f = self
721 .band_f_memo
722 .get_or_compute([num_bands as f64, sample_rate], || {
723 let mut f = [0.0; MAX_VOCODER_BANDS];
724 for (i, fi) in f.iter_mut().enumerate().take(num_bands) {
725 let freq = band_freqs[i * MAX_VOCODER_BANDS / num_bands];
726 let coef = 2.0 * Libm::<f64>::sin(core::f64::consts::PI * freq / sample_rate);
727 *fi = coef.min(0.99); }
729 f
730 });
731
732 let q = 2.0;
734
735 let mut output = 0.0;
736
737 for (i, &f) in band_f.iter().enumerate().take(num_bands) {
738 let analysis_band =
740 Self::process_svf_bandpass(&mut self.analysis_state[i], modulator, f, q);
741
742 let rectified = analysis_band.abs();
744 if rectified > self.envelopes[i] {
745 self.envelopes[i] =
746 attack_coef * self.envelopes[i] + (1.0 - attack_coef) * rectified;
747 } else {
748 self.envelopes[i] =
749 release_coef * self.envelopes[i] + (1.0 - release_coef) * rectified;
750 }
751
752 let synthesis_band =
754 Self::process_svf_bandpass(&mut self.synthesis_state[i], carrier, f, q);
755
756 output += synthesis_band * self.envelopes[i];
758 }
759
760 output /= num_bands as f64;
762
763 outputs.set(10, output * 4.0);
765 }
766
767 fn reset(&mut self) {
768 self.analysis_state = [[0.0; 2]; MAX_VOCODER_BANDS];
769 self.synthesis_state = [[0.0; 2]; MAX_VOCODER_BANDS];
770 self.envelopes = [0.0; MAX_VOCODER_BANDS];
771 }
772
773 fn set_sample_rate(&mut self, sample_rate: f64) {
774 self.sample_rate = sample_rate;
775 self.compute_band_freqs();
776 self.reset();
777 }
778
779 fn type_id(&self) -> &'static str {
780 "vocoder"
781 }
782}
783
784const MAX_GRAINS: usize = 16;
790
791const GRANULAR_BUFFER_SIZE: usize = 96000;
793
794#[derive(Clone, Copy)]
796struct Grain {
797 active: bool,
799 start_pos: usize,
801 phase: f64,
803 size: usize,
805 speed: f64,
807}
808
809impl Default for Grain {
810 fn default() -> Self {
811 Self {
812 active: false,
813 start_pos: 0,
814 phase: 0.0,
815 size: 4410, speed: 1.0,
817 }
818 }
819}
820
821pub struct Granular {
838 buffer: Vec<f64>,
840 write_pos: usize,
842
843 grains: [Grain; MAX_GRAINS],
845
846 spawn_timer: usize,
848
849 rng: crate::rng::Rng,
851
852 norm_smooth: f64,
856
857 norm_smooth_coef: f64,
861
862 speed_memo: Memo<1, f64>,
865
866 sample_rate: f64,
867 spec: PortSpec,
868}
869
870impl Granular {
871 pub fn new(sample_rate: f64) -> Self {
873 Self {
874 buffer: vec![0.0; GRANULAR_BUFFER_SIZE],
875 write_pos: 0,
876 grains: [Grain::default(); MAX_GRAINS],
877 spawn_timer: 0,
878 rng: crate::rng::Rng::from_seed(42),
879 norm_smooth: 1.0,
880 norm_smooth_coef: env_coef(0.05, sample_rate),
881 speed_memo: Memo::new(0.0),
882 sample_rate,
883 spec: PortSpec {
884 inputs: vec![
885 PortDef::new(0, "in", SignalKind::Audio),
886 PortDef::new(1, "position", SignalKind::CvUnipolar).with_default(0.5),
887 PortDef::new(2, "size", SignalKind::CvUnipolar).with_default(0.3),
888 PortDef::new(3, "density", SignalKind::CvUnipolar).with_default(0.5),
889 PortDef::new(4, "pitch", SignalKind::CvBipolar).with_default(0.0),
890 PortDef::new(5, "spray", SignalKind::CvUnipolar).with_default(0.1),
891 PortDef::new(6, "freeze", SignalKind::Gate).with_default(0.0),
892 ],
893 outputs: vec![PortDef::new(10, "out", SignalKind::Audio)],
894 },
895 }
896 }
897
898 #[inline]
900 fn hann_window(phase: f64) -> f64 {
901 0.5 * (1.0 - Libm::<f64>::cos(2.0 * core::f64::consts::PI * phase))
902 }
903
904 #[inline]
906 pub fn read_buffer(&self, pos: f64) -> f64 {
907 let pos = pos % GRANULAR_BUFFER_SIZE as f64;
908 let index = pos as usize;
909 let frac = pos - index as f64;
910
911 let s0 = self.buffer[index % GRANULAR_BUFFER_SIZE];
912 let s1 = self.buffer[(index + 1) % GRANULAR_BUFFER_SIZE];
913
914 s0 + frac * (s1 - s0)
915 }
916
917 fn spawn_grain(&mut self, position: f64, size: usize, speed: f64, spray: f64) {
919 for grain in &mut self.grains {
921 if !grain.active {
922 let spray_offset = if spray > 0.0 {
924 (self.rng.next_f64() - 0.5) * spray * GRANULAR_BUFFER_SIZE as f64 * 0.5
925 } else {
926 0.0
927 };
928
929 let base_pos = position * GRANULAR_BUFFER_SIZE as f64;
930 let pos = (base_pos + spray_offset) as usize % GRANULAR_BUFFER_SIZE;
931
932 grain.active = true;
933 grain.start_pos = pos;
934 grain.phase = 0.0;
935 grain.size = size.max(100); grain.speed = speed;
937 break;
938 }
939 }
940 }
941}
942
943impl Default for Granular {
944 fn default() -> Self {
945 Self::new(44100.0)
946 }
947}
948
949impl GraphModule for Granular {
950 fn port_spec(&self) -> &PortSpec {
951 &self.spec
952 }
953
954 fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
955 let input = inputs.get_or(0, 0.0);
956 let position = inputs.get_or(1, 0.5).clamp(0.0, 1.0);
957 let size_cv = inputs.get_or(2, 0.3).clamp(0.0, 1.0);
958 let density_cv = inputs.get_or(3, 0.5).clamp(0.0, 1.0);
959 let pitch_cv = inputs.get_or(4, 0.0).clamp(-5.0, 5.0);
960 let spray = inputs.get_or(5, 0.1).clamp(0.0, 1.0);
961 let freeze = inputs.get_or(6, 0.0);
962
963 let grains_per_sec = 1.0 + density_cv * 19.0;
965 let spawn_interval = (self.sample_rate / grains_per_sec) as usize;
966
967 let semitones = (pitch_cv * 4.8).clamp(-24.0, 24.0);
970 let speed = self
972 .speed_memo
973 .get_or_compute([semitones], || Libm::<f64>::exp2(semitones / 12.0));
974
975 let max_size = (GRANULAR_BUFFER_SIZE as f64 / speed) as usize;
980 let size_samples = (((0.01 + size_cv * 0.49) * self.sample_rate) as usize).min(max_size);
981
982 if freeze <= GATE_THRESHOLD_V {
984 self.buffer[self.write_pos] = input;
985 self.write_pos = (self.write_pos + 1) % GRANULAR_BUFFER_SIZE;
986 }
987
988 if self.spawn_timer == 0 {
990 self.spawn_grain(position, size_samples, speed, spray);
991
992 let jitter = 1.0 + (self.rng.next_f64() - 0.5) * 0.4;
994 self.spawn_timer = ((spawn_interval as f64) * jitter) as usize;
995 } else {
996 self.spawn_timer -= 1;
997 }
998
999 let mut output = 0.0;
1001
1002 for i in 0..MAX_GRAINS {
1003 if self.grains[i].active {
1004 let grain = &self.grains[i];
1005
1006 let read_offset = grain.phase * grain.size as f64 * grain.speed;
1008 let read_pos = grain.start_pos as f64 + read_offset;
1009
1010 let envelope = Self::hann_window(grain.phase);
1012
1013 let pos = read_pos % GRANULAR_BUFFER_SIZE as f64;
1015 let index = pos as usize;
1016 let frac = pos - index as f64;
1017 let s0 = self.buffer[index % GRANULAR_BUFFER_SIZE];
1018 let s1 = self.buffer[(index + 1) % GRANULAR_BUFFER_SIZE];
1019 let sample = s0 + frac * (s1 - s0);
1020
1021 output += sample * envelope;
1022
1023 let new_phase = self.grains[i].phase + 1.0 / self.grains[i].size as f64;
1025 self.grains[i].phase = new_phase;
1026
1027 if new_phase >= 1.0 {
1028 self.grains[i].active = false;
1029 }
1030 }
1031 }
1032
1033 let grain_seconds = size_samples as f64 / self.sample_rate;
1040 let expected_overlap = grains_per_sec * grain_seconds;
1041 let target_norm = Libm::<f64>::sqrt(expected_overlap).max(1.0);
1043 let smooth = self.norm_smooth_coef; self.norm_smooth = smooth * self.norm_smooth + (1.0 - smooth) * target_norm;
1045 output /= self.norm_smooth.max(1.0);
1046
1047 outputs.set(10, output);
1048 }
1049
1050 fn reset(&mut self) {
1051 self.buffer.iter_mut().for_each(|x| *x = 0.0);
1052 self.write_pos = 0;
1053 self.grains = [Grain::default(); MAX_GRAINS];
1054 self.spawn_timer = 0;
1055 self.rng = crate::rng::Rng::from_seed(42);
1056 self.norm_smooth = 1.0;
1057 }
1058
1059 fn set_sample_rate(&mut self, sample_rate: f64) {
1060 self.sample_rate = sample_rate;
1061 self.norm_smooth_coef = env_coef(0.05, sample_rate);
1063 self.reset();
1064 }
1065
1066 fn type_id(&self) -> &'static str {
1067 "granular"
1068 }
1069}
1070
1071pub struct Wavefolder {
1079 pub(crate) threshold: f64,
1080 oversampler: Oversampler,
1083 spec: PortSpec,
1084}
1085
1086impl Wavefolder {
1087 pub fn new(threshold: f64) -> Self {
1088 Self {
1089 threshold: threshold.max(0.1),
1090 oversampler: Oversampler::new(Oversample::Off),
1091 spec: PortSpec {
1092 inputs: vec![
1093 PortDef::new(0, "in", SignalKind::Audio),
1094 PortDef::new(1, "threshold", SignalKind::CvUnipolar)
1095 .with_default(threshold)
1096 .with_attenuverter(),
1097 ],
1098 outputs: vec![PortDef::new(10, "out", SignalKind::Audio)],
1099 },
1100 }
1101 }
1102
1103 pub fn set_oversample(&mut self, mode: Oversample) {
1109 self.oversampler = Oversampler::new(mode);
1110 }
1111
1112 pub fn oversample_factor(&self) -> usize {
1114 self.oversampler.factor()
1115 }
1116}
1117
1118impl Default for Wavefolder {
1119 fn default() -> Self {
1120 Self::new(1.0)
1121 }
1122}
1123
1124impl GraphModule for Wavefolder {
1125 fn port_spec(&self) -> &PortSpec {
1126 &self.spec
1127 }
1128
1129 fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
1130 let input = inputs.get_or(0, 0.0);
1131 let threshold = inputs.get_or(1, self.threshold).max(0.1);
1132
1133 let folded = self
1136 .oversampler
1137 .process(input, |x| saturation::fold(x / 5.0, threshold) * 5.0);
1138 outputs.set(10, folded);
1139 }
1140
1141 fn reset(&mut self) {
1142 self.oversampler.reset();
1143 }
1144
1145 fn set_sample_rate(&mut self, _: f64) {}
1146
1147 fn type_id(&self) -> &'static str {
1148 "wavefolder"
1149 }
1150
1151 crate::impl_introspect!();
1153}
1154
1155#[cfg(test)]
1156mod tests {
1157 use super::*;
1158
1159 #[test]
1160 fn test_bitcrusher() {
1161 let mut bc = Bitcrusher::new();
1162 let mut inputs = PortValues::new();
1163 let mut outputs = PortValues::new();
1164
1165 inputs.set(0, 2.5);
1166 inputs.set(1, 0.3); inputs.set(2, 0.5); bc.tick(&inputs, &mut outputs);
1169
1170 let out = outputs.get(10).unwrap();
1171 assert!(out.is_finite());
1172 }
1173 #[test]
1174 fn test_bitcrusher_default() {
1175 let bc = Bitcrusher::default();
1176 assert_eq!(bc.type_id(), "bitcrusher");
1177 }
1178 #[test]
1179 fn test_ring_modulator() {
1180 let mut rm = RingModulator::new();
1181 let mut inputs = PortValues::new();
1182 let mut outputs = PortValues::new();
1183
1184 inputs.set(0, 5.0); inputs.set(1, 5.0); rm.tick(&inputs, &mut outputs);
1188 assert!((outputs.get(10).unwrap() - 5.0).abs() < 0.1);
1189
1190 inputs.set(0, 5.0);
1192 inputs.set(1, -5.0);
1193 rm.tick(&inputs, &mut outputs);
1194 assert!((outputs.get(10).unwrap() - (-5.0)).abs() < 0.1);
1195
1196 inputs.set(0, 5.0);
1198 inputs.set(1, 0.0);
1199 rm.tick(&inputs, &mut outputs);
1200 assert!((outputs.get(10).unwrap()).abs() < 0.01);
1201 }
1202 #[test]
1203 fn test_ring_modulator_default_reset_sample_rate() {
1204 let mut rm = RingModulator::default();
1205 rm.reset();
1206 rm.set_sample_rate(48000.0);
1207 assert_eq!(rm.type_id(), "ring_mod");
1208 }
1209 #[test]
1210 fn test_pitch_shifter_default_reset_sample_rate() {
1211 let mut ps = PitchShifter::default();
1212 assert_eq!(ps.sample_rate, 44100.0);
1213
1214 let mut inputs = PortValues::new();
1216 let mut outputs = PortValues::new();
1217 inputs.set(0, 2.5); for _ in 0..100 {
1219 ps.tick(&inputs, &mut outputs);
1220 }
1221
1222 assert!(ps.write_pos > 0);
1224
1225 ps.reset();
1227 assert_eq!(ps.write_pos, 0);
1228 assert_eq!(ps.grain_phase, [0.0, 0.5]);
1229
1230 ps.set_sample_rate(48000.0);
1232 assert_eq!(ps.sample_rate, 48000.0);
1233
1234 assert_eq!(ps.type_id(), "pitch_shifter");
1235 assert_eq!(ps.port_spec().inputs.len(), 4);
1236 assert_eq!(ps.port_spec().outputs.len(), 1);
1237 }
1238 #[test]
1239 fn test_pitch_shifter_hann_window() {
1240 let start = PitchShifter::hann_window(0.0);
1242 let peak = PitchShifter::hann_window(0.5);
1243 let end = PitchShifter::hann_window(1.0);
1244
1245 assert!(start.abs() < 0.01, "Window should start at 0: {}", start);
1246 assert!(
1247 (peak - 1.0).abs() < 0.01,
1248 "Window should peak at 1: {}",
1249 peak
1250 );
1251 assert!(end.abs() < 0.01, "Window should end at 0: {}", end);
1252 }
1253 #[test]
1254 fn test_pitch_shifter_passthrough() {
1255 let mut ps = PitchShifter::new(44100.0);
1256 let mut inputs = PortValues::new();
1257 let mut outputs = PortValues::new();
1258
1259 inputs.set(1, 0.0); inputs.set(3, 1.0); let mut sum_out = 0.0;
1265 for i in 0..1000 {
1266 let input = Libm::<f64>::sin(i as f64 * 0.1) * 5.0;
1267 inputs.set(0, input);
1268 ps.tick(&inputs, &mut outputs);
1269 sum_out += outputs.get(10).unwrap().abs();
1270 }
1271
1272 assert!(sum_out > 100.0, "Should have output signal: {}", sum_out);
1274 }
1275 #[test]
1276 fn test_pitch_shifter_dry_wet_mix() {
1277 let mut ps = PitchShifter::new(44100.0);
1278 let mut inputs = PortValues::new();
1279 let mut outputs = PortValues::new();
1280
1281 inputs.set(1, 0.0);
1283 inputs.set(3, 0.0); let input_val = 2.5; inputs.set(0, input_val);
1287
1288 ps.tick(&inputs, &mut outputs);
1289 let dry_out = outputs.get(10).unwrap();
1290
1291 assert!(
1293 (dry_out - input_val).abs() < 0.1,
1294 "Dry output should match input: {} vs {}",
1295 dry_out,
1296 input_val
1297 );
1298 }
1299 #[test]
1300 fn test_pitch_shifter_shift_changes_output() {
1301 let mut ps = PitchShifter::new(44100.0);
1302
1303 let collect_output = |ps: &mut PitchShifter, shift_cv: f64| -> f64 {
1305 let mut inputs = PortValues::new();
1306 let mut outputs = PortValues::new();
1307 inputs.set(1, shift_cv);
1308 inputs.set(3, 1.0);
1309 ps.reset();
1310
1311 let mut sum = 0.0;
1312 for i in 0..2000 {
1313 let input = Libm::<f64>::sin(i as f64 * 0.05) * 5.0;
1314 inputs.set(0, input);
1315 ps.tick(&inputs, &mut outputs);
1316 sum += outputs.get(10).unwrap();
1317 }
1318 sum
1319 };
1320
1321 let sum_no_shift = collect_output(&mut ps, 0.0);
1322 let sum_up_octave = collect_output(&mut ps, 2.5); let sum_down_octave = collect_output(&mut ps, -2.5); assert!(
1327 (sum_no_shift - sum_up_octave).abs() > 1.0,
1328 "Up shift should differ"
1329 );
1330 assert!(
1331 (sum_no_shift - sum_down_octave).abs() > 1.0,
1332 "Down shift should differ"
1333 );
1334 }
1335 #[test]
1336 fn test_pitch_shifter_buffer_wraparound() {
1337 let mut ps = PitchShifter::new(44100.0);
1338 let mut inputs = PortValues::new();
1339 let mut outputs = PortValues::new();
1340
1341 inputs.set(0, 2.5);
1342 inputs.set(1, 0.0);
1343 inputs.set(3, 1.0);
1344
1345 for _ in 0..10000 {
1347 ps.tick(&inputs, &mut outputs);
1348 let out = outputs.get(10).unwrap();
1349 assert!(out.is_finite(), "Output should be finite");
1350 }
1351
1352 assert!(ps.write_pos < PitchShifter::BUFFER_SIZE);
1354 }
1355 #[test]
1356 fn test_vocoder_default_reset_sample_rate() {
1357 let mut vocoder = Vocoder::default();
1358 assert_eq!(vocoder.sample_rate, 44100.0);
1359
1360 let mut inputs = PortValues::new();
1362 let mut outputs = PortValues::new();
1363 inputs.set(0, 0.5); inputs.set(1, 0.5); vocoder.tick(&inputs, &mut outputs);
1366
1367 vocoder.reset();
1369 assert_eq!(vocoder.envelopes, [0.0; MAX_VOCODER_BANDS]);
1370
1371 vocoder.set_sample_rate(48000.0);
1373 assert_eq!(vocoder.sample_rate, 48000.0);
1374
1375 assert_eq!(vocoder.type_id(), "vocoder");
1376 assert_eq!(vocoder.port_spec().inputs.len(), 5);
1377 assert_eq!(vocoder.port_spec().outputs.len(), 1);
1378 }
1379 #[test]
1380 fn test_vocoder_band_frequencies() {
1381 let vocoder = Vocoder::new(44100.0);
1382
1383 assert!(vocoder.band_freqs[0] >= VOCODER_FREQ_MIN - 1.0);
1385 assert!(vocoder.band_freqs[MAX_VOCODER_BANDS - 1] <= VOCODER_FREQ_MAX + 1.0);
1386
1387 for i in 1..MAX_VOCODER_BANDS {
1389 assert!(
1390 vocoder.band_freqs[i] > vocoder.band_freqs[i - 1],
1391 "Band frequencies should be ascending"
1392 );
1393 }
1394 }
1395 #[test]
1396 fn test_vocoder_silent_when_no_modulator() {
1397 let mut vocoder = Vocoder::new(44100.0);
1398 let mut inputs = PortValues::new();
1399 let mut outputs = PortValues::new();
1400
1401 inputs.set(0, 0.8);
1403 inputs.set(1, 0.0);
1404
1405 for _ in 0..1000 {
1407 vocoder.tick(&inputs, &mut outputs);
1408 }
1409
1410 let out = outputs.get(10).unwrap();
1411 assert!(
1413 out.abs() < 0.1,
1414 "Output should be near zero without modulator, got {}",
1415 out
1416 );
1417 }
1418 #[test]
1419 fn test_vocoder_output_when_both_active() {
1420 let mut vocoder = Vocoder::new(44100.0);
1421 let mut inputs = PortValues::new();
1422 let mut outputs = PortValues::new();
1423
1424 let mut total_output = 0.0;
1426 for i in 0..2000 {
1427 let phase = i as f64 * 0.05;
1428 inputs.set(0, Libm::<f64>::sin(phase)); inputs.set(1, Libm::<f64>::sin(phase * 0.1)); vocoder.tick(&inputs, &mut outputs);
1431 total_output += outputs.get(10).unwrap().abs();
1432 }
1433
1434 assert!(
1435 total_output > 1.0,
1436 "Should produce output when both signals active, got {}",
1437 total_output
1438 );
1439 }
1440 #[test]
1441 fn test_vocoder_band_count() {
1442 let mut vocoder_few = Vocoder::new(44100.0);
1443 let mut vocoder_many = Vocoder::new(44100.0);
1444 let mut inputs_few = PortValues::new();
1445 let mut inputs_many = PortValues::new();
1446 let mut outputs_few = PortValues::new();
1447 let mut outputs_many = PortValues::new();
1448
1449 inputs_few.set(2, 0.0); inputs_many.set(2, 1.0); let mut total_few = 0.0;
1455 let mut total_many = 0.0;
1456
1457 for i in 0..1000 {
1458 let phase = i as f64 * 0.05;
1459 let carrier = Libm::<f64>::sin(phase);
1460 let modulator = Libm::<f64>::sin(phase * 0.2);
1461
1462 inputs_few.set(0, carrier);
1463 inputs_few.set(1, modulator);
1464 inputs_many.set(0, carrier);
1465 inputs_many.set(1, modulator);
1466
1467 vocoder_few.tick(&inputs_few, &mut outputs_few);
1468 vocoder_many.tick(&inputs_many, &mut outputs_many);
1469
1470 total_few += outputs_few.get(10).unwrap().abs();
1471 total_many += outputs_many.get(10).unwrap().abs();
1472 }
1473
1474 assert!(total_few > 0.5, "Few bands should produce output");
1476 assert!(total_many > 0.5, "Many bands should produce output");
1477 }
1478 #[test]
1479 fn test_vocoder_envelope_attack_release() {
1480 let mut vocoder = Vocoder::new(44100.0);
1481 let mut inputs = PortValues::new();
1482 let mut outputs = PortValues::new();
1483
1484 inputs.set(0, 1.0); inputs.set(1, 1.0); inputs.set(3, 0.0); inputs.set(4, 0.0); for _ in 0..100 {
1492 vocoder.tick(&inputs, &mut outputs);
1493 }
1494 let fast_envelope = vocoder.envelopes[0];
1495
1496 vocoder.reset();
1497 inputs.set(3, 1.0); for _ in 0..100 {
1500 vocoder.tick(&inputs, &mut outputs);
1501 }
1502 let slow_envelope = vocoder.envelopes[0];
1503
1504 assert!(
1506 fast_envelope > slow_envelope,
1507 "Fast attack should build envelope faster"
1508 );
1509 }
1510 #[test]
1511 fn test_granular_default_reset_sample_rate() {
1512 let mut granular = Granular::default();
1513 assert_eq!(granular.sample_rate, 44100.0);
1514
1515 let mut inputs = PortValues::new();
1517 let mut outputs = PortValues::new();
1518 inputs.set(0, 0.5);
1519 granular.tick(&inputs, &mut outputs);
1520
1521 assert_eq!(granular.write_pos, 1);
1523
1524 granular.reset();
1526 assert_eq!(granular.write_pos, 0);
1527 assert!(granular.grains.iter().all(|g| !g.active));
1528
1529 granular.set_sample_rate(48000.0);
1531 assert_eq!(granular.sample_rate, 48000.0);
1532
1533 assert_eq!(granular.type_id(), "granular");
1534 assert_eq!(granular.port_spec().inputs.len(), 7);
1535 assert_eq!(granular.port_spec().outputs.len(), 1);
1536 }
1537 #[test]
1538 fn test_granular_hann_window() {
1539 assert!(Granular::hann_window(0.0).abs() < 0.001);
1541 assert!((Granular::hann_window(0.5) - 1.0).abs() < 0.001);
1542 assert!(Granular::hann_window(1.0).abs() < 0.001);
1543 }
1544 #[test]
1545 fn test_granular_records_to_buffer() {
1546 let mut granular = Granular::new(44100.0);
1547 let mut inputs = PortValues::new();
1548 let mut outputs = PortValues::new();
1549
1550 for i in 0..100 {
1552 inputs.set(0, i as f64 * 0.01);
1553 granular.tick(&inputs, &mut outputs);
1554 }
1555
1556 assert!((granular.buffer[50] - 0.5).abs() < 0.01);
1558 }
1559 #[test]
1560 fn test_granular_freeze_stops_recording() {
1561 let mut granular = Granular::new(44100.0);
1562 let mut inputs = PortValues::new();
1563 let mut outputs = PortValues::new();
1564
1565 inputs.set(0, 1.0);
1567 for _ in 0..100 {
1568 granular.tick(&inputs, &mut outputs);
1569 }
1570 let pos_before = granular.write_pos;
1571
1572 inputs.set(6, 5.0); for _ in 0..100 {
1577 granular.tick(&inputs, &mut outputs);
1578 }
1579
1580 assert_eq!(granular.write_pos, pos_before);
1581 }
1582 #[test]
1583 fn test_granular_produces_output() {
1584 let mut granular = Granular::new(44100.0);
1585 let mut inputs = PortValues::new();
1586 let mut outputs = PortValues::new();
1587
1588 inputs.set(1, 0.05); for i in 0..10000 {
1593 let phase = i as f64 * 0.01;
1594 inputs.set(0, Libm::<f64>::sin(phase));
1595 granular.tick(&inputs, &mut outputs);
1596 }
1597
1598 let mut total_output = 0.0;
1600 for _ in 0..5000 {
1601 inputs.set(0, 0.0);
1602 granular.tick(&inputs, &mut outputs);
1603 total_output += outputs.get(10).unwrap().abs();
1604 }
1605
1606 assert!(
1607 total_output > 1.0,
1608 "Granular should produce output, got {}",
1609 total_output
1610 );
1611 }
1612 #[test]
1613 fn test_granular_density_affects_grain_count() {
1614 let mut granular_low = Granular::new(44100.0);
1615 let mut granular_high = Granular::new(44100.0);
1616 let mut inputs_low = PortValues::new();
1617 let mut inputs_high = PortValues::new();
1618 let mut outputs = PortValues::new();
1619
1620 inputs_low.set(3, 0.0); inputs_high.set(3, 1.0); for i in 0..5000 {
1625 let sample = Libm::<f64>::sin(i as f64 * 0.05);
1626 inputs_low.set(0, sample);
1627 inputs_high.set(0, sample);
1628 granular_low.tick(&inputs_low, &mut outputs);
1629 granular_high.tick(&inputs_high, &mut outputs);
1630 }
1631
1632 let active_low = granular_low.grains.iter().filter(|g| g.active).count();
1634 let active_high = granular_high.grains.iter().filter(|g| g.active).count();
1635
1636 assert!(
1639 active_high >= active_low || (active_low == 0 && active_high == 0),
1640 "Higher density should produce more concurrent grains"
1641 );
1642 }
1643 #[test]
1644 fn test_granular_buffer_interpolation() {
1645 let granular = Granular::new(44100.0);
1646
1647 let mut granular = granular;
1649 granular.buffer[0] = 0.0;
1650 granular.buffer[1] = 1.0;
1651
1652 let val = granular.read_buffer(0.5);
1654 assert!(
1655 (val - 0.5).abs() < 0.01,
1656 "Interpolation should give 0.5, got {}",
1657 val
1658 );
1659 }
1660 #[test]
1661 fn test_grain_default() {
1662 let grain = Grain::default();
1663 assert!(!grain.active);
1664 assert_eq!(grain.phase, 0.0);
1665 assert_eq!(grain.speed, 1.0);
1666 }
1667
1668 #[test]
1676 fn test_distortion_tone_is_real_filter() {
1677 let sr = 44100.0;
1678 let rms = |freq: f64, tone: f64| -> f64 {
1681 let mut d = Distortion::new(sr);
1682 let mut inputs = PortValues::new();
1683 let mut outputs = PortValues::new();
1684 inputs.set(1, 0.0); inputs.set(2, tone); inputs.set(3, 0.0); inputs.set(4, 1.0); let n = 8000usize;
1689 let mut sumsq = 0.0;
1690 for i in 0..n {
1691 let x = Libm::<f64>::sin(2.0 * core::f64::consts::PI * freq * i as f64 / sr);
1692 inputs.set(0, x); d.tick(&inputs, &mut outputs);
1694 let out = outputs.get(10).unwrap();
1695 if i >= n / 2 {
1696 sumsq += out * out;
1697 }
1698 }
1699 Libm::<f64>::sqrt(sumsq / (n / 2) as f64)
1700 };
1701
1702 let input_rms = 1.0 / Libm::<f64>::sqrt(2.0); let high_at_min = rms(5000.0, 0.0);
1706 let low_at_min = rms(200.0, 0.0);
1707 assert!(
1708 high_at_min < 0.5 * low_at_min,
1709 "tone min should attenuate highs more than lows: high={high_at_min} low={low_at_min}"
1710 );
1711
1712 let high_at_max = rms(5000.0, 1.0);
1714 assert!(
1715 high_at_max > 0.8 * input_rms,
1716 "tone max should be ~transparent: out_rms={high_at_max} in_rms={input_rms}"
1717 );
1718 assert!(
1719 high_at_max > 3.0 * high_at_min,
1720 "tone max should pass highs that tone min blocks: max={high_at_max} min={high_at_min}"
1721 );
1722 }
1723
1724 #[test]
1727 fn test_distortion_all_algorithms_bounded() {
1728 for drive in [0.0, 0.5, 1.0] {
1730 let mut x = -12.0;
1731 while x <= 12.0 {
1732 for out in [
1733 Distortion::soft_clip(x, drive),
1734 Distortion::hard_clip(x, drive),
1735 Distortion::foldback(x, drive),
1736 Distortion::asymmetric(x, drive),
1737 ] {
1738 assert!(
1739 out.is_finite() && out.abs() <= 5.05,
1740 "shaper out {out} exceeds ±5.05 at x={x} drive={drive}"
1741 );
1742 }
1743 x += 0.05;
1744 }
1745 }
1746
1747 for mode_cv in [0.0f64, 0.34, 0.67, 1.0] {
1749 for &v in &[5.0f64, -5.0] {
1750 let mut d = Distortion::new(44100.0);
1751 let mut inputs = PortValues::new();
1752 let mut outputs = PortValues::new();
1753 inputs.set(1, 1.0); inputs.set(2, 1.0); inputs.set(3, mode_cv);
1756 inputs.set(4, 1.0); inputs.set(0, v);
1758 let mut out = 0.0;
1759 for _ in 0..500 {
1760 d.tick(&inputs, &mut outputs);
1761 out = outputs.get(10).unwrap();
1762 }
1763 assert!(
1764 out.abs() <= 5.05,
1765 "mode {mode_cv} at {v}V max drive should stay ≤5.05V, got {out}"
1766 );
1767 }
1768 }
1769 }
1770
1771 #[test]
1774 fn test_distortion_unity_at_low_drive() {
1775 assert!((Distortion::hard_clip(0.5, 0.0) - 0.5).abs() < 1e-9);
1777 let out = Distortion::soft_clip(1.0, 0.0);
1779 assert!((out - 1.0).abs() < 0.05, "soft_clip near unity, got {out}");
1780 }
1781
1782 #[test]
1785 fn test_triangle_fold_matches_reference_loop() {
1786 fn reference(gained: f64, threshold: f64) -> f64 {
1787 let mut folded = gained;
1788 while folded > threshold || folded < -threshold {
1789 if folded > threshold {
1790 folded = 2.0 * threshold - folded;
1791 } else if folded < -threshold {
1792 folded = -2.0 * threshold - folded;
1793 }
1794 }
1795 folded
1796 }
1797 let threshold = 1.0;
1798 let mut x = -1000.0;
1799 while x <= 1000.0 {
1800 let a = Distortion::triangle_fold(x, threshold);
1801 let b = reference(x, threshold);
1802 assert!((a - b).abs() < 1e-6, "fold mismatch at {x}: {a} vs {b}");
1803 x += 0.05;
1804 }
1805 for &x in &[1000.0, -1000.0, 5.0, -5.0, 3.0, -3.0, 1.0, -1.0, 0.0] {
1806 let a = Distortion::triangle_fold(x, threshold);
1807 let b = reference(x, threshold);
1808 assert!(
1809 (a - b).abs() < 1e-6,
1810 "fold mismatch at extreme {x}: {a} vs {b}"
1811 );
1812 }
1813 }
1814
1815 #[test]
1818 fn test_vocoder_band_coefficients_strictly_increasing() {
1819 for &sr in &[44100.0, 22050.0, 32000.0] {
1820 let v = Vocoder::new(sr);
1821 let mut prev = -1.0;
1822 for i in 0..MAX_VOCODER_BANDS {
1823 let coef = (2.0 * Libm::<f64>::sin(core::f64::consts::PI * v.band_freqs[i] / sr))
1824 .min(0.99);
1825 assert!(
1826 coef > prev + 1e-9,
1827 "band {i} coef {coef} not strictly greater than {prev} at sr {sr}"
1828 );
1829 prev = coef;
1830 }
1831 }
1832 }
1833
1834 #[test]
1837 fn test_granular_no_amplitude_zipper() {
1838 let mut g = Granular::new(44100.0);
1839 let mut inputs = PortValues::new();
1840 let mut outputs = PortValues::new();
1841 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) {
1849 g.tick(&inputs, &mut outputs);
1850 }
1851
1852 let mut prev = outputs.get(10).unwrap();
1853 let mut max_delta = 0.0f64;
1854 for _ in 0..30000 {
1855 g.tick(&inputs, &mut outputs);
1856 let out = outputs.get(10).unwrap();
1857 max_delta = max_delta.max((out - prev).abs());
1858 prev = out;
1859 }
1860 assert!(
1861 max_delta < 0.05,
1862 "granular output should have no zipper jumps, max delta {max_delta}"
1863 );
1864 }
1865
1866 #[test]
1869 fn test_bitcrusher_fractional_downsample_period() {
1870 let mut bc = Bitcrusher::new();
1871 let mut inputs = PortValues::new();
1872 let mut outputs = PortValues::new();
1873 inputs.set(2, 0.5 / 63.0);
1875 inputs.set(1, 1.0); let n = 3000usize;
1878 let mut transitions = 0usize;
1879 let mut prev = f64::NAN;
1880 for i in 0..n {
1881 inputs.set(0, i as f64 * 0.001); bc.tick(&inputs, &mut outputs);
1883 let out = outputs.get(10).unwrap();
1884 if i > 0 && (out - prev).abs() > 1e-9 {
1885 transitions += 1;
1886 }
1887 prev = out;
1888 }
1889 let avg_period = n as f64 / transitions as f64;
1890 assert!(
1891 (avg_period - 1.5).abs() < 0.1,
1892 "fractional downsample average period should be ~1.5, got {avg_period}"
1893 );
1894 }
1895
1896 #[test]
1899 fn test_bitcrusher_no_dc_bias() {
1900 let mut bc = Bitcrusher::new();
1901 let mut inputs = PortValues::new();
1902 let mut outputs = PortValues::new();
1903 inputs.set(1, 2.0 / 15.0); inputs.set(2, 0.0); let n = 20000usize;
1906 let mut sum = 0.0;
1907 for i in 0..n {
1908 let v = Libm::<f64>::sin(i as f64 * 0.01) * 4.0; inputs.set(0, v);
1910 bc.tick(&inputs, &mut outputs);
1911 sum += outputs.get(10).unwrap();
1912 }
1913 let mean = sum / n as f64;
1914 assert!(
1915 mean.abs() < 0.1,
1916 "quantizer DC bias should be ~0, got {mean}"
1917 );
1918 }
1919
1920 #[test]
1921 fn test_bitcrusher_full_scale_maps_in_range() {
1922 let mut bc = Bitcrusher::new();
1923 let mut inputs = PortValues::new();
1924 let mut outputs = PortValues::new();
1925 inputs.set(1, 0.3);
1926 inputs.set(2, 0.0); for &(v, expected) in &[(5.0, 5.0), (-5.0, -5.0)] {
1928 inputs.set(0, v);
1929 bc.tick(&inputs, &mut outputs);
1930 let out = outputs.get(10).unwrap();
1931 assert!(
1932 out.abs() <= 5.0 + 1e-9 && (out - expected).abs() < 1e-9,
1933 "full-scale {v}V should map to {expected}V in range, got {out}"
1934 );
1935 }
1936 }
1937
1938 #[test]
1941 fn test_granular_pitch_clamped_and_bounded() {
1942 let mut g = Granular::new(44100.0);
1943 let mut inputs = PortValues::new();
1944 let mut outputs = PortValues::new();
1945 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);
1950 g.tick(&inputs, &mut outputs); let grain = g
1953 .grains
1954 .iter()
1955 .find(|gr| gr.active)
1956 .expect("a grain should be active after the first tick");
1957 assert!(
1958 (grain.speed - 4.0).abs() < 1e-6,
1959 "pitch +5V should be +24 st (speed 4), got speed {}",
1960 grain.speed
1961 );
1962 assert!(
1963 grain.size as f64 * grain.speed <= GRANULAR_BUFFER_SIZE as f64,
1964 "grain read span {} must not exceed buffer {}",
1965 grain.size as f64 * grain.speed,
1966 GRANULAR_BUFFER_SIZE
1967 );
1968
1969 for &pitch in &[5.0f64, -5.0] {
1971 let mut g = Granular::new(44100.0);
1972 inputs.set(4, pitch);
1973 let mut total = 0.0;
1974 let mut max_abs = 0.0f64;
1975 for i in 0..20000 {
1976 inputs.set(0, Libm::<f64>::sin(i as f64 * 0.05) * 5.0);
1977 g.tick(&inputs, &mut outputs);
1978 let out = outputs.get(10).unwrap();
1979 assert!(out.is_finite(), "granular output must be finite");
1980 max_abs = max_abs.max(out.abs());
1981 total += out.abs();
1982 }
1983 assert!(max_abs < 50.0, "output should stay bounded, got {max_abs}");
1984 assert!(total > 1.0, "output should be non-silent, got {total}");
1985 }
1986 }
1987
1988 #[test]
1991 fn test_pitch_shifter_max_pitch_up_bounded() {
1992 let mut ps = PitchShifter::new(44100.0);
1993 let mut inputs = PortValues::new();
1994 let mut outputs = PortValues::new();
1995 inputs.set(1, 5.0); inputs.set(3, 1.0); let mut total = 0.0;
1999 let mut max_abs = 0.0f64;
2000 for i in 0..10000 {
2001 inputs.set(0, Libm::<f64>::sin(i as f64 * 0.1) * 5.0);
2002 ps.tick(&inputs, &mut outputs);
2003 let out = outputs.get(10).unwrap();
2004 assert!(out.is_finite(), "pitch-up output must be finite");
2005 max_abs = max_abs.max(out.abs());
2006 total += out.abs();
2007 }
2008 assert!(
2009 max_abs <= 5.5,
2010 "wet output should stay near ±5V (COLA), got {max_abs}"
2011 );
2012 assert!(
2013 total > 10.0,
2014 "pitch-up output should be non-silent, got {total}"
2015 );
2016 }
2017
2018 fn dft_mag(sig: &[f64], k: usize) -> f64 {
2024 let n = sig.len();
2025 let mut re = 0.0;
2026 let mut im = 0.0;
2027 for (i, &s) in sig.iter().enumerate() {
2028 let ang = -core::f64::consts::TAU * (k as f64) * (i as f64) / (n as f64);
2029 re += s * Libm::<f64>::cos(ang);
2030 im += s * Libm::<f64>::sin(ang);
2031 }
2032 Libm::<f64>::sqrt(re * re + im * im) / (n as f64)
2033 }
2034
2035 fn alias_energy(sig: &[f64], fund: usize) -> f64 {
2038 let n = sig.len();
2039 let mut total = 0.0;
2040 for k in 1..(n / 2) {
2041 if k % fund != 0 {
2042 total += dft_mag(sig, k);
2043 }
2044 }
2045 total
2046 }
2047
2048 fn distortion_hardclip_capture(mode: Oversample, n: usize) -> Vec<f64> {
2051 let sr = 44100.0;
2052 let mut d = Distortion::new(sr);
2053 d.set_oversample(mode);
2054 let mut inputs = PortValues::new();
2055 let mut outputs = PortValues::new();
2056 inputs.set(1, 1.0); inputs.set(2, 1.0); inputs.set(3, 0.4); inputs.set(4, 1.0); let freq = 4200.0;
2063 let mut out = Vec::with_capacity(n);
2064 for i in 0..(n * 3) {
2066 let t = i as f64 / sr;
2067 let x = Libm::<f64>::sin(core::f64::consts::TAU * freq * t) * 5.0;
2068 inputs.set(0, x);
2069 d.tick(&inputs, &mut outputs);
2070 if i >= n * 2 {
2071 out.push(outputs.get(10).unwrap());
2072 }
2073 }
2074 out
2075 }
2076
2077 #[test]
2078 fn test_distortion_oversampling_reduces_aliasing() {
2079 let n = 441;
2080 let fund = 42;
2081 let off = distortion_hardclip_capture(Oversample::Off, n);
2082 let x4 = distortion_hardclip_capture(Oversample::X4, n);
2083
2084 let a_off = alias_energy(&off, fund);
2085 let a_x4 = alias_energy(&x4, fund);
2086
2087 assert!(
2088 a_x4 < 0.7 * a_off,
2089 "4x oversampling should materially reduce alias energy: off={a_off} x4={a_x4}"
2090 );
2091 }
2092
2093 #[test]
2094 fn test_distortion_oversample_off_is_default_and_transparent() {
2095 let sr = 44100.0;
2097 let mut a = Distortion::new(sr);
2098 let mut b = Distortion::new(sr);
2099 b.set_oversample(Oversample::Off);
2100 let mut ia = PortValues::new();
2101 let mut oa = PortValues::new();
2102 let mut ib = PortValues::new();
2103 let mut ob = PortValues::new();
2104 for i in 0..500 {
2105 let x = Libm::<f64>::sin(i as f64 * 0.3) * 5.0;
2106 ia.set(0, x);
2107 ib.set(0, x);
2108 a.tick(&ia, &mut oa);
2109 b.tick(&ib, &mut ob);
2110 assert!((oa.get(10).unwrap() - ob.get(10).unwrap()).abs() < 1e-12);
2111 }
2112 }
2113
2114 #[test]
2115 fn test_wavefolder_oversampling_reduces_aliasing() {
2116 let sr = 44100.0;
2117 let n = 441;
2118 let fund = 42;
2119 let freq = 4200.0;
2120
2121 let capture = |mode: Oversample| -> Vec<f64> {
2122 let mut wf = Wavefolder::new(0.3);
2123 wf.set_oversample(mode);
2124 let mut inputs = PortValues::new();
2125 let mut outputs = PortValues::new();
2126 let mut out = Vec::with_capacity(n);
2127 for i in 0..(n * 3) {
2128 let t = i as f64 / sr;
2129 inputs.set(0, Libm::<f64>::sin(core::f64::consts::TAU * freq * t) * 5.0);
2130 wf.tick(&inputs, &mut outputs);
2131 if i >= n * 2 {
2132 out.push(outputs.get(10).unwrap());
2133 }
2134 }
2135 out
2136 };
2137
2138 let a_off = alias_energy(&capture(Oversample::Off), fund);
2139 let a_x4 = alias_energy(&capture(Oversample::X4), fund);
2140 assert!(
2141 a_x4 < 0.7 * a_off,
2142 "wavefolder 4x oversampling should reduce alias energy: off={a_off} x4={a_x4}"
2143 );
2144 }
2145
2146 #[test]
2149 fn test_distortion_reset_and_sample_rate() {
2150 let mut dist = Distortion::default();
2151 assert_eq!(dist.type_id(), "distortion");
2152 assert_eq!(dist.sample_rate, 44100.0);
2153 let mut inputs = PortValues::new();
2154 let mut outputs = PortValues::new();
2155 inputs.set(0, 3.0);
2156 inputs.set(1, 0.8); inputs.set(2, 0.2); for _ in 0..500 {
2159 dist.tick(&inputs, &mut outputs);
2160 }
2161 assert!(dist.tone_lp != 0.0, "tone low-pass should hold state");
2162 dist.reset();
2163 assert_eq!(dist.tone_lp, 0.0);
2164 dist.set_sample_rate(48000.0);
2165 assert_eq!(dist.sample_rate, 48000.0);
2166 for _ in 0..100 {
2167 dist.tick(&inputs, &mut outputs);
2168 assert!(outputs.get(10).unwrap().is_finite());
2169 }
2170 }
2171
2172 #[test]
2180 fn test_vocoder_matches_per_sample_reference() {
2181 let sample_rate = 44100.0;
2182 let mut voc = Vocoder::new(sample_rate);
2183 let band_freqs = voc.band_freqs;
2184 let mut inputs = PortValues::new();
2185 let mut outputs = PortValues::new();
2186
2187 let mut analysis = [[0.0f64; 2]; MAX_VOCODER_BANDS];
2189 let mut synthesis = [[0.0f64; 2]; MAX_VOCODER_BANDS];
2190 let mut envelopes = [0.0f64; MAX_VOCODER_BANDS];
2191
2192 let ref_svf = |state: &mut [f64; 2], input: f64, freq: f64, q: f64| -> f64 {
2194 let f = 2.0 * Libm::<f64>::sin(core::f64::consts::PI * freq / sample_rate);
2195 let f = f.min(0.99);
2196 let q_inv = 1.0 / q;
2197 let low = state[0];
2198 let high = input - low - q_inv * state[1];
2199 let band = f * high + state[1];
2200 let new_low = f * band + low;
2201 state[0] = new_low;
2202 state[1] = band;
2203 band
2204 };
2205
2206 for n in 0..8_000u32 {
2207 let t = n as f64;
2208 let carrier = Libm::<f64>::sin(t * 0.11) * 4.0;
2209 let modulator = Libm::<f64>::sin(t * 0.017) * 3.0;
2210 let bands_cv = if n < 4_000 { 0.7 } else { 0.2 };
2211 inputs.set(0, carrier);
2212 inputs.set(1, modulator);
2213 inputs.set(2, bands_cv);
2214 inputs.set(3, 0.3);
2215 inputs.set(4, 0.4);
2216
2217 voc.tick(&inputs, &mut outputs);
2218 let got = outputs.get(10).unwrap();
2219
2220 let num_bands = Libm::<f64>::round(4.0 + bands_cv * 12.0) as usize;
2222 let num_bands = num_bands.min(MAX_VOCODER_BANDS);
2223 let attack_time = 0.01 + 0.3 * 0.19;
2224 let release_time = 0.01 + 0.4 * 0.19;
2225 let attack_coef = env_coef(attack_time, sample_rate);
2226 let release_coef = env_coef(release_time, sample_rate);
2227 let q = 2.0;
2228 let mut output = 0.0;
2229 for i in 0..num_bands {
2230 let freq = band_freqs[i * MAX_VOCODER_BANDS / num_bands];
2231 let analysis_band = ref_svf(&mut analysis[i], modulator, freq, q);
2232 let rectified = analysis_band.abs();
2233 if rectified > envelopes[i] {
2234 envelopes[i] = attack_coef * envelopes[i] + (1.0 - attack_coef) * rectified;
2235 } else {
2236 envelopes[i] = release_coef * envelopes[i] + (1.0 - release_coef) * rectified;
2237 }
2238 let synthesis_band = ref_svf(&mut synthesis[i], carrier, freq, q);
2239 output += synthesis_band * envelopes[i];
2240 }
2241 output /= num_bands as f64;
2242 let want = output * 4.0;
2243
2244 assert_eq!(
2245 got.to_bits(),
2246 want.to_bits(),
2247 "Vocoder diverged from per-sample reference at sample {n}"
2248 );
2249 }
2250 assert_eq!(voc.band_f_memo.recompute_count(), 2);
2252 assert_eq!(voc.env_memo.recompute_count(), 1);
2253 }
2254
2255 #[test]
2261 fn test_nonlinear_memos_bit_identical() {
2262 let mut dist_m = Distortion::new(44100.0);
2263 let mut dist_f = Distortion::new(44100.0);
2264 let mut bc_m = Bitcrusher::new();
2265 let mut bc_f = Bitcrusher::new();
2266 let mut ps_m = PitchShifter::new(44100.0);
2267 let mut ps_f = PitchShifter::new(44100.0);
2268 let mut gr_m = Granular::new(44100.0);
2269 let mut gr_f = Granular::new(44100.0);
2270 let mut inputs = PortValues::new();
2271 let mut out_m = PortValues::new();
2272 let mut out_f = PortValues::new();
2273
2274 for n in 0..10_000u32 {
2275 let t = n as f64;
2276 let audio = Libm::<f64>::sin(t * 0.061) * 4.0;
2277 let sweep = if n < 5_000 {
2279 0.5
2280 } else {
2281 0.5 + 0.3 * Libm::<f64>::sin(t * 0.003)
2282 };
2283
2284 inputs.set(0, audio);
2285 inputs.set(1, 0.6);
2286 inputs.set(2, sweep); dist_m.tick(&inputs, &mut out_m);
2288 dist_f.alpha_memo.invalidate();
2289 dist_f.tick(&inputs, &mut out_f);
2290 assert_eq!(
2291 out_m.get(10).unwrap().to_bits(),
2292 out_f.get(10).unwrap().to_bits(),
2293 "Distortion diverged at sample {n}"
2294 );
2295
2296 inputs.set(1, sweep); inputs.set(2, 0.3);
2298 bc_m.tick(&inputs, &mut out_m);
2299 bc_f.levels_memo.invalidate();
2300 bc_f.tick(&inputs, &mut out_f);
2301 assert_eq!(
2302 out_m.get(10).unwrap().to_bits(),
2303 out_f.get(10).unwrap().to_bits(),
2304 "Bitcrusher diverged at sample {n}"
2305 );
2306
2307 inputs.set(1, (sweep - 0.5) * 6.0); inputs.set(2, 0.5);
2309 ps_m.tick(&inputs, &mut out_m);
2310 ps_f.rate_memo.invalidate();
2311 ps_f.tick(&inputs, &mut out_f);
2312 assert_eq!(
2313 out_m.get(10).unwrap().to_bits(),
2314 out_f.get(10).unwrap().to_bits(),
2315 "PitchShifter diverged at sample {n}"
2316 );
2317
2318 inputs.set(4, (sweep - 0.5) * 4.0); gr_m.tick(&inputs, &mut out_m);
2320 gr_f.speed_memo.invalidate();
2321 gr_f.tick(&inputs, &mut out_f);
2322 assert_eq!(
2323 out_m.get(10).unwrap().to_bits(),
2324 out_f.get(10).unwrap().to_bits(),
2325 "Granular diverged at sample {n}"
2326 );
2327 }
2328 assert!(dist_m.alpha_memo.recompute_count() <= 5_001);
2329 assert!(bc_m.levels_memo.recompute_count() <= 5_001);
2330 assert!(ps_m.rate_memo.recompute_count() <= 5_001);
2331 assert!(gr_m.speed_memo.recompute_count() <= 5_001);
2332 }
2333}