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};
29pub(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
100const 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}