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