Skip to main content

secs_types/
rules.rs

1//! Service rule values the screen shows and the mock service reuses (ADR 0014).
2//!
3//! The service judges with these named constants instead of literals. This
4//! crate's tests also write their values into `ipc/bindings/rules.ts` next to
5//! the generated types, so the screen and the mock import them rather than
6//! copying numbers by hand; CI's bindings diff (ADR 0003) covers that file.
7
8use serde::Serialize;
9use ts_rs::TS;
10
11use crate::role::{HEIGHT_GROUP, TOP_GROUP};
12
13/// Inclusive `(low, high)` range of each value `calibration_settings_edit`
14/// clamps (upstream calibration.py and step_calibration.py constants).
15#[derive(Debug, Clone, Copy, PartialEq, Serialize, TS)]
16#[ts(export)]
17pub struct CalibrationLimits {
18    /// RC low edge (Hz) an edit accepts. SECS 2R rejects below 20 Hz (ADR
19    /// 0024); a project file is read with `RC_LO_READ` instead.
20    pub rc_lo: (f64, f64),
21    /// RC high edge (Hz) when it is not automatic.
22    pub rc_hi: (f64, f64),
23    /// Either edge of an extra correction band (Hz, ADR 0024).
24    pub rc_band: (f64, f64),
25    /// Number of extra correction bands (ADR 0024).
26    pub rc_bands_max: (f64, f64),
27    /// Room volume (m³); a value at or below the low end clears it.
28    pub volume: (f64, f64),
29    /// One room dimension (m) as entered.
30    pub dim: (f64, f64),
31    /// One room dimension (m) that yields a room volume.
32    pub dim_valid: (f64, f64),
33    /// Maximum boost (dB).
34    pub boost: (f64, f64),
35    /// Common input high-pass (Hz) when it is on (ADR 0031).
36    pub input_hp: (f64, f64),
37    /// Latency budget of the ultra mode (ms).
38    pub ultra_ms: (f64, f64),
39    /// Crossover of one group (Hz).
40    pub xo: (f64, f64),
41    /// XO PEQ band edges (Hz).
42    pub xo_peq: (f64, f64),
43    /// Infra crossover (Hz).
44    pub infra_hz: (f64, f64),
45    /// Maximum infra boost (dB).
46    pub infra_boost: (f64, f64),
47    /// Extra delay of the mains against the subs (ms).
48    pub main_delay: (f64, f64),
49    /// Rear-row trim of a sub group (ms).
50    pub rear_trim: (f64, f64),
51    /// MIMO support upper limit (Hz).
52    pub fsiso: (f64, f64),
53    /// MIMO low limit (Hz) when it is not automatic.
54    pub mimo_low: (f64, f64),
55    /// Maximum number of MIMO supports (0 = no cap).
56    pub mimo_max: (f64, f64),
57    /// Protected MIMO onset interval (ms, ADR 0037).
58    pub mimo_early_ms: (f64, f64),
59}
60
61/// Upstream's clamp of a stored RC low edge (calibration.py), kept for reading
62/// project files so a stored 10–19 Hz still computes as before (ADR 0024, D48).
63pub const RC_LO_READ: (f64, f64) = (10.0, 200.0);
64
65pub const CALIBRATION_LIMITS: CalibrationLimits = CalibrationLimits {
66    rc_lo: (20.0, 200.0),
67    rc_hi: (100.0, 20000.0),
68    rc_band: (20.0, 20000.0),
69    rc_bands_max: (0.0, 4.0),
70    volume: (0.0, 100000.0),
71    dim: (0.0, 100.0),
72    dim_valid: (0.5, 100.0),
73    boost: (0.0, 10.0),
74    input_hp: (10.0, 50.0),
75    ultra_ms: (30.0, 200.0),
76    xo: (40.0, 200.0),
77    xo_peq: (0.0, 1000.0),
78    infra_hz: (10.0, 300.0),
79    infra_boost: (0.0, 24.0),
80    main_delay: (0.0, 100.0),
81    rear_trim: (-20.0, 20.0),
82    fsiso: (50.0, 150.0),
83    mimo_low: (15.0, 150.0),
84    mimo_max: (0.0, 12.0),
85    mimo_early_ms: (5.0, 60.0),
86};
87
88/// Sweep lengths a measurement plan accepts, shortest first.
89pub const SWEEP_LENGTHS: [&str; 7] = ["64k", "128k", "256k", "512k", "1M", "2M", "4M"];
90
91/// Start delays before each measured position, as upstream stores them in
92/// `extra['start_delay']` (step 4 of the wizard).
93pub const START_DELAYS: [&str; 4] = ["끄기", "5 초", "10 초", "15 초"];
94
95/// How many recent projects the app settings keep.
96pub const RECENT_PROJECTS_MAX: usize = 10;
97
98/// Configuration groups in the order the service lists them: the upstream
99/// (Korean) group name that keys the XO map, then the presentation id.
100pub const CONFIG_GROUPS: [(&str, &str); 9] = [
101    ("프론트", "front"),
102    ("센터", "center"),
103    ("서브", "sub"),
104    ("서라운드", "surround"),
105    ("백", "back"),
106    ("와이드", "wide"),
107    (TOP_GROUP, "top"),
108    (HEIGHT_GROUP, "height"),
109    ("기타", "other"),
110];
111
112/// Measurement arrangements by position count.
113pub const ARRANGEMENT_COUNTS: [usize; 4] = [1, 9, 13, 17];
114
115/// Finished jobs the service keeps in its job list beside every active one
116/// (ADR 0009).
117pub const JOB_HISTORY_MAX: usize = 32;
118
119#[cfg(test)]
120mod typescript {
121    //! `rules.ts`: the values above as TypeScript, deterministic text.
122    use serde::Serialize;
123    use serde_json::Value;
124
125    use super::*;
126
127    const HEADER: &str = "// This file was generated by `cargo test -p secs-types` from crates/secs-types/src/rules.rs. Do not edit this file manually.\n";
128
129    fn literal(value: &Value) -> String {
130        match value {
131            Value::Null => "null".into(),
132            Value::Bool(value) => value.to_string(),
133            // Display prints the shortest round-trip form without a trailing
134            // `.0`, as JavaScript does for these magnitudes.
135            Value::Number(number) => match (number.as_i64(), number.as_f64()) {
136                (Some(int), _) => int.to_string(),
137                (None, Some(float)) => float.to_string(),
138                (None, None) => number.to_string(),
139            },
140            Value::String(_) => value.to_string(),
141            Value::Array(items) => {
142                format!(
143                    "[{}]",
144                    items.iter().map(literal).collect::<Vec<_>>().join(", ")
145                )
146            }
147            Value::Object(fields) => format!(
148                "{{\n{}}}",
149                fields
150                    .iter()
151                    .map(|(key, value)| format!("  {key}: {},\n", literal(value)))
152                    .collect::<String>()
153            ),
154        }
155    }
156
157    /// One `export const`; arrays and objects are `as const`.
158    pub(super) fn constant(name: &str, ty: Option<&str>, value: &impl Serialize) -> String {
159        let value = serde_json::to_value(value).expect("rule constants serialize");
160        let annotation = ty.map(|ty| format!(": {ty}")).unwrap_or_default();
161        let suffix = if value.is_array() || value.is_object() {
162            " as const"
163        } else {
164            ""
165        };
166        format!(
167            "export const {name}{annotation} = {}{suffix};\n",
168            literal(&value)
169        )
170    }
171
172    pub(super) fn rules_ts() -> String {
173        [
174            HEADER.to_owned(),
175            "import type { CalibrationLimits } from \"./CalibrationLimits\";\n\n".to_owned(),
176            constant(
177                "CALIBRATION_LIMITS",
178                Some("CalibrationLimits"),
179                &CALIBRATION_LIMITS,
180            ),
181            constant("SWEEP_LENGTHS", None, &SWEEP_LENGTHS),
182            constant("START_DELAYS", None, &START_DELAYS),
183            constant("RECENT_PROJECTS_MAX", None, &RECENT_PROJECTS_MAX),
184            constant("CONFIG_GROUPS", None, &CONFIG_GROUPS),
185            constant("ARRANGEMENT_COUNTS", None, &ARRANGEMENT_COUNTS),
186            constant("JOB_HISTORY_MAX", None, &JOB_HISTORY_MAX),
187        ]
188        .concat()
189    }
190}
191
192#[cfg(test)]
193mod tests {
194    use super::typescript::{constant, rules_ts};
195    use super::*;
196
197    #[test]
198    fn renders_constants_as_typescript() {
199        assert_eq!(
200            constant("RECENT_PROJECTS_MAX", None, &RECENT_PROJECTS_MAX),
201            "export const RECENT_PROJECTS_MAX = 10;\n"
202        );
203        assert_eq!(
204            constant("EDGES", None, &[(0.5, 100.0), (-20.0, 0.71)]),
205            "export const EDGES = [[0.5, 100], [-20, 0.71]] as const;\n"
206        );
207        assert_eq!(
208            constant("GROUPS", None, &[("탑", "top")]),
209            "export const GROUPS = [[\"탑\", \"top\"]] as const;\n"
210        );
211        let limits = constant("LIMITS", Some("CalibrationLimits"), &CALIBRATION_LIMITS);
212        assert!(
213            limits.starts_with(
214                "export const LIMITS: CalibrationLimits = {\n  rc_lo: [20, 200],\n  rc_hi: [100, 20000],\n  rc_band: [20, 20000],\n  rc_bands_max: [0, 4],\n"
215            ),
216            "{limits}"
217        );
218        assert!(
219            limits.ends_with("  mimo_max: [0, 12],\n  mimo_early_ms: [5, 60],\n} as const;\n"),
220            "{limits}"
221        );
222    }
223
224    /// Writes `rules.ts` where ts-rs writes the bindings (`TS_RS_EXPORT_DIR`,
225    /// `./bindings` by default), so CI's bindings diff compares it too.
226    #[test]
227    fn export_rules_ts() {
228        let dir = ts_rs::Config::from_env().out_dir().to_path_buf();
229        std::fs::create_dir_all(&dir).unwrap();
230        std::fs::write(dir.join("rules.ts"), rules_ts()).unwrap();
231    }
232}