Skip to main content

secs_project/
config.rs

1use crate::ProjectError;
2use crate::pyjson::Shape;
3use secs_types::ipc::Notice;
4use secs_types::role::{ROLES, RoleKind, SUB_ROLES, role_group, role_kind, role_order};
5use serde::{Deserialize, Serialize};
6use std::collections::{BTreeMap, BTreeSet};
7
8#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
9pub struct Band {
10    pub band: String,
11    pub out: i64,
12    #[serde(default = "plus")]
13    pub pol: String,
14}
15fn plus() -> String {
16    "+".into()
17}
18
19pub const MAX_OUTPUT_NUMBER: i64 = 256;
20
21static BAND_SHAPE: Shape = Shape::Record {
22    optional: &[],
23    nested: &[],
24};
25static SPEAKER_SHAPE: Shape = Shape::Record {
26    optional: &[],
27    nested: &[("bands", Shape::List(&BAND_SHAPE))],
28};
29/// The `config` object [`SpeakerConfig::to_json`] writes (F16).
30pub(crate) static CONFIG_SHAPE: Shape = Shape::Record {
31    optional: &[],
32    nested: &[("speakers", Shape::List(&SPEAKER_SHAPE))],
33};
34
35#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
36pub struct Speaker {
37    pub role: String,
38    #[serde(default = "small")]
39    pub size: String,
40    #[serde(default)]
41    pub bands: Vec<Band>,
42}
43fn small() -> String {
44    "small".into()
45}
46
47fn python_repr(value: &str) -> String {
48    let quote = if value.contains('\'') && !value.contains('"') {
49        '"'
50    } else {
51        '\''
52    };
53    let mut out = String::from(quote);
54    for ch in value.chars() {
55        match ch {
56            '\\' => out.push_str("\\\\"),
57            '\'' if quote == '\'' => out.push_str("\\'"),
58            '"' if quote == '"' => out.push_str("\\\""),
59            '\n' => out.push_str("\\n"),
60            '\r' => out.push_str("\\r"),
61            '\t' => out.push_str("\\t"),
62            '\u{0008}' => out.push_str("\\x08"),
63            '\u{000c}' => out.push_str("\\x0c"),
64            ch if ch.is_control() => out.push_str(&format!("\\x{:02x}", ch as u32)),
65            ch => out.push(ch),
66        }
67    }
68    out.push(quote);
69    out
70}
71
72pub fn default_bands(role: &str, start: i64, biamp: bool) -> Vec<Band> {
73    if role_kind(role) == RoleKind::Main && biamp {
74        vec![
75            Band {
76                band: "LF".into(),
77                out: start,
78                pol: plus(),
79            },
80            Band {
81                band: "HF".into(),
82                out: start + 1,
83                pol: plus(),
84            },
85        ]
86    } else {
87        vec![Band {
88            band: if role_kind(role) == RoleKind::Sub {
89                "SUB"
90            } else {
91                "FULL"
92            }
93            .into(),
94            out: start,
95            pol: plus(),
96        }]
97    }
98}
99
100/// Upstream `model.PRESETS` plus layouts SECS 2 does not have (docs/DEVIATIONS.md U13): the
101/// quadraphonic `4.0`, `4.1`, `4.2` (5.1 without the centre) and `7.1` (7.1.4 without the
102/// heights), in upstream's order.
103const PRESETS: [(&str, &[&str]); 10] = [
104    ("2.0", &["L", "R"]),
105    ("2.1", &["L", "R", "SW1"]),
106    ("2.2", &["L", "R", "SW1", "SW2"]),
107    ("4.0", &["L", "R", "Ls", "Rs"]),
108    ("4.1", &["L", "R", "SW1", "Ls", "Rs"]),
109    ("4.2", &["L", "R", "SW1", "SW2", "Ls", "Rs"]),
110    ("5.1", &["L", "R", "C", "SW1", "Ls", "Rs"]),
111    ("7.1", &["L", "R", "C", "SW1", "Ls", "Rs", "Lb", "Rb"]),
112    (
113        "7.1.4",
114        &[
115            "L", "R", "C", "SW1", "Ls", "Rs", "Lb", "Rb", "Tfl", "Tfr", "Tbl", "Tbr",
116        ],
117    ),
118    (
119        "9.1.6",
120        &[
121            "L", "R", "C", "SW1", "Ls", "Rs", "Lb", "Rb", "Lw", "Rw", "Tfl", "Tfr", "Tml", "Tmr",
122            "Tbl", "Tbr",
123        ],
124    ),
125];
126
127#[derive(Debug, Clone, PartialEq, Eq)]
128pub struct SpeakerConfig {
129    pub speakers: Vec<Speaker>,
130    pub device: String,
131    pub preset: String,
132    pub biamp_default: bool,
133}
134impl Default for SpeakerConfig {
135    fn default() -> Self {
136        Self {
137            speakers: vec![],
138            device: String::new(),
139            preset: "custom".into(),
140            biamp_default: true,
141        }
142    }
143}
144impl SpeakerConfig {
145    pub(crate) fn repair_output_numbers(&mut self) -> Vec<crate::MigrationWarning> {
146        let mut warnings = Vec::new();
147        let mut used = self
148            .speakers
149            .iter()
150            .flat_map(|speaker| &speaker.bands)
151            .filter_map(|band| {
152                (1..=MAX_OUTPUT_NUMBER)
153                    .contains(&band.out)
154                    .then_some(band.out)
155            })
156            .collect::<BTreeSet<_>>();
157
158        for speaker in &mut self.speakers {
159            for band in &mut speaker.bands {
160                if (1..=MAX_OUTPUT_NUMBER).contains(&band.out) {
161                    continue;
162                }
163                let original = band.out;
164                band.out = (1..=MAX_OUTPUT_NUMBER)
165                    .find(|output| !used.contains(output))
166                    .unwrap_or(1);
167                used.insert(band.out);
168                warnings.push(crate::MigrationWarning {
169                    code: "config.band.out.outside",
170                    entry: format!(
171                        "role {}, band {}, output {original}",
172                        speaker.role, band.band
173                    ),
174                });
175            }
176        }
177        warnings
178    }
179
180    pub fn to_json(&self) -> crate::PyJson {
181        crate::PyJson::Object(vec![
182            (
183                "speakers".into(),
184                crate::PyJson::Array(
185                    self.speakers
186                        .iter()
187                        .map(|speaker| {
188                            crate::PyJson::Object(vec![
189                                ("role".into(), crate::PyJson::Str(speaker.role.clone())),
190                                ("size".into(), crate::PyJson::Str(speaker.size.clone())),
191                                (
192                                    "bands".into(),
193                                    crate::PyJson::Array(
194                                        speaker
195                                            .bands
196                                            .iter()
197                                            .map(|band| {
198                                                crate::PyJson::Object(vec![
199                                                    (
200                                                        "band".into(),
201                                                        crate::PyJson::Str(band.band.clone()),
202                                                    ),
203                                                    (
204                                                        "out".into(),
205                                                        crate::PyJson::Int(band.out.to_string()),
206                                                    ),
207                                                    (
208                                                        "pol".into(),
209                                                        crate::PyJson::Str(band.pol.clone()),
210                                                    ),
211                                                ])
212                                            })
213                                            .collect(),
214                                    ),
215                                ),
216                            ])
217                        })
218                        .collect(),
219                ),
220            ),
221            ("device".into(), crate::PyJson::Str(self.device.clone())),
222            ("preset".into(), crate::PyJson::Str(self.preset.clone())),
223            (
224                "biamp_default".into(),
225                crate::PyJson::Bool(self.biamp_default),
226            ),
227        ])
228    }
229
230    pub fn from_json(value: crate::PyJson) -> Result<Self, ProjectError> {
231        let object = value
232            .object()
233            .ok_or_else(|| ProjectError::Invalid("config must be an object".into()))?;
234        let get = |key: &str| {
235            object
236                .iter()
237                .find(|(name, _)| name == key)
238                .map(|(_, value)| value)
239        };
240        let speakers = match get("speakers") {
241            None | Some(crate::PyJson::Null) => Vec::new(),
242            Some(crate::PyJson::Array(values)) => values
243                .iter()
244                .map(|value| {
245                    let object = value
246                        .object()
247                        .ok_or_else(|| ProjectError::Invalid("speaker must be an object".into()))?;
248                    let get = |key: &str| {
249                        object
250                            .iter()
251                            .find(|(name, _)| name == key)
252                            .map(|(_, value)| value)
253                    };
254                    let bands = match get("bands") {
255                        None | Some(crate::PyJson::Null) => Vec::new(),
256                        Some(crate::PyJson::Array(values)) => values
257                            .iter()
258                            .map(|value| {
259                                let object = value.object().ok_or_else(|| {
260                                    ProjectError::Invalid("band must be an object".into())
261                                })?;
262                                let get = |key: &str| {
263                                    object
264                                        .iter()
265                                        .find(|(name, _)| name == key)
266                                        .map(|(_, value)| value)
267                                };
268                                Ok(Band {
269                                    band: crate::project::string(get("band"), ""),
270                                    out: crate::project::integer(get("out"), 0, "band.out")?,
271                                    pol: crate::project::string(get("pol"), "+"),
272                                })
273                            })
274                            .collect::<Result<Vec<_>, ProjectError>>()?,
275                        Some(_) => {
276                            return Err(ProjectError::Invalid(
277                                "speaker.bands must be an array".into(),
278                            ));
279                        }
280                    };
281                    Ok(Speaker {
282                        role: crate::project::string(get("role"), ""),
283                        size: crate::project::string(get("size"), "small"),
284                        bands,
285                    })
286                })
287                .collect::<Result<Vec<_>, ProjectError>>()?,
288            Some(_) => {
289                return Err(ProjectError::Invalid(
290                    "config.speakers must be an array".into(),
291                ));
292            }
293        };
294        Ok(Self {
295            speakers,
296            device: crate::project::string(get("device"), ""),
297            preset: crate::project::string(get("preset"), "custom"),
298            biamp_default: get("biamp_default")
299                .and_then(crate::PyJson::bool)
300                .unwrap_or(true),
301        })
302    }
303    pub fn from_preset(name: &str, device: &str, biamp: bool) -> Result<Self, ProjectError> {
304        let roles = PRESETS
305            .iter()
306            .find(|x| x.0 == name)
307            .ok_or_else(|| ProjectError::Invalid(format!("unknown preset {name:?}")))?
308            .1;
309        let mut out = 1;
310        let mut speakers = vec![];
311        for role in roles {
312            let bands = default_bands(role, out, biamp);
313            out += bands.len() as i64;
314            speakers.push(Speaker {
315                role: (*role).into(),
316                size: if role_kind(role) == RoleKind::Sub {
317                    "sub"
318                } else {
319                    "small"
320                }
321                .into(),
322                bands,
323            });
324        }
325        Ok(Self {
326            speakers,
327            device: device.into(),
328            preset: name.into(),
329            biamp_default: biamp,
330        })
331    }
332    pub fn roles(&self) -> Vec<&str> {
333        self.speakers.iter().map(|s| s.role.as_str()).collect()
334    }
335    pub fn sub_count(&self) -> usize {
336        self.speakers
337            .iter()
338            .filter(|s| SUB_ROLES.contains(&s.role.as_str()))
339            .count()
340    }
341    pub fn used_outs(&self) -> BTreeSet<i64> {
342        self.speakers
343            .iter()
344            .flat_map(|s| &s.bands)
345            .map(|b| b.out)
346            .collect()
347    }
348    pub fn next_free_out(&self) -> i64 {
349        let used = self.used_outs();
350        (1..).find(|n| !used.contains(n)).unwrap()
351    }
352    pub fn groups(&self) -> Vec<(&'static str, Vec<&Speaker>)> {
353        let mut out: Vec<(&'static str, Vec<&Speaker>)> = Vec::new();
354        for speaker in &self.speakers {
355            let group = role_group(&speaker.role);
356            if let Some((_, speakers)) = out.iter_mut().find(|(name, _)| *name == group) {
357                speakers.push(speaker);
358            } else {
359                out.push((group, vec![speaker]));
360            }
361        }
362        out
363    }
364    pub fn set_sub_count(&mut self, n: i64) {
365        let n = n.clamp(0, 8) as usize;
366        while self.sub_count() < n {
367            let role = SUB_ROLES
368                .into_iter()
369                .find(|r| !self.speakers.iter().any(|s| s.role == *r));
370            if let Some(role) = role {
371                self.speakers.push(Speaker {
372                    role: role.into(),
373                    size: "sub".into(),
374                    bands: default_bands(role, self.next_free_out(), false),
375                });
376            } else {
377                break;
378            }
379        }
380        while self.sub_count() > n {
381            if let Some(i) = self
382                .speakers
383                .iter()
384                .rposition(|s| SUB_ROLES.contains(&s.role.as_str()))
385            {
386                self.speakers.remove(i);
387            }
388        }
389        self.preset = "custom".into();
390    }
391    pub fn renumber(&mut self) {
392        for (out, band) in (1_i64..).zip(self.speakers.iter_mut().flat_map(|s| &mut s.bands)) {
393            band.out = out;
394        }
395        self.preset = "custom".into();
396    }
397    pub fn inverted_mains(&self) -> Vec<String> {
398        self.speakers
399            .iter()
400            .filter(|speaker| role_kind(&speaker.role) != RoleKind::Sub)
401            .filter(|speaker| speaker.bands.first().is_some_and(|band| band.pol == "-"))
402            .map(|speaker| speaker.role.clone())
403            .collect()
404    }
405    pub fn set_speaker(
406        &mut self,
407        index: usize,
408        role: Option<String>,
409        size: Option<String>,
410        band_index: Option<usize>,
411        out: Option<i64>,
412        pol: Option<String>,
413    ) -> Result<(), ProjectError> {
414        let speaker = self
415            .speakers
416            .get_mut(index)
417            .ok_or_else(|| ProjectError::Invalid("speaker index out of range".into()))?;
418        if let Some(role) = role {
419            let was_sub = role_kind(&speaker.role) == RoleKind::Sub;
420            let is_sub = role_kind(&role) == RoleKind::Sub;
421            speaker.role = role;
422            if is_sub && !was_sub {
423                let output = speaker.bands.first().map_or(1, |band| band.out);
424                speaker.size = "sub".into();
425                speaker.bands = default_bands(&speaker.role, output, false);
426            } else if was_sub && !is_sub {
427                for band in &mut speaker.bands {
428                    if band.band == "SUB" {
429                        band.band = "FULL".into();
430                    }
431                }
432                if speaker.size == "sub" {
433                    speaker.size = "small".into();
434                }
435            }
436        }
437        if let Some(size) = size {
438            speaker.size = size;
439        }
440        if out.is_some() || pol.is_some() {
441            let band_index = band_index.ok_or_else(|| {
442                ProjectError::Invalid("band index is required for band fields".into())
443            })?;
444            let band = speaker
445                .bands
446                .get_mut(band_index)
447                .ok_or_else(|| ProjectError::Invalid("band index out of range".into()))?;
448            if let Some(out) = out {
449                band.out = out;
450            }
451            if let Some(pol) = pol {
452                band.pol = pol;
453            }
454        } else if band_index.is_some() {
455            return Err(ProjectError::Invalid(
456                "band index without a band field".into(),
457            ));
458        }
459        self.preset = "custom".into();
460        Ok(())
461    }
462    pub fn add_speaker(&mut self) -> Result<(), ProjectError> {
463        let role = ROLES
464            .into_iter()
465            .find(|role| !self.speakers.iter().any(|speaker| speaker.role == *role))
466            .unwrap_or("custom");
467        self.speakers.push(Speaker {
468            role: role.into(),
469            size: if role_kind(role) == RoleKind::Sub {
470                "sub"
471            } else {
472                "small"
473            }
474            .into(),
475            bands: default_bands(role, self.next_free_out(), false),
476        });
477        self.preset = "custom".into();
478        Ok(())
479    }
480    pub fn remove_speaker(&mut self, index: usize) -> Result<(), ProjectError> {
481        if index >= self.speakers.len() {
482            return Err(ProjectError::Invalid("speaker index out of range".into()));
483        }
484        self.speakers.remove(index);
485        self.preset = "custom".into();
486        Ok(())
487    }
488    pub fn validation_notices(&self) -> Vec<Notice> {
489        self.validation_rows()
490            .into_iter()
491            .map(|row| Notice {
492                code: row.code.into(),
493                params: row.params,
494            })
495            .collect()
496    }
497    pub fn validate(&self) -> Vec<String> {
498        self.validation_rows()
499            .into_iter()
500            .map(|row| row.message)
501            .collect()
502    }
503
504    fn validation_rows(&self) -> Vec<ValidationRow> {
505        let mut rows = vec![];
506        let mut roles = BTreeSet::new();
507        for role in self.roles() {
508            if !roles.insert(role) {
509                rows.push(validation_row(
510                    "role_duplicate",
511                    [("role", role)],
512                    format!("역할 중복: {role}"),
513                ));
514            }
515        }
516
517        let mut outs = BTreeMap::new();
518        for speaker in &self.speakers {
519            for band in &speaker.bands {
520                if band.out < 1 {
521                    let out = band.out.to_string();
522                    rows.push(validation_row(
523                        "out_invalid",
524                        [("role", speaker.role.as_str()), ("out", out.as_str())],
525                        format!(
526                            "{}: 출력 채널 번호가 잘못되었습니다({}, 1 이상 정수)",
527                            speaker.role, band.out
528                        ),
529                    ));
530                    continue;
531                }
532                if let Some(first_role) = outs.get(&band.out) {
533                    let out = band.out.to_string();
534                    rows.push(validation_row(
535                        "out_duplicate",
536                        [
537                            ("out", out.as_str()),
538                            ("first_role", *first_role),
539                            ("role", speaker.role.as_str()),
540                        ],
541                        format!(
542                            "출력 채널 중복: {} ({} ↔ {})",
543                            band.out, first_role, speaker.role
544                        ),
545                    ));
546                } else {
547                    outs.insert(band.out, speaker.role.as_str());
548                }
549            }
550        }
551
552        for speaker in &self.speakers {
553            let names: Vec<&str> = speaker
554                .bands
555                .iter()
556                .map(|band| band.band.as_str())
557                .collect();
558            for band in &speaker.bands {
559                if !["FULL", "LF", "MF", "HF", "SUB"].contains(&band.band.as_str()) {
560                    rows.push(validation_row(
561                        "band_unknown",
562                        [
563                            ("role", speaker.role.as_str()),
564                            ("band", band.band.as_str()),
565                        ],
566                        format!(
567                            "{}: 알 수 없는 대역 {}",
568                            speaker.role,
569                            python_repr(&band.band)
570                        ),
571                    ));
572                }
573                if !["+", "-"].contains(&band.pol.as_str()) {
574                    rows.push(validation_row(
575                        "pol_invalid",
576                        [("role", speaker.role.as_str()), ("pol", band.pol.as_str())],
577                        format!(
578                            "{}: 극성은 + 또는 - 여야 합니다({})",
579                            speaker.role,
580                            python_repr(&band.pol)
581                        ),
582                    ));
583                }
584            }
585            let combination = names.join("/");
586            if names.is_empty() {
587                rows.push(validation_row(
588                    "bands_empty",
589                    [("role", speaker.role.as_str())],
590                    format!("{}: 대역이 없습니다", speaker.role),
591                ));
592            } else if role_kind(&speaker.role) == RoleKind::Sub && names != ["SUB"] {
593                rows.push(validation_row(
594                    "sub_bands",
595                    [
596                        ("role", speaker.role.as_str()),
597                        ("bands", combination.as_str()),
598                    ],
599                    format!(
600                        "{}: 서브우퍼는 SUB 대역 하나만 가질 수 있습니다({combination})",
601                        speaker.role
602                    ),
603                ));
604            } else if role_kind(&speaker.role) != RoleKind::Sub && names.contains(&"SUB") {
605                rows.push(validation_row(
606                    "main_with_sub_band",
607                    [("role", speaker.role.as_str())],
608                    format!(
609                        "{}: 메인/탑/하이트 스피커에는 SUB 대역을 쓸 수 없습니다",
610                        speaker.role
611                    ),
612                ));
613            } else if role_kind(&speaker.role) != RoleKind::Sub
614                && ![vec!["FULL"], vec!["LF", "HF"], vec!["LF", "MF", "HF"]].contains(&names)
615            {
616                rows.push(validation_row(
617                    "band_combination",
618                    [
619                        ("role", speaker.role.as_str()),
620                        ("bands", combination.as_str()),
621                    ],
622                    format!(
623                        "{}: 대역 조합 오류 {combination} (가능: FULL · LF/HF · LF/MF/HF)",
624                        speaker.role
625                    ),
626                ));
627            }
628        }
629        rows
630    }
631}
632
633struct ValidationRow {
634    code: &'static str,
635    params: BTreeMap<String, String>,
636    message: String,
637}
638
639fn validation_row<const N: usize>(
640    code: &'static str,
641    params: [(&str, &str); N],
642    message: String,
643) -> ValidationRow {
644    ValidationRow {
645        code,
646        params: params
647            .into_iter()
648            .map(|(key, value)| (key.into(), value.into()))
649            .collect(),
650        message,
651    }
652}
653
654pub fn ordered_speakers(speakers: &[Speaker], subs_last: bool) -> Vec<&Speaker> {
655    let mut out: Vec<_> = speakers.iter().collect();
656    out.sort_by_key(|s| {
657        (
658            subs_last && role_kind(&s.role) == RoleKind::Sub,
659            role_order(&s.role),
660        )
661    });
662    out
663}