1use 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#[derive(Debug, Clone, PartialEq)]
17pub enum Op {
18 Peq {
20 frequency: f64,
22 q: f64,
24 gain_db: f64,
26 fixed: bool,
28 },
29 Fir {
31 name: String,
33 taps: Vec<f64>,
35 fixed: bool,
37 },
38 FirLowRate {
40 name: String,
42 taps_low: Vec<f64>,
44 fs_low: f64,
46 fixed: bool,
48 },
49 Gain {
51 db: f64,
53 fixed: bool,
55 },
56 Invert {
58 fixed: bool,
60 },
61 Hp {
63 frequency: f64,
65 kind: BandType,
67 fixed: bool,
69 },
70 Lp {
72 frequency: f64,
74 kind: BandType,
76 fixed: bool,
78 },
79 Ap {
81 frequency: f64,
83 q: f64,
85 fixed: bool,
87 },
88 Delay {
90 ms: f64,
92 fixed: bool,
94 },
95}
96
97impl Op {
98 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#[derive(Debug, Clone, PartialEq)]
116pub struct SubFeed {
117 pub sources: Vec<String>,
119 pub gain_db: f64,
121 pub polarity: i8,
123 pub ops: Vec<Op>,
125}
126
127#[derive(Debug, Clone, PartialEq)]
129pub struct MimoFeed {
130 pub input: String,
132 pub support: String,
134 pub ops: Vec<Op>,
136}
137
138#[derive(Debug, Clone, PartialEq)]
140pub struct FilterPlan {
141 pub inputs: Vec<String>,
143 pub outputs: Vec<String>,
145 pub pre_ops: Vec<Op>,
147 pub input_ops: Vec<(String, Vec<Op>)>,
149 pub main_ops: Vec<(String, Vec<Op>)>,
151 pub send_ops: Vec<(String, Vec<Op>)>,
153 pub lfe_ops: Vec<Op>,
155 pub subs: Vec<(String, SubFeed)>,
157 pub mimo: Vec<MimoFeed>,
159}
160
161pub(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#[derive(Debug, Clone, Copy)]
170pub enum FirView<'a> {
171 Full(&'a [f64]),
173 LowRate(&'a [f64], f64),
175}
176
177impl<'a> FirView<'a> {
178 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 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 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 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 let Some(sub) = chain.subs.iter().find(|sub| sub.role == role) else {
253 return Vec::new();
254 };
255 let mut ops = Vec::new();
256 if sub.hp_hz != 0.0 {
257 ops.push(Op::Hp {
258 frequency: sub.hp_hz,
259 kind: sub.hp_type,
260 fixed: true,
261 });
262 }
263 if sub.lp_hz != 0.0 {
264 ops.push(Op::Lp {
265 frequency: sub.lp_hz,
266 kind: sub.lp_type,
267 fixed: true,
268 });
269 }
270 ops.extend(sub.peq.iter().map(|&(frequency, q, gain_db)| Op::Peq {
271 frequency,
272 q,
273 gain_db,
274 fixed: true,
275 }));
276 ops
277}
278
279pub(crate) fn make(
281 chain: &SignalChain,
282 mimo: Option<&MimoPaths>,
283 lfe_choice: LfeChoice,
284) -> FilterPlan {
285 let lfe_gain_db = match lfe_choice {
286 LfeChoice::Omit => None,
287 LfeChoice::Include { gain_db, lp_hz } => Some((gain_db, lp_hz)),
288 };
289 let lfe = !chain.subs.is_empty() && lfe_gain_db.is_some();
290 let mut plan = FilterPlan {
291 inputs: chain.mains.iter().map(|main| main.role.clone()).collect(),
292 outputs: chain
293 .mains
294 .iter()
295 .map(|main| main.role.clone())
296 .chain(chain.subs.iter().map(|sub| sub.role.clone()))
297 .collect(),
298 pre_ops: chain
299 .input_hp_hz
300 .into_iter()
301 .map(|frequency| Op::Hp {
302 frequency,
303 kind: BandType::Bw48,
304 fixed: false,
305 })
306 .collect(),
307 input_ops: Vec::new(),
308 main_ops: Vec::new(),
309 send_ops: Vec::new(),
310 lfe_ops: Vec::new(),
311 subs: Vec::new(),
312 mimo: Vec::new(),
313 };
314 if lfe {
315 plan.inputs.push("LFE".into());
316 }
317 for main in &chain.mains {
318 let mut input = main
319 .input_peq
320 .iter()
321 .map(|&(frequency, q, gain_db)| Op::Peq {
322 frequency,
323 q,
324 gain_db,
325 fixed: false,
326 })
327 .collect::<Vec<_>>();
328 if main.fir.len() > 1 {
329 input.push(Op::Fir {
330 name: format!("{}_fir", main.role),
331 taps: main.fir.clone(),
332 fixed: false,
333 });
334 }
335 plan.input_ops.push((main.role.clone(), input));
336 let mut path = vec![Op::Gain {
337 db: main.gain_db,
338 fixed: false,
339 }];
340 path.extend(main.top_peq.iter().map(|&(frequency, q, gain_db)| Op::Peq {
341 frequency,
342 q,
343 gain_db,
344 fixed: false,
345 }));
346 if let Some(frequency) = main.xo_hz.filter(|value| *value != 0.0) {
347 path.push(Op::Hp {
348 frequency,
349 kind: BandType::Lr24,
350 fixed: false,
351 });
352 plan.send_ops.push((
353 main.role.clone(),
354 vec![Op::Lp {
355 frequency,
356 kind: BandType::Lr24,
357 fixed: false,
358 }],
359 ));
360 }
361 path.push(Op::Delay {
362 ms: main.delay_ms,
363 fixed: false,
364 });
365 plan.main_ops.push((main.role.clone(), path));
366 }
367 if lfe {
368 let (gain_db, lp_hz) = lfe_gain_db.expect("LFE is enabled");
369 plan.lfe_ops = vec![
370 Op::Gain {
371 db: gain_db,
372 fixed: false,
373 },
374 Op::Lp {
375 frequency: lp_hz,
376 kind: BandType::Lr24,
377 fixed: false,
378 },
379 ];
380 }
381 let mut sources = plan
382 .send_ops
383 .iter()
384 .map(|(role, _)| role.clone())
385 .collect::<Vec<_>>();
386 if lfe {
387 sources.push("LFE".into());
388 }
389 for sub in &chain.subs {
390 let mut ops = chain
391 .sub_bus_peq
392 .iter()
393 .map(|&(frequency, q, gain_db)| Op::Peq {
394 frequency,
395 q,
396 gain_db,
397 fixed: false,
398 })
399 .collect::<Vec<_>>();
400 ops.extend(fixed_ops(chain, &sub.role));
401 ops.extend(sub.allpass.iter().map(|&(frequency, q)| Op::Ap {
402 frequency,
403 q,
404 fixed: false,
405 }));
406 ops.push(Op::Delay {
407 ms: sub.delay_ms,
408 fixed: false,
409 });
410 plan.subs.push((
411 sub.role.clone(),
412 SubFeed {
413 sources: sources.clone(),
414 gain_db: sub.gain_db,
415 polarity: if sub.polarity > 0 { 1 } else { -1 },
416 ops,
417 },
418 ));
419 }
420 if let Some(paths) = mimo {
421 for path in &paths.paths {
422 let mut ops = match &path.filter {
423 MimoFilter::Fir { taps_48k } => vec![
424 Op::Delay {
425 ms: path.delay_ms,
426 fixed: false,
427 },
428 Op::Hp {
429 frequency: path.low_hz,
430 kind: BandType::Lr24,
431 fixed: false,
432 },
433 Op::Fir {
434 name: format!("mimo_{}_{}", path.input, path.support),
435 taps: taps_48k.clone(),
436 fixed: false,
437 },
438 ],
439 MimoFilter::FirLowRate { taps_low, fs_low } => vec![
440 Op::Delay {
441 ms: path.delay_ms,
442 fixed: false,
443 },
444 Op::Hp {
445 frequency: path.low_hz,
446 kind: BandType::Lr24,
447 fixed: false,
448 },
449 Op::FirLowRate {
450 name: format!("mimo_{}_{}", path.input, path.support),
451 taps_low: taps_low.clone(),
452 fs_low: *fs_low,
453 fixed: false,
454 },
455 ],
456 MimoFilter::Iir {
457 gain_db,
458 delay_ms,
459 polarity,
460 peq,
461 allpass,
462 } => {
463 let mut ops = vec![Op::Gain {
464 db: *gain_db,
465 fixed: false,
466 }];
467 if *polarity < 0 {
468 ops.push(Op::Invert { fixed: false });
469 }
470 ops.push(Op::Delay {
471 ms: path.delay_ms + delay_ms,
472 fixed: false,
473 });
474 ops.extend(peq.iter().map(|&(frequency, q, gain_db)| Op::Peq {
475 frequency,
476 q,
477 gain_db,
478 fixed: false,
479 }));
480 ops.extend(allpass.iter().map(|&(frequency, q)| Op::Ap {
481 frequency,
482 q,
483 fixed: false,
484 }));
485 ops.push(Op::Hp {
486 frequency: path.low_hz,
487 kind: BandType::Lr24,
488 fixed: false,
489 });
490 ops.push(Op::Lp {
491 frequency: paths.fsiso,
492 kind: BandType::Lr24,
493 fixed: false,
494 });
495 ops
496 }
497 };
498 ops.extend(fixed_ops(chain, &path.support));
499 plan.mimo.push(MimoFeed {
500 input: path.input.clone(),
501 support: path.support.clone(),
502 ops,
503 });
504 }
505 }
506 plan
507}
508
509fn multiply(out: &mut [Complex64], values: impl IntoIterator<Item = Complex64>) {
510 for (left, right) in out.iter_mut().zip(values) {
511 *left *= right;
512 }
513}
514
515fn op_response(op: &Op, f: &[f64], skip_fixed: bool) -> Result<Vec<Complex64>, GraphError> {
516 if skip_fixed && op.is_fixed() {
517 return Ok(vec![Complex64::new(1.0, 0.0); f.len()]);
518 }
519 let result = match op {
520 Op::Peq {
521 frequency,
522 q,
523 gain_db,
524 ..
525 } => peaking_response(f, *frequency, *q, *gain_db, f64::from(SAMPLE_RATE)),
526 Op::Fir { taps, .. } => {
527 let n = 2 * f.len().saturating_sub(1);
528 if n == 0 {
529 return Ok(Vec::new());
530 }
531 let mut padded = vec![0.0; n];
532 let count = n.min(taps.len());
533 padded[..count].copy_from_slice(&taps[..count]);
534 fft::rfft(&padded)
535 }
536 Op::FirLowRate {
537 taps_low, fs_low, ..
538 } => {
539 FirView::LowRate(taps_low, *fs_low).validate()?;
540 let taps = secs_dsp::fir::lowrate_to_48k(taps_low, *fs_low);
541 let n = 2 * f.len().saturating_sub(1);
542 if n == 0 {
543 return Ok(Vec::new());
544 }
545 let mut padded = vec![0.0; n];
546 let count = n.min(taps.len());
547 padded[..count].copy_from_slice(&taps[..count]);
548 fft::rfft(&padded)
549 }
550 Op::Gain { db, .. } => vec![Complex64::new(10_f64.powf(db / 20.0), 0.0); f.len()],
551 Op::Invert { .. } => vec![Complex64::new(-1.0, 0.0); f.len()],
552 Op::Hp {
553 frequency, kind, ..
554 } => band_filter(f, *frequency, "HP", kind.name())
555 .map_err(|error| GraphError::Invalid(error.to_string()))?,
556 Op::Lp {
557 frequency, kind, ..
558 } => band_filter(f, *frequency, "LP", kind.name())
559 .map_err(|error| GraphError::Invalid(error.to_string()))?,
560 Op::Ap { frequency, q, .. } => allpass2(f, *frequency, *q),
561 Op::Delay { ms, .. } => f
562 .iter()
563 .map(|frequency| Complex64::from_polar(1.0, -2.0 * PI * frequency * ms / 1000.0))
564 .collect(),
565 };
566 Ok(result)
567}
568
569fn response_chain(ops: &[Op], f: &[f64], skip_fixed: bool) -> Result<Vec<Complex64>, GraphError> {
570 let mut out = vec![Complex64::new(1.0, 0.0); f.len()];
571 for op in ops {
572 multiply(&mut out, op_response(op, f, skip_fixed)?);
573 }
574 Ok(out)
575}
576
577fn add_response(out: &mut Vec<(String, Vec<Complex64>)>, role: &str, value: Vec<Complex64>) {
578 if let Some((_, current)) = out.iter_mut().find(|(name, _)| name == role) {
579 for (left, right) in current.iter_mut().zip(value) {
580 *left += right;
581 }
582 } else {
583 out.push((role.to_owned(), value));
584 }
585}
586
587pub fn plan_responses(
595 plan: &FilterPlan,
596 f: &[f64],
597 source: &str,
598 skip_fixed: bool,
599) -> Result<Vec<(String, Vec<Complex64>)>, GraphError> {
600 if !plan.firs().is_empty() {
601 check_rfft_grid(f)?;
602 }
603 plan_responses_prepared(plan, f, source, skip_fixed, None)
604}
605
606fn check_rfft_grid(f: &[f64]) -> Result<(), GraphError> {
607 let n = 2 * f.len().saturating_sub(1);
608 let step = f64::from(SAMPLE_RATE) / n as f64;
609 let on_grid = n > 0
611 && f.iter()
612 .enumerate()
613 .all(|(k, &value)| (value - k as f64 * step).abs() <= step * 1e-6);
614 if on_grid {
615 Ok(())
616 } else {
617 Err(GraphError::Invalid(format!(
618 "FIR responses need the 48 kHz real-FFT grid of {} points",
619 f.len()
620 )))
621 }
622}
623
624fn plan_responses_prepared(
625 plan: &FilterPlan,
626 f: &[f64],
627 source: &str,
628 skip_fixed: bool,
629 sub_responses: Option<&[Vec<Complex64>]>,
630) -> Result<Vec<(String, Vec<Complex64>)>, GraphError> {
631 let mut out = Vec::new();
632 let (pre, send) = if source == "LFE" {
633 if !plan.inputs.iter().any(|role| role == "LFE") {
634 return Err(GraphError::Invalid("unknown input LFE".into()));
635 }
636 (
637 response_chain(&plan.lfe_ops, f, false)?,
638 Some(vec![Complex64::new(1.0, 0.0); f.len()]),
639 )
640 } else {
641 let input = find_ops(&plan.input_ops, source)
642 .ok_or_else(|| GraphError::Invalid(format!("unknown input {source}")))?;
643 let pre = response_chain(input, f, false)?;
644 let mut main = pre.clone();
645 multiply(
646 &mut main,
647 response_chain(
648 find_ops(&plan.main_ops, source).expect("plan main"),
649 f,
650 false,
651 )?,
652 );
653 add_response(&mut out, source, main);
654 let send = find_ops(&plan.send_ops, source)
655 .map(|ops| response_chain(ops, f, false))
656 .transpose()?;
657 (pre, send)
658 };
659 for (sub_index, (role, feed)) in plan.subs.iter().enumerate() {
660 if feed.sources.iter().any(|item| item == source)
661 && let Some(send) = &send
662 {
663 let factor = f64::from(feed.polarity) * 10_f64.powf(feed.gain_db / 20.0);
664 let mut value = pre.clone();
665 multiply(&mut value, send.iter().copied());
666 for item in &mut value {
667 *item *= factor;
668 }
669 if let Some(responses) = sub_responses {
670 multiply(&mut value, responses[sub_index].iter().copied());
671 } else {
672 multiply(&mut value, response_chain(&feed.ops, f, skip_fixed)?);
673 }
674 add_response(&mut out, role, value);
675 }
676 }
677 for path in &plan.mimo {
678 if path.input == source {
679 add_response(
680 &mut out,
681 &path.support,
682 response_chain(&path.ops, f, skip_fixed)?,
683 );
684 }
685 }
686 if !plan.pre_ops.is_empty() {
689 let common = response_chain(&plan.pre_ops, f, false)?;
690 for (_, response) in &mut out {
691 multiply(response, common.iter().copied());
692 }
693 }
694 Ok(out)
695}
696
697pub fn headroom_db(plan: &FilterPlan) -> Result<f64, GraphError> {
699 let firs = plan.firs();
700 for (_, taps) in &firs {
701 taps.validate()?;
702 }
703 let taps = firs
704 .iter()
705 .map(|(_, taps)| taps.len_48k())
706 .max()
707 .unwrap_or(1);
708 let n = 65_536_usize.max(taps.max(2).next_power_of_two());
709 let f = (0..=n / 2)
710 .map(|index| index as f64 * f64::from(SAMPLE_RATE) / n as f64)
711 .collect::<Vec<_>>();
712 let sub_responses = plan
715 .subs
716 .iter()
717 .map(|(_, feed)| response_chain(&feed.ops, &f, false))
718 .collect::<Result<Vec<_>, _>>()?;
719 let mut peak = 0.0_f64;
720 for source in plan.inputs.iter().filter(|source| source.as_str() != "LFE") {
721 for (_, response) in plan_responses_prepared(plan, &f, source, false, Some(&sub_responses))?
722 {
723 for (&frequency, value) in f.iter().zip(response) {
724 if (20.0..=20_000.0).contains(&frequency) {
725 peak = peak.max(value.norm());
726 }
727 }
728 }
729 }
730 Ok(if peak > 0.0 {
731 (20.0 * peak.log10()).max(0.0)
732 } else {
733 0.0
734 })
735}