1use crate::domain::dto::effect::effect_dto::EffectDto;
2use crate::domain::dto::tone_stack_dto::ToneStackDto;
3use crate::domain::effect::Effect;
4use crate::domain::tone_stack::ToneStack;
5use atomic_float::AtomicF32;
6use std::collections::HashMap;
7use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
8use std::sync::{Arc, Mutex};
9use tracing::{error, info};
10
11struct EffectHandles {
15 is_active: Arc<AtomicBool>,
16 params: HashMap<&'static str, ParamValue>,
19 cabinet_ir_file_path: Option<String>,
22}
23
24pub struct Channel {
44 id: u32,
45 name: String,
46 gain: Arc<AtomicF32>,
47 tone_stack: Arc<ToneStack>,
48 volume: Arc<AtomicF32>,
49 effect_chain: Arc<Mutex<Vec<Box<dyn Effect>>>>,
50 effect_handles: HashMap<u32, EffectHandles>,
53 next_effect_id: u32,
54}
55
56impl Channel {
57 fn build_effect_handles(effect: &dyn Effect) -> EffectHandles {
58 let cabinet_ir_file_path = match effect.to_dto() {
59 EffectDto::Cabinet(cabinet) => Some(cabinet.ir_file_path),
60 _ => None,
61 };
62
63 let mut params = HashMap::new();
64
65 for (name, arc) in effect.f32_params() {
66 params.insert(name, ParamValue::Float(arc));
67 }
68
69 for (name, arc) in effect.u32_params() {
70 params.insert(name, ParamValue::Uint(arc));
71 }
72
73 EffectHandles {
74 is_active: effect.active_flag(),
75 params,
76 cabinet_ir_file_path,
77 }
78 }
79
80 pub fn new(id: u32, name: String, gain: Option<f32>, volume: Option<f32>) -> Self {
92 let gain = gain.unwrap_or(1.0);
93 let volume = volume.unwrap_or(1.0);
94
95 Self {
96 id,
97 name,
98 gain: Arc::new(AtomicF32::new(gain)),
99 tone_stack: Arc::new(ToneStack::new()),
100 volume: Arc::new(AtomicF32::new(volume)),
101 effect_chain: Arc::new(Mutex::new(Vec::new())),
102 effect_handles: HashMap::new(),
103 next_effect_id: 0,
104 }
105 }
106
107 pub fn set_gain(&self, gain: f32) {
122 if gain.is_sign_positive() {
123 self.gain.store(gain, Ordering::Relaxed);
124 } else {
125 error!("Gain must be a positive number");
126 panic!("Gain must be positive");
127 }
128 }
129
130 pub fn gain(&self) -> Arc<AtomicF32> {
135 Arc::clone(&self.gain)
136 }
137
138 pub fn set_tone_stack(&self, tone_stack: ToneStackDto) {
152 self.tone_stack.set_bass(tone_stack.bass);
153 self.tone_stack.set_middle(tone_stack.middle);
154 self.tone_stack.set_treble(tone_stack.treble);
155 }
156
157 pub fn set_bass(&self, bass: f32) {
169 self.tone_stack.set_bass(bass);
170 }
171
172 pub fn set_middle(&self, middle: f32) {
184 self.tone_stack.set_middle(middle);
185 }
186
187 pub fn set_treble(&self, treble: f32) {
199 self.tone_stack.set_treble(treble);
200 }
201
202 pub fn tone_stack(&self) -> Arc<ToneStack> {
206 Arc::clone(&self.tone_stack)
207 }
208
209 pub fn set_volume(&self, volume: f32) {
221 if volume.is_sign_positive() {
222 self.volume.store(volume, Ordering::Relaxed);
223 } else {
224 error!("Volume must be a positive number");
225 panic!("Volume must be positive");
226 }
227 }
228
229 pub fn volume(&self) -> Arc<AtomicF32> {
233 Arc::clone(&self.volume)
234 }
235
236 pub fn set_name(&mut self, name: String) {
244 self.name = name;
245 }
246
247 pub fn name(&self) -> &String {
249 &self.name
250 }
251
252 pub fn id(&self) -> u32 {
254 self.id
255 }
256
257 pub fn effect_chain(&self) -> Arc<Mutex<Vec<Box<dyn Effect>>>> {
261 Arc::clone(&self.effect_chain)
262 }
263
264 pub fn restore_effect_chain(&mut self, effects: Vec<Box<dyn Effect>>) {
266 self.effect_handles.clear();
267 for effect in &effects {
268 self.effect_handles
269 .insert(effect.id(), Self::build_effect_handles(effect.as_ref()));
270 }
271
272 if let Ok(mut chain) = self.effect_chain.lock() {
273 *chain = effects;
274 }
275 }
276
277 pub fn add_effect_to_chain(&mut self, effect: Box<dyn Effect>) {
283 info!(
284 "Added effect '{}' (id={}) to chain",
285 effect.name(),
286 effect.id()
287 );
288
289 let mut combined_params = HashMap::new();
290
291 for (name, arc) in effect.f32_params() {
293 combined_params.insert(name, ParamValue::Float(arc));
294 }
295
296 for (name, arc) in effect.u32_params() {
298 combined_params.insert(name, ParamValue::Uint(arc));
299 }
300
301 self.effect_handles
302 .insert(effect.id(), Self::build_effect_handles(effect.as_ref()));
303
304 if let Ok(mut chain) = self.effect_chain.lock() {
305 chain.push(effect);
306 self.next_effect_id += 1;
307 }
308 }
309
310 pub fn remove_effect_from_chain(&mut self, effect_id: u32) {
318 if let Ok(mut chain) = self.effect_chain.lock() {
319 if let Some(pos) = chain.iter().position(|e| e.id() == effect_id) {
320 info!("Removed effect: {} from chain", chain[pos].name());
321 chain.remove(pos);
322 self.effect_handles.remove(&effect_id);
323 } else {
324 error!("Effect with id {} not found in chain", effect_id);
325 }
326 }
327 }
328
329 pub fn next_effect_id(&self) -> u32 {
331 self.next_effect_id
332 }
333
334 pub fn used_cabinet_ir_profiles(&self) -> std::collections::HashSet<String> {
339 self.effect_handles
340 .values()
341 .filter_map(|handles| handles.cabinet_ir_file_path.clone())
342 .collect()
343 }
344
345 pub fn toggle_effect(&self, effect_id: u32) -> Result<bool, String> {
359 let h = self
360 .effect_handles
361 .get(&effect_id)
362 .ok_or_else(|| format!("No effect with id {effect_id}"))?;
363 let next = !h.is_active.load(Ordering::Relaxed);
364 h.is_active.store(next, Ordering::Relaxed);
365 info!(
366 "Effect {effect_id} → {}",
367 if next { "active" } else { "bypassed" }
368 );
369 Ok(next)
370 }
371
372 pub fn set_effect_param(
398 &self,
399 effect_id: u32,
400 param: &str,
401 value: impl Into<ParamInput>,
402 ) -> Result<(), String> {
403 let h = self
404 .effect_handles
405 .get(&effect_id)
406 .ok_or_else(|| format!("No effect with id {effect_id}"))?;
407
408 let variant = h
409 .params
410 .get(param)
411 .ok_or_else(|| format!("Param '{param}' not found on effect {effect_id}"))?;
412
413 match (variant, value.into()) {
414 (ParamValue::Float(arc), ParamInput::F32(v)) => {
415 arc.store(v, Ordering::Relaxed);
416 }
417 (ParamValue::Uint(arc), ParamInput::U32(v)) => {
418 arc.store(v, Ordering::Relaxed);
419 }
420 _ => return Err(format!("Type mismatch for parameter '{param}'")),
421 }
422
423 Ok(())
424 }
425}
426
427pub enum ParamInput {
428 F32(f32),
429 U32(u32),
430}
431
432pub enum ParamValue {
433 Float(Arc<AtomicF32>),
434 Uint(Arc<AtomicU32>),
435}
436
437impl From<f32> for ParamInput {
438 fn from(f: f32) -> Self {
439 Self::F32(f)
440 }
441}
442impl From<u32> for ParamInput {
443 fn from(u: u32) -> Self {
444 Self::U32(u)
445 }
446}
447
448#[cfg(test)]
449mod tests {
450 use super::*;
451
452 mod success_path {
453 use super::*;
454 use crate::services::effects::cabinet::cabinet::Cabinet;
455 use crate::services::effects::distortion::hc_distortion::HCDistortion;
456
457 #[test]
458 fn gain_set_to_positive_value_should_succeed() {
459 let channel = Channel::new(1, "Test".to_string(), None, None);
460 channel.set_gain(0.5);
461 assert_eq!(channel.gain().load(Ordering::Relaxed), 0.5);
462 }
463
464 #[test]
465 fn volume_set_to_positive_value_should_succeed() {
466 let channel = Channel::new(1, "Test".to_string(), None, None);
467 channel.set_volume(0.5);
468 assert_eq!(channel.volume().load(Ordering::Relaxed), 0.5);
469 }
470
471 #[test]
472 fn toggle_effect_flips_active_state() {
473 let mut channel = Channel::new(0, "Test".to_string(), None, None);
474 let effect_id = channel.next_effect_id();
475 channel.add_effect_to_chain(Box::new(HCDistortion::new(
476 effect_id,
477 "Test Effect".to_string(),
478 false,
479 0.5,
480 0.0,
481 "#e67e22".to_string(),
482 )));
483
484 let was = channel.effect_handles[&effect_id]
485 .is_active
486 .load(Ordering::Relaxed);
487 let next = channel.toggle_effect(effect_id).unwrap();
488 assert_eq!(next, !was);
489 }
490
491 #[test]
492 fn set_effect_param_updates_threshold() {
493 let mut channel = Channel::new(0, "Test".to_string(), None, None);
494 let effect_id = channel.next_effect_id();
495 channel.add_effect_to_chain(Box::new(HCDistortion::new(
496 effect_id,
497 "Test Effect".to_string(),
498 false,
499 0.5,
500 0.0,
501 "#e67e22".to_string(),
502 )));
503
504 channel
506 .set_effect_param(effect_id, "threshold", 0.3f32)
507 .unwrap();
508
509 let handle = &channel.effect_handles[&effect_id];
511 let param = &handle.params["threshold"];
512
513 if let ParamValue::Float(arc) = param {
515 let v = arc.load(Ordering::Relaxed);
516 assert!((v - 0.3).abs() < 1e-6);
517 } else {
518 panic!("Expected threshold to be a Float parameter");
519 }
520 }
521
522 #[test]
523 fn adding_effect_to_effect_chain_should_add_an_effect_to_effect_chain() {
524 let mut channel = Channel::new(1, "Test".to_string(), None, None);
525 let effect_id = channel.next_effect_id();
526
527 channel.add_effect_to_chain(Box::new(HCDistortion::new(
528 effect_id,
529 "Test Effect".to_string(),
530 false,
531 0.5,
532 0.0,
533 "#e67e22".to_string(),
534 )));
535
536 let chain = channel.effect_chain.lock().unwrap();
537 assert_eq!(chain.len(), 1);
538 }
539
540 #[test]
541 fn removing_effect_from_effect_chain_should_remove_an_effect_from_effect_chain() {
542 let mut channel = Channel::new(1, "Test".to_string(), None, None);
543 let effect_id = channel.next_effect_id();
544 let effect = Box::new(HCDistortion::new(
545 effect_id,
546 "Test Effect".to_string(),
547 false,
548 0.5,
549 0.0,
550 "#e67e22".to_string(),
551 ));
552
553 channel.add_effect_to_chain(effect);
554
555 {
556 let chain_before = channel.effect_chain.lock().unwrap();
557 assert_eq!(chain_before.len(), 1);
558 }
559
560 channel.remove_effect_from_chain(effect_id);
561
562 let chain_after = channel.effect_chain.lock().unwrap();
563 assert_eq!(chain_after.len(), 0);
564 assert!(!channel.effect_handles.contains_key(&effect_id));
565 }
566
567 #[test]
568 fn restore_effect_chain_replaces_and_reorders_existing_chain() {
569 let mut channel = Channel::new(1, "Test".to_string(), None, None);
570
571 let id_1 = channel.next_effect_id();
572 let effect_1 = Box::new(HCDistortion::new(
573 id_1,
574 "Effect 1".to_string(),
575 false,
576 0.5,
577 0.0,
578 "#color1".to_string(),
579 ));
580
581 let id_2 = channel.next_effect_id();
582 let effect_2 = Box::new(HCDistortion::new(
583 id_2,
584 "Effect 2".to_string(),
585 false,
586 0.5,
587 0.0,
588 "#color2".to_string(),
589 ));
590
591 channel.add_effect_to_chain(effect_1);
592 channel.add_effect_to_chain(effect_2);
593
594 let reordered_1 = Box::new(HCDistortion::new(
595 id_1,
596 "Effect 1".to_string(),
597 false,
598 0.5,
599 0.0,
600 "#color1".to_string(),
601 ));
602 let reordered_2 = Box::new(HCDistortion::new(
603 id_2,
604 "Effect 2".to_string(),
605 false,
606 0.5,
607 0.0,
608 "#color2".to_string(),
609 ));
610
611 let new_order: Vec<Box<dyn Effect>> = vec![reordered_2, reordered_1];
612
613 channel.restore_effect_chain(new_order);
614
615 let chain = channel.effect_chain.lock().unwrap();
616 assert_eq!(chain.len(), 2, "Chain should still have 2 effects");
617 assert_eq!(chain[0].id(), id_2, "First effect should now be ID 2");
618 assert_eq!(chain[1].id(), id_1, "Second effect should now be ID 1");
619 }
620
621 #[test]
622 fn used_cabinet_ir_profiles_tracks_added_and_removed_cabinet_effects_without_locking_chain()
623 {
624 let mut channel = Channel::new(1, "Test".to_string(), None, None);
625 let cabinet_id = channel.next_effect_id();
626 channel.add_effect_to_chain(Box::new(Cabinet::new(
627 cabinet_id,
628 "Cab A".to_string(),
629 true,
630 "#111111".to_string(),
631 "Vox-ac30.wav".to_string(),
632 48_000,
633 )));
634
635 let used = channel.used_cabinet_ir_profiles();
636 assert_eq!(used.len(), 1);
637 assert!(used.contains("Vox-ac30.wav"));
638
639 channel.remove_effect_from_chain(cabinet_id);
640
641 let used_after_remove = channel.used_cabinet_ir_profiles();
642 assert!(used_after_remove.is_empty());
643 }
644
645 #[test]
646 fn restore_effect_chain_refreshes_cabinet_ir_metadata_snapshot() {
647 let mut channel = Channel::new(1, "Test".to_string(), None, None);
648
649 channel.add_effect_to_chain(Box::new(Cabinet::new(
650 0,
651 "Old Cab".to_string(),
652 true,
653 "#111111".to_string(),
654 "Vox-ac30.wav".to_string(),
655 48_000,
656 )));
657
658 channel.restore_effect_chain(vec![Box::new(Cabinet::new(
659 7,
660 "New Cab".to_string(),
661 true,
662 "#222222".to_string(),
663 "Reverb-oxford-lean.wav".to_string(),
664 48_000,
665 ))]);
666
667 let used = channel.used_cabinet_ir_profiles();
668 assert_eq!(used.len(), 1);
669 assert!(used.contains("Reverb-oxford-lean.wav"));
670 assert!(!used.contains("Vox-ac30.wav"));
671 }
672 }
673
674 mod failure_path {
675 use super::*;
676 use crate::services::effects::distortion::hc_distortion::HCDistortion;
677
678 #[test]
679 #[should_panic(expected = "Gain must be positive")]
680 fn gain_set_to_negative_value_should_panic() {
681 let channel = Channel::new(1, "Test".to_string(), None, None);
682 channel.set_gain(-0.5);
683 }
684
685 #[test]
686 #[should_panic(expected = "Volume must be positive")]
687 fn volume_set_to_negative_value_should_panic() {
688 let channel = Channel::new(1, "Test".to_string(), None, None);
689 channel.set_volume(-0.5);
690 }
691
692 #[test]
693 fn toggle_unknown_effect_returns_err() {
694 let channel = Channel::new(1, "Test".to_string(), None, None);
695 assert!(channel.toggle_effect(999).is_err());
696 }
697
698 #[test]
699 fn removing_invalid_effect_id_should_not_remove_any_effect() {
700 let mut channel = Channel::new(1, "Test".to_string(), None, None);
701 let effect_id = channel.next_effect_id();
702 let effect = Box::new(HCDistortion::new(
703 effect_id,
704 "Test Effect".to_string(),
705 false,
706 0.5,
707 0.0,
708 "#e67e22".to_string(),
709 ));
710
711 channel.add_effect_to_chain(effect);
712
713 let len_before = channel.effect_chain.lock().unwrap().len();
714 channel.remove_effect_from_chain(effect_id + 1);
715
716 let len_after = channel.effect_chain.lock().unwrap().len();
717 assert_eq!(len_before, len_after);
718 }
719 }
720}