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 input_ops: Vec<(String, Vec<Op>)>,
147 pub main_ops: Vec<(String, Vec<Op>)>,
149 pub send_ops: Vec<(String, Vec<Op>)>,
151 pub lfe_ops: Vec<Op>,
153 pub subs: Vec<(String, SubFeed)>,
155 pub mimo: Vec<MimoFeed>,
157}
158
159pub(crate) fn lowrate_fits(taps: usize, fs_low: f64) -> bool {
162 fs_low <= f64::from(SAMPLE_RATE) && secs_dsp::fir::lowrate_len_48k(taps, fs_low).is_some()
163}
164
165#[derive(Debug, Clone, Copy)]
168pub enum FirView<'a> {
169 Full(&'a [f64]),
171 LowRate(&'a [f64], f64),
173}
174
175impl<'a> FirView<'a> {
176 pub fn validate(self) -> Result<(), GraphError> {
180 if let Self::LowRate(taps, fs_low) = self
181 && (!lowrate_fits(taps.len(), fs_low) || taps.iter().any(|tap| !tap.is_finite()))
182 {
183 return Err(GraphError::Invalid(
184 "invalid or oversized low-rate FIR recipe".into(),
185 ));
186 }
187 Ok(())
188 }
189
190 pub fn len_48k(self) -> usize {
192 match self {
193 Self::Full(taps) => taps.len(),
194 Self::LowRate(taps, fs_low) => {
195 (taps.len() as f64 * (f64::from(SAMPLE_RATE) / fs_low)).ceil() as usize
196 }
197 }
198 }
199
200 pub fn materialize(self) -> Cow<'a, [f64]> {
202 match self {
203 Self::Full(taps) => Cow::Borrowed(taps),
204 Self::LowRate(taps, fs_low) => Cow::Owned(secs_dsp::fir::lowrate_to_48k(taps, fs_low)),
205 }
206 }
207}
208
209impl FilterPlan {
210 pub fn firs(&self) -> Vec<(&str, FirView<'_>)> {
213 let mut out: Vec<(&str, FirView<'_>)> = Vec::new();
214 for ops in self
215 .input_ops
216 .iter()
217 .map(|(_, ops)| ops)
218 .chain(self.mimo.iter().map(|m| &m.ops))
219 {
220 for op in ops {
221 let (name, view) = match op {
222 Op::Fir { name, taps, .. } => (name.as_str(), FirView::Full(taps)),
223 Op::FirLowRate {
224 name,
225 taps_low,
226 fs_low,
227 ..
228 } => (name.as_str(), FirView::LowRate(taps_low, *fs_low)),
229 _ => continue,
230 };
231 if let Some(found) = out.iter_mut().find(|(key, _)| *key == name) {
232 found.1 = view;
233 } else {
234 out.push((name, view));
235 }
236 }
237 }
238 out
239 }
240}
241
242fn find_ops<'a>(items: &'a [(String, Vec<Op>)], role: &str) -> Option<&'a [Op]> {
243 items
244 .iter()
245 .find(|(key, _)| key == role)
246 .map(|(_, value)| value.as_slice())
247}
248
249fn fixed_ops(chain: &SignalChain, role: &str) -> Vec<Op> {
250 let Some(sub) = chain.subs.iter().find(|sub| sub.role == role) else {
251 return Vec::new();
252 };
253 let mut ops = Vec::new();
254 if sub.hp_hz != 0.0 {
255 ops.push(Op::Hp {
256 frequency: sub.hp_hz,
257 kind: sub.hp_type,
258 fixed: true,
259 });
260 }
261 if sub.lp_hz != 0.0 {
262 ops.push(Op::Lp {
263 frequency: sub.lp_hz,
264 kind: sub.lp_type,
265 fixed: true,
266 });
267 }
268 ops.extend(sub.peq.iter().map(|&(frequency, q, gain_db)| Op::Peq {
269 frequency,
270 q,
271 gain_db,
272 fixed: true,
273 }));
274 ops
275}
276
277pub(crate) fn make(
279 chain: &SignalChain,
280 mimo: Option<&MimoPaths>,
281 lfe_choice: LfeChoice,
282) -> FilterPlan {
283 let lfe_gain_db = match lfe_choice {
284 LfeChoice::Omit => None,
285 LfeChoice::Include { gain_db, lp_hz } => Some((gain_db, lp_hz)),
286 };
287 let lfe = !chain.subs.is_empty() && lfe_gain_db.is_some();
288 let mut plan = FilterPlan {
289 inputs: chain.mains.iter().map(|main| main.role.clone()).collect(),
290 outputs: chain
291 .mains
292 .iter()
293 .map(|main| main.role.clone())
294 .chain(chain.subs.iter().map(|sub| sub.role.clone()))
295 .collect(),
296 input_ops: Vec::new(),
297 main_ops: Vec::new(),
298 send_ops: Vec::new(),
299 lfe_ops: Vec::new(),
300 subs: Vec::new(),
301 mimo: Vec::new(),
302 };
303 if lfe {
304 plan.inputs.push("LFE".into());
305 }
306 for main in &chain.mains {
307 let mut input = main
308 .input_peq
309 .iter()
310 .map(|&(frequency, q, gain_db)| Op::Peq {
311 frequency,
312 q,
313 gain_db,
314 fixed: false,
315 })
316 .collect::<Vec<_>>();
317 if main.fir.len() > 1 {
318 input.push(Op::Fir {
319 name: format!("{}_fir", main.role),
320 taps: main.fir.clone(),
321 fixed: false,
322 });
323 }
324 plan.input_ops.push((main.role.clone(), input));
325 let mut path = vec![Op::Gain {
326 db: main.gain_db,
327 fixed: false,
328 }];
329 path.extend(main.top_peq.iter().map(|&(frequency, q, gain_db)| Op::Peq {
330 frequency,
331 q,
332 gain_db,
333 fixed: false,
334 }));
335 if let Some(frequency) = main.xo_hz.filter(|value| *value != 0.0) {
336 path.push(Op::Hp {
337 frequency,
338 kind: BandType::Lr24,
339 fixed: false,
340 });
341 plan.send_ops.push((
342 main.role.clone(),
343 vec![Op::Lp {
344 frequency,
345 kind: BandType::Lr24,
346 fixed: false,
347 }],
348 ));
349 }
350 path.push(Op::Delay {
351 ms: main.delay_ms,
352 fixed: false,
353 });
354 plan.main_ops.push((main.role.clone(), path));
355 }
356 if lfe {
357 let (gain_db, lp_hz) = lfe_gain_db.expect("LFE is enabled");
358 plan.lfe_ops = vec![
359 Op::Gain {
360 db: gain_db,
361 fixed: false,
362 },
363 Op::Lp {
364 frequency: lp_hz,
365 kind: BandType::Lr24,
366 fixed: false,
367 },
368 ];
369 }
370 let mut sources = plan
371 .send_ops
372 .iter()
373 .map(|(role, _)| role.clone())
374 .collect::<Vec<_>>();
375 if lfe {
376 sources.push("LFE".into());
377 }
378 for sub in &chain.subs {
379 let mut ops = chain
380 .sub_bus_peq
381 .iter()
382 .map(|&(frequency, q, gain_db)| Op::Peq {
383 frequency,
384 q,
385 gain_db,
386 fixed: false,
387 })
388 .collect::<Vec<_>>();
389 ops.extend(fixed_ops(chain, &sub.role));
390 ops.extend(sub.allpass.iter().map(|&(frequency, q)| Op::Ap {
391 frequency,
392 q,
393 fixed: false,
394 }));
395 ops.push(Op::Delay {
396 ms: sub.delay_ms,
397 fixed: false,
398 });
399 plan.subs.push((
400 sub.role.clone(),
401 SubFeed {
402 sources: sources.clone(),
403 gain_db: sub.gain_db,
404 polarity: if sub.polarity > 0 { 1 } else { -1 },
405 ops,
406 },
407 ));
408 }
409 if let Some(paths) = mimo {
410 for path in &paths.paths {
411 let mut ops = match &path.filter {
412 MimoFilter::Fir { taps_48k } => vec![
413 Op::Delay {
414 ms: path.delay_ms,
415 fixed: false,
416 },
417 Op::Hp {
418 frequency: path.low_hz,
419 kind: BandType::Lr24,
420 fixed: false,
421 },
422 Op::Fir {
423 name: format!("mimo_{}_{}", path.input, path.support),
424 taps: taps_48k.clone(),
425 fixed: false,
426 },
427 ],
428 MimoFilter::FirLowRate { taps_low, fs_low } => vec![
429 Op::Delay {
430 ms: path.delay_ms,
431 fixed: false,
432 },
433 Op::Hp {
434 frequency: path.low_hz,
435 kind: BandType::Lr24,
436 fixed: false,
437 },
438 Op::FirLowRate {
439 name: format!("mimo_{}_{}", path.input, path.support),
440 taps_low: taps_low.clone(),
441 fs_low: *fs_low,
442 fixed: false,
443 },
444 ],
445 MimoFilter::Iir {
446 gain_db,
447 delay_ms,
448 polarity,
449 peq,
450 allpass,
451 } => {
452 let mut ops = vec![Op::Gain {
453 db: *gain_db,
454 fixed: false,
455 }];
456 if *polarity < 0 {
457 ops.push(Op::Invert { fixed: false });
458 }
459 ops.push(Op::Delay {
460 ms: path.delay_ms + delay_ms,
461 fixed: false,
462 });
463 ops.extend(peq.iter().map(|&(frequency, q, gain_db)| Op::Peq {
464 frequency,
465 q,
466 gain_db,
467 fixed: false,
468 }));
469 ops.extend(allpass.iter().map(|&(frequency, q)| Op::Ap {
470 frequency,
471 q,
472 fixed: false,
473 }));
474 ops.push(Op::Hp {
475 frequency: path.low_hz,
476 kind: BandType::Lr24,
477 fixed: false,
478 });
479 ops.push(Op::Lp {
480 frequency: paths.fsiso,
481 kind: BandType::Lr24,
482 fixed: false,
483 });
484 ops
485 }
486 };
487 ops.extend(fixed_ops(chain, &path.support));
488 plan.mimo.push(MimoFeed {
489 input: path.input.clone(),
490 support: path.support.clone(),
491 ops,
492 });
493 }
494 }
495 plan
496}
497
498fn multiply(out: &mut [Complex64], values: impl IntoIterator<Item = Complex64>) {
499 for (left, right) in out.iter_mut().zip(values) {
500 *left *= right;
501 }
502}
503
504fn op_response(op: &Op, f: &[f64], skip_fixed: bool) -> Result<Vec<Complex64>, GraphError> {
505 if skip_fixed && op.is_fixed() {
506 return Ok(vec![Complex64::new(1.0, 0.0); f.len()]);
507 }
508 let result = match op {
509 Op::Peq {
510 frequency,
511 q,
512 gain_db,
513 ..
514 } => peaking_response(f, *frequency, *q, *gain_db, f64::from(SAMPLE_RATE)),
515 Op::Fir { taps, .. } => {
516 let n = 2 * f.len().saturating_sub(1);
517 if n == 0 {
518 return Ok(Vec::new());
519 }
520 let mut padded = vec![0.0; n];
521 let count = n.min(taps.len());
522 padded[..count].copy_from_slice(&taps[..count]);
523 fft::rfft(&padded)
524 }
525 Op::FirLowRate {
526 taps_low, fs_low, ..
527 } => {
528 FirView::LowRate(taps_low, *fs_low).validate()?;
529 let taps = secs_dsp::fir::lowrate_to_48k(taps_low, *fs_low);
530 let n = 2 * f.len().saturating_sub(1);
531 if n == 0 {
532 return Ok(Vec::new());
533 }
534 let mut padded = vec![0.0; n];
535 let count = n.min(taps.len());
536 padded[..count].copy_from_slice(&taps[..count]);
537 fft::rfft(&padded)
538 }
539 Op::Gain { db, .. } => vec![Complex64::new(10_f64.powf(db / 20.0), 0.0); f.len()],
540 Op::Invert { .. } => vec![Complex64::new(-1.0, 0.0); f.len()],
541 Op::Hp {
542 frequency, kind, ..
543 } => band_filter(f, *frequency, "HP", kind.name())
544 .map_err(|error| GraphError::Invalid(error.to_string()))?,
545 Op::Lp {
546 frequency, kind, ..
547 } => band_filter(f, *frequency, "LP", kind.name())
548 .map_err(|error| GraphError::Invalid(error.to_string()))?,
549 Op::Ap { frequency, q, .. } => allpass2(f, *frequency, *q),
550 Op::Delay { ms, .. } => f
551 .iter()
552 .map(|frequency| Complex64::from_polar(1.0, -2.0 * PI * frequency * ms / 1000.0))
553 .collect(),
554 };
555 Ok(result)
556}
557
558fn response_chain(ops: &[Op], f: &[f64], skip_fixed: bool) -> Result<Vec<Complex64>, GraphError> {
559 let mut out = vec![Complex64::new(1.0, 0.0); f.len()];
560 for op in ops {
561 multiply(&mut out, op_response(op, f, skip_fixed)?);
562 }
563 Ok(out)
564}
565
566fn add_response(out: &mut Vec<(String, Vec<Complex64>)>, role: &str, value: Vec<Complex64>) {
567 if let Some((_, current)) = out.iter_mut().find(|(name, _)| name == role) {
568 for (left, right) in current.iter_mut().zip(value) {
569 *left += right;
570 }
571 } else {
572 out.push((role.to_owned(), value));
573 }
574}
575
576pub fn plan_responses(
584 plan: &FilterPlan,
585 f: &[f64],
586 source: &str,
587 skip_fixed: bool,
588) -> Result<Vec<(String, Vec<Complex64>)>, GraphError> {
589 if !plan.firs().is_empty() {
590 check_rfft_grid(f)?;
591 }
592 plan_responses_prepared(plan, f, source, skip_fixed, None)
593}
594
595fn check_rfft_grid(f: &[f64]) -> Result<(), GraphError> {
596 let n = 2 * f.len().saturating_sub(1);
597 let step = f64::from(SAMPLE_RATE) / n as f64;
598 let on_grid = n > 0
600 && f.iter()
601 .enumerate()
602 .all(|(k, &value)| (value - k as f64 * step).abs() <= step * 1e-6);
603 if on_grid {
604 Ok(())
605 } else {
606 Err(GraphError::Invalid(format!(
607 "FIR responses need the 48 kHz real-FFT grid of {} points",
608 f.len()
609 )))
610 }
611}
612
613fn plan_responses_prepared(
614 plan: &FilterPlan,
615 f: &[f64],
616 source: &str,
617 skip_fixed: bool,
618 sub_responses: Option<&[Vec<Complex64>]>,
619) -> Result<Vec<(String, Vec<Complex64>)>, GraphError> {
620 let mut out = Vec::new();
621 let (pre, send) = if source == "LFE" {
622 if !plan.inputs.iter().any(|role| role == "LFE") {
623 return Err(GraphError::Invalid("unknown input LFE".into()));
624 }
625 (
626 response_chain(&plan.lfe_ops, f, false)?,
627 Some(vec![Complex64::new(1.0, 0.0); f.len()]),
628 )
629 } else {
630 let input = find_ops(&plan.input_ops, source)
631 .ok_or_else(|| GraphError::Invalid(format!("unknown input {source}")))?;
632 let pre = response_chain(input, f, false)?;
633 let mut main = pre.clone();
634 multiply(
635 &mut main,
636 response_chain(
637 find_ops(&plan.main_ops, source).expect("plan main"),
638 f,
639 false,
640 )?,
641 );
642 add_response(&mut out, source, main);
643 let send = find_ops(&plan.send_ops, source)
644 .map(|ops| response_chain(ops, f, false))
645 .transpose()?;
646 (pre, send)
647 };
648 for (sub_index, (role, feed)) in plan.subs.iter().enumerate() {
649 if feed.sources.iter().any(|item| item == source)
650 && let Some(send) = &send
651 {
652 let factor = f64::from(feed.polarity) * 10_f64.powf(feed.gain_db / 20.0);
653 let mut value = pre.clone();
654 multiply(&mut value, send.iter().copied());
655 for item in &mut value {
656 *item *= factor;
657 }
658 if let Some(responses) = sub_responses {
659 multiply(&mut value, responses[sub_index].iter().copied());
660 } else {
661 multiply(&mut value, response_chain(&feed.ops, f, skip_fixed)?);
662 }
663 add_response(&mut out, role, value);
664 }
665 }
666 for path in &plan.mimo {
667 if path.input == source {
668 add_response(
669 &mut out,
670 &path.support,
671 response_chain(&path.ops, f, skip_fixed)?,
672 );
673 }
674 }
675 Ok(out)
676}
677
678pub fn headroom_db(plan: &FilterPlan) -> Result<f64, GraphError> {
680 let firs = plan.firs();
681 for (_, taps) in &firs {
682 taps.validate()?;
683 }
684 let taps = firs
685 .iter()
686 .map(|(_, taps)| taps.len_48k())
687 .max()
688 .unwrap_or(1);
689 let n = 65_536_usize.max(taps.max(2).next_power_of_two());
690 let f = (0..=n / 2)
691 .map(|index| index as f64 * f64::from(SAMPLE_RATE) / n as f64)
692 .collect::<Vec<_>>();
693 let sub_responses = plan
696 .subs
697 .iter()
698 .map(|(_, feed)| response_chain(&feed.ops, &f, false))
699 .collect::<Result<Vec<_>, _>>()?;
700 let mut peak = 0.0_f64;
701 for source in plan.inputs.iter().filter(|source| source.as_str() != "LFE") {
702 for (_, response) in plan_responses_prepared(plan, &f, source, false, Some(&sub_responses))?
703 {
704 for (&frequency, value) in f.iter().zip(response) {
705 if (20.0..=20_000.0).contains(&frequency) {
706 peak = peak.max(value.norm());
707 }
708 }
709 }
710 }
711 Ok(if peak > 0.0 {
712 (20.0 * peak.log10()).max(0.0)
713 } else {
714 0.0
715 })
716}