Skip to main content

secs_graph/
plan.rs

1//! The upstream `make_plan` topology and its as-written transfer functions.
2//!
3//! Upstream source: `export_filters.py` (`make_plan`, `plan_responses`,
4//! `headroom_db`), moved here from `secs-filters` so every writer reads one
5//! plan (#92 C11).
6
7use crate::{GraphError, LfeChoice, SAMPLE_RATE};
8use secs_dsp::Complex64;
9use secs_dsp::fft;
10use secs_dsp::response::{allpass2, band_filter, peaking_response};
11use secs_types::chain::{BandType, MimoFilter, MimoPaths, SignalChain};
12use std::borrow::Cow;
13use std::f64::consts::PI;
14
15/// One filter-plan operation. `fixed` marks a physical-output filter.
16#[derive(Debug, Clone, PartialEq)]
17pub enum Op {
18    /// RBJ peaking EQ.
19    Peq {
20        /// Center frequency in hertz.
21        frequency: f64,
22        /// Quality factor.
23        q: f64,
24        /// Gain in dB.
25        gain_db: f64,
26        /// Whether the band is a physical-output filter.
27        fixed: bool,
28    },
29    /// 48 kHz FIR and its plan-local stem.
30    Fir {
31        /// FIR stem before the writer-specific prefix.
32        name: String,
33        /// IEEE-float samples before WAV serialization.
34        taps: Vec<f64>,
35        /// Fixed-filter marker (normally false for FIRs).
36        fixed: bool,
37    },
38    /// Compact design-rate FIR, expanded only for a consumer that needs 48 kHz taps.
39    FirLowRate {
40        /// FIR stem before the writer-specific prefix.
41        name: String,
42        /// Design-rate coefficients including the isolation low-pass.
43        taps_low: Vec<f64>,
44        /// Design sample rate in hertz.
45        fs_low: f64,
46        /// Fixed-filter marker.
47        fixed: bool,
48    },
49    /// Gain in dB and its fixed marker.
50    Gain {
51        /// Gain in dB.
52        db: f64,
53        /// Fixed-filter marker.
54        fixed: bool,
55    },
56    /// Polarity inversion and its fixed marker.
57    Invert {
58        /// Fixed-filter marker.
59        fixed: bool,
60    },
61    /// Analog-prototype high-pass.
62    Hp {
63        /// Cutoff frequency in hertz.
64        frequency: f64,
65        /// Filter family and order.
66        kind: BandType,
67        /// Fixed-filter marker.
68        fixed: bool,
69    },
70    /// Analog-prototype low-pass.
71    Lp {
72        /// Cutoff frequency in hertz.
73        frequency: f64,
74        /// Filter family and order.
75        kind: BandType,
76        /// Fixed-filter marker.
77        fixed: bool,
78    },
79    /// Second-order analog all-pass.
80    Ap {
81        /// Center frequency in hertz.
82        frequency: f64,
83        /// Quality factor.
84        q: f64,
85        /// Fixed-filter marker.
86        fixed: bool,
87    },
88    /// Delay in milliseconds.
89    Delay {
90        /// Delay in milliseconds.
91        ms: f64,
92        /// Fixed-filter marker.
93        fixed: bool,
94    },
95}
96
97impl Op {
98    /// Whether the operation belongs at the physical output.
99    pub fn is_fixed(&self) -> bool {
100        match self {
101            Self::Peq { fixed, .. }
102            | Self::Fir { fixed, .. }
103            | Self::FirLowRate { fixed, .. }
104            | Self::Gain { fixed, .. }
105            | Self::Invert { fixed }
106            | Self::Hp { fixed, .. }
107            | Self::Lp { fixed, .. }
108            | Self::Ap { fixed, .. }
109            | Self::Delay { fixed, .. } => *fixed,
110        }
111    }
112}
113
114/// One subwoofer bus output, preserving upstream insertion order.
115#[derive(Debug, Clone, PartialEq)]
116pub struct SubFeed {
117    /// Main sends and optional LFE input summed into this bus.
118    pub sources: Vec<String>,
119    /// Per-sub bus gain.
120    pub gain_db: f64,
121    /// `1` or `-1`.
122    pub polarity: i8,
123    /// Bus, fixed, all-pass, and delay operations.
124    pub ops: Vec<Op>,
125}
126
127/// One MIMO support feed.
128#[derive(Debug, Clone, PartialEq)]
129pub struct MimoFeed {
130    /// Raw input role.
131    pub input: String,
132    /// Support output role.
133    pub support: String,
134    /// Path operations.
135    pub ops: Vec<Op>,
136}
137
138/// The complete export plan, with every map represented as an ordered vector.
139#[derive(Debug, Clone, PartialEq)]
140pub struct FilterPlan {
141    /// Main roles followed by optional LFE.
142    pub inputs: Vec<String>,
143    /// Main roles followed by sub roles.
144    pub outputs: Vec<String>,
145    /// Common operations before input correction and the MIMO fork, including LFE.
146    pub pre_ops: Vec<Op>,
147    /// Input-correction operations by role.
148    pub input_ops: Vec<(String, Vec<Op>)>,
149    /// Main-path operations by role.
150    pub main_ops: Vec<(String, Vec<Op>)>,
151    /// Crossover sends by role.
152    pub send_ops: Vec<(String, Vec<Op>)>,
153    /// LFE input operations.
154    pub lfe_ops: Vec<Op>,
155    /// Sub feeds by role.
156    pub subs: Vec<(String, SubFeed)>,
157    /// MIMO paths.
158    pub mimo: Vec<MimoFeed>,
159}
160
161/// The low-rate FIR rule, written once: a design rate of at most 48 kHz
162/// whose 48 kHz expansion has a length `secs_dsp` accepts.
163pub(crate) fn lowrate_fits(taps: usize, fs_low: f64) -> bool {
164    fs_low <= f64::from(SAMPLE_RATE) && secs_dsp::fir::lowrate_len_48k(taps, fs_low).is_some()
165}
166
167/// One FIR of a plan, borrowed: collecting export files never clones stored
168/// FIR arrays.
169#[derive(Debug, Clone, Copy)]
170pub enum FirView<'a> {
171    /// 48 kHz taps.
172    Full(&'a [f64]),
173    /// Design-rate taps and their rate in hertz.
174    LowRate(&'a [f64], f64),
175}
176
177impl<'a> FirView<'a> {
178    /// Check a low-rate recipe against the low-rate FIR rule and its taps for
179    /// finite values. A plan from [`ExportGraph`](crate::ExportGraph) always
180    /// passes; a hand-built or revived plan may not.
181    pub fn validate(self) -> Result<(), GraphError> {
182        if let Self::LowRate(taps, fs_low) = self
183            && (!lowrate_fits(taps.len(), fs_low) || taps.iter().any(|tap| !tap.is_finite()))
184        {
185            return Err(GraphError::Invalid(
186                "invalid or oversized low-rate FIR recipe".into(),
187            ));
188        }
189        Ok(())
190    }
191
192    /// Number of taps at 48 kHz.
193    pub fn len_48k(self) -> usize {
194        match self {
195            Self::Full(taps) => taps.len(),
196            Self::LowRate(taps, fs_low) => {
197                (taps.len() as f64 * (f64::from(SAMPLE_RATE) / fs_low)).ceil() as usize
198            }
199        }
200    }
201
202    /// The 48 kHz taps, expanding a low-rate recipe.
203    pub fn materialize(self) -> Cow<'a, [f64]> {
204        match self {
205            Self::Full(taps) => Cow::Borrowed(taps),
206            Self::LowRate(taps, fs_low) => Cow::Owned(secs_dsp::fir::lowrate_to_48k(taps, fs_low)),
207        }
208    }
209}
210
211impl FilterPlan {
212    /// The plan's FIRs by stem, input corrections first, then MIMO paths; a
213    /// repeated stem keeps its first position and its last taps.
214    pub fn firs(&self) -> Vec<(&str, FirView<'_>)> {
215        let mut out: Vec<(&str, FirView<'_>)> = Vec::new();
216        for ops in self
217            .input_ops
218            .iter()
219            .map(|(_, ops)| ops)
220            .chain(self.mimo.iter().map(|m| &m.ops))
221        {
222            for op in ops {
223                let (name, view) = match op {
224                    Op::Fir { name, taps, .. } => (name.as_str(), FirView::Full(taps)),
225                    Op::FirLowRate {
226                        name,
227                        taps_low,
228                        fs_low,
229                        ..
230                    } => (name.as_str(), FirView::LowRate(taps_low, *fs_low)),
231                    _ => continue,
232                };
233                if let Some(found) = out.iter_mut().find(|(key, _)| *key == name) {
234                    found.1 = view;
235                } else {
236                    out.push((name, view));
237                }
238            }
239        }
240        out
241    }
242}
243
244fn find_ops<'a>(items: &'a [(String, Vec<Op>)], role: &str) -> Option<&'a [Op]> {
245    items
246        .iter()
247        .find(|(key, _)| key == role)
248        .map(|(_, value)| value.as_slice())
249}
250
251fn fixed_ops(chain: &SignalChain, role: &str) -> Vec<Op> {
252    if let Some(main) = chain.mains.iter().find(|main| main.role == role) {
253        return if main.polarity.unwrap_or(1) < 0 {
254            vec![Op::Invert { fixed: true }]
255        } else {
256            Vec::new()
257        };
258    }
259    let Some(sub) = chain.subs.iter().find(|sub| sub.role == role) else {
260        return Vec::new();
261    };
262    let mut ops = Vec::new();
263    if sub.hp_hz != 0.0 {
264        ops.push(Op::Hp {
265            frequency: sub.hp_hz,
266            kind: sub.hp_type,
267            fixed: true,
268        });
269    }
270    if sub.lp_hz != 0.0 {
271        ops.push(Op::Lp {
272            frequency: sub.lp_hz,
273            kind: sub.lp_type,
274            fixed: true,
275        });
276    }
277    ops.extend(sub.peq.iter().map(|&(frequency, q, gain_db)| Op::Peq {
278        frequency,
279        q,
280        gain_db,
281        fixed: true,
282    }));
283    ops
284}
285
286/// Convert a validated chain into the upstream filter plan.
287pub(crate) fn make(
288    chain: &SignalChain,
289    mimo: Option<&MimoPaths>,
290    lfe_choice: LfeChoice,
291) -> FilterPlan {
292    let lfe_gain_db = match lfe_choice {
293        LfeChoice::Omit => None,
294        LfeChoice::Include { gain_db, lp_hz } => Some((gain_db, lp_hz)),
295    };
296    let lfe = !chain.subs.is_empty() && lfe_gain_db.is_some();
297    let mut plan = FilterPlan {
298        inputs: chain.mains.iter().map(|main| main.role.clone()).collect(),
299        outputs: chain
300            .mains
301            .iter()
302            .map(|main| main.role.clone())
303            .chain(chain.subs.iter().map(|sub| sub.role.clone()))
304            .collect(),
305        pre_ops: chain
306            .input_hp_hz
307            .into_iter()
308            .map(|frequency| Op::Hp {
309                frequency,
310                kind: BandType::Bw48,
311                fixed: false,
312            })
313            .collect(),
314        input_ops: Vec::new(),
315        main_ops: Vec::new(),
316        send_ops: Vec::new(),
317        lfe_ops: Vec::new(),
318        subs: Vec::new(),
319        mimo: Vec::new(),
320    };
321    if lfe {
322        plan.inputs.push("LFE".into());
323    }
324    for main in &chain.mains {
325        let mut input = main
326            .input_peq
327            .iter()
328            .map(|&(frequency, q, gain_db)| Op::Peq {
329                frequency,
330                q,
331                gain_db,
332                fixed: false,
333            })
334            .collect::<Vec<_>>();
335        if main.fir.len() > 1 {
336            input.push(Op::Fir {
337                name: format!("{}_fir", main.role),
338                taps: main.fir.clone(),
339                fixed: false,
340            });
341        }
342        plan.input_ops.push((main.role.clone(), input));
343        let mut path = vec![Op::Gain {
344            db: main.gain_db,
345            fixed: false,
346        }];
347        path.extend(main.top_peq.iter().map(|&(frequency, q, gain_db)| Op::Peq {
348            frequency,
349            q,
350            gain_db,
351            fixed: false,
352        }));
353        if let Some(frequency) = main.xo_hz.filter(|value| *value != 0.0) {
354            path.push(Op::Hp {
355                frequency,
356                kind: BandType::Lr24,
357                fixed: false,
358            });
359            plan.send_ops.push((
360                main.role.clone(),
361                vec![Op::Lp {
362                    frequency,
363                    kind: BandType::Lr24,
364                    fixed: false,
365                }],
366            ));
367        }
368        path.push(Op::Delay {
369            ms: main.delay_ms,
370            fixed: false,
371        });
372        path.extend(fixed_ops(chain, &main.role));
373        plan.main_ops.push((main.role.clone(), path));
374    }
375    if lfe {
376        let (gain_db, lp_hz) = lfe_gain_db.expect("LFE is enabled");
377        plan.lfe_ops = vec![
378            Op::Gain {
379                db: gain_db,
380                fixed: false,
381            },
382            Op::Lp {
383                frequency: lp_hz,
384                kind: BandType::Lr24,
385                fixed: false,
386            },
387        ];
388    }
389    let mut sources = plan
390        .send_ops
391        .iter()
392        .map(|(role, _)| role.clone())
393        .collect::<Vec<_>>();
394    if lfe {
395        sources.push("LFE".into());
396    }
397    for sub in &chain.subs {
398        let mut ops = chain
399            .sub_bus_peq
400            .iter()
401            .map(|&(frequency, q, gain_db)| Op::Peq {
402                frequency,
403                q,
404                gain_db,
405                fixed: false,
406            })
407            .collect::<Vec<_>>();
408        ops.extend(fixed_ops(chain, &sub.role));
409        ops.extend(sub.allpass.iter().map(|&(frequency, q)| Op::Ap {
410            frequency,
411            q,
412            fixed: false,
413        }));
414        ops.push(Op::Delay {
415            ms: sub.delay_ms,
416            fixed: false,
417        });
418        plan.subs.push((
419            sub.role.clone(),
420            SubFeed {
421                sources: sources.clone(),
422                gain_db: sub.gain_db,
423                polarity: if sub.polarity > 0 { 1 } else { -1 },
424                ops,
425            },
426        ));
427    }
428    if let Some(paths) = mimo {
429        for path in &paths.paths {
430            let mut ops = match &path.filter {
431                MimoFilter::Fir { taps_48k } => vec![
432                    Op::Delay {
433                        ms: path.delay_ms,
434                        fixed: false,
435                    },
436                    Op::Hp {
437                        frequency: path.low_hz,
438                        kind: BandType::Lr24,
439                        fixed: false,
440                    },
441                    Op::Fir {
442                        name: format!("mimo_{}_{}", path.input, path.support),
443                        taps: taps_48k.clone(),
444                        fixed: false,
445                    },
446                ],
447                MimoFilter::FirLowRate { taps_low, fs_low } => vec![
448                    Op::Delay {
449                        ms: path.delay_ms,
450                        fixed: false,
451                    },
452                    Op::Hp {
453                        frequency: path.low_hz,
454                        kind: BandType::Lr24,
455                        fixed: false,
456                    },
457                    Op::FirLowRate {
458                        name: format!("mimo_{}_{}", path.input, path.support),
459                        taps_low: taps_low.clone(),
460                        fs_low: *fs_low,
461                        fixed: false,
462                    },
463                ],
464                MimoFilter::Iir {
465                    gain_db,
466                    delay_ms,
467                    polarity,
468                    peq,
469                    allpass,
470                } => {
471                    let mut ops = vec![Op::Gain {
472                        db: *gain_db,
473                        fixed: false,
474                    }];
475                    if *polarity < 0 {
476                        ops.push(Op::Invert { fixed: false });
477                    }
478                    ops.push(Op::Delay {
479                        ms: path.delay_ms + delay_ms,
480                        fixed: false,
481                    });
482                    ops.extend(peq.iter().map(|&(frequency, q, gain_db)| Op::Peq {
483                        frequency,
484                        q,
485                        gain_db,
486                        fixed: false,
487                    }));
488                    ops.extend(allpass.iter().map(|&(frequency, q)| Op::Ap {
489                        frequency,
490                        q,
491                        fixed: false,
492                    }));
493                    ops.push(Op::Hp {
494                        frequency: path.low_hz,
495                        kind: BandType::Lr24,
496                        fixed: false,
497                    });
498                    ops.push(Op::Lp {
499                        frequency: paths.fsiso,
500                        kind: BandType::Lr24,
501                        fixed: false,
502                    });
503                    ops
504                }
505            };
506            ops.extend(fixed_ops(chain, &path.support));
507            plan.mimo.push(MimoFeed {
508                input: path.input.clone(),
509                support: path.support.clone(),
510                ops,
511            });
512        }
513    }
514    plan
515}
516
517fn multiply(out: &mut [Complex64], values: impl IntoIterator<Item = Complex64>) {
518    for (left, right) in out.iter_mut().zip(values) {
519        *left *= right;
520    }
521}
522
523fn op_response(op: &Op, f: &[f64], skip_fixed: bool) -> Result<Vec<Complex64>, GraphError> {
524    if skip_fixed && op.is_fixed() {
525        return Ok(vec![Complex64::new(1.0, 0.0); f.len()]);
526    }
527    let result = match op {
528        Op::Peq {
529            frequency,
530            q,
531            gain_db,
532            ..
533        } => peaking_response(f, *frequency, *q, *gain_db, f64::from(SAMPLE_RATE)),
534        Op::Fir { taps, .. } => {
535            let n = 2 * f.len().saturating_sub(1);
536            if n == 0 {
537                return Ok(Vec::new());
538            }
539            let mut padded = vec![0.0; n];
540            let count = n.min(taps.len());
541            padded[..count].copy_from_slice(&taps[..count]);
542            fft::rfft(&padded)
543        }
544        Op::FirLowRate {
545            taps_low, fs_low, ..
546        } => {
547            FirView::LowRate(taps_low, *fs_low).validate()?;
548            let taps = secs_dsp::fir::lowrate_to_48k(taps_low, *fs_low);
549            let n = 2 * f.len().saturating_sub(1);
550            if n == 0 {
551                return Ok(Vec::new());
552            }
553            let mut padded = vec![0.0; n];
554            let count = n.min(taps.len());
555            padded[..count].copy_from_slice(&taps[..count]);
556            fft::rfft(&padded)
557        }
558        Op::Gain { db, .. } => vec![Complex64::new(10_f64.powf(db / 20.0), 0.0); f.len()],
559        Op::Invert { .. } => vec![Complex64::new(-1.0, 0.0); f.len()],
560        Op::Hp {
561            frequency, kind, ..
562        } => band_filter(f, *frequency, "HP", kind.name())
563            .map_err(|error| GraphError::Invalid(error.to_string()))?,
564        Op::Lp {
565            frequency, kind, ..
566        } => band_filter(f, *frequency, "LP", kind.name())
567            .map_err(|error| GraphError::Invalid(error.to_string()))?,
568        Op::Ap { frequency, q, .. } => allpass2(f, *frequency, *q),
569        Op::Delay { ms, .. } => f
570            .iter()
571            .map(|frequency| Complex64::from_polar(1.0, -2.0 * PI * frequency * ms / 1000.0))
572            .collect(),
573    };
574    Ok(result)
575}
576
577fn response_chain(ops: &[Op], f: &[f64], skip_fixed: bool) -> Result<Vec<Complex64>, GraphError> {
578    let mut out = vec![Complex64::new(1.0, 0.0); f.len()];
579    for op in ops {
580        multiply(&mut out, op_response(op, f, skip_fixed)?);
581    }
582    Ok(out)
583}
584
585fn add_response(out: &mut Vec<(String, Vec<Complex64>)>, role: &str, value: Vec<Complex64>) {
586    if let Some((_, current)) = out.iter_mut().find(|(name, _)| name == role) {
587        for (left, right) in current.iter_mut().zip(value) {
588            *left += right;
589        }
590    } else {
591        out.push((role.to_owned(), value));
592    }
593}
594
595/// Compute electrical responses from one input to every reachable output.
596///
597/// A FIR's response is one real FFT of its taps, which lands on that FFT's
598/// grid, not on `f`. So when the plan holds a FIR, `f` must be the 48 kHz
599/// real-FFT grid `f[k] = k * 48000 / (2 * (f.len() - 1))` with at least two
600/// points, and any other `f` is [`GraphError::Invalid`]. Without a FIR,
601/// every response is evaluated on `f` itself.
602pub fn plan_responses(
603    plan: &FilterPlan,
604    f: &[f64],
605    source: &str,
606    skip_fixed: bool,
607) -> Result<Vec<(String, Vec<Complex64>)>, GraphError> {
608    if !plan.firs().is_empty() {
609        check_rfft_grid(f)?;
610    }
611    plan_responses_prepared(plan, f, source, skip_fixed, None)
612}
613
614fn check_rfft_grid(f: &[f64]) -> Result<(), GraphError> {
615    let n = 2 * f.len().saturating_sub(1);
616    let step = f64::from(SAMPLE_RATE) / n as f64;
617    // A millionth of a bin absorbs how a caller rounds `k * step`.
618    let on_grid = n > 0
619        && f.iter()
620            .enumerate()
621            .all(|(k, &value)| (value - k as f64 * step).abs() <= step * 1e-6);
622    if on_grid {
623        Ok(())
624    } else {
625        Err(GraphError::Invalid(format!(
626            "FIR responses need the 48 kHz real-FFT grid of {} points",
627            f.len()
628        )))
629    }
630}
631
632fn plan_responses_prepared(
633    plan: &FilterPlan,
634    f: &[f64],
635    source: &str,
636    skip_fixed: bool,
637    sub_responses: Option<&[Vec<Complex64>]>,
638) -> Result<Vec<(String, Vec<Complex64>)>, GraphError> {
639    let mut out = Vec::new();
640    let (pre, send) = if source == "LFE" {
641        if !plan.inputs.iter().any(|role| role == "LFE") {
642            return Err(GraphError::Invalid("unknown input LFE".into()));
643        }
644        (
645            response_chain(&plan.lfe_ops, f, false)?,
646            Some(vec![Complex64::new(1.0, 0.0); f.len()]),
647        )
648    } else {
649        let input = find_ops(&plan.input_ops, source)
650            .ok_or_else(|| GraphError::Invalid(format!("unknown input {source}")))?;
651        let pre = response_chain(input, f, false)?;
652        let mut main = pre.clone();
653        multiply(
654            &mut main,
655            response_chain(
656                find_ops(&plan.main_ops, source).expect("plan main"),
657                f,
658                skip_fixed,
659            )?,
660        );
661        add_response(&mut out, source, main);
662        let send = find_ops(&plan.send_ops, source)
663            .map(|ops| response_chain(ops, f, false))
664            .transpose()?;
665        (pre, send)
666    };
667    for (sub_index, (role, feed)) in plan.subs.iter().enumerate() {
668        if feed.sources.iter().any(|item| item == source)
669            && let Some(send) = &send
670        {
671            let factor = f64::from(feed.polarity) * 10_f64.powf(feed.gain_db / 20.0);
672            let mut value = pre.clone();
673            multiply(&mut value, send.iter().copied());
674            for item in &mut value {
675                *item *= factor;
676            }
677            if let Some(responses) = sub_responses {
678                multiply(&mut value, responses[sub_index].iter().copied());
679            } else {
680                multiply(&mut value, response_chain(&feed.ops, f, skip_fixed)?);
681            }
682            add_response(&mut out, role, value);
683        }
684    }
685    for path in &plan.mimo {
686        if path.input == source {
687            add_response(
688                &mut out,
689                &path.support,
690                response_chain(&path.ops, f, skip_fixed)?,
691            );
692        }
693    }
694    // Every route starts at the same input. Factor its common response out
695    // of the sum so base and MIMO contributions receive it exactly once.
696    if !plan.pre_ops.is_empty() {
697        let common = response_chain(&plan.pre_ops, f, false)?;
698        for (_, response) in &mut out {
699            multiply(response, common.iter().copied());
700        }
701    }
702    Ok(out)
703}
704
705/// Largest non-LFE output boost from 20 Hz through 20 kHz.
706pub fn headroom_db(plan: &FilterPlan) -> Result<f64, GraphError> {
707    let firs = plan.firs();
708    for (_, taps) in &firs {
709        taps.validate()?;
710    }
711    let taps = firs
712        .iter()
713        .map(|(_, taps)| taps.len_48k())
714        .max()
715        .unwrap_or(1);
716    let n = 65_536_usize.max(taps.max(2).next_power_of_two());
717    let f = (0..=n / 2)
718        .map(|index| index as f64 * f64::from(SAMPLE_RATE) / n as f64)
719        .collect::<Vec<_>>();
720    // The same sub output chain is reached from every input. Compute it once;
721    // keep multiplication and summation in the original per-input order.
722    let sub_responses = plan
723        .subs
724        .iter()
725        .map(|(_, feed)| response_chain(&feed.ops, &f, false))
726        .collect::<Result<Vec<_>, _>>()?;
727    let mut peak = 0.0_f64;
728    for source in plan.inputs.iter().filter(|source| source.as_str() != "LFE") {
729        for (_, response) in plan_responses_prepared(plan, &f, source, false, Some(&sub_responses))?
730        {
731            for (&frequency, value) in f.iter().zip(response) {
732                if (20.0..=20_000.0).contains(&frequency) {
733                    peak = peak.max(value.norm());
734                }
735            }
736        }
737    }
738    Ok(if peak > 0.0 {
739        (20.0 * peak.log10()).max(0.0)
740    } else {
741        0.0
742    })
743}