1use std::f32::consts::{PI, TAU};
7
8pub const SAMPLE_RATE: f32 = 48_000.0;
9pub const SAMPLE_RATE_INV: f32 = 1.0 / SAMPLE_RATE;
10
11#[derive(Clone, Debug)]
15pub struct Adsr {
16 pub attack: f32, pub decay: f32, pub sustain: f32, pub release: f32, }
21
22impl Adsr {
23 pub fn new(attack: f32, decay: f32, sustain: f32, release: f32) -> Self {
24 Self { attack, decay, sustain, release }
25 }
26
27 pub fn pluck() -> Self { Self::new(0.002, 0.1, 0.0, 0.05) }
29
30 pub fn pad() -> Self { Self::new(0.3, 0.5, 0.7, 0.8) }
32
33 pub fn hit() -> Self { Self::new(0.001, 0.05, 0.0, 0.02) }
35
36 pub fn drone() -> Self { Self::new(0.01, 0.1, 0.9, 1.2) }
38
39 pub fn level(&self, age: f32, note_off: Option<f32>) -> f32 {
42 if let Some(off) = note_off {
43 let rel = (age - off).max(0.0);
44 let sustain_level = self.level(off, None);
45 let t = (1.0 - rel / self.release.max(0.0001)).max(0.0);
46 return sustain_level * t * t;
47 }
48 if age < self.attack {
49 return age / self.attack.max(0.0001);
50 }
51 let after_attack = age - self.attack;
52 if after_attack < self.decay {
53 let t = after_attack / self.decay.max(0.0001);
54 return 1.0 - t * (1.0 - self.sustain);
55 }
56 self.sustain
57 }
58
59 pub fn is_silent(&self, age: f32, note_off: Option<f32>) -> bool {
61 if let Some(off) = note_off {
62 let rel = (age - off).max(0.0);
63 return rel >= self.release;
64 }
65 false
66 }
67}
68
69#[derive(Clone, Copy, Debug, PartialEq)]
73pub enum Waveform {
74 Sine,
75 Triangle,
76 Square,
77 Sawtooth,
78 ReverseSaw,
79 Noise,
80 Pulse(f32),
82}
83
84pub fn oscillator(waveform: Waveform, phase: f32) -> f32 {
86 let p = phase - phase.floor(); match waveform {
88 Waveform::Sine => (p * TAU).sin(),
89 Waveform::Triangle => {
90 if p < 0.5 { 4.0 * p - 1.0 } else { 3.0 - 4.0 * p }
91 }
92 Waveform::Square => if p < 0.5 { 1.0 } else { -1.0 },
93 Waveform::Sawtooth => 2.0 * p - 1.0,
94 Waveform::ReverseSaw => 1.0 - 2.0 * p,
95 Waveform::Pulse(duty) => if p < duty { 1.0 } else { -1.0 },
96 Waveform::Noise => {
97 let h = (phase * 13371.333) as u64;
98 let h = h.wrapping_mul(0x9e3779b97f4a7c15).wrapping_add(0x62b821756295c58d);
99 (h >> 32) as f32 / u32::MAX as f32 * 2.0 - 1.0
100 }
101 }
102}
103
104#[derive(Clone, Debug)]
108pub struct Oscillator {
109 pub waveform: Waveform,
110 pub frequency: f32,
111 pub amplitude: f32,
112 pub phase: f32,
113 pub pm_depth: f32,
115}
116
117impl Oscillator {
118 pub fn new(waveform: Waveform, frequency: f32, amplitude: f32) -> Self {
119 Self { waveform, frequency, amplitude, phase: 0.0, pm_depth: 0.0 }
120 }
121
122 pub fn sine(frequency: f32) -> Self { Self::new(Waveform::Sine, frequency, 1.0) }
123 pub fn saw(frequency: f32) -> Self { Self::new(Waveform::Sawtooth, frequency, 1.0) }
124 pub fn square(frequency: f32) -> Self { Self::new(Waveform::Square, frequency, 1.0) }
125 pub fn tri(frequency: f32) -> Self { Self::new(Waveform::Triangle, frequency, 1.0) }
126 pub fn noise() -> Self { Self::new(Waveform::Noise, 1.0, 1.0) }
127
128 pub fn tick(&mut self) -> f32 {
130 let sample = oscillator(self.waveform, self.phase);
131 self.phase += self.frequency * SAMPLE_RATE_INV;
132 if self.phase >= 1.0 { self.phase -= 1.0; }
133 sample * self.amplitude
134 }
135
136 pub fn tick_fm(&mut self, modulator: f32) -> f32 {
138 let modulated_phase = self.phase + modulator * self.pm_depth;
139 let sample = oscillator(self.waveform, modulated_phase);
140 self.phase += self.frequency * SAMPLE_RATE_INV;
141 if self.phase >= 1.0 { self.phase -= 1.0; }
142 sample * self.amplitude
143 }
144
145 pub fn retrigger(&mut self) { self.phase = 0.0; }
147}
148
149#[derive(Clone, Copy, Debug, PartialEq)]
153pub enum FilterMode {
154 LowPass,
155 HighPass,
156 BandPass,
157 Notch,
158 AllPass,
159 LowShelf,
160 HighShelf,
161 Peak,
162}
163
164#[derive(Clone, Debug)]
166pub struct BiquadFilter {
167 b0: f32, b1: f32, b2: f32,
169 a1: f32, a2: f32,
170 x1: f32, x2: f32,
172 y1: f32, y2: f32,
173 pub mode: FilterMode,
175 pub cutoff_hz: f32,
176 pub resonance: f32, pub gain_db: f32, }
179
180impl BiquadFilter {
181 pub fn new(mode: FilterMode, cutoff_hz: f32, resonance: f32) -> Self {
182 let mut f = Self {
183 b0: 1.0, b1: 0.0, b2: 0.0,
184 a1: 0.0, a2: 0.0,
185 x1: 0.0, x2: 0.0,
186 y1: 0.0, y2: 0.0,
187 mode,
188 cutoff_hz,
189 resonance,
190 gain_db: 0.0,
191 };
192 f.update_coefficients();
193 f
194 }
195
196 pub fn low_pass(cutoff_hz: f32, q: f32) -> Self {
197 Self::new(FilterMode::LowPass, cutoff_hz, q)
198 }
199
200 pub fn high_pass(cutoff_hz: f32, q: f32) -> Self {
201 Self::new(FilterMode::HighPass, cutoff_hz, q)
202 }
203
204 pub fn band_pass(cutoff_hz: f32, q: f32) -> Self {
205 Self::new(FilterMode::BandPass, cutoff_hz, q)
206 }
207
208 pub fn notch(cutoff_hz: f32, q: f32) -> Self {
209 Self::new(FilterMode::Notch, cutoff_hz, q)
210 }
211
212 pub fn update_coefficients(&mut self) {
214 let w0 = TAU * self.cutoff_hz / SAMPLE_RATE;
215 let cos_w0 = w0.cos();
216 let sin_w0 = w0.sin();
217 let alpha = sin_w0 / (2.0 * self.resonance.max(0.0001));
218 let a = 10.0f32.powf(self.gain_db / 40.0);
219
220 let (b0, b1, b2, a0, a1, a2) = match self.mode {
221 FilterMode::LowPass => (
222 (1.0 - cos_w0) / 2.0,
223 1.0 - cos_w0,
224 (1.0 - cos_w0) / 2.0,
225 1.0 + alpha, -2.0 * cos_w0, 1.0 - alpha,
226 ),
227 FilterMode::HighPass => (
228 (1.0 + cos_w0) / 2.0,
229 -(1.0 + cos_w0),
230 (1.0 + cos_w0) / 2.0,
231 1.0 + alpha, -2.0 * cos_w0, 1.0 - alpha,
232 ),
233 FilterMode::BandPass => (
234 sin_w0 / 2.0, 0.0, -sin_w0 / 2.0,
235 1.0 + alpha, -2.0 * cos_w0, 1.0 - alpha,
236 ),
237 FilterMode::Notch => (
238 1.0, -2.0 * cos_w0, 1.0,
239 1.0 + alpha, -2.0 * cos_w0, 1.0 - alpha,
240 ),
241 FilterMode::AllPass => (
242 1.0 - alpha, -2.0 * cos_w0, 1.0 + alpha,
243 1.0 + alpha, -2.0 * cos_w0, 1.0 - alpha,
244 ),
245 FilterMode::LowShelf => {
246 let sq = (a * ((a + 1.0 / a) * (1.0 / 1.0 - 1.0) + 2.0)).sqrt();
247 (
248 a * ((a + 1.0) - (a - 1.0) * cos_w0 + 2.0 * a.sqrt() * alpha),
249 2.0 * a * ((a - 1.0) - (a + 1.0) * cos_w0),
250 a * ((a + 1.0) - (a - 1.0) * cos_w0 - 2.0 * a.sqrt() * alpha),
251 (a + 1.0) + (a - 1.0) * cos_w0 + 2.0 * a.sqrt() * alpha,
252 -2.0 * ((a - 1.0) + (a + 1.0) * cos_w0),
253 (a + 1.0) + (a - 1.0) * cos_w0 - 2.0 * a.sqrt() * alpha + sq * 0.0,
254 )
255 }
256 FilterMode::HighShelf => {
257 (
258 a * ((a + 1.0) + (a - 1.0) * cos_w0 + 2.0 * a.sqrt() * alpha),
259 -2.0 * a * ((a - 1.0) + (a + 1.0) * cos_w0),
260 a * ((a + 1.0) + (a - 1.0) * cos_w0 - 2.0 * a.sqrt() * alpha),
261 (a + 1.0) - (a - 1.0) * cos_w0 + 2.0 * a.sqrt() * alpha,
262 2.0 * ((a - 1.0) - (a + 1.0) * cos_w0),
263 (a + 1.0) - (a - 1.0) * cos_w0 - 2.0 * a.sqrt() * alpha,
264 )
265 }
266 FilterMode::Peak => (
267 1.0 + alpha * a,
268 -2.0 * cos_w0,
269 1.0 - alpha * a,
270 1.0 + alpha / a,
271 -2.0 * cos_w0,
272 1.0 - alpha / a,
273 ),
274 };
275
276 let a0_inv = 1.0 / a0;
277 self.b0 = b0 * a0_inv;
278 self.b1 = b1 * a0_inv;
279 self.b2 = b2 * a0_inv;
280 self.a1 = a1 * a0_inv;
281 self.a2 = a2 * a0_inv;
282 }
283
284 pub fn set_cutoff(&mut self, hz: f32) {
286 self.cutoff_hz = hz.clamp(20.0, SAMPLE_RATE * 0.49);
287 self.update_coefficients();
288 }
289
290 pub fn set_resonance(&mut self, q: f32) {
292 self.resonance = q.max(0.1);
293 self.update_coefficients();
294 }
295
296 pub fn tick(&mut self, input: f32) -> f32 {
298 let y = self.b0 * input + self.b1 * self.x1 + self.b2 * self.x2
299 - self.a1 * self.y1 - self.a2 * self.y2;
300 self.x2 = self.x1;
301 self.x1 = input;
302 self.y2 = self.y1;
303 self.y1 = y;
304 y
305 }
306
307 pub fn reset(&mut self) {
309 self.x1 = 0.0; self.x2 = 0.0;
310 self.y1 = 0.0; self.y2 = 0.0;
311 }
312}
313
314#[derive(Clone, Debug)]
318pub struct Lfo {
319 pub waveform: Waveform,
320 pub rate_hz: f32,
321 pub depth: f32,
322 pub offset: f32, phase: f32,
324}
325
326impl Lfo {
327 pub fn new(waveform: Waveform, rate_hz: f32, depth: f32) -> Self {
328 Self { waveform, rate_hz, depth, offset: 0.0, phase: 0.0 }
329 }
330
331 pub fn sine(rate_hz: f32, depth: f32) -> Self { Self::new(Waveform::Sine, rate_hz, depth) }
332 pub fn tri(rate_hz: f32, depth: f32) -> Self { Self::new(Waveform::Triangle, rate_hz, depth) }
333 pub fn square(rate_hz: f32, depth: f32) -> Self { Self::new(Waveform::Square, rate_hz, depth) }
334
335 pub fn tick(&mut self) -> f32 {
337 let val = oscillator(self.waveform, self.phase);
338 self.phase += self.rate_hz * SAMPLE_RATE_INV;
339 if self.phase >= 1.0 { self.phase -= 1.0; }
340 val * self.depth + self.offset
341 }
342
343 pub fn set_phase(&mut self, phase: f32) { self.phase = phase.fract(); }
345}
346
347#[derive(Clone, Debug)]
351pub struct FmOperator {
352 pub osc: Oscillator,
353 pub adsr: Adsr,
354 pub age: f32,
355 pub note_off_age: Option<f32>,
356 pub output_level: f32,
357}
358
359impl FmOperator {
360 pub fn new(frequency: f32, adsr: Adsr, output_level: f32) -> Self {
361 Self {
362 osc: Oscillator::sine(frequency),
363 adsr,
364 age: 0.0,
365 note_off_age: None,
366 output_level,
367 }
368 }
369
370 pub fn tick(&mut self, modulator: f32) -> f32 {
372 self.age += SAMPLE_RATE_INV;
373 let env = self.adsr.level(self.age, self.note_off_age);
374 let sample = self.osc.tick_fm(modulator);
375 sample * env * self.output_level
376 }
377
378 pub fn note_on(&mut self) {
379 self.age = 0.0;
380 self.note_off_age = None;
381 self.osc.retrigger();
382 }
383
384 pub fn note_off(&mut self) {
385 self.note_off_age = Some(self.age);
386 }
387
388 pub fn is_silent(&self) -> bool {
389 self.adsr.is_silent(self.age, self.note_off_age)
390 }
391}
392
393#[derive(Clone, Debug)]
397pub struct FmVoice {
398 pub carrier: FmOperator,
399 pub modulator: FmOperator,
400 pub mod_ratio: f32,
402 pub mod_index: f32,
404}
405
406impl FmVoice {
407 pub fn new(base_freq: f32, mod_ratio: f32, mod_index: f32) -> Self {
408 let carrier_adsr = Adsr::drone();
409 let mod_adsr = Adsr::new(0.01, 0.2, 0.5, 0.5);
410 Self {
411 carrier: FmOperator::new(base_freq, carrier_adsr, 1.0),
412 modulator: FmOperator::new(base_freq * mod_ratio, mod_adsr, mod_index),
413 mod_ratio,
414 mod_index,
415 }
416 }
417
418 pub fn tick(&mut self) -> f32 {
419 let mod_out = self.modulator.tick(0.0);
420 self.carrier.tick(mod_out)
421 }
422
423 pub fn note_on(&mut self, frequency: f32) {
424 self.carrier.osc.frequency = frequency;
425 self.modulator.osc.frequency = frequency * self.mod_ratio;
426 self.carrier.note_on();
427 self.modulator.note_on();
428 }
429
430 pub fn note_off(&mut self) {
431 self.carrier.note_off();
432 self.modulator.note_off();
433 }
434
435 pub fn is_silent(&self) -> bool {
436 self.carrier.is_silent()
437 }
438}
439
440#[derive(Clone, Debug)]
444pub struct DelayLine {
445 buffer: Vec<f32>,
446 write_pos: usize,
447 delay_samples: usize,
448}
449
450impl DelayLine {
451 pub fn new(max_delay_ms: f32) -> Self {
452 let max_samples = (SAMPLE_RATE * max_delay_ms * 0.001) as usize + 1;
453 Self {
454 buffer: vec![0.0; max_samples],
455 write_pos: 0,
456 delay_samples: max_samples / 2,
457 }
458 }
459
460 pub fn set_delay_ms(&mut self, ms: f32) {
461 self.delay_samples = ((SAMPLE_RATE * ms * 0.001) as usize)
462 .clamp(1, self.buffer.len() - 1);
463 }
464
465 pub fn tick(&mut self, input: f32) -> f32 {
466 let read_pos = (self.write_pos + self.buffer.len() - self.delay_samples) % self.buffer.len();
467 let out = self.buffer[read_pos];
468 self.buffer[self.write_pos] = input;
469 self.write_pos = (self.write_pos + 1) % self.buffer.len();
470 out
471 }
472
473 pub fn clear(&mut self) {
474 self.buffer.iter_mut().for_each(|x| *x = 0.0);
475 }
476}
477
478#[derive(Clone, Debug)]
480pub struct Echo {
481 pub delay: DelayLine,
482 pub feedback: f32, pub wet: f32, }
485
486impl Echo {
487 pub fn new(delay_ms: f32, feedback: f32, wet: f32) -> Self {
488 Self {
489 delay: DelayLine::new(delay_ms + 50.0),
490 feedback: feedback.clamp(0.0, 0.99),
491 wet: wet.clamp(0.0, 1.0),
492 }
493 }
494
495 pub fn tick(&mut self, input: f32) -> f32 {
496 let delayed = self.delay.tick(input + self.delay.buffer[self.delay.write_pos] * self.feedback);
497 input * (1.0 - self.wet) + delayed * self.wet
498 }
499}
500
501#[derive(Debug)]
503pub struct Reverb {
504 comb: [CombFilter; 4],
505 allpass: [AllpassFilter; 2],
506 pub wet: f32,
507 pub room: f32, pub damp: f32, }
510
511impl Reverb {
512 pub fn new() -> Self {
513 let room = 0.5;
514 let damp = 0.5;
515 Self {
516 comb: [
517 CombFilter::new(1116, room, damp),
518 CombFilter::new(1188, room, damp),
519 CombFilter::new(1277, room, damp),
520 CombFilter::new(1356, room, damp),
521 ],
522 allpass: [
523 AllpassFilter::new(556, 0.5),
524 AllpassFilter::new(441, 0.5),
525 ],
526 wet: 0.3,
527 room,
528 damp,
529 }
530 }
531
532 pub fn set_room(&mut self, room: f32) {
533 self.room = room.clamp(0.0, 1.0);
534 for c in &mut self.comb { c.feedback = self.room * 0.9; }
535 }
536
537 pub fn set_damp(&mut self, damp: f32) {
538 self.damp = damp.clamp(0.0, 1.0);
539 for c in &mut self.comb { c.damp = self.damp; }
540 }
541
542 pub fn tick(&mut self, input: f32) -> f32 {
543 let mut out = 0.0f32;
544 for c in &mut self.comb {
545 out += c.tick(input);
546 }
547 for a in &mut self.allpass {
548 out = a.tick(out);
549 }
550 input * (1.0 - self.wet) + out * self.wet * 0.25
551 }
552}
553
554#[derive(Debug)]
555struct CombFilter {
556 buffer: Vec<f32>,
557 pos: usize,
558 pub feedback: f32,
559 pub damp: f32,
560 last: f32,
561}
562
563impl CombFilter {
564 fn new(delay_samples: usize, feedback: f32, damp: f32) -> Self {
565 Self {
566 buffer: vec![0.0; delay_samples],
567 pos: 0,
568 feedback,
569 damp,
570 last: 0.0,
571 }
572 }
573
574 fn tick(&mut self, input: f32) -> f32 {
575 let out = self.buffer[self.pos];
576 self.last = out * (1.0 - self.damp) + self.last * self.damp;
577 self.buffer[self.pos] = input + self.last * self.feedback;
578 self.pos = (self.pos + 1) % self.buffer.len();
579 out
580 }
581}
582
583#[derive(Debug)]
584struct AllpassFilter {
585 buffer: Vec<f32>,
586 pos: usize,
587 feedback: f32,
588}
589
590impl AllpassFilter {
591 fn new(delay_samples: usize, feedback: f32) -> Self {
592 Self { buffer: vec![0.0; delay_samples], pos: 0, feedback }
593 }
594
595 fn tick(&mut self, input: f32) -> f32 {
596 let buffered = self.buffer[self.pos];
597 let output = -input + buffered;
598 self.buffer[self.pos] = input + buffered * self.feedback;
599 self.pos = (self.pos + 1) % self.buffer.len();
600 output
601 }
602}
603
604#[derive(Clone, Debug)]
606pub struct Saturator {
607 pub drive: f32, pub output: f32, }
610
611impl Saturator {
612 pub fn new(drive: f32) -> Self {
613 Self { drive, output: 1.0 / drive.max(1.0) }
614 }
615
616 pub fn tick(&self, input: f32) -> f32 {
617 let x = input * self.drive;
618 let shaped = x / (1.0 + x.abs());
620 shaped * self.output
621 }
622}
623
624#[derive(Clone, Debug, Default)]
626pub struct DcBlocker {
627 x_prev: f32,
628 y_prev: f32,
629}
630
631impl DcBlocker {
632 pub fn tick(&mut self, input: f32) -> f32 {
633 let y = input - self.x_prev + 0.9975 * self.y_prev;
634 self.x_prev = input;
635 self.y_prev = y;
636 y
637 }
638}
639
640pub fn midi_to_hz(note: u8) -> f32 {
644 440.0 * 2.0f32.powf((note as f32 - 69.0) / 12.0)
645}
646
647pub fn hz_to_midi(hz: f32) -> u8 {
649 (69.0 + 12.0 * (hz / 440.0).log2()).round().clamp(0.0, 127.0) as u8
650}
651
652pub fn detune_cents(hz: f32, cents: f32) -> f32 {
654 hz * 2.0f32.powf(cents / 1200.0)
655}
656
657pub fn db_to_linear(db: f32) -> f32 {
659 10.0f32.powf(db / 20.0)
660}
661
662pub fn linear_to_db(gain: f32) -> f32 {
664 20.0 * gain.abs().max(1e-10).log10()
665}
666
667pub const MAJOR_SCALE: [i32; 7] = [0, 2, 4, 5, 7, 9, 11];
671pub const MINOR_SCALE: [i32; 7] = [0, 2, 3, 5, 7, 8, 10];
673pub const PENTATONIC_MAJ: [i32; 5] = [0, 2, 4, 7, 9];
675pub const PENTATONIC_MIN: [i32; 5] = [0, 3, 5, 7, 10];
677pub const WHOLE_TONE: [i32; 6] = [0, 2, 4, 6, 8, 10];
679pub const DIMINISHED: [i32; 8] = [0, 1, 3, 4, 6, 7, 9, 10];
681
682pub fn chord_freqs(root_midi: u8, intervals: &[i32], degrees: &[usize]) -> Vec<f32> {
684 degrees.iter()
685 .filter_map(|&d| intervals.get(d))
686 .map(|&semi| midi_to_hz((root_midi as i32 + semi).clamp(0, 127) as u8))
687 .collect()
688}
689
690#[cfg(test)]
693mod tests {
694 use super::*;
695
696 #[test]
697 fn adsr_attack_phase() {
698 let adsr = Adsr::new(1.0, 0.5, 0.7, 0.5);
699 assert!((adsr.level(0.5, None) - 0.5).abs() < 0.01);
700 assert!((adsr.level(1.0, None) - 1.0).abs() < 0.01);
701 }
702
703 #[test]
704 fn adsr_sustain_phase() {
705 let adsr = Adsr::new(0.01, 0.01, 0.7, 0.5);
706 assert!((adsr.level(0.1, None) - 0.7).abs() < 0.01);
707 }
708
709 #[test]
710 fn adsr_release_decays_to_zero() {
711 let adsr = Adsr::new(0.01, 0.01, 0.7, 1.0);
712 let level_at_release = adsr.level(1.0, Some(0.1));
713 assert!(level_at_release < 0.01);
714 }
715
716 #[test]
717 fn oscillator_sine_at_zero_phase() {
718 let s = oscillator(Waveform::Sine, 0.0);
719 assert!((s - 0.0).abs() < 0.001);
720 }
721
722 #[test]
723 fn oscillator_square_is_one_or_neg_one() {
724 let s0 = oscillator(Waveform::Square, 0.25);
725 let s1 = oscillator(Waveform::Square, 0.75);
726 assert!((s0 - 1.0).abs() < 0.001);
727 assert!((s1 + 1.0).abs() < 0.001);
728 }
729
730 #[test]
731 fn biquad_low_pass_attenuates_high_freq() {
732 let mut f = BiquadFilter::low_pass(1000.0, 0.707);
733 let mut osc = Oscillator::sine(10_000.0);
735 for _ in 0..4800 { let s = osc.tick(); f.tick(s); }
737 let rms: f32 = (0..480).map(|_| { let s = osc.tick(); f.tick(s).powi(2) }).sum::<f32>() / 480.0;
738 let rms = rms.sqrt();
739 assert!(rms < 0.5, "rms was {rms}");
741 }
742
743 #[test]
744 fn midi_hz_roundtrip() {
745 let hz = midi_to_hz(69);
746 assert!((hz - 440.0).abs() < 0.01);
747 assert_eq!(hz_to_midi(440.0), 69);
748 }
749
750 #[test]
751 fn fm_voice_produces_output() {
752 let mut voice = FmVoice::new(220.0, 2.0, 1.5);
753 voice.note_on(220.0);
754 let samples: Vec<f32> = (0..100).map(|_| voice.tick()).collect();
755 let any_nonzero = samples.iter().any(|&s| s.abs() > 0.001);
756 assert!(any_nonzero);
757 }
758
759 #[test]
760 fn delay_line_delays_signal() {
761 let mut dl = DelayLine::new(100.0);
762 dl.set_delay_ms(10.0);
763 let delay_samples = (SAMPLE_RATE * 0.01) as usize;
764 dl.tick(1.0);
766 for _ in 1..delay_samples {
767 let out = dl.tick(0.0);
768 let _ = out;
769 }
770 let out = dl.tick(0.0);
771 assert!(out.abs() > 0.5, "Expected delayed impulse, got {out}");
772 }
773}