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 set_speaker(
398        &mut self,
399        index: usize,
400        role: Option<String>,
401        size: Option<String>,
402        band_index: Option<usize>,
403        out: Option<i64>,
404        pol: Option<String>,
405    ) -> Result<(), ProjectError> {
406        let speaker = self
407            .speakers
408            .get_mut(index)
409            .ok_or_else(|| ProjectError::Invalid("speaker index out of range".into()))?;
410        if let Some(role) = role {
411            let was_sub = role_kind(&speaker.role) == RoleKind::Sub;
412            let is_sub = role_kind(&role) == RoleKind::Sub;
413            speaker.role = role;
414            if is_sub && !was_sub {
415                let output = speaker.bands.first().map_or(1, |band| band.out);
416                speaker.size = "sub".into();
417                speaker.bands = default_bands(&speaker.role, output, false);
418            } else if was_sub && !is_sub {
419                for band in &mut speaker.bands {
420                    if band.band == "SUB" {
421                        band.band = "FULL".into();
422                    }
423                }
424                if speaker.size == "sub" {
425                    speaker.size = "small".into();
426                }
427            }
428        }
429        if let Some(size) = size {
430            speaker.size = size;
431        }
432        if out.is_some() || pol.is_some() {
433            let band_index = band_index.ok_or_else(|| {
434                ProjectError::Invalid("band index is required for band fields".into())
435            })?;
436            let band = speaker
437                .bands
438                .get_mut(band_index)
439                .ok_or_else(|| ProjectError::Invalid("band index out of range".into()))?;
440            if let Some(out) = out {
441                band.out = out;
442            }
443            if let Some(pol) = pol {
444                band.pol = pol;
445            }
446        } else if band_index.is_some() {
447            return Err(ProjectError::Invalid(
448                "band index without a band field".into(),
449            ));
450        }
451        self.preset = "custom".into();
452        Ok(())
453    }
454    pub fn add_speaker(&mut self) -> Result<(), ProjectError> {
455        let role = ROLES
456            .into_iter()
457            .find(|role| !self.speakers.iter().any(|speaker| speaker.role == *role))
458            .unwrap_or("custom");
459        self.speakers.push(Speaker {
460            role: role.into(),
461            size: if role_kind(role) == RoleKind::Sub {
462                "sub"
463            } else {
464                "small"
465            }
466            .into(),
467            bands: default_bands(role, self.next_free_out(), false),
468        });
469        self.preset = "custom".into();
470        Ok(())
471    }
472    pub fn remove_speaker(&mut self, index: usize) -> Result<(), ProjectError> {
473        if index >= self.speakers.len() {
474            return Err(ProjectError::Invalid("speaker index out of range".into()));
475        }
476        self.speakers.remove(index);
477        self.preset = "custom".into();
478        Ok(())
479    }
480    pub fn validation_notices(&self) -> Vec<Notice> {
481        self.validation_rows()
482            .into_iter()
483            .map(|row| Notice {
484                code: row.code.into(),
485                params: row.params,
486            })
487            .collect()
488    }
489    pub fn validate(&self) -> Vec<String> {
490        self.validation_rows()
491            .into_iter()
492            .map(|row| row.message)
493            .collect()
494    }
495
496    fn validation_rows(&self) -> Vec<ValidationRow> {
497        let mut rows = vec![];
498        let mut roles = BTreeSet::new();
499        for role in self.roles() {
500            if !roles.insert(role) {
501                rows.push(validation_row(
502                    "role_duplicate",
503                    [("role", role)],
504                    format!("역할 중복: {role}"),
505                ));
506            }
507        }
508
509        let mut outs = BTreeMap::new();
510        for speaker in &self.speakers {
511            for band in &speaker.bands {
512                if band.out < 1 {
513                    let out = band.out.to_string();
514                    rows.push(validation_row(
515                        "out_invalid",
516                        [("role", speaker.role.as_str()), ("out", out.as_str())],
517                        format!(
518                            "{}: 출력 채널 번호가 잘못되었습니다({}, 1 이상 정수)",
519                            speaker.role, band.out
520                        ),
521                    ));
522                    continue;
523                }
524                if let Some(first_role) = outs.get(&band.out) {
525                    let out = band.out.to_string();
526                    rows.push(validation_row(
527                        "out_duplicate",
528                        [
529                            ("out", out.as_str()),
530                            ("first_role", *first_role),
531                            ("role", speaker.role.as_str()),
532                        ],
533                        format!(
534                            "출력 채널 중복: {} ({} ↔ {})",
535                            band.out, first_role, speaker.role
536                        ),
537                    ));
538                } else {
539                    outs.insert(band.out, speaker.role.as_str());
540                }
541            }
542        }
543
544        for speaker in &self.speakers {
545            let names: Vec<&str> = speaker
546                .bands
547                .iter()
548                .map(|band| band.band.as_str())
549                .collect();
550            for band in &speaker.bands {
551                if !["FULL", "LF", "MF", "HF", "SUB"].contains(&band.band.as_str()) {
552                    rows.push(validation_row(
553                        "band_unknown",
554                        [
555                            ("role", speaker.role.as_str()),
556                            ("band", band.band.as_str()),
557                        ],
558                        format!(
559                            "{}: 알 수 없는 대역 {}",
560                            speaker.role,
561                            python_repr(&band.band)
562                        ),
563                    ));
564                }
565                if !["+", "-"].contains(&band.pol.as_str()) {
566                    rows.push(validation_row(
567                        "pol_invalid",
568                        [("role", speaker.role.as_str()), ("pol", band.pol.as_str())],
569                        format!(
570                            "{}: 극성은 + 또는 - 여야 합니다({})",
571                            speaker.role,
572                            python_repr(&band.pol)
573                        ),
574                    ));
575                }
576            }
577            let combination = names.join("/");
578            if names.is_empty() {
579                rows.push(validation_row(
580                    "bands_empty",
581                    [("role", speaker.role.as_str())],
582                    format!("{}: 대역이 없습니다", speaker.role),
583                ));
584            } else if role_kind(&speaker.role) == RoleKind::Sub && names != ["SUB"] {
585                rows.push(validation_row(
586                    "sub_bands",
587                    [
588                        ("role", speaker.role.as_str()),
589                        ("bands", combination.as_str()),
590                    ],
591                    format!(
592                        "{}: 서브우퍼는 SUB 대역 하나만 가질 수 있습니다({combination})",
593                        speaker.role
594                    ),
595                ));
596            } else if role_kind(&speaker.role) != RoleKind::Sub && names.contains(&"SUB") {
597                rows.push(validation_row(
598                    "main_with_sub_band",
599                    [("role", speaker.role.as_str())],
600                    format!(
601                        "{}: 메인/탑/하이트 스피커에는 SUB 대역을 쓸 수 없습니다",
602                        speaker.role
603                    ),
604                ));
605            } else if role_kind(&speaker.role) != RoleKind::Sub
606                && ![vec!["FULL"], vec!["LF", "HF"], vec!["LF", "MF", "HF"]].contains(&names)
607            {
608                rows.push(validation_row(
609                    "band_combination",
610                    [
611                        ("role", speaker.role.as_str()),
612                        ("bands", combination.as_str()),
613                    ],
614                    format!(
615                        "{}: 대역 조합 오류 {combination} (가능: FULL · LF/HF · LF/MF/HF)",
616                        speaker.role
617                    ),
618                ));
619            }
620        }
621        rows
622    }
623}
624
625struct ValidationRow {
626    code: &'static str,
627    params: BTreeMap<String, String>,
628    message: String,
629}
630
631fn validation_row<const N: usize>(
632    code: &'static str,
633    params: [(&str, &str); N],
634    message: String,
635) -> ValidationRow {
636    ValidationRow {
637        code,
638        params: params
639            .into_iter()
640            .map(|(key, value)| (key.into(), value.into()))
641            .collect(),
642        message,
643    }
644}
645
646pub fn ordered_speakers(speakers: &[Speaker], subs_last: bool) -> Vec<&Speaker> {
647    let mut out: Vec<_> = speakers.iter().collect();
648    out.sort_by_key(|s| {
649        (
650            subs_last && role_kind(&s.role) == RoleKind::Sub,
651            role_order(&s.role),
652        )
653    });
654    out
655}