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    },
19    RcLimit {
20        hz: f64,
21        auto: bool,
22        schroeder_hz: Option<f64>,
23        basis: secs_types::calibration::RcHiBasis,
24        factor: u8,
25        band_hz: Option<f64>,
26        band_t60_s: Option<f64>,
27        positions: Option<usize>,
28    },
29    RcBands {
30        bands: Vec<(f64, f64)>,
31    },
32    /// ADR 0025, one per main when `lf_align` is on and the mode has FIR
33    /// taps. `arrival_ms` is the median LF arrival left after correction
34    /// (positive = later than HF); `limited` means the pull clamp cut the
35    /// requested correction by more than 1 ms somewhere in the band.
36    LfAlign {
37        role: String,
38        strength: f64,
39        arrival_ms: Option<f64>,
40        limited: bool,
41    },
42    /// ADR 0025, one per main when `mode_decay` is on. Each mode is
43    /// `(hz, measured T60 s, cut depth dB)` with the depth positive; an empty
44    /// list means no mode qualified.
45    ModeDecay {
46        role: String,
47        target_t60_s: f64,
48        modes: Vec<(f64, f64, f64)>,
49    },
50    Latency {
51        latency_ms: f64,
52        lift_ms: f64,
53        main_extra_ms: f64,
54    },
55    PhaseStrength(Vec<(String, f64)>),
56    PhaseBand(Vec<(String, Option<f64>)>),
57    // RC (P09)
58    LongModeBoost {
59        cap_db: f64,
60        modes: Vec<(f64, f64)>,
61    },
62    GdCuts {
63        role: String,
64        cuts: Vec<Peq>,
65    },
66    // BA (P10)
67    BaCost(f64),
68    BaBusPass {
69        first: f64,
70        second: f64,
71        kept_first: bool,
72    },
73    BaManual,
74    LayoutFallback(String),
75    Layout(String),
76    TwoWay {
77        roles: Vec<String>,
78        gain_db: f64,
79        xo_hz: f64,
80    },
81    BusGainAnchored(f64),
82    Mimo(MimoNote),
83}
84
85/// Semantic diagnostics in producer order; never translated presentation text.
86#[derive(Debug, Clone, PartialEq)]
87pub enum MimoNote {
88    InsufficientPositions {
89        measured: usize,
90    },
91    ExcludedSupports(Vec<String>),
92    SupportRanges(Vec<(String, Option<(f64, f64)>)>),
93    GuardReduced {
94        role: String,
95        gain_db: f64,
96    },
97    GuardDisabled {
98        role: String,
99    },
100    SupportLimit {
101        max_supports: usize,
102    },
103    FirBudget {
104        support_count: usize,
105        taps_total: usize,
106    },
107}
108
109#[cfg(test)]
110impl MimoNote {
111    // Only the oracle adapter renders the legacy diagnostic strings.
112    pub(crate) fn oracle_text(&self) -> String {
113        match self {
114            Self::InsufficientPositions { measured } => {
115                format!("측정 위치 {measured}곳 — MIMO 는 3곳 이상일 때만")
116            }
117            Self::ExcludedSupports(roles) => format!("서포트 제외(사용자): {}", roles.join(", ")),
118            Self::SupportRanges(ranges) => format!(
119                "서포트 대역 {}",
120                ranges
121                    .iter()
122                    .map(|(role, range)| match range {
123                        Some((lo, hi)) => format!("{role} {lo:.0}–{hi:.0}"),
124                        None => format!("{role} 제외"),
125                    })
126                    .collect::<Vec<_>>()
127                    .join(", ")
128            ),
129            Self::GuardReduced { role, gain_db } => {
130                format!("{role}: 대역 늦은 에너지 악화로 서포트 {gain_db:.0} dB")
131            }
132            Self::GuardDisabled { role } => format!("{role}: MIMO 끔(악화)"),
133            Self::SupportLimit { max_supports } => format!("입력당 서포트 ≤ {max_supports}"),
134            Self::FirBudget {
135                support_count,
136                taps_total,
137            } => {
138                let digits = taps_total.to_string();
139                let grouped = digits
140                    .chars()
141                    .enumerate()
142                    .fold(String::new(), |mut out, (i, ch)| {
143                        if i > 0 && (digits.len() - i).is_multiple_of(3) {
144                            out.push(',');
145                        }
146                        out.push(ch);
147                        out
148                    });
149                format!("FIR 서포트 {support_count}개 · 총 {grouped}탭(48 kHz)")
150            }
151        }
152    }
153}