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 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}