1pub 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#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct DspStatus {
52 pub profile: String,
53 pub eq: bool,
56 pub convolution_rate: Option<u32>,
60}
61
62#[derive(Debug, Clone, PartialEq)]
64pub struct Setup {
65 pub name: String,
66 preamp_db: Option<f64>,
67 filters: Vec<DspFilter>,
68 impulses: BTreeMap<u32, Vec<Impulse>>,
71}
72
73impl Setup {
74 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 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 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 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
158pub 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
174pub 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 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 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 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 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 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
308fn peak_gain(plan: &[Planned], impulse: Option<&Impulse>, channels: usize, rate: u32) -> f64 {
314 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
369struct Resample {
371 inner: Fft<f64>,
372 in_rate: u32,
373 out_rate: u32,
374 channels: usize,
375 pending: Vec<f64>,
377 scratch: Vec<f64>,
378 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 fn target(&self) -> u64 {
415 self.fed * self.out_rate as u64 / self.in_rate as u64
416 }
417
418 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 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 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 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 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 #[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 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 assert!(db < -140.0, "residual {db:.1} dB");
577 }
578
579 #[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 let ratio = rms(&out[24000..]) / rms(&input[24000..]);
635 assert!((ratio - 1.0).abs() < 0.02, "ratio {ratio}");
636
637 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 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 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 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}