Skip to main content

koan_core/audio/dsp/
mod.rs

1//! Equalisation and convolution, between the decoder and the ring buffer.
2//!
3//! The chain runs on the decode thread, so the render callback stays what it
4//! was: atomics and a ring buffer. With no profile for the output device there
5//! is no chain at all — samples reach the ring untouched, and bit-perfect stays
6//! something that can be checked.
7//!
8//! Order: preamp, resampling (only to reach an impulse response's rate), the
9//! profile's filters at the output rate in the order listed (`steps`), then
10//! convolution. Running the filters after resampling means their coefficients
11//! belong to the session rather than the track, so a gapless change of source
12//! rate keeps the filters' state.
13//!
14//! Stages that delay the audio — the resampler, and a linear-phase response's
15//! pre-ringing — have that delay trimmed from the front of the session and
16//! flushed at its end. Output frame `n` is then input frame `n` at the output
17//! rate, which is what the timeline counts and the playhead reads.
18
19pub mod apo;
20pub mod camilla;
21pub mod convolver;
22pub mod import;
23pub mod impulse;
24pub mod profiles;
25pub mod raw;
26mod steps;
27
28use std::collections::BTreeMap;
29use std::path::{Path, PathBuf};
30
31use realfft::RealFftPlanner;
32use rubato::audioadapter_buffers::direct::InterleavedSlice;
33use rubato::{Fft, FixedSync, Indexing, Resampler};
34use thiserror::Error;
35
36use impulse::{Convolve, read_audio};
37pub use impulse::{Impulse, Route};
38
39use steps::{Planned, Steps, gain_matrix, plan};
40
41use crate::config::{DspFilter, DspProfile};
42
43#[derive(Debug, Error)]
44pub enum DspError {
45    #[error("{}: {reason}", path.display())]
46    Impulse { path: PathBuf, reason: String },
47}
48
49/// What is being done to the audio, for the format badge.
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct DspStatus {
52    pub profile: String,
53    /// Filters ahead of convolution are running: bands, delays, mixes or
54    /// a graphic curve.
55    pub eq: bool,
56    /// The rate the impulse response in use was designed at, which is the
57    /// rate the output runs at. A source at another rate was resampled to
58    /// reach it.
59    pub convolution_rate: Option<u32>,
60}
61
62/// A profile ready to run, its impulse responses read from disk.
63#[derive(Debug, Clone, PartialEq)]
64pub struct Setup {
65    pub name: String,
66    preamp_db: Option<f64>,
67    filters: Vec<DspFilter>,
68    /// By rate. More than one at a rate where they are for different channel
69    /// counts.
70    impulses: BTreeMap<u32, Vec<Impulse>>,
71}
72
73impl Setup {
74    /// `None` for a profile that would leave the audio as it is.
75    pub fn load(profile: &DspProfile, base: &Path) -> Result<Option<Self>, DspError> {
76        let mut impulses: BTreeMap<u32, Vec<Impulse>> = BTreeMap::new();
77        for path in &profile.impulses {
78            for impulse in load_impulses(path, base)? {
79                impulses.entry(impulse.rate).or_default().push(impulse);
80            }
81        }
82        if profile.filters.is_empty()
83            && impulses.is_empty()
84            && profile.preamp_db.unwrap_or(0.0) == 0.0
85        {
86            return Ok(None);
87        }
88        Ok(Some(Self {
89            name: profile.name.clone(),
90            preamp_db: profile.preamp_db,
91            filters: profile.filters.clone(),
92            impulses,
93        }))
94    }
95
96    /// The preamp at `rate` for `channels`: the profile's own, or the one
97    /// derived from the peak gain of its bands and response there.
98    pub fn preamp_db(&self, rate: u32, channels: usize) -> f64 {
99        let impulse = self
100            .impulses
101            .get(&rate)
102            .and_then(|at_rate| at_rate.iter().find(|i| i.fits(channels)));
103        self.preamp_db.unwrap_or_else(|| {
104            let plan = plan(&self.filters, rate, channels);
105            -20.0 * peak_gain(&plan, impulse, channels, rate).log10().max(0.0)
106        })
107    }
108
109    #[cfg(test)]
110    pub(crate) fn with_preamp(mut self, db: f64) -> Self {
111        self.preamp_db = Some(db);
112        self
113    }
114
115    #[cfg(test)]
116    pub(crate) fn new(filters: Vec<DspFilter>, impulses: Vec<Impulse>) -> Self {
117        Self {
118            name: "test".into(),
119            preamp_db: None,
120            filters,
121            impulses: impulses.into_iter().fold(BTreeMap::new(), |mut m, i| {
122                m.entry(i.rate).or_insert_with(Vec::new).push(i);
123                m
124            }),
125        }
126    }
127
128    /// The rate a source at `source` plays at: its own, unless convolution
129    /// needs another. A response exported at the source's rate is used as it
130    /// is; otherwise the nearest one, and the audio is resampled to it.
131    /// Resampling the response instead would low-pass hi-res material at the
132    /// response's Nyquist.
133    pub fn output_rate(&self, source: u32) -> u32 {
134        if self.impulses.is_empty() || self.impulses.contains_key(&source) {
135            return source;
136        }
137        *self
138            .impulses
139            .keys()
140            .min_by_key(|&&r| (r.abs_diff(source), std::cmp::Reverse(r)))
141            .expect("not empty")
142    }
143
144    /// Rates there are responses for.
145    pub fn rates(&self) -> Vec<u32> {
146        self.impulses.keys().copied().collect()
147    }
148
149    pub fn status(&self, source: u32) -> DspStatus {
150        DspStatus {
151            profile: self.name.clone(),
152            eq: !self.filters.is_empty(),
153            convolution_rate: (!self.impulses.is_empty()).then(|| self.output_rate(source)),
154        }
155    }
156}
157
158/// The responses one entry of a profile's `impulses` names: a WAV (or any
159/// audio file) at its own rate, or a Convolver `.cfg`.
160pub fn load_impulses(path: &Path, base: &Path) -> Result<Vec<Impulse>, DspError> {
161    let path = if path.is_absolute() {
162        path.to_path_buf()
163    } else {
164        base.join(path)
165    };
166    let read = if convolver::is_cfg(&path) {
167        convolver::read(&path)
168    } else {
169        read_audio(&path).map(|(rate, channels)| vec![Impulse::from_channels(rate, channels)])
170    };
171    read.map_err(|reason| DspError::Impulse { path, reason })
172}
173
174/// The processing for one session, built for the first track's format.
175///
176/// Samples become 64-bit floats on the way in and 32-bit on the way out, and
177/// nothing is rounded between: resampling, filters and convolution all run in
178/// 64-bit, as CamillaDSP and Roon do. `null_test_against_direct_convolution`
179/// measures what is left.
180pub struct Chain {
181    channels: usize,
182    out_rate: u32,
183    gain: f64,
184    resample: Option<Resample>,
185    steps: Steps,
186    convolve: Option<Convolve>,
187    input: Vec<f64>,
188    work: Vec<f64>,
189    out: Vec<f32>,
190}
191
192impl Chain {
193    pub fn new(setup: &Setup, source_rate: u32, channels: u16) -> Self {
194        let channels = channels as usize;
195        let out_rate = setup.output_rate(source_rate);
196        let impulse = setup.impulses.get(&out_rate).and_then(|at_rate| {
197            let fitting = at_rate.iter().find(|i| i.fits(channels));
198            if fitting.is_none() {
199                log::warn!(
200                    "dsp: no impulse at {out_rate}Hz for {channels} channels; convolution skipped"
201                );
202            }
203            fitting
204        });
205
206        let preamp_db = setup.preamp_db(out_rate, channels);
207        log::info!(
208            "dsp: '{}' at {out_rate}Hz — {} filters, preamp {preamp_db:.2} dB{}",
209            setup.name,
210            setup.filters.len(),
211            impulse.map_or(String::new(), |i| format!(
212                ", {} routes of {} taps, {} frames delay",
213                i.routes.len(),
214                i.taps(),
215                i.delay()
216            ))
217        );
218
219        Self {
220            channels,
221            out_rate,
222            gain: 10f64.powf(preamp_db / 20.0),
223            resample: Resample::new(source_rate, out_rate, channels),
224            steps: Steps::new(plan(&setup.filters, out_rate, channels), out_rate, channels),
225            convolve: impulse.map(|i| Convolve::new(i, channels)),
226            input: Vec::new(),
227            work: Vec::new(),
228            out: Vec::new(),
229        }
230    }
231
232    pub fn output_rate(&self) -> u32 {
233        self.out_rate
234    }
235
236    /// Carry the chain into a track at `source_rate`, the output rate
237    /// unchanged. Returns the outgoing resampler's tail, which belongs to the
238    /// track before and is still to be written.
239    pub fn set_source_rate(&mut self, source_rate: u32) -> &[f32] {
240        self.work.clear();
241        if self.resample.as_ref().map_or(self.out_rate, |r| r.in_rate) != source_rate {
242            if let Some(mut r) = self.resample.take() {
243                r.flush(&mut self.work);
244            }
245            self.resample = Resample::new(source_rate, self.out_rate, self.channels);
246            self.post();
247        }
248        self.emit()
249    }
250
251    /// Process interleaved `input`. Returns what is ready to be written, and
252    /// how many samples of output time `input` amounts to — the figure the
253    /// timeline counts, which with a resampler is not what came out this call.
254    pub fn process(&mut self, input: &[f32]) -> (&[f32], u64) {
255        self.input.clear();
256        self.input.extend(input.iter().map(|&s| s as f64));
257        self.work.clear();
258        let length = match self.resample.as_mut() {
259            Some(r) => {
260                r.run(&self.input, &mut self.work);
261                r.counted() * self.channels as u64
262            }
263            None => {
264                self.work.extend_from_slice(&self.input);
265                input.len() as u64
266            }
267        };
268        self.post();
269        (self.emit(), length)
270    }
271
272    /// What the chain still holds at the end of a session.
273    pub fn flush(&mut self) -> &[f32] {
274        self.work.clear();
275        if let Some(r) = self.resample.as_mut() {
276            r.flush(&mut self.work);
277        }
278        self.post();
279        if let Some(c) = self.convolve.as_mut() {
280            c.flush(&mut self.work);
281        }
282        self.emit()
283    }
284
285    /// Gain, filters and convolution over `self.work`, at the output rate.
286    fn post(&mut self) {
287        if self.gain != 1.0 {
288            for s in &mut self.work {
289                *s *= self.gain;
290            }
291        }
292        if !self.steps.is_empty() {
293            self.steps.run(&mut self.work);
294        }
295        if let Some(c) = self.convolve.as_mut() {
296            c.run(&mut self.work);
297        }
298    }
299
300    /// `self.work` as the ring buffer takes it.
301    fn emit(&mut self) -> &[f32] {
302        self.out.clear();
303        self.out.extend(self.work.iter().map(|&s| s as f32));
304        &self.out
305    }
306}
307
308/// The largest gain, linear, the filters and the response apply to any channel
309/// at any frequency. A preamp of its inverse keeps a full-scale sine at that
310/// frequency at full scale, as AutoEQ's `Preamp` line does. Whatever sums into
311/// one channel — a mix, routes into one output — is added by magnitude, which
312/// bounds what it can reach.
313fn peak_gain(plan: &[Planned], impulse: Option<&Impulse>, channels: usize, rate: u32) -> f64 {
314    // Each channel's gain at `w` with every input at full scale, in phase.
315    let channel_gains = |w: f64| -> Vec<f64> {
316        gain_matrix(plan, channels.max(1), w, rate)
317            .iter()
318            .map(|row| row.iter().sum())
319            .collect()
320    };
321    let Some(impulse) = impulse else {
322        let (lo, hi) = (10f64.ln(), (rate as f64 * 0.499).ln());
323        return (0..=4096)
324            .flat_map(|i| {
325                let f = (lo + (hi - lo) * i as f64 / 4096.0).exp();
326                let w = std::f64::consts::TAU * f / rate as f64;
327                channel_gains(w)
328            })
329            .fold(0.0, f64::max);
330    };
331    let routes = impulse.routes_for(channels);
332    let size = impulse.taps().next_power_of_two().max(8192);
333    let fft = RealFftPlanner::<f64>::new().plan_fft_forward(size);
334    let mut spectrum = fft.make_output_vec();
335    let mut per_output = vec![vec![0.0f64; spectrum.len()]; channels];
336    let gains: Vec<Vec<f64>> = if plan.is_empty() {
337        Vec::new()
338    } else {
339        (0..spectrum.len())
340            .map(|k| channel_gains(std::f64::consts::TAU * k as f64 / size as f64))
341            .collect()
342    };
343    for r in &routes {
344        let mut input = fft.make_input_vec();
345        for (d, &s) in input.iter_mut().zip(&r.ir) {
346            *d = s as f64;
347        }
348        if fft.process(&mut input, &mut spectrum).is_err() {
349            continue;
350        }
351        for (k, bin) in spectrum.iter().enumerate() {
352            let fed: f64 = r
353                .inputs
354                .iter()
355                .map(|&(c, g)| {
356                    g.abs() as f64 * gains.get(k).and_then(|g| g.get(c)).map_or(1.0, |&g| g)
357                })
358                .sum();
359            for &(o, g) in &r.outputs {
360                if let Some(out) = per_output.get_mut(o) {
361                    out[k] += bin.norm() * fed * g.abs() as f64;
362                }
363            }
364        }
365    }
366    per_output.iter().flatten().copied().fold(0.0, f64::max)
367}
368
369/// Sample-rate conversion to an impulse response's rate.
370struct Resample {
371    inner: Fft<f64>,
372    in_rate: u32,
373    out_rate: u32,
374    channels: usize,
375    /// Interleaved input not yet a whole chunk.
376    pending: Vec<f64>,
377    scratch: Vec<f64>,
378    /// Output frames of the resampler's own delay still to drop.
379    skip: usize,
380    fed: u64,
381    emitted: u64,
382    counted: u64,
383}
384
385impl Resample {
386    fn new(in_rate: u32, out_rate: u32, channels: usize) -> Option<Self> {
387        if in_rate == out_rate {
388            return None;
389        }
390        let inner = Fft::<f64>::new(
391            in_rate as usize,
392            out_rate as usize,
393            1024,
394            channels,
395            FixedSync::Input,
396        )
397        .inspect_err(|e| log::error!("dsp: no resampler {in_rate}→{out_rate}Hz: {e}"))
398        .ok()?;
399        Some(Self {
400            skip: inner.output_delay(),
401            scratch: vec![0.0; inner.output_frames_max() * channels],
402            inner,
403            in_rate,
404            out_rate,
405            channels,
406            pending: Vec::new(),
407            fed: 0,
408            emitted: 0,
409            counted: 0,
410        })
411    }
412
413    /// Output frames `fed` input frames amount to.
414    fn target(&self) -> u64 {
415        self.fed * self.out_rate as u64 / self.in_rate as u64
416    }
417
418    /// Output frames of time fed since the last call.
419    fn counted(&mut self) -> u64 {
420        let target = self.target();
421        let new = target - self.counted;
422        self.counted = target;
423        new
424    }
425
426    fn run(&mut self, input: &[f64], dst: &mut Vec<f64>) {
427        self.fed += (input.len() / self.channels) as u64;
428        self.pending.extend_from_slice(input);
429        loop {
430            let need = self.inner.input_frames_next();
431            if self.pending.len() / self.channels < need {
432                break;
433            }
434            self.chunk(need, None, dst);
435            self.pending.drain(..need * self.channels);
436        }
437    }
438
439    /// Everything still held, padded with silence until the output reaches
440    /// the length of what was fed.
441    fn flush(&mut self, dst: &mut Vec<f64>) {
442        let start = dst.len();
443        let target = self.target();
444        let mut partial = self.pending.len() / self.channels;
445        while self.emitted < target {
446            let need = self.inner.input_frames_next();
447            self.pending.resize(need * self.channels, 0.0);
448            if self.chunk(need, Some(partial), dst) == 0 {
449                break;
450            }
451            partial = 0;
452        }
453        self.pending.clear();
454        let over = (self.emitted - target) as usize * self.channels;
455        dst.truncate((dst.len() - over).max(start));
456        self.emitted = target;
457    }
458
459    /// Resample one chunk into `dst`. Returns the frames the resampler made,
460    /// its delay included.
461    fn chunk(&mut self, need: usize, partial: Option<usize>, dst: &mut Vec<f64>) -> usize {
462        let ch = self.channels;
463        let frames_out = self.scratch.len() / ch;
464        let (Ok(input), Ok(mut output)) = (
465            InterleavedSlice::new(&self.pending[..need * ch], ch, need),
466            InterleavedSlice::new_mut(&mut self.scratch, ch, frames_out),
467        ) else {
468            return 0;
469        };
470        let indexing = partial.map(|p| Indexing::new().partial_len(p));
471        match self
472            .inner
473            .process_into_buffer(&input, &mut output, indexing.as_ref())
474        {
475            Ok((_, produced)) => {
476                let drop = self.skip.min(produced);
477                self.skip -= drop;
478                dst.extend_from_slice(&self.scratch[drop * ch..produced * ch]);
479                self.emitted += (produced - drop) as u64;
480                produced
481            }
482            Err(e) => {
483                log::error!("dsp: resampling failed: {e}");
484                0
485            }
486        }
487    }
488}
489
490#[cfg(test)]
491mod tests {
492    use super::*;
493    use crate::config::{EqFilter, EqFilterKind};
494
495    fn band(kind: EqFilterKind, freq: f64, gain_db: f64, q: f64) -> EqFilter {
496        EqFilter {
497            kind,
498            freq,
499            gain_db,
500            q,
501            channels: vec![],
502        }
503    }
504
505    fn sine(rate: u32, freq: f64, frames: usize, channels: usize, amp: f32) -> Vec<f32> {
506        (0..frames)
507            .flat_map(|i| {
508                let v = amp * (std::f64::consts::TAU * freq * i as f64 / rate as f64).sin() as f32;
509                std::iter::repeat_n(v, channels)
510            })
511            .collect()
512    }
513
514    fn rms(s: &[f32]) -> f64 {
515        (s.iter().map(|&v| (v as f64).powi(2)).sum::<f64>() / s.len() as f64).sqrt()
516    }
517
518    /// Run `input` through in packets, as the decoder hands it over.
519    fn run_all(chain: &mut Chain, input: &[f32], packet: usize) -> (Vec<f32>, u64) {
520        let mut out = Vec::new();
521        let mut counted = 0;
522        for p in input.chunks(packet) {
523            let (o, n) = chain.process(p);
524            out.extend_from_slice(o);
525            counted += n;
526        }
527        out.extend_from_slice(chain.flush());
528        (out, counted)
529    }
530
531    /// Deterministic noise in [-1, 1).
532    fn noise(seed: &mut u64) -> f64 {
533        *seed = seed
534            .wrapping_mul(6_364_136_223_846_793_005)
535            .wrapping_add(1_442_695_040_888_963_407);
536        ((*seed >> 11) as f64 / (1u64 << 53) as f64) * 2.0 - 1.0
537    }
538
539    /// What is left after subtracting a textbook convolution — every output
540    /// sample summed tap by tap in 64-bit — from the chain's. The partitioned
541    /// FFT is the same sum reordered, so the residual is rounding: what the
542    /// 64-bit chain adds, and the 32-bit floats it hands the ring buffer.
543    #[test]
544    fn null_test_against_direct_convolution() {
545        let (rate, taps, frames, peak) = (48000, 4096, 8192, 200);
546        let mut seed = 1;
547        let mut ir: Vec<f32> = (0..taps)
548            .map(|i| (noise(&mut seed) * 0.2 * (-(i as f64) / 600.0).exp()) as f32)
549            .collect();
550        ir[peak] = 1.0;
551        let input: Vec<f32> = (0..frames * 2)
552            .map(|_| (noise(&mut seed) * 0.25) as f32)
553            .collect();
554        let mut setup = Setup::new(vec![], vec![Impulse::from_channels(rate, vec![ir.clone()])]);
555        setup.preamp_db = Some(0.0);
556        let mut chain = Chain::new(&setup, rate, 2);
557        let (out, _) = run_all(&mut chain, &input, 1152);
558        assert_eq!(out.len(), input.len());
559
560        let (mut err, mut sig) = (0.0f64, 0.0f64);
561        for c in 0..2 {
562            for n in 0..frames {
563                // Output frame n is input frame n: the peak's delay is trimmed.
564                let m = n + peak;
565                let y: f64 = (0..taps)
566                    .filter(|&k| k <= m && m - k < frames)
567                    .map(|k| ir[k] as f64 * input[(m - k) * 2 + c] as f64)
568                    .sum();
569                err += (out[n * 2 + c] as f64 - y).powi(2);
570                sig += y.powi(2);
571            }
572        }
573        let db = 10.0 * (err / sig).log10();
574        eprintln!("residual against direct convolution: {db:.1} dB");
575        // 32-bit output rounding alone sits near -150 dB.
576        assert!(db < -140.0, "residual {db:.1} dB");
577    }
578
579    /// The heaviest response there is: 262,145 taps a channel at 192 kHz, as
580    /// the Harman 780 Roon pack ships. Run it in release:
581    /// `cargo test --release -p koan-core --lib bench_long_response -- --ignored --nocapture`
582    #[test]
583    #[ignore]
584    fn bench_long_response() {
585        let (rate, taps, secs) = (192000, 262_145, 10);
586        let mut seed = 7;
587        let ir: Vec<Vec<f32>> = (0..2)
588            .map(|_| {
589                (0..taps)
590                    .map(|i| (noise(&mut seed) * (-(i as f64) / 20000.0).exp()) as f32)
591                    .collect()
592            })
593            .collect();
594        let input: Vec<f32> = (0..rate as usize * secs * 2)
595            .map(|_| (noise(&mut seed) * 0.25) as f32)
596            .collect();
597        let setup = Setup::new(vec![], vec![Impulse::from_channels(rate, ir)]);
598        let started = std::time::Instant::now();
599        let mut chain = Chain::new(&setup, rate, 2);
600        let built = started.elapsed();
601        let started = std::time::Instant::now();
602        for p in input.chunks(4096 * 2) {
603            chain.process(p);
604        }
605        let took = started.elapsed();
606        eprintln!(
607            "{taps} taps at {rate} Hz, stereo: built in {built:?}; {secs} s of audio in {took:?}, {:.1}x real time, {:.1}% of one core",
608            secs as f64 / took.as_secs_f64(),
609            100.0 * took.as_secs_f64() / secs as f64
610        );
611    }
612
613    #[test]
614    fn a_profile_that_changes_nothing_is_no_chain() {
615        let profile = DspProfile {
616            name: "flat".into(),
617            ..Default::default()
618        };
619        assert!(Setup::load(&profile, Path::new("/")).unwrap().is_none());
620    }
621
622    #[test]
623    fn a_peaking_band_boosts_its_frequency_and_the_preamp_pays_for_it() {
624        let setup = Setup::new(
625            vec![band(EqFilterKind::Peaking, 1000.0, 6.0, 1.0).into()],
626            vec![],
627        );
628        let mut chain = Chain::new(&setup, 48000, 2);
629        let input = sine(48000, 1000.0, 48000, 2, 0.5);
630        let (out, counted) = run_all(&mut chain, &input, 4096);
631        assert_eq!(out.len(), input.len());
632        assert_eq!(counted, input.len() as u64);
633        // +6 dB at the centre, -6 dB of derived preamp: unity.
634        let ratio = rms(&out[24000..]) / rms(&input[24000..]);
635        assert!((ratio - 1.0).abs() < 0.02, "ratio {ratio}");
636
637        // Far from the band only the preamp is left.
638        let mut chain = Chain::new(&setup, 48000, 2);
639        let input = sine(48000, 100.0, 48000, 2, 0.5);
640        let (out, _) = run_all(&mut chain, &input, 4096);
641        let db = 20.0 * (rms(&out[24000..]) / rms(&input[24000..])).log10();
642        assert!((db + 6.0).abs() < 0.2, "{db} dB");
643    }
644
645    #[test]
646    fn a_band_past_nyquist_is_skipped_not_fatal() {
647        let setup = Setup::new(
648            vec![band(EqFilterKind::Peaking, 30000.0, 6.0, 1.0).into()],
649            vec![],
650        );
651        let mut chain = Chain::new(&setup, 44100, 2);
652        let input = sine(44100, 1000.0, 4410, 2, 0.5);
653        let (out, _) = run_all(&mut chain, &input, 1024);
654        assert_eq!(out, input);
655    }
656
657    fn delayed_impulse(rate: u32, delay: usize) -> Impulse {
658        let mut ir = vec![0.0; delay * 2 + 1];
659        ir[delay] = 1.0;
660        Impulse::from_channels(rate, vec![ir])
661    }
662
663    #[test]
664    fn convolution_delay_is_trimmed_and_the_tail_flushed() {
665        let setup = Setup::new(vec![], vec![delayed_impulse(48000, 300)]);
666        let mut chain = Chain::new(&setup, 48000, 2);
667        let input = sine(48000, 440.0, 10000, 2, 0.5);
668        let (out, counted) = run_all(&mut chain, &input, 1152);
669        assert_eq!(out.len(), input.len());
670        assert_eq!(counted, input.len() as u64);
671        for (a, b) in out.iter().zip(&input) {
672            assert!((a - b).abs() < 1e-4);
673        }
674    }
675
676    #[test]
677    fn a_source_without_its_own_response_is_resampled_to_the_nearest() {
678        let setup = Setup::new(
679            vec![],
680            vec![delayed_impulse(44100, 0), delayed_impulse(48000, 0)],
681        );
682        assert_eq!(setup.output_rate(44100), 44100);
683        assert_eq!(setup.output_rate(96000), 48000);
684        assert_eq!(setup.output_rate(88200), 48000);
685        assert_eq!(setup.output_rate(22050), 44100);
686        assert_eq!(
687            setup.status(96000),
688            DspStatus {
689                profile: "test".into(),
690                eq: false,
691                convolution_rate: Some(48000)
692            }
693        );
694
695        let mut chain = Chain::new(&setup, 96000, 2);
696        assert_eq!(chain.output_rate(), 48000);
697        let input = sine(96000, 1000.0, 96000, 2, 0.5);
698        let (out, counted) = run_all(&mut chain, &input, 4096);
699        assert_eq!(out.len(), 48000 * 2);
700        assert_eq!(counted, 48000 * 2);
701        let ratio = rms(&out[4800..43200]) / rms(&input[9600..86400]);
702        assert!((ratio - 1.0).abs() < 0.02, "ratio {ratio}");
703        // Delay trimmed: the output starts in phase with the input.
704        let expect = sine(48000, 1000.0, 2000, 2, 0.5);
705        let err = out[2000..4000]
706            .iter()
707            .zip(&expect[2000..4000])
708            .map(|(a, b)| (a - b).abs())
709            .fold(0.0f32, f32::max);
710        assert!(err < 0.02, "max error {err}");
711    }
712
713    #[test]
714    fn a_gapless_rate_change_keeps_the_output_rate_and_its_length() {
715        let setup = Setup::new(vec![], vec![delayed_impulse(48000, 64)]);
716        let mut chain = Chain::new(&setup, 44100, 2);
717        let first = sine(44100, 500.0, 44100, 2, 0.5);
718        let mut out = Vec::new();
719        let mut counted = 0;
720        for p in first.chunks(4096) {
721            let (o, n) = chain.process(p);
722            out.extend_from_slice(o);
723            counted += n;
724        }
725        out.extend_from_slice(chain.set_source_rate(48000));
726        let second = sine(48000, 500.0, 48000, 2, 0.5);
727        for p in second.chunks(4096) {
728            let (o, n) = chain.process(p);
729            out.extend_from_slice(o);
730            counted += n;
731        }
732        out.extend_from_slice(chain.flush());
733        assert_eq!(counted, 96000 * 2);
734        assert_eq!(out.len(), 96000 * 2);
735    }
736
737    #[test]
738    fn the_preamp_covers_the_response_gain() {
739        let mut ir = vec![0.0; 64];
740        ir[0] = 2.0;
741        let setup = Setup::new(vec![], vec![Impulse::from_channels(48000, vec![ir])]);
742        let mut chain = Chain::new(&setup, 48000, 1);
743        let (out, _) = run_all(&mut chain, &[0.5; 4800], 480);
744        assert!(out.iter().all(|&s| (s - 0.5).abs() < 1e-4));
745    }
746
747    #[test]
748    fn bands_for_one_channel_leave_the_other_alone() {
749        let mut b = band(EqFilterKind::Gain, 1000.0, -6.0, 1.0);
750        b.channels = vec![1];
751        let mut setup = Setup::new(vec![b.into()], vec![]);
752        setup.preamp_db = Some(0.0);
753        let mut chain = Chain::new(&setup, 48000, 2);
754        let (out, _) = run_all(&mut chain, &[0.5, 0.5, 0.5, 0.5], 4);
755        assert_eq!(out[0], 0.5);
756        assert!((out[1] - 0.25).abs() < 1e-3);
757    }
758
759    #[test]
760    fn a_route_can_feed_one_channel_into_the_other() {
761        // Left passes; right is left at half, plus right.
762        let impulse = Impulse {
763            rate: 48000,
764            channels: Some(2),
765            routes: vec![
766                Route {
767                    ir: vec![1.0],
768                    inputs: vec![(0, 1.0)],
769                    outputs: vec![(0, 1.0)],
770                },
771                Route {
772                    ir: vec![1.0],
773                    inputs: vec![(0, 0.5), (1, 1.0)],
774                    outputs: vec![(1, 1.0)],
775                },
776            ],
777            in_delays: vec![],
778            out_delays: vec![0, 1],
779        };
780        let mut setup = Setup::new(vec![], vec![impulse]);
781        setup.preamp_db = Some(0.0);
782        let mut chain = Chain::new(&setup, 48000, 2);
783        let (out, _) = run_all(&mut chain, &[0.4, 0.2, 0.0, 0.0], 4);
784        // The right output is a frame late, by its delay.
785        assert_eq!(out, vec![0.4, 0.0, 0.0, 0.4]);
786    }
787
788    #[test]
789    fn an_impulse_is_read_from_a_wav_at_its_own_rate() {
790        let dir = std::env::temp_dir().join(format!("koan-dsp-{}", std::process::id()));
791        std::fs::create_dir_all(&dir).unwrap();
792        let path = dir.join("ir.wav");
793        let frames: [i16; 8] = [0, 0, 16384, 16384, 0, 0, 0, 0];
794        let mut wav = Vec::new();
795        let data_len = (frames.len() * 2) as u32;
796        wav.extend_from_slice(b"RIFF");
797        wav.extend_from_slice(&(36 + data_len).to_le_bytes());
798        wav.extend_from_slice(b"WAVEfmt ");
799        wav.extend_from_slice(&16u32.to_le_bytes());
800        wav.extend_from_slice(&1u16.to_le_bytes());
801        wav.extend_from_slice(&2u16.to_le_bytes());
802        wav.extend_from_slice(&96000u32.to_le_bytes());
803        wav.extend_from_slice(&(96000u32 * 4).to_le_bytes());
804        wav.extend_from_slice(&4u16.to_le_bytes());
805        wav.extend_from_slice(&16u16.to_le_bytes());
806        wav.extend_from_slice(b"data");
807        wav.extend_from_slice(&data_len.to_le_bytes());
808        for f in frames {
809            wav.extend_from_slice(&f.to_le_bytes());
810        }
811        std::fs::write(&path, wav).unwrap();
812
813        let profile = DspProfile {
814            name: "room".into(),
815            impulses: vec!["ir.wav".into()],
816            ..Default::default()
817        };
818        let setup = Setup::load(&profile, &dir).unwrap().unwrap();
819        let impulse = &setup.impulses[&96000][0];
820        assert_eq!(impulse.channels, Some(2));
821        assert_eq!(impulse.routes[0].ir, vec![0.0, 0.5, 0.0, 0.0]);
822        assert_eq!(impulse.delay(), 1);
823
824        let missing = DspProfile {
825            impulses: vec!["nope.wav".into()],
826            ..profile
827        };
828        assert!(Setup::load(&missing, &dir).is_err());
829    }
830}