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