Skip to main content

secs_engine/
notes.rs

1//! Typed engine diagnostics; presentation owns their translated text.
2
3use crate::input::Peq;
4
5#[derive(Debug, Clone, PartialEq)]
6pub enum Note {
7    LiftCapped {
8        from_ms: f64,
9        taps: usize,
10        lift_ms: f64,
11    },
12    SubFilters(Vec<(String, crate::setup::SubFilter)>),
13    LowProtect {
14        role: String,
15        rolloff_hz: f64,
16        hp_hz: Option<f64>,
17        limited: bool,
18        reduced: usize,
19        cuts: usize,
20    },
21    RcLimit {
22        hz: f64,
23        auto: bool,
24        schroeder_hz: Option<f64>,
25        basis: secs_types::calibration::RcHiBasis,
26        factor: u8,
27        band_hz: Option<f64>,
28        band_t60_s: Option<f64>,
29        positions: Option<usize>,
30    },
31    RcBands {
32        bands: Vec<(f64, f64)>,
33    },
34    /// ADR 0025, one per main when `lf_align` is on and the mode has FIR
35    /// taps. `arrival_ms` is the median LF arrival left after correction
36    /// (positive = later than HF); `limited` means the pull clamp cut the
37    /// requested correction by more than 1 ms somewhere in the band.
38    LfAlign {
39        role: String,
40        strength: f64,
41        arrival_ms: Option<f64>,
42        limited: bool,
43    },
44    /// ADR 0025, one per main when `mode_decay` is on. Each mode is
45    /// `(hz, measured T60 s, cut depth dB)` with the depth positive; an empty
46    /// list means no mode qualified.
47    ModeDecay {
48        role: String,
49        target_t60_s: f64,
50        modes: Vec<(f64, f64, f64)>,
51    },
52    /// ADR 0030, one per channel whose FIR realizes excess phase when
53    /// `hf_phase` is on. `rotation_deg` is the wrapped rotation unwound above
54    /// the band, `None` when less than an octave is left below 0.45·SR;
55    /// `limited` means the `[−5 ms, lift]` clamp kept part of it.
56    HfPhase {
57        role: String,
58        rotation_deg: Option<f64>,
59        limited: bool,
60    },
61    /// ADR 0031, once when `input_hp_hz` is set: the common input high-pass,
62    /// 8th-order Butterworth, at `hz`.
63    InputHp {
64        hz: f64,
65    },
66    Latency {
67        latency_ms: f64,
68        lift_ms: f64,
69        main_extra_ms: f64,
70    },
71    PhaseStrength(Vec<(String, f64)>),
72    PhaseBand(Vec<(String, Option<f64>)>),
73    // RC (P09)
74    LongModeBoost {
75        cap_db: f64,
76        modes: Vec<(f64, f64)>,
77    },
78    GdCuts {
79        role: String,
80        cuts: Vec<Peq>,
81    },
82    // BA (P10)
83    BaCost(f64),
84    BaBusPass {
85        first: f64,
86        second: f64,
87        kept_first: bool,
88    },
89    BaManual,
90    LayoutFallback(String),
91    Layout(String),
92    TwoWay {
93        roles: Vec<String>,
94        gain_db: f64,
95        xo_hz: f64,
96    },
97    BusGainAnchored(f64),
98    Mimo(MimoNote),
99}
100
101/// Semantic diagnostics in producer order; never translated presentation text.
102#[derive(Debug, Clone, PartialEq)]
103pub enum MimoNote {
104    InsufficientPositions {
105        measured: usize,
106    },
107    ExcludedSupports(Vec<String>),
108    SupportRanges(Vec<(String, Option<(f64, f64)>)>),
109    GuardReduced {
110        role: String,
111        gain_db: f64,
112        cause: GuardCause,
113    },
114    GuardDisabled {
115        role: String,
116        cause: GuardCause,
117    },
118    SupportLimit {
119        max_supports: usize,
120    },
121    FirBudget {
122        support_count: usize,
123        taps_total: usize,
124    },
125}
126
127/// What the MIMO guard rejected last: the support level of that trial
128/// (3 dB above `GuardReduced::gain_db`, or −12 dB before `GuardDisabled`)
129/// and the guard values there that broke their 1 dB limits.
130#[derive(Debug, Clone, PartialEq)]
131pub struct GuardCause {
132    pub rejected_db: f64,
133    /// Late energy rise over every position, `(lo Hz, hi Hz, dB)`: a
134    /// third-octave band or a ±1.5 Hz band around a late mode.
135    pub band: Option<(f64, f64, f64)>,
136    /// Modal tail energy rise over every position, `(mode Hz, dB)`.
137    pub tail: Option<(f64, f64)>,
138}
139
140#[cfg(test)]
141impl MimoNote {
142    // Only the oracle adapter renders the legacy diagnostic strings.
143    pub(crate) fn oracle_text(&self) -> String {
144        match self {
145            Self::InsufficientPositions { measured } => {
146                format!("측정 위치 {measured}곳 — MIMO 는 3곳 이상일 때만")
147            }
148            Self::ExcludedSupports(roles) => format!("서포트 제외(사용자): {}", roles.join(", ")),
149            Self::SupportRanges(ranges) => format!(
150                "서포트 대역 {}",
151                ranges
152                    .iter()
153                    .map(|(role, range)| match range {
154                        Some((lo, hi)) => format!("{role} {lo:.0}–{hi:.0}"),
155                        None => format!("{role} 제외"),
156                    })
157                    .collect::<Vec<_>>()
158                    .join(", ")
159            ),
160            Self::GuardReduced { role, gain_db, .. } => {
161                format!("{role}: 대역 늦은 에너지 악화로 서포트 {gain_db:.0} dB")
162            }
163            Self::GuardDisabled { role, .. } => format!("{role}: MIMO 끔(악화)"),
164            Self::SupportLimit { max_supports } => format!("입력당 서포트 ≤ {max_supports}"),
165            Self::FirBudget {
166                support_count,
167                taps_total,
168            } => {
169                let digits = taps_total.to_string();
170                let grouped = digits
171                    .chars()
172                    .enumerate()
173                    .fold(String::new(), |mut out, (i, ch)| {
174                        if i > 0 && (digits.len() - i).is_multiple_of(3) {
175                            out.push(',');
176                        }
177                        out.push(ch);
178                        out
179                    });
180                format!("FIR 서포트 {support_count}개 · 총 {grouped}탭(48 kHz)")
181            }
182        }
183    }
184}