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 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
286pub(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
595pub 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 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 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
705pub 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 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}