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