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