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    Latency {
53        latency_ms: f64,
54        lift_ms: f64,
55        main_extra_ms: f64,
56    },
57    PhaseStrength(Vec<(String, f64)>),
58    PhaseBand(Vec<(String, Option<f64>)>),
59    // RC (P09)
60    LongModeBoost {
61        cap_db: f64,
62        modes: Vec<(f64, f64)>,
63    },
64    GdCuts {
65        role: String,
66        cuts: Vec<Peq>,
67    },
68    // BA (P10)
69    BaCost(f64),
70    BaBusPass {
71        first: f64,
72        second: f64,
73        kept_first: bool,
74    },
75    BaManual,
76    LayoutFallback(String),
77    Layout(String),
78    TwoWay {
79        roles: Vec<String>,
80        gain_db: f64,
81        xo_hz: f64,
82    },
83    BusGainAnchored(f64),
84    Mimo(MimoNote),
85}
86
87/// Semantic diagnostics in producer order; never translated presentation text.
88#[derive(Debug, Clone, PartialEq)]
89pub enum MimoNote {
90    InsufficientPositions {
91        measured: usize,
92    },
93    ExcludedSupports(Vec<String>),
94    SupportRanges(Vec<(String, Option<(f64, f64)>)>),
95    GuardReduced {
96        role: String,
97        gain_db: f64,
98    },
99    GuardDisabled {
100        role: String,
101    },
102    SupportLimit {
103        max_supports: usize,
104    },
105    FirBudget {
106        support_count: usize,
107        taps_total: usize,
108    },
109}
110
111#[cfg(test)]
112impl MimoNote {
113    // Only the oracle adapter renders the legacy diagnostic strings.
114    pub(crate) fn oracle_text(&self) -> String {
115        match self {
116            Self::InsufficientPositions { measured } => {
117                format!("측정 위치 {measured}곳 — MIMO 는 3곳 이상일 때만")
118            }
119            Self::ExcludedSupports(roles) => format!("서포트 제외(사용자): {}", roles.join(", ")),
120            Self::SupportRanges(ranges) => format!(
121                "서포트 대역 {}",
122                ranges
123                    .iter()
124                    .map(|(role, range)| match range {
125                        Some((lo, hi)) => format!("{role} {lo:.0}–{hi:.0}"),
126                        None => format!("{role} 제외"),
127                    })
128                    .collect::<Vec<_>>()
129                    .join(", ")
130            ),
131            Self::GuardReduced { role, gain_db } => {
132                format!("{role}: 대역 늦은 에너지 악화로 서포트 {gain_db:.0} dB")
133            }
134            Self::GuardDisabled { role } => format!("{role}: MIMO 끔(악화)"),
135            Self::SupportLimit { max_supports } => format!("입력당 서포트 ≤ {max_supports}"),
136            Self::FirBudget {
137                support_count,
138                taps_total,
139            } => {
140                let digits = taps_total.to_string();
141                let grouped = digits
142                    .chars()
143                    .enumerate()
144                    .fold(String::new(), |mut out, (i, ch)| {
145                        if i > 0 && (digits.len() - i).is_multiple_of(3) {
146                            out.push(',');
147                        }
148                        out.push(ch);
149                        out
150                    });
151                format!("FIR 서포트 {support_count}개 · 총 {grouped}탭(48 kHz)")
152            }
153        }
154    }
155}