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