1pub mod apo;
20pub mod autoeq;
21pub mod autoeq_squig;
22pub mod camilla;
23pub mod convolver;
24pub mod import;
25pub mod impulse;
26pub mod klippel;
27#[cfg(test)]
28mod null;
29pub mod profiles;
30pub mod raw;
31pub mod squig;
32mod steps;
33pub mod targets;
34
35use std::collections::BTreeMap;
36use std::path::{Path, PathBuf};
37
38use realfft::RealFftPlanner;
39use rubato::audioadapter_buffers::direct::InterleavedSlice;
40use rubato::{Fft, FixedSync, Indexing, Resampler};
41use thiserror::Error;
42
43use impulse::{Convolve, read_audio};
44pub use impulse::{Impulse, Route};
45
46use steps::{Planned, Steps, gain_matrix, plan};
47
48use crate::config::{DspFilter, DspProfile};
49
50#[derive(Debug, Error)]
51pub enum DspError {
52 #[error("{}: {reason}", path.display())]
53 Impulse { path: PathBuf, reason: String },
54 #[error("no target called {0}")]
56 Target(String),
57 #[error("{0}")]
60 Layer(String),
61 #[error("{0}'s measurement is missing")]
63 Measurement(String),
64}
65
66const MAX_LAYER_DEPTH: usize = 8;
68const MAX_CHAIN_FILTERS: usize = 256;
72const MAX_LAYER_VISITS: usize = 1024;
75
76pub fn response(filters: &[DspFilter], freqs: &[f64], rate: u32) -> Vec<f64> {
81 response_on(filters, freqs, rate, 0)
82}
83
84pub fn response_on(filters: &[DspFilter], freqs: &[f64], rate: u32, channel: usize) -> Vec<f64> {
86 let plan = steps::plan(filters, rate, 2);
87 freqs
88 .iter()
89 .map(|&hz| {
90 let w = std::f64::consts::TAU * hz / rate as f64;
91 let gain: f64 = steps::gain_matrix(&plan, 2, w, rate)[channel].iter().sum();
92 20.0 * gain.max(1e-6).log10()
93 })
94 .collect()
95}
96
97pub struct OutputChain {
100 pub profile: DspProfile,
101 pub all: Vec<DspProfile>,
102 pub name: String,
105 pub left_out: Option<String>,
108 pub left_out_eqs: Vec<String>,
111 pub eq_notes: Vec<(String, String)>,
113 pub tuning_plays: bool,
115 pub refit: Option<(String, String)>,
118}
119
120pub fn output_chain(dsp: &crate::config::DspConfig, device: &str) -> Option<OutputChain> {
141 let refitted = output_chain_with(dsp, device, true)?;
142 let Some((_, by)) = &refitted.refit else {
143 return Some(refitted);
144 };
145 let plain = output_chain_with(dsp, device, false)?;
146 let worse = refitted.left_out_eqs.contains(by)
147 || refitted.left_out_eqs.len() > plain.left_out_eqs.len()
148 || refitted.eq_notes.len() > plain.eq_notes.len();
149 Some(if worse { plain } else { refitted })
150}
151
152fn output_chain_with(
153 dsp: &crate::config::DspConfig,
154 device: &str,
155 may_refit: bool,
156) -> Option<OutputChain> {
157 use crate::config::{DspLayer, DspRole, dsp_bounds};
158 let all = &dsp.profiles;
159 let chosen = dsp.profile_for(device);
160 let correction = chosen.map(|c| member_playing(c, all));
161 let baked = chosen.is_some_and(|c| profiles::shown_role(c, all) == DspRole::Baked);
162 let mut notes: Vec<(String, String)> = Vec::new();
163 let mut dropped: Vec<String> = Vec::new();
164 let mut tunings: Vec<&DspProfile> = Vec::new();
166 if dsp.enabled {
167 for entry in dsp.tunings.iter().filter(|t| t.device == device && t.on) {
168 let why = match all.iter().find(|p| p.name == entry.tuning) {
169 None => Some("there is no EQ by that name".to_owned()),
170 Some(t) if !responses(t, all).is_empty() => {
171 Some("an EQ with impulse responses is never a tuning".to_owned())
172 }
173 Some(t) if profiles::shown_role(t, all) != DspRole::Tuning => {
174 Some("it is no longer a tuning".to_owned())
175 }
176 Some(_) => match (chosen, correction) {
177 (Some(c), _) if baked => Some(format!("{} already includes a tuning", c.name)),
178 (Some(c), Some(m)) if m.group => {
181 Some(format!("{} has no member playing", c.name))
182 }
183 _ => None,
184 },
185 };
186 match why {
187 Some(why) => {
188 notes.push((
189 entry.tuning.clone(),
190 format!("{} is left out: {why}.", entry.tuning),
191 ));
192 dropped.push(entry.tuning.clone());
193 }
194 None => tunings.extend(all.iter().find(|p| p.name == entry.tuning)),
195 }
196 }
197 }
198 let name = match chosen.or(tunings.first().copied()) {
199 Some(p) => p.name.clone(),
200 None => dsp
202 .tunings
203 .iter()
204 .find(|t| t.device == device && t.on && !notes.is_empty())?
205 .tuning
206 .clone(),
207 };
208 let said = |notes: &[(String, String)]| {
209 (!notes.is_empty()).then(|| {
210 notes
211 .iter()
212 .map(|(_, n)| n.as_str())
213 .collect::<Vec<_>>()
214 .join(" ")
215 })
216 };
217 let alone = |notes: &[(String, String)], dropped: &[String]| -> Option<OutputChain> {
218 let left_out = said(notes);
219 Some(match correction {
220 Some(c) => OutputChain {
221 profile: c.clone(),
222 all: all.clone(),
223 name: name.clone(),
224 left_out,
225 left_out_eqs: dropped.to_vec(),
226 eq_notes: notes.to_vec(),
227 tuning_plays: false,
228 refit: None,
229 },
230 None if left_out.is_some() => OutputChain {
233 profile: DspProfile {
234 name: name.clone(),
235 ..Default::default()
236 },
237 all: all.clone(),
238 name: name.clone(),
239 left_out,
240 left_out_eqs: dropped.to_vec(),
241 eq_notes: notes.to_vec(),
242 tuning_plays: false,
243 refit: None,
244 },
245 None => return None,
246 })
247 };
248 if tunings.is_empty() {
249 return alone(¬es, &dropped);
250 }
251 let curves = |p: &DspProfile| {
254 chain(p, all, &mut Vec::new()).map_or(0, |f| {
255 f.iter()
256 .filter(|f| matches!(f, DspFilter::Graphic(_)))
257 .count()
258 })
259 };
260 let mut held = correction.map_or(0, curves);
261 let mut kept = Vec::new();
262 for t in tunings {
263 let c = curves(t);
264 if held + c > dsp_bounds::CHAIN_GRAPHICS {
265 notes.push((
266 t.name.clone(),
267 format!(
268 "{} is left out: with what plays before it, it would play more graphic curves than a chain holds.",
269 t.name
270 ),
271 ));
272 dropped.push(t.name.clone());
273 continue;
274 }
275 held += c;
276 kept.push(t);
277 }
278 let aim = correction.and_then(|c| profiles::aims_at(c, all));
282 let refit = correction
289 .zip(aim.as_ref())
290 .filter(|_| may_refit)
291 .and_then(|(c, aim)| {
292 kept.iter()
293 .filter_map(|t| Some((made_against(t, all, 0)?, t.name.clone())))
294 .find(|(made, _)| made != aim && targets::same_ear(aim, made))
295 .and_then(|(made, by)| Some((profiles::fitted_to(c, &made)?, made, by)))
296 });
297 let (refit_filters, refit) = match refit {
298 Some((filters, made, by)) => (Some(filters), Some((made, by))),
299 None => (None, None),
300 };
301 let aim = refit.as_ref().map(|(made, _)| made.clone()).or(aim);
302 let mut layers: Vec<(Option<DspProfile>, DspProfile, String)> = Vec::new();
303 for (i, t) in kept.into_iter().enumerate() {
304 let mut on_top = t.clone();
305 on_top.name = format!("\u{1}{i}\u{1}{}", t.name);
307 on_top.devices.clear();
308 let wanted = aim
309 .clone()
310 .zip(made_against(t, all, 0))
311 .filter(|(aim, made)| aim != made && targets::same_ear(aim, made));
312 let step = match wanted {
313 Some(_) if held >= dsp_bounds::CHAIN_GRAPHICS => {
314 notes.push((
315 t.name.clone(),
316 format!(
317 "{} plays without the target difference: the chain's graphic curves are full, so it may not sound as made.",
318 t.name
319 ),
320 ));
321 None
322 }
323 Some((aim, made)) => {
324 let step = targets::choice_curve(&aim)
325 .zip(targets::choice_curve(&made))
326 .zip(correction)
327 .map(|((from, to), c)| DspProfile {
328 name: format!("\u{1}{i}\u{1}Target difference"),
329 filters: vec![DspFilter::Graphic(profiles::target_step(
330 c, all, &from, &to,
331 ))],
332 ..Default::default()
333 });
334 held += usize::from(step.is_some());
335 step
336 }
337 None => None,
338 };
339 layers.push((step, on_top, t.name.clone()));
340 }
341 let build = |layers: &[(Option<DspProfile>, DspProfile, String)]| {
342 let mut among = all.clone();
343 let mut names = Vec::new();
344 for (step, on_top, _) in layers {
345 for p in step.iter().chain([on_top]) {
346 names.push(p.name.clone());
347 among.push(p.clone());
348 }
349 }
350 let profile = match correction {
351 Some(c) => {
352 let mut c = c.clone();
353 if !names.is_empty() {
358 c.preamp_db = None;
359 }
360 if let (Some(filters), Some((to, _))) = (&refit_filters, &refit) {
361 c.filters = filters.clone();
362 c.fitted = c.measured().map(|m| crate::config::DspMeasurement {
363 target: to.clone(),
364 ..m.clone()
365 });
366 c.measurement = None;
367 }
368 c.layers.extend(
369 names
370 .into_iter()
371 .map(|profile| DspLayer { profile, on: true }),
372 );
373 c
374 }
375 None => DspProfile {
377 name: format!("\u{1}{name}"),
378 layers: names
379 .into_iter()
380 .map(|profile| DspLayer { profile, on: true })
381 .collect(),
382 ..Default::default()
383 },
384 };
385 chain(&profile, &among, &mut Vec::new()).map(|_| (profile, among))
386 };
387 let (profile, among) = loop {
390 match build(&layers) {
391 Ok(built) => break built,
392 Err(e) => {
393 if let Some((step, _, tuning)) = layers.iter_mut().rev().find(|l| l.0.is_some()) {
394 *step = None;
395 notes.push((
396 tuning.clone(),
397 format!(
398 "{tuning} plays without the target difference: with it, the chain is more than one can hold, so it may not sound as made."
399 ),
400 ));
401 } else if let Some((_, _, tuning)) = layers.pop() {
402 notes.push((
403 tuning.clone(),
404 format!(
405 "{tuning} is left out: with what plays before it, the chain is more than one can hold ({e})."
406 ),
407 ));
408 dropped.push(tuning);
409 if layers.is_empty() {
410 return alone(¬es, &dropped);
411 }
412 } else {
413 return alone(¬es, &dropped);
414 }
415 }
416 }
417 };
418 let left_out = said(¬es);
419 Some(OutputChain {
420 profile,
421 all: among,
422 name,
423 left_out,
424 left_out_eqs: dropped,
425 eq_notes: notes,
426 tuning_plays: true,
427 refit,
428 })
429}
430
431pub(crate) fn member_playing<'a>(profile: &'a DspProfile, all: &'a [DspProfile]) -> &'a DspProfile {
434 let mut p = profile;
435 for _ in 0..MAX_LAYER_DEPTH {
436 match p.group.then(|| playing(p, all)).flatten() {
437 Some(m) => p = m,
438 None => break,
439 }
440 }
441 p
442}
443
444pub(crate) fn made_against(
447 profile: &DspProfile,
448 all: &[DspProfile],
449 depth: usize,
450) -> Option<String> {
451 if depth > MAX_LAYER_DEPTH {
452 return None;
453 }
454 if profile.group {
455 return playing(profile, all)
456 .and_then(|m| made_against(m, all, depth + 1))
457 .or_else(|| profile.tuned_for.clone());
458 }
459 profile.tuned_for.clone().or_else(|| {
460 profile
461 .layers
462 .iter()
463 .filter(|l| l.on)
464 .filter_map(|l| all.iter().find(|p| p.name == l.profile))
465 .find_map(|p| made_against(p, all, depth + 1))
466 })
467}
468
469pub fn chain(
478 profile: &DspProfile,
479 all: &[DspProfile],
480 stack: &mut Vec<String>,
481) -> Result<Vec<DspFilter>, DspError> {
482 Ok(chain_noted(profile, all, stack)?.0)
483}
484
485pub fn chain_noted(
488 profile: &DspProfile,
489 all: &[DspProfile],
490 stack: &mut Vec<String>,
491) -> Result<(Vec<DspFilter>, Vec<String>), DspError> {
492 let mut visits = 0;
493 let mut filters = resolve(profile, all, stack, &mut visits)?;
494 let notes = crate::config::budget(&mut filters);
495 Ok((filters, notes))
496}
497
498fn resolve(
499 profile: &DspProfile,
500 all: &[DspProfile],
501 stack: &mut Vec<String>,
502 visits: &mut usize,
503) -> Result<Vec<DspFilter>, DspError> {
504 if stack.contains(&profile.name) {
505 return Err(DspError::Layer(format!(
506 "{} plays itself, through {}",
507 profile.name,
508 stack.join(" → ")
509 )));
510 }
511 if stack.len() >= MAX_LAYER_DEPTH {
512 return Err(DspError::Layer(format!(
513 "{} plays EQs that play others more than {MAX_LAYER_DEPTH} deep",
514 stack[0]
515 )));
516 }
517 *visits += 1;
518 if *visits > MAX_LAYER_VISITS {
519 return Err(DspError::Layer(format!(
520 "{} reaches the EQs it plays more than {MAX_LAYER_VISITS} times over",
521 stack.first().unwrap_or(&profile.name)
522 )));
523 }
524 stack.push(profile.name.clone());
525 let mut out = Vec::new();
526 let too_many = |stack: &[String]| {
527 DspError::Layer(format!(
528 "{} comes to more than {MAX_CHAIN_FILTERS} filters with the EQs it plays",
529 stack[0]
530 ))
531 };
532 let playing: Vec<&crate::config::DspLayer> = if profile.group {
535 profile
536 .layers
537 .iter()
538 .find(|l| l.on)
539 .or(profile.layers.first())
540 .into_iter()
541 .collect()
542 } else {
543 profile.layers.iter().filter(|l| l.on).collect()
544 };
545 for layer in playing {
546 let p = all
547 .iter()
548 .find(|p| p.name == layer.profile)
549 .ok_or_else(|| {
550 DspError::Layer(format!(
551 "{} plays {}, which is not there",
552 profile.name, layer.profile
553 ))
554 })?;
555 if !profile.group && !responses(p, all).is_empty() {
558 return Err(DspError::Layer(format!(
559 "{} has impulse responses: only bands can be played by another EQ",
560 p.name
561 )));
562 }
563 out.extend(resolve(p, all, stack, visits)?);
564 if out.len() > MAX_CHAIN_FILTERS {
565 return Err(too_many(stack));
566 }
567 }
568 let dir = profiles::dir(&profile.name);
569 let curve = |id: &str| targets::choice_curve(id).ok_or(DspError::Target(id.into()));
570 let moved = profile.target.as_ref().and_then(|t| {
574 Some((
575 t.chosen.as_ref().filter(|c| **c != t.made_for)?,
576 &t.made_for,
577 ))
578 });
579 let rebuilt = moved.and_then(|_| targets::autoeq_measurement(&dir));
580 if rebuilt.is_none() {
581 out.extend(profile.sanitized().filters);
583 }
584 if let Some((chosen, made_for)) = moved {
585 let step = targets::difference(&curve(made_for)?, &curve(chosen)?);
586 out.push(DspFilter::Graphic(match rebuilt {
587 Some((raw, target)) => targets::correction(&raw, &targets::moved(&target, &step)),
588 None => step,
589 }));
590 }
591 if let Some(m) = profile
598 .measurement
599 .as_ref()
600 .filter(|_| profile.fitted.is_none())
601 {
602 let measured = targets::measurement(&dir)
603 .ok_or_else(|| DspError::Measurement(profile.name.clone()))?;
604 out.extend(profiles::squig_fit(&measured, &curve(&m.target)?).0);
605 }
606 if out.len() > MAX_CHAIN_FILTERS && stack.len() > 1 {
607 return Err(too_many(stack));
608 }
609 stack.pop();
610 Ok(out)
611}
612
613pub fn playing<'a>(profile: &DspProfile, all: &'a [DspProfile]) -> Option<&'a DspProfile> {
616 let layer = profile
617 .layers
618 .iter()
619 .find(|l| l.on)
620 .or(profile.layers.first())?;
621 all.iter().find(|p| p.name == layer.profile)
622}
623
624pub fn responses(profile: &DspProfile, all: &[DspProfile]) -> Vec<PathBuf> {
627 fn walk(p: &DspProfile, all: &[DspProfile], depth: usize, out: &mut Vec<PathBuf>) {
628 out.extend(p.impulses.iter().cloned());
629 if p.group
630 && depth < MAX_LAYER_DEPTH
631 && let Some(m) = playing(p, all)
632 {
633 walk(m, all, depth + 1, out);
634 }
635 }
636 let mut out = Vec::new();
637 walk(profile, all, 0, &mut out);
638 out
639}
640
641#[derive(Debug, Clone, PartialEq, Eq)]
643pub struct DspStatus {
644 pub profile: String,
645 pub eq: bool,
648 pub convolution_rate: Option<u32>,
652}
653
654#[derive(Debug, Clone, PartialEq)]
656pub struct Setup {
657 pub name: String,
658 preamp_db: Option<f64>,
659 filters: Vec<DspFilter>,
660 impulses: BTreeMap<u32, Vec<Impulse>>,
663}
664
665impl Setup {
666 pub fn load(
669 profile: &DspProfile,
670 all: &[DspProfile],
671 base: &Path,
672 ) -> Result<Option<Self>, DspError> {
673 let mut impulses: BTreeMap<u32, Vec<Impulse>> = BTreeMap::new();
674 for path in &responses(profile, all) {
675 for mut impulse in load_impulses(path, base)? {
676 for route in &mut impulse.routes {
679 route.ir.truncate(crate::config::dsp_bounds::IMPULSE_TAPS);
680 }
681 impulses.entry(impulse.rate).or_default().push(impulse);
682 }
683 }
684 let filters = chain(profile, all, &mut Vec::new())?;
685 if filters.is_empty() && impulses.is_empty() && profile.preamp_db.unwrap_or(0.0) == 0.0 {
686 return Ok(None);
687 }
688 Ok(Some(Self {
689 name: profile.name.clone(),
690 preamp_db: profile.sanitized().preamp_db,
691 filters,
692 impulses,
693 }))
694 }
695
696 pub fn gain_db_on(&self, channel: u16) -> f64 {
699 self.filters
700 .iter()
701 .filter_map(|f| match f {
702 DspFilter::Band(b)
703 if b.kind == crate::config::EqFilterKind::Gain
704 && (b.channels.is_empty() || b.channels.contains(&channel)) =>
705 {
706 Some(b.gain_db)
707 }
708 _ => None,
709 })
710 .sum()
711 }
712
713 pub fn preamp_db(&self, rate: u32, channels: usize) -> f64 {
716 let impulse = self
717 .impulses
718 .get(&rate)
719 .and_then(|at_rate| at_rate.iter().find(|i| i.fits(channels)));
720 let plan = plan(&self.filters, rate, channels);
721 let headroom = -20.0 * peak_gain(&plan, impulse, channels, rate).log10().max(0.0);
722 self.preamp_db.map_or(headroom, |set| set.min(headroom))
725 }
726
727 pub fn headroom_cut(&self, rate: u32, channels: usize) -> Option<f64> {
730 let set = self.preamp_db?;
731 let used = self.preamp_db(rate, channels);
732 (used < set - 0.05).then_some(set - used)
733 }
734
735 pub fn response(&self, freqs: &[f64], rate: u32) -> Vec<f64> {
740 let rate = self.output_rate(rate);
741 let Some(impulse) = self
742 .impulses
743 .get(&rate)
744 .and_then(|at_rate| at_rate.iter().find(|i| i.fits(2)))
745 else {
746 return response(&self.filters, freqs, rate);
747 };
748 let plan = plan(&self.filters, rate, 2);
749 let routes = impulse.routes_for(2);
750 let size = impulse.taps().next_power_of_two().max(8192);
751 let fft = RealFftPlanner::<f64>::new().plan_fft_forward(size);
752 let magnitudes: Vec<Vec<f64>> = routes
753 .iter()
754 .map(|r| {
755 let mut input = fft.make_input_vec();
756 for (d, &s) in input.iter_mut().zip(&r.ir) {
757 *d = s as f64;
758 }
759 let mut spectrum = fft.make_output_vec();
760 if fft.process(&mut input, &mut spectrum).is_err() {
761 return vec![0.0; spectrum.len()];
762 }
763 spectrum.iter().map(|b| b.norm()).collect()
764 })
765 .collect();
766 freqs
767 .iter()
768 .map(|&hz| {
769 let w = std::f64::consts::TAU * hz / rate as f64;
770 let gains: Vec<f64> = gain_matrix(&plan, 2, w, rate)
771 .iter()
772 .map(|row| row.iter().sum())
773 .collect();
774 let k = hz * size as f64 / rate as f64;
776 let gain: f64 = routes
777 .iter()
778 .zip(&magnitudes)
779 .map(|(r, m)| {
780 let i = (k.floor() as usize).min(m.len() - 1);
781 let next = m[(i + 1).min(m.len() - 1)];
782 let at = m[i] + (next - m[i]) * (k - i as f64).clamp(0.0, 1.0);
783 let fed: f64 = r
784 .inputs
785 .iter()
786 .map(|&(c, g)| g.abs() as f64 * gains.get(c).copied().unwrap_or(1.0))
787 .sum();
788 r.outputs
789 .iter()
790 .filter(|&&(o, _)| o == 0)
791 .map(|&(_, g)| at * fed * g.abs() as f64)
792 .sum::<f64>()
793 })
794 .sum();
795 20.0 * gain.max(1e-6).log10()
796 })
797 .collect()
798 }
799
800 #[cfg(test)]
801 pub(crate) fn with_preamp(mut self, db: f64) -> Self {
802 self.preamp_db = Some(db);
803 self
804 }
805
806 #[cfg(test)]
807 pub(crate) fn new(filters: Vec<DspFilter>, impulses: Vec<Impulse>) -> Self {
808 Self {
809 name: "test".into(),
810 preamp_db: None,
811 filters,
812 impulses: impulses.into_iter().fold(BTreeMap::new(), |mut m, i| {
813 m.entry(i.rate).or_insert_with(Vec::new).push(i);
814 m
815 }),
816 }
817 }
818
819 pub fn output_rate(&self, source: u32) -> u32 {
825 if self.impulses.is_empty() || self.impulses.contains_key(&source) {
826 return source;
827 }
828 *self
829 .impulses
830 .keys()
831 .min_by_key(|&&r| (r.abs_diff(source), std::cmp::Reverse(r)))
832 .expect("not empty")
833 }
834
835 pub fn rates(&self) -> Vec<u32> {
837 self.impulses.keys().copied().collect()
838 }
839
840 pub fn status(&self, source: u32) -> DspStatus {
841 DspStatus {
842 profile: self.name.clone(),
843 eq: !self.filters.is_empty(),
844 convolution_rate: (!self.impulses.is_empty()).then(|| self.output_rate(source)),
845 }
846 }
847}
848
849pub fn load_impulses(path: &Path, base: &Path) -> Result<Vec<Impulse>, DspError> {
852 let path = if path.is_absolute() {
853 path.to_path_buf()
854 } else {
855 base.join(path)
856 };
857 let read = if convolver::is_cfg(&path) {
858 convolver::read(&path)
859 } else {
860 read_audio(&path).map(|(rate, channels)| vec![Impulse::from_channels(rate, channels)])
861 };
862 read.map_err(|reason| DspError::Impulse { path, reason })
863}
864
865pub struct Chain {
872 channels: usize,
873 out_rate: u32,
874 gain: f64,
875 resample: Option<Resample>,
876 steps: Steps,
877 convolve: Option<Convolve>,
878 input: Vec<f64>,
879 work: Vec<f64>,
880 out: Vec<f32>,
881}
882
883impl Chain {
884 pub fn new(setup: &Setup, source_rate: u32, channels: u16) -> Self {
885 let channels = channels as usize;
886 let out_rate = setup.output_rate(source_rate);
887 let impulse = setup.impulses.get(&out_rate).and_then(|at_rate| {
888 let fitting = at_rate.iter().find(|i| i.fits(channels));
889 if fitting.is_none() {
890 log::warn!(
891 "dsp: no impulse at {out_rate}Hz for {channels} channels; convolution skipped"
892 );
893 }
894 fitting
895 });
896
897 let preamp_db = setup.preamp_db(out_rate, channels);
898 log::info!(
899 "dsp: '{}' at {out_rate}Hz — {} filters, preamp {preamp_db:.2} dB{}",
900 setup.name,
901 setup.filters.len(),
902 impulse.map_or(String::new(), |i| format!(
903 ", {} routes of {} taps, {} frames delay",
904 i.routes.len(),
905 i.taps(),
906 i.delay()
907 ))
908 );
909
910 Self {
911 channels,
912 out_rate,
913 gain: 10f64.powf(preamp_db / 20.0),
914 resample: Resample::new(source_rate, out_rate, channels),
915 steps: Steps::new(plan(&setup.filters, out_rate, channels), out_rate, channels),
916 convolve: impulse.map(|i| Convolve::new(i, channels)),
917 input: Vec::new(),
918 work: Vec::new(),
919 out: Vec::new(),
920 }
921 }
922
923 pub fn output_rate(&self) -> u32 {
924 self.out_rate
925 }
926
927 pub fn set_source_rate(&mut self, source_rate: u32) -> &[f32] {
931 self.work.clear();
932 if self.resample.as_ref().map_or(self.out_rate, |r| r.in_rate) != source_rate {
933 if let Some(mut r) = self.resample.take() {
934 r.flush(&mut self.work);
935 }
936 self.resample = Resample::new(source_rate, self.out_rate, self.channels);
937 self.post();
938 }
939 self.emit()
940 }
941
942 pub fn process(&mut self, input: &[f32]) -> (&[f32], u64) {
946 self.input.clear();
947 self.input.extend(input.iter().map(|&s| s as f64));
948 self.work.clear();
949 let length = match self.resample.as_mut() {
950 Some(r) => {
951 r.run(&self.input, &mut self.work);
952 r.counted() * self.channels as u64
953 }
954 None => {
955 self.work.extend_from_slice(&self.input);
956 input.len() as u64
957 }
958 };
959 self.post();
960 (self.emit(), length)
961 }
962
963 pub fn flush(&mut self) -> &[f32] {
965 self.work.clear();
966 if let Some(r) = self.resample.as_mut() {
967 r.flush(&mut self.work);
968 }
969 self.post();
970 if let Some(c) = self.convolve.as_mut() {
971 c.flush(&mut self.work);
972 }
973 self.emit()
974 }
975
976 fn post(&mut self) {
978 if self.gain != 1.0 {
979 for s in &mut self.work {
980 *s *= self.gain;
981 }
982 }
983 if !self.steps.is_empty() {
984 self.steps.run(&mut self.work);
985 }
986 if let Some(c) = self.convolve.as_mut() {
987 c.run(&mut self.work);
988 }
989 }
990
991 fn emit(&mut self) -> &[f32] {
997 self.out.clear();
998 self.out.extend(self.work.iter().map(|&s| {
999 if s.is_finite() {
1000 s.clamp(-1.0, 1.0) as f32
1001 } else {
1002 0.0
1003 }
1004 }));
1005 &self.out
1006 }
1007}
1008
1009fn peak_gain(plan: &[Planned], impulse: Option<&Impulse>, channels: usize, rate: u32) -> f64 {
1015 let channel_gains = |w: f64| -> Vec<f64> {
1017 gain_matrix(plan, channels.max(1), w, rate)
1018 .iter()
1019 .map(|row| row.iter().sum())
1020 .collect()
1021 };
1022 let Some(impulse) = impulse else {
1023 let (lo, hi) = (10f64.ln(), (rate as f64 * 0.499).ln());
1024 return (0..=4096)
1025 .flat_map(|i| {
1026 let f = (lo + (hi - lo) * i as f64 / 4096.0).exp();
1027 let w = std::f64::consts::TAU * f / rate as f64;
1028 channel_gains(w)
1029 })
1030 .fold(0.0, f64::max);
1031 };
1032 let routes = impulse.routes_for(channels);
1033 let size = impulse.taps().next_power_of_two().max(8192);
1034 let fft = RealFftPlanner::<f64>::new().plan_fft_forward(size);
1035 let mut spectrum = fft.make_output_vec();
1036 let mut per_output = vec![vec![0.0f64; spectrum.len()]; channels];
1037 let gains: Vec<Vec<f64>> = if plan.is_empty() {
1038 Vec::new()
1039 } else {
1040 (0..spectrum.len())
1041 .map(|k| channel_gains(std::f64::consts::TAU * k as f64 / size as f64))
1042 .collect()
1043 };
1044 for r in &routes {
1045 let mut input = fft.make_input_vec();
1046 for (d, &s) in input.iter_mut().zip(&r.ir) {
1047 *d = s as f64;
1048 }
1049 if fft.process(&mut input, &mut spectrum).is_err() {
1050 continue;
1051 }
1052 for (k, bin) in spectrum.iter().enumerate() {
1053 let fed: f64 = r
1054 .inputs
1055 .iter()
1056 .map(|&(c, g)| {
1057 g.abs() as f64 * gains.get(k).and_then(|g| g.get(c)).map_or(1.0, |&g| g)
1058 })
1059 .sum();
1060 for &(o, g) in &r.outputs {
1061 if let Some(out) = per_output.get_mut(o) {
1062 out[k] += bin.norm() * fed * g.abs() as f64;
1063 }
1064 }
1065 }
1066 }
1067 per_output.iter().flatten().copied().fold(0.0, f64::max)
1068}
1069
1070struct Resample {
1072 inner: Fft<f64>,
1073 in_rate: u32,
1074 out_rate: u32,
1075 channels: usize,
1076 pending: Vec<f64>,
1078 scratch: Vec<f64>,
1079 skip: usize,
1081 fed: u64,
1082 emitted: u64,
1083 counted: u64,
1084}
1085
1086impl Resample {
1087 fn new(in_rate: u32, out_rate: u32, channels: usize) -> Option<Self> {
1088 if in_rate == out_rate {
1089 return None;
1090 }
1091 let inner = Fft::<f64>::new(
1092 in_rate as usize,
1093 out_rate as usize,
1094 1024,
1095 channels,
1096 FixedSync::Input,
1097 )
1098 .inspect_err(|e| log::error!("dsp: no resampler {in_rate}→{out_rate}Hz: {e}"))
1099 .ok()?;
1100 Some(Self {
1101 skip: inner.output_delay(),
1102 scratch: vec![0.0; inner.output_frames_max() * channels],
1103 inner,
1104 in_rate,
1105 out_rate,
1106 channels,
1107 pending: Vec::new(),
1108 fed: 0,
1109 emitted: 0,
1110 counted: 0,
1111 })
1112 }
1113
1114 fn target(&self) -> u64 {
1116 self.fed * self.out_rate as u64 / self.in_rate as u64
1117 }
1118
1119 fn counted(&mut self) -> u64 {
1121 let target = self.target();
1122 let new = target - self.counted;
1123 self.counted = target;
1124 new
1125 }
1126
1127 fn run(&mut self, input: &[f64], dst: &mut Vec<f64>) {
1128 self.fed += (input.len() / self.channels) as u64;
1129 self.pending.extend_from_slice(input);
1130 loop {
1131 let need = self.inner.input_frames_next();
1132 if self.pending.len() / self.channels < need {
1133 break;
1134 }
1135 self.chunk(need, None, dst);
1136 self.pending.drain(..need * self.channels);
1137 }
1138 }
1139
1140 fn flush(&mut self, dst: &mut Vec<f64>) {
1143 let start = dst.len();
1144 let target = self.target();
1145 let mut partial = self.pending.len() / self.channels;
1146 while self.emitted < target {
1147 let need = self.inner.input_frames_next();
1148 self.pending.resize(need * self.channels, 0.0);
1149 if self.chunk(need, Some(partial), dst) == 0 {
1150 break;
1151 }
1152 partial = 0;
1153 }
1154 self.pending.clear();
1155 let over = (self.emitted - target) as usize * self.channels;
1156 dst.truncate((dst.len() - over).max(start));
1157 self.emitted = target;
1158 }
1159
1160 fn chunk(&mut self, need: usize, partial: Option<usize>, dst: &mut Vec<f64>) -> usize {
1163 let ch = self.channels;
1164 let frames_out = self.scratch.len() / ch;
1165 let (Ok(input), Ok(mut output)) = (
1166 InterleavedSlice::new(&self.pending[..need * ch], ch, need),
1167 InterleavedSlice::new_mut(&mut self.scratch, ch, frames_out),
1168 ) else {
1169 return 0;
1170 };
1171 let indexing = partial.map(|p| Indexing::new().partial_len(p));
1172 match self
1173 .inner
1174 .process_into_buffer(&input, &mut output, indexing.as_ref())
1175 {
1176 Ok((_, produced)) => {
1177 let drop = self.skip.min(produced);
1178 self.skip -= drop;
1179 dst.extend_from_slice(&self.scratch[drop * ch..produced * ch]);
1180 self.emitted += (produced - drop) as u64;
1181 produced
1182 }
1183 Err(e) => {
1184 log::error!("dsp: resampling failed: {e}");
1185 0
1186 }
1187 }
1188 }
1189}
1190
1191#[cfg(test)]
1192mod tests {
1193 use super::*;
1194 use crate::config::{EqFilter, EqFilterKind};
1195
1196 fn band(kind: EqFilterKind, freq: f64, gain_db: f64, q: f64) -> EqFilter {
1197 EqFilter {
1198 kind,
1199 freq,
1200 gain_db,
1201 q,
1202 channels: vec![],
1203 }
1204 }
1205
1206 fn sine(rate: u32, freq: f64, frames: usize, channels: usize, amp: f32) -> Vec<f32> {
1207 (0..frames)
1208 .flat_map(|i| {
1209 let v = amp * (std::f64::consts::TAU * freq * i as f64 / rate as f64).sin() as f32;
1210 std::iter::repeat_n(v, channels)
1211 })
1212 .collect()
1213 }
1214
1215 fn rms(s: &[f32]) -> f64 {
1216 (s.iter().map(|&v| (v as f64).powi(2)).sum::<f64>() / s.len() as f64).sqrt()
1217 }
1218
1219 fn run_all(chain: &mut Chain, input: &[f32], packet: usize) -> (Vec<f32>, u64) {
1221 let mut out = Vec::new();
1222 let mut counted = 0;
1223 for p in input.chunks(packet) {
1224 let (o, n) = chain.process(p);
1225 out.extend_from_slice(o);
1226 counted += n;
1227 }
1228 out.extend_from_slice(chain.flush());
1229 (out, counted)
1230 }
1231
1232 fn noise(seed: &mut u64) -> f64 {
1234 *seed = seed
1235 .wrapping_mul(6_364_136_223_846_793_005)
1236 .wrapping_add(1_442_695_040_888_963_407);
1237 ((*seed >> 11) as f64 / (1u64 << 53) as f64) * 2.0 - 1.0
1238 }
1239
1240 #[test]
1245 fn null_test_against_direct_convolution() {
1246 let (rate, taps, frames, peak) = (48000, 4096, 8192, 200);
1247 let mut seed = 1;
1248 let mut ir: Vec<f32> = (0..taps)
1249 .map(|i| (noise(&mut seed) * 0.2 * (-(i as f64) / 600.0).exp()) as f32)
1250 .collect();
1251 ir[peak] = 1.0;
1252 let input: Vec<f32> = (0..frames * 2)
1253 .map(|_| (noise(&mut seed) * 0.25) as f32)
1254 .collect();
1255 let mut setup = Setup::new(vec![], vec![Impulse::from_channels(rate, vec![ir.clone()])]);
1256 setup.preamp_db = Some(0.0);
1257 let preamp = 10f64.powf(setup.preamp_db(rate, 2) / 20.0);
1259 let mut chain = Chain::new(&setup, rate, 2);
1260 let (out, _) = run_all(&mut chain, &input, 1152);
1261 assert_eq!(out.len(), input.len());
1262
1263 let (mut err, mut sig) = (0.0f64, 0.0f64);
1264 for c in 0..2 {
1265 for n in 0..frames {
1266 let m = n + peak;
1268 let y: f64 = (0..taps)
1269 .filter(|&k| k <= m && m - k < frames)
1270 .map(|k| ir[k] as f64 * input[(m - k) * 2 + c] as f64)
1271 .sum::<f64>()
1272 * preamp;
1273 err += (out[n * 2 + c] as f64 - y).powi(2);
1274 sig += y.powi(2);
1275 }
1276 }
1277 let db = 10.0 * (err / sig).log10();
1278 eprintln!("residual against direct convolution: {db:.1} dB");
1279 assert!(db < -140.0, "residual {db:.1} dB");
1281 }
1282
1283 #[test]
1287 #[ignore]
1288 fn bench_long_response() {
1289 let (rate, taps, secs) = (192000, 262_145, 10);
1290 let mut seed = 7;
1291 let ir: Vec<Vec<f32>> = (0..2)
1292 .map(|_| {
1293 (0..taps)
1294 .map(|i| (noise(&mut seed) * (-(i as f64) / 20000.0).exp()) as f32)
1295 .collect()
1296 })
1297 .collect();
1298 let input: Vec<f32> = (0..rate as usize * secs * 2)
1299 .map(|_| (noise(&mut seed) * 0.25) as f32)
1300 .collect();
1301 let setup = Setup::new(vec![], vec![Impulse::from_channels(rate, ir)]);
1302 let started = std::time::Instant::now();
1303 let mut chain = Chain::new(&setup, rate, 2);
1304 let built = started.elapsed();
1305 let started = std::time::Instant::now();
1306 for p in input.chunks(4096 * 2) {
1307 chain.process(p);
1308 }
1309 let took = started.elapsed();
1310 eprintln!(
1311 "{taps} taps at {rate} Hz, stereo: built in {built:?}; {secs} s of audio in {took:?}, {:.1}x real time, {:.1}% of one core",
1312 secs as f64 / took.as_secs_f64(),
1313 100.0 * took.as_secs_f64() / secs as f64
1314 );
1315 }
1316
1317 #[test]
1323 #[ignore]
1324 fn bench_typical_profile() {
1325 use crate::config::{EqFilter, EqFilterKind, GraphicEq};
1326 let (rate, secs) = (48000u32, 20usize);
1327 let band = |kind, freq, gain_db, q| {
1328 DspFilter::Band(EqFilter {
1329 kind,
1330 freq,
1331 gain_db,
1332 q,
1333 channels: vec![],
1334 })
1335 };
1336 let bands: Vec<DspFilter> = [
1337 (EqFilterKind::LowShelf, 105.0, 5.5, 0.7),
1338 (EqFilterKind::Peaking, 180.0, -2.8, 0.9),
1339 (EqFilterKind::Peaking, 650.0, 1.2, 1.1),
1340 (EqFilterKind::Peaking, 1400.0, 2.1, 1.4),
1341 (EqFilterKind::Peaking, 2600.0, -1.6, 3.0),
1342 (EqFilterKind::Peaking, 3200.0, -4.0, 2.2),
1343 (EqFilterKind::Peaking, 4800.0, 2.4, 4.0),
1344 (EqFilterKind::Peaking, 6100.0, 3.4, 3.0),
1345 (EqFilterKind::Peaking, 9000.0, -3.1, 5.0),
1346 (EqFilterKind::HighShelf, 10000.0, -2.5, 0.7),
1347 ]
1348 .into_iter()
1349 .map(|(k, f, g, q)| band(k, f, g, q))
1350 .collect();
1351 let target = DspFilter::Graphic(GraphicEq {
1352 points: vec![
1353 (20.0, 4.0),
1354 (120.0, 3.0),
1355 (1000.0, 0.0),
1356 (3000.0, -1.0),
1357 (10000.0, -3.0),
1358 (20000.0, -6.0),
1359 ],
1360 channels: vec![],
1361 });
1362 let mut seed = 11;
1363 let room: Vec<Vec<f32>> = (0..2)
1364 .map(|_| {
1365 let mut ir: Vec<f32> = (0..65536)
1366 .map(|i| (noise(&mut seed) * 0.2 * (-(i as f64) / 6000.0).exp()) as f32)
1367 .collect();
1368 ir[0] = 1.0;
1369 ir
1370 })
1371 .collect();
1372 let input: Vec<f32> = (0..rate as usize * secs * 2)
1373 .map(|_| (noise(&mut seed) * 0.25) as f32)
1374 .collect();
1375
1376 let cases: [(&str, Vec<DspFilter>, Vec<Impulse>); 4] = [
1377 ("ten bands", bands.clone(), vec![]),
1378 ("target FIR", vec![target.clone()], vec![]),
1379 (
1380 "65,536-tap convolution",
1381 vec![],
1382 vec![Impulse::from_channels(rate, room.clone())],
1383 ),
1384 (
1385 "all three",
1386 [bands, vec![target]].concat(),
1387 vec![Impulse::from_channels(rate, room)],
1388 ),
1389 ];
1390 for (name, filters, impulses) in cases {
1391 let setup = Setup::new(filters, impulses);
1392 let started = std::time::Instant::now();
1393 let mut chain = Chain::new(&setup, rate, 2);
1394 let built = started.elapsed();
1395 let started = std::time::Instant::now();
1396 for p in input.chunks(4096 * 2) {
1397 chain.process(p);
1398 }
1399 let took = started.elapsed();
1400 eprintln!(
1401 "{name}: built in {built:.1?}; {:.2} ms a second of audio, {:.0}x real time, {:.2}% of one core",
1402 1000.0 * took.as_secs_f64() / secs as f64,
1403 secs as f64 / took.as_secs_f64(),
1404 100.0 * took.as_secs_f64() / secs as f64
1405 );
1406 }
1407 }
1408
1409 #[test]
1410 fn a_profile_that_changes_nothing_is_no_chain() {
1411 let profile = DspProfile {
1412 name: "flat".into(),
1413 ..Default::default()
1414 };
1415 assert!(
1416 Setup::load(&profile, &[], Path::new("/"))
1417 .unwrap()
1418 .is_none()
1419 );
1420 }
1421
1422 #[test]
1423 fn a_peaking_band_boosts_its_frequency_and_the_preamp_pays_for_it() {
1424 let setup = Setup::new(
1425 vec![band(EqFilterKind::Peaking, 1000.0, 6.0, 1.0).into()],
1426 vec![],
1427 );
1428 let mut chain = Chain::new(&setup, 48000, 2);
1429 let input = sine(48000, 1000.0, 48000, 2, 0.5);
1430 let (out, counted) = run_all(&mut chain, &input, 4096);
1431 assert_eq!(out.len(), input.len());
1432 assert_eq!(counted, input.len() as u64);
1433 let ratio = rms(&out[24000..]) / rms(&input[24000..]);
1435 assert!((ratio - 1.0).abs() < 0.02, "ratio {ratio}");
1436
1437 let mut chain = Chain::new(&setup, 48000, 2);
1439 let input = sine(48000, 100.0, 48000, 2, 0.5);
1440 let (out, _) = run_all(&mut chain, &input, 4096);
1441 let db = 20.0 * (rms(&out[24000..]) / rms(&input[24000..])).log10();
1442 assert!((db + 6.0).abs() < 0.2, "{db} dB");
1443 }
1444
1445 #[test]
1446 fn a_band_past_nyquist_is_skipped_not_fatal() {
1447 let setup = Setup::new(
1448 vec![band(EqFilterKind::Peaking, 30000.0, 6.0, 1.0).into()],
1449 vec![],
1450 );
1451 let mut chain = Chain::new(&setup, 44100, 2);
1452 let input = sine(44100, 1000.0, 4410, 2, 0.5);
1453 let (out, _) = run_all(&mut chain, &input, 1024);
1454 assert_eq!(out, input);
1455 }
1456
1457 fn delayed_impulse(rate: u32, delay: usize) -> Impulse {
1458 let mut ir = vec![0.0; delay * 2 + 1];
1459 ir[delay] = 1.0;
1460 Impulse::from_channels(rate, vec![ir])
1461 }
1462
1463 #[test]
1465 fn a_response_includes_the_impulse() {
1466 let half = Impulse::from_channels(48000, vec![vec![0.5]]);
1467 let peak = DspFilter::Band(crate::config::EqFilter {
1468 kind: crate::config::EqFilterKind::Peaking,
1469 freq: 1000.0,
1470 gain_db: 3.0,
1471 q: 1.0,
1472 channels: vec![],
1473 });
1474 let setup = Setup::new(vec![peak], vec![half]);
1475 let db = setup.response(&[20.0, 1000.0], 48000);
1476 assert!((db[0] + 6.02).abs() < 0.05, "{db:?}");
1477 assert!((db[1] + 3.02).abs() < 0.1, "{db:?}");
1478 }
1479
1480 fn loaded(profile: DspProfile, all: &[DspProfile]) -> Setup {
1481 Setup::load(&profile, all, Path::new("/nonexistent"))
1482 .unwrap()
1483 .unwrap()
1484 }
1485
1486 fn gain_band(db: f64) -> DspFilter {
1487 DspFilter::Band(crate::config::EqFilter {
1488 kind: crate::config::EqFilterKind::Gain,
1489 freq: 1000.0,
1490 gain_db: db,
1491 q: 1.0,
1492 channels: vec![],
1493 })
1494 }
1495
1496 #[test]
1499 fn delays_add_up_to_two_seconds_at_most() {
1500 let delay = DspFilter::Delay(crate::config::Delay {
1501 ms: 2000.0,
1502 samples: 768_000.0,
1503 ..Default::default()
1504 });
1505 let profile = DspProfile {
1506 name: "Echo".into(),
1507 filters: vec![delay; 256],
1508 ..Default::default()
1509 };
1510 let setup = loaded(profile, &[]);
1511 let total: f64 = setup
1512 .filters
1513 .iter()
1514 .map(|f| match f {
1515 DspFilter::Delay(d) => d.ms + d.samples / 44.1,
1516 _ => 0.0,
1517 })
1518 .sum();
1519 assert!(total <= 2000.0 + 1e-6, "{total} ms");
1520 let chain = Chain::new(&setup, 384_000, 2);
1522 drop(chain);
1523 }
1524
1525 #[test]
1528 fn stacked_boost_never_overloads() {
1529 let profile = DspProfile {
1530 name: "Loud".into(),
1531 preamp_db: Some(0.0),
1532 filters: vec![gain_band(30.0); 64],
1533 ..Default::default()
1534 };
1535 let setup = loaded(profile, &[]);
1536 assert!(setup.headroom_cut(48000, 2).unwrap() > 1900.0);
1537 let mut chain = Chain::new(&setup, 48000, 2);
1538 let (out, _) = run_all(&mut chain, &sine(48000, 1000.0, 48000, 2, 1.0), 4096);
1539 assert!(out.iter().all(|s| s.is_finite() && s.abs() <= 1.0));
1540 }
1541
1542 #[test]
1545 fn graphic_curves_are_budgeted_across_layers() {
1546 let curve = DspFilter::Graphic(crate::config::GraphicEq {
1547 points: (1..2048)
1548 .map(|i| (i as f64 * 10.0, if i % 2 == 0 { 30.0 } else { -30.0 }))
1549 .collect(),
1550 channels: vec![],
1551 });
1552 let mut all: Vec<DspProfile> = (0..32)
1553 .map(|i| DspProfile {
1554 name: format!("Layer {i}"),
1555 filters: vec![curve.clone(); 256],
1556 ..Default::default()
1557 })
1558 .collect();
1559 let stack = DspProfile {
1560 name: "Stack".into(),
1561 layers: all
1562 .iter()
1563 .map(|p| crate::config::DspLayer {
1564 profile: p.name.clone(),
1565 on: true,
1566 })
1567 .collect(),
1568 ..Default::default()
1569 };
1570 all.push(stack.clone());
1571 let (filters, notes) = chain_noted(&stack, &all, &mut Vec::new()).unwrap();
1572 let graphics = filters
1573 .iter()
1574 .filter(|f| matches!(f, DspFilter::Graphic(_)))
1575 .count();
1576 assert_eq!(graphics, crate::config::dsp_bounds::CHAIN_GRAPHICS);
1577 assert!(
1578 notes.iter().any(|n| n.starts_with("graphic EQs past")),
1579 "{notes:?}"
1580 );
1581 }
1582
1583 #[test]
1586 fn the_output_is_finite_and_within_full_scale() {
1587 let setup = Setup::new(vec![gain_band(0.0)], vec![]).with_preamp(0.0);
1588 let mut chain = Chain::new(&setup, 48000, 2);
1589 let (out, _) = run_all(&mut chain, &[1.5, -3.0, f32::NAN, f32::INFINITY], 4);
1590 assert_eq!(out, vec![1.0, -1.0, 0.0, 0.0]);
1591 }
1592
1593 #[test]
1597 #[ignore]
1598 fn budget_runs_in_real_time() {
1599 let curve = DspFilter::Graphic(crate::config::GraphicEq {
1600 points: (1..2048)
1601 .map(|i| (i as f64 * 10.0, if i % 2 == 0 { 6.0 } else { -6.0 }))
1602 .collect(),
1603 channels: vec![],
1604 });
1605 let mut filters = vec![curve; crate::config::dsp_bounds::CHAIN_GRAPHICS];
1606 filters.extend((0..64).map(|i| {
1607 DspFilter::Band(crate::config::EqFilter {
1608 kind: crate::config::EqFilterKind::Peaking,
1609 freq: 20.0 * 1.1f64.powi(i),
1610 gain_db: 1.0,
1611 q: 2.0,
1612 channels: vec![],
1613 })
1614 }));
1615 filters.push(DspFilter::Delay(crate::config::Delay {
1616 ms: 2000.0,
1617 ..Default::default()
1618 }));
1619 let ir: Vec<f32> = (0..crate::config::dsp_bounds::IMPULSE_TAPS)
1620 .map(|i| (-(i as f32) / 20000.0).exp() * if i % 2 == 0 { 0.01 } else { -0.01 })
1621 .collect();
1622 for rate in [48_000u32, 192_000] {
1623 let impulse = Impulse::from_channels(rate, vec![ir.clone()]);
1624 let setup = Setup::new(filters.clone(), vec![impulse]);
1625 let mut chain = Chain::new(&setup, rate, 2);
1626 let seconds = 10;
1627 let input = sine(rate, 440.0, rate as usize * seconds, 2, 0.5);
1628 let start = std::time::Instant::now();
1629 run_all(&mut chain, &input, 4096);
1630 let took = start.elapsed().as_secs_f64();
1631 println!(
1632 "{rate} Hz: {seconds} s of stereo in {took:.2} s, {:.1}x real time",
1633 seconds as f64 / took
1634 );
1635 }
1636 }
1637
1638 #[test]
1639 fn convolution_delay_is_trimmed_and_the_tail_flushed() {
1640 let setup = Setup::new(vec![], vec![delayed_impulse(48000, 300)]);
1641 let mut chain = Chain::new(&setup, 48000, 2);
1642 let input = sine(48000, 440.0, 10000, 2, 0.5);
1643 let (out, counted) = run_all(&mut chain, &input, 1152);
1644 assert_eq!(out.len(), input.len());
1645 assert_eq!(counted, input.len() as u64);
1646 for (a, b) in out.iter().zip(&input) {
1647 assert!((a - b).abs() < 1e-4);
1648 }
1649 }
1650
1651 #[test]
1652 fn a_source_without_its_own_response_is_resampled_to_the_nearest() {
1653 let setup = Setup::new(
1654 vec![],
1655 vec![delayed_impulse(44100, 0), delayed_impulse(48000, 0)],
1656 );
1657 assert_eq!(setup.output_rate(44100), 44100);
1658 assert_eq!(setup.output_rate(96000), 48000);
1659 assert_eq!(setup.output_rate(88200), 48000);
1660 assert_eq!(setup.output_rate(22050), 44100);
1661 assert_eq!(
1662 setup.status(96000),
1663 DspStatus {
1664 profile: "test".into(),
1665 eq: false,
1666 convolution_rate: Some(48000)
1667 }
1668 );
1669
1670 let mut chain = Chain::new(&setup, 96000, 2);
1671 assert_eq!(chain.output_rate(), 48000);
1672 let input = sine(96000, 1000.0, 96000, 2, 0.5);
1673 let (out, counted) = run_all(&mut chain, &input, 4096);
1674 assert_eq!(out.len(), 48000 * 2);
1675 assert_eq!(counted, 48000 * 2);
1676 let ratio = rms(&out[4800..43200]) / rms(&input[9600..86400]);
1677 assert!((ratio - 1.0).abs() < 0.02, "ratio {ratio}");
1678 let expect = sine(48000, 1000.0, 2000, 2, 0.5);
1680 let err = out[2000..4000]
1681 .iter()
1682 .zip(&expect[2000..4000])
1683 .map(|(a, b)| (a - b).abs())
1684 .fold(0.0f32, f32::max);
1685 assert!(err < 0.02, "max error {err}");
1686 }
1687
1688 #[test]
1689 fn a_gapless_rate_change_keeps_the_output_rate_and_its_length() {
1690 let setup = Setup::new(vec![], vec![delayed_impulse(48000, 64)]);
1691 let mut chain = Chain::new(&setup, 44100, 2);
1692 let first = sine(44100, 500.0, 44100, 2, 0.5);
1693 let mut out = Vec::new();
1694 let mut counted = 0;
1695 for p in first.chunks(4096) {
1696 let (o, n) = chain.process(p);
1697 out.extend_from_slice(o);
1698 counted += n;
1699 }
1700 out.extend_from_slice(chain.set_source_rate(48000));
1701 let second = sine(48000, 500.0, 48000, 2, 0.5);
1702 for p in second.chunks(4096) {
1703 let (o, n) = chain.process(p);
1704 out.extend_from_slice(o);
1705 counted += n;
1706 }
1707 out.extend_from_slice(chain.flush());
1708 assert_eq!(counted, 96000 * 2);
1709 assert_eq!(out.len(), 96000 * 2);
1710 }
1711
1712 #[test]
1713 fn the_preamp_covers_the_response_gain() {
1714 let mut ir = vec![0.0; 64];
1715 ir[0] = 2.0;
1716 let setup = Setup::new(vec![], vec![Impulse::from_channels(48000, vec![ir])]);
1717 let mut chain = Chain::new(&setup, 48000, 1);
1718 let (out, _) = run_all(&mut chain, &[0.5; 4800], 480);
1719 assert!(out.iter().all(|&s| (s - 0.5).abs() < 1e-4));
1720 }
1721
1722 #[test]
1723 fn bands_for_one_channel_leave_the_other_alone() {
1724 let mut b = band(EqFilterKind::Gain, 1000.0, -6.0, 1.0);
1725 b.channels = vec![1];
1726 let mut setup = Setup::new(vec![b.into()], vec![]);
1727 setup.preamp_db = Some(0.0);
1728 let mut chain = Chain::new(&setup, 48000, 2);
1729 let (out, _) = run_all(&mut chain, &[0.5, 0.5, 0.5, 0.5], 4);
1730 assert_eq!(out[0], 0.5);
1731 assert!((out[1] - 0.25).abs() < 1e-3);
1732 }
1733
1734 #[test]
1735 fn a_route_can_feed_one_channel_into_the_other() {
1736 let impulse = Impulse {
1739 rate: 48000,
1740 channels: Some(2),
1741 routes: vec![
1742 Route {
1743 ir: vec![1.0],
1744 inputs: vec![(0, 1.0)],
1745 outputs: vec![(0, 1.0)],
1746 },
1747 Route {
1748 ir: vec![1.0],
1749 inputs: vec![(0, 0.5), (1, 0.5)],
1750 outputs: vec![(1, 1.0)],
1751 },
1752 ],
1753 in_delays: vec![],
1754 out_delays: vec![0, 1],
1755 };
1756 let mut setup = Setup::new(vec![], vec![impulse]);
1757 setup.preamp_db = Some(0.0);
1758 let mut chain = Chain::new(&setup, 48000, 2);
1759 let (out, _) = run_all(&mut chain, &[0.4, 0.2, 0.0, 0.0], 4);
1760 assert_eq!(out, vec![0.4, 0.0, 0.0, 0.3]);
1762 }
1763
1764 #[test]
1765 fn an_impulse_is_read_from_a_wav_at_its_own_rate() {
1766 let dir = std::env::temp_dir().join(format!("koan-dsp-{}", std::process::id()));
1767 std::fs::create_dir_all(&dir).unwrap();
1768 let path = dir.join("ir.wav");
1769 let frames: [i16; 8] = [0, 0, 16384, 16384, 0, 0, 0, 0];
1770 let mut wav = Vec::new();
1771 let data_len = (frames.len() * 2) as u32;
1772 wav.extend_from_slice(b"RIFF");
1773 wav.extend_from_slice(&(36 + data_len).to_le_bytes());
1774 wav.extend_from_slice(b"WAVEfmt ");
1775 wav.extend_from_slice(&16u32.to_le_bytes());
1776 wav.extend_from_slice(&1u16.to_le_bytes());
1777 wav.extend_from_slice(&2u16.to_le_bytes());
1778 wav.extend_from_slice(&96000u32.to_le_bytes());
1779 wav.extend_from_slice(&(96000u32 * 4).to_le_bytes());
1780 wav.extend_from_slice(&4u16.to_le_bytes());
1781 wav.extend_from_slice(&16u16.to_le_bytes());
1782 wav.extend_from_slice(b"data");
1783 wav.extend_from_slice(&data_len.to_le_bytes());
1784 for f in frames {
1785 wav.extend_from_slice(&f.to_le_bytes());
1786 }
1787 std::fs::write(&path, wav).unwrap();
1788
1789 let profile = DspProfile {
1790 name: "room".into(),
1791 impulses: vec!["ir.wav".into()],
1792 ..Default::default()
1793 };
1794 let setup = Setup::load(&profile, &[], &dir).unwrap().unwrap();
1795 let impulse = &setup.impulses[&96000][0];
1796 assert_eq!(impulse.channels, Some(2));
1797 assert_eq!(impulse.routes[0].ir, vec![0.0, 0.5, 0.0, 0.0]);
1798 assert_eq!(impulse.delay(), 1);
1799
1800 let missing = DspProfile {
1801 impulses: vec!["nope.wav".into()],
1802 ..profile
1803 };
1804 assert!(Setup::load(&missing, &[], &dir).is_err());
1805 }
1806}