1use crate::domain::audio_processor::AudioProcessor;
2use crate::domain::channel::Channel;
3use crate::domain::dto::amp_config_dto::AmpConfigDto;
4use crate::domain::dto::effect::effect_dto::EffectDto;
5use crate::infrastructure::audio_handler::{AudioHandler, AudioHandlerTrait};
6use crate::services::analyzers::spectrum_tap::SpectrumTap;
7use crate::services::processors::gain::gain_processor::GainProcessor;
8use crate::services::processors::resampler::resampler::ResamplePolicy;
9use crate::services::processors::tone_stack::tone_stack_processor::ToneStackProcessor;
10use atomic_float::AtomicF32;
11use cpal::{BufferSize, Device, StreamConfig};
12use derive_getters::Getters;
13use ringbuf::consumer::Consumer;
14use ringbuf::producer::Producer;
15use std::sync::atomic::{AtomicBool, Ordering};
16use std::sync::Arc;
17use std::thread;
18use std::thread::JoinHandle;
19use std::time::Duration;
20use tracing::{error, info};
21
22const DEFAULT_ANALYZER_SAMPLE_RATE_HZ: u32 = 48_000;
23
24#[derive(Getters)]
47pub struct AudioService {
48 audio_handler: Arc<dyn AudioHandlerTrait>,
49 loopback_thread: Option<JoinHandle<()>>,
50 is_active: bool,
51 channels: Vec<Channel>,
52 current_channel_id: u32,
53 master_volume: Arc<AtomicF32>,
54 next_channel_id: u32,
55 spectrum_tap: Arc<SpectrumTap>,
56}
57
58impl AudioService {
59 pub fn dsp_chain_sample_rate(&self) -> u32 {
63 self.audio_handler
64 .input_sample_rate()
65 .min(self.audio_handler.output_sample_rate())
66 }
67 pub fn new(
78 input_device: Device,
79 output_device: Device,
80 input_config: StreamConfig,
81 output_config: StreamConfig,
82 ) -> Self {
83 let handler = AudioHandler::new(input_device, output_device, input_config, output_config);
84 Self::new_with_handler(Arc::new(handler))
85 }
86
87 pub fn new_with_handler(handler: Arc<dyn AudioHandlerTrait>) -> Self {
97 Self {
98 audio_handler: handler,
99 loopback_thread: None,
100 is_active: false,
101 channels: vec![Channel::new(0, "Default".to_string(), None, None)],
102 master_volume: Arc::new(AtomicF32::new(1.0)),
103 current_channel_id: 0,
104 next_channel_id: 1,
105 spectrum_tap: Arc::new(SpectrumTap::new(DEFAULT_ANALYZER_SAMPLE_RATE_HZ)),
108 }
109 }
110
111 pub fn start_loopback(&mut self) {
130 if self.is_active {
131 return;
132 }
133
134 info!("Starting audio loopback");
135 self.is_active = true;
136
137 let handler = self.audio_handler.clone();
138 let channel_id = self.current_channel_id;
139 let master_volume_arc = self.master_volume.clone();
140 let dsp_sample_rate = self.dsp_chain_sample_rate();
141 let spectrum_tap = self.spectrum_tap.clone();
142 spectrum_tap.set_sample_rate_hz(dsp_sample_rate);
143
144 let (gain_arc, volume_arc, tone_stack_arc, effect_chain_arc) = {
145 let channel = self
146 .channels
147 .iter_mut()
148 .find(|c| c.id() == channel_id)
149 .unwrap();
150
151 (
152 channel.gain(),
153 channel.volume(),
154 channel.tone_stack(),
155 channel.effect_chain(),
156 )
157 };
158
159 let thread = thread::spawn(move || {
160 const RESAMPLER_CHUNK_SIZE: usize = 256;
163
164 let ringbuffer_size = handler
165 .input_sample_rate()
166 .max(handler.output_sample_rate()) as usize;
167
168 let mut policy = ResamplePolicy::from_rates(
180 handler.input_sample_rate(),
181 handler.output_sample_rate(),
182 RESAMPLER_CHUNK_SIZE,
183 );
184
185 let (i_producer, mut i_consumer) = AudioHandler::create_ringbuffer(ringbuffer_size);
186 let (mut o_producer, o_consumer) = AudioHandler::create_ringbuffer(ringbuffer_size);
187
188 let input_stream = handler.build_input_stream(i_producer);
189 let output_stream = handler.build_output_stream(o_consumer);
190
191 let shutdown = Arc::new(AtomicBool::new(false));
192 let worker_shutdown = shutdown.clone();
193
194 let worker = thread::spawn(move || {
195 let mut gain = GainProcessor::new(gain_arc);
196 let mut volume = GainProcessor::new(volume_arc);
197 let mut master_volume = GainProcessor::new(master_volume_arc);
198 let mut tone_stack = ToneStackProcessor::new(tone_stack_arc, dsp_sample_rate);
199
200 let mut run_dsp = |sample: f32| -> f32 {
201 let sample = gain.process(sample);
202 let mut sample = tone_stack.process(sample);
203 if let Ok(mut chain) = effect_chain_arc.lock() {
204 for effect in chain.iter_mut() {
205 sample = effect.process_if_active(sample);
206 }
207 }
208 let sample = volume.process(sample);
209 let sample = master_volume.process(sample);
210 spectrum_tap.push_sample(sample);
211 sample
212 };
213
214 loop {
215 if worker_shutdown.load(Ordering::SeqCst) {
216 break;
217 }
218
219 if let Some(sample) = i_consumer.try_pop() {
220 for processed_sample in policy
225 .process(sample, &mut |resampled_sample| run_dsp(resampled_sample))
226 {
227 let _ = o_producer.try_push(processed_sample);
228 }
229 } else {
230 thread::sleep(Duration::from_millis(1));
231 }
232 }
233
234 for processed_sample in
235 policy.flush(&mut |resampled_sample| run_dsp(resampled_sample))
236 {
237 let _ = o_producer.try_push(processed_sample);
238 }
239 });
240
241 input_stream.play();
242 output_stream.play();
243
244 thread::park();
245
246 shutdown.store(true, Ordering::SeqCst);
247 let _ = worker.join();
248 });
249
250 self.loopback_thread = Some(thread);
251 }
252
253 pub fn stop_loopback(&mut self) {
259 if !self.is_active {
260 return;
261 }
262
263 info!("Stopping audio loopback");
264
265 if let Some(handle) = self.loopback_thread.take() {
266 handle.thread().unpark();
267 let _ = handle.join();
268 }
269
270 self.is_active = false;
271 }
272
273 pub fn set_master_volume(&self, master_volume: f32) {
286 if master_volume.is_sign_positive() {
287 self.master_volume.store(master_volume, Ordering::Relaxed);
288 } else {
289 error!("Master volume must be a positive number");
290 panic!("Master volume must be positive");
291 }
292 }
293
294 pub(crate) fn set_audio_handler(&mut self, new_handler: Arc<dyn AudioHandlerTrait>) {
303 let was_active = self.is_active;
304 if was_active {
305 self.stop_loopback();
306 }
307
308 self.audio_handler = new_handler;
309 self.spectrum_tap
310 .set_sample_rate_hz(self.dsp_chain_sample_rate());
311
312 if was_active {
313 self.start_loopback();
314 }
315 }
316
317 pub fn set_input_device(&mut self, input: Device, input_config: StreamConfig) {
328 info!("Switching input device");
329
330 let old = self.audio_handler.clone();
331 let new_handler = AudioHandler::new(
332 input,
333 old.output_device().clone(),
334 input_config,
335 old.output_config().clone(),
336 );
337
338 self.set_audio_handler(Arc::new(new_handler));
339 }
340
341 pub fn set_output_device(&mut self, output: Device, output_config: StreamConfig) {
352 info!("Switching output device");
353
354 let old = self.audio_handler.clone();
355 let new_handler = AudioHandler::new(
356 old.input_device().clone(),
357 output,
358 old.input_config().clone(),
359 output_config,
360 );
361
362 self.set_audio_handler(Arc::new(new_handler));
363 }
364
365 pub fn toggle_loopback(&mut self, is_on: bool) {
374 if self.is_active == is_on {
375 return;
376 }
377 if is_on {
378 self.start_loopback();
379 } else {
380 self.stop_loopback();
381 }
382 }
383
384 pub fn add_channel(&mut self, channel_name: String) -> u32 {
394 if channel_name.len() <= 30 {
395 let id = self.next_channel_id;
396 self.next_channel_id += 1;
397
398 let new_channel = Channel::new(id, channel_name, None, None);
399
400 self.channels.push(new_channel);
401 self.set_current_channel_id(id);
402 id
403 } else {
404 error!("Channel name must be 30 characters or less");
405 panic!("Channel name must be 30 characters or less");
406 }
407 }
408
409 pub fn channels_mut(&mut self) -> &mut Vec<Channel> {
411 &mut self.channels
412 }
413
414 pub fn remove_channel(&mut self, channel_id: u32) {
422 if channel_id != 0 {
423 self.channels.retain(|c| c.id() != channel_id);
424 self.set_current_channel_id(0);
425 } else {
426 error!("Cannot remove default channel");
427 }
428 }
429
430 pub fn set_current_channel_id(&mut self, new_current_channel_id: u32) {
439 let was_on = self.is_active;
440 self.stop_loopback();
441 self.current_channel_id = new_current_channel_id;
442 if was_on {
443 self.start_loopback();
444 }
445 }
446
447 pub fn buffer_size_frames(&self) -> u32 {
451 match self.audio_handler.input_config().buffer_size {
452 BufferSize::Fixed(frames) => frames,
453 BufferSize::Default => 256,
454 }
455 }
456
457 pub fn set_buffer_size_frames(&mut self, frames: u32) -> Result<(), String> {
466 let old = self.audio_handler.clone();
467 let mut input_config = old.input_config().clone();
468 let mut output_config = old.output_config().clone();
469 input_config.buffer_size = BufferSize::Fixed(frames);
470 output_config.buffer_size = BufferSize::Fixed(frames);
471 let new_handler = AudioHandler::new(
472 old.input_device().clone(),
473 old.output_device().clone(),
474 input_config,
475 output_config,
476 );
477 self.set_audio_handler(std::sync::Arc::new(new_handler));
478 Ok(())
479 }
480
481 pub fn apply_amp_config(&mut self, config: AmpConfigDto) {
496 let mut restored_channels = Vec::new();
497
498 let normalize_tone_value = |value: f32| -> f32 {
501 if value > 1.0 {
502 (value / 100.0).clamp(0.0, 1.0)
503 } else {
504 value.clamp(0.0, 1.0)
505 }
506 };
507
508 for channel_dto in config.channels {
509 let mut channel = Channel::new(
510 channel_dto.id,
511 channel_dto.name,
512 Some(channel_dto.gain.max(0.0001)),
513 Some(channel_dto.volume.max(0.0001)),
514 );
515
516 channel.set_bass(normalize_tone_value(channel_dto.tone_stack.bass));
517 channel.set_middle(normalize_tone_value(channel_dto.tone_stack.middle));
518 channel.set_treble(normalize_tone_value(channel_dto.tone_stack.treble));
519
520 let restored_effects = channel_dto
521 .effect_chain
522 .into_iter()
523 .map(|effect_dto: EffectDto| effect_dto.to_domain(self.dsp_chain_sample_rate()))
524 .collect::<Vec<_>>();
525
526 if !restored_effects.is_empty() {
527 channel.restore_effect_chain(restored_effects);
528 }
529 restored_channels.push(channel);
530 }
531
532 if restored_channels.is_empty() {
533 restored_channels.push(Channel::new(0, "Default".to_string(), None, None));
534 }
535
536 let current_channel = if restored_channels
537 .iter()
538 .any(|c| c.id() == config.current_channel)
539 {
540 config.current_channel
541 } else {
542 restored_channels[0].id()
543 };
544
545 self.channels = restored_channels;
546 self.current_channel_id = current_channel;
547 self.next_channel_id = self.channels.iter().map(|c| c.id()).max().unwrap_or(0) + 1;
548 self.master_volume
549 .store(config.master_volume.max(0.0001), Ordering::Relaxed);
550
551 if config.is_active {
552 self.start_loopback();
553 } else {
554 self.stop_loopback();
555 }
556 }
557}
558
559#[cfg(test)]
560mod tests {
561 use super::*;
562 use crate::domain::dto::amp_config_dto::AmpConfigDto;
563 use crate::domain::dto::channel_dto::ChannelDto;
564 use crate::domain::dto::effect::cabinet_dto::CabinetDto;
565 use crate::domain::dto::effect::effect_dto::EffectDto;
566 use crate::domain::dto::effect::hcdistortion_dto::HcDistortionDto;
567 use crate::domain::dto::tone_stack_dto::ToneStackDto;
568 use crate::infrastructure::audio_handler::MockAudioHandlerTrait;
569 use crate::tests::mock::make_mock_handler;
570 use std::sync::atomic::Ordering;
571 use std::sync::Arc;
572
573 fn build_service(handler: MockAudioHandlerTrait) -> AudioService {
574 AudioService::new_with_handler(Arc::new(handler))
575 }
576
577 fn tone_stack(bass: f32, middle: f32, treble: f32) -> ToneStackDto {
578 ToneStackDto {
579 bass,
580 middle,
581 treble,
582 }
583 }
584
585 fn distortion_effect(
586 id: u32,
587 name: &str,
588 is_active: bool,
589 threshold: f32,
590 level: f32,
591 color: &str,
592 ) -> EffectDto {
593 EffectDto::HCDistortion(HcDistortionDto {
594 id,
595 name: name.to_string(),
596 is_active,
597 color: color.to_string(),
598 threshold,
599 level,
600 })
601 }
602
603 fn cabinet_effect(
604 id: u32,
605 name: &str,
606 is_active: bool,
607 color: &str,
608 ir_file_path: &str,
609 ) -> EffectDto {
610 EffectDto::Cabinet(CabinetDto {
611 id,
612 name: name.to_string(),
613 is_active,
614 color: color.to_string(),
615 ir_file_path: ir_file_path.to_string(),
616 })
617 }
618
619 fn channel_dto(
620 id: u32,
621 name: &str,
622 gain: f32,
623 volume: f32,
624 tone_stack: ToneStackDto,
625 effect_chain: Vec<EffectDto>,
626 ) -> ChannelDto {
627 ChannelDto {
628 id,
629 name: name.to_string(),
630 gain,
631 tone_stack,
632 volume,
633 effect_chain,
634 }
635 }
636
637 #[cfg(test)]
638 mod success_path {
639 use super::*;
640
641 #[test]
642 fn master_volume_set_to_positive_value_should_succeed() {
643 let mock = MockAudioHandlerTrait::new();
644 let service = AudioService::new_with_handler(Arc::new(mock));
645 service.set_master_volume(0.5);
646 assert_eq!(service.master_volume().load(Ordering::Relaxed), 0.5);
647 }
648
649 #[test]
650 fn add_channel_should_add_a_channel_with_correct_values_and_sets_current_channel_id_to_new_id(
651 ) {
652 let mock = MockAudioHandlerTrait::new();
653 let mut service = AudioService::new_with_handler(Arc::new(mock));
654 let test_channel_id = service.add_channel("TestChannel".to_string());
655 let test_channel = service
656 .channels
657 .iter()
658 .find(|c| c.id() == test_channel_id)
659 .unwrap();
660
661 assert_eq!(service.channels.len(), 2);
662 assert_eq!(test_channel.name(), "TestChannel");
663 assert_eq!(test_channel.id(), 1);
664 assert_eq!(test_channel.gain().load(Ordering::Relaxed), 1.0);
665 assert_eq!(test_channel.volume().load(Ordering::Relaxed), 1.0);
666 assert_eq!(
667 test_channel.tone_stack().bass().load(Ordering::Relaxed),
668 1.0
669 );
670 assert_eq!(
671 test_channel.tone_stack().middle().load(Ordering::Relaxed),
672 1.0
673 );
674 assert_eq!(
675 test_channel.tone_stack().treble().load(Ordering::Relaxed),
676 1.0
677 );
678 assert_eq!(*service.current_channel_id(), test_channel.id());
679 }
680
681 #[test]
682 fn remove_channel_removes_channel_and_sets_current_channel_id_to_0() {
683 let mock = MockAudioHandlerTrait::new();
684 let mut service = AudioService::new_with_handler(Arc::new(mock));
685 let test_channel_id = service.add_channel("TestChannel".to_string());
686 service.remove_channel(test_channel_id);
687
688 assert_eq!(service.channels.len(), 1);
689 assert_eq!(*service.current_channel_id(), 0);
690 }
691
692 #[test]
693 fn apply_amp_config_restores_channels_tones_effects_and_master_volume() {
694 let mut service = build_service(make_mock_handler());
695 let config = AmpConfigDto {
696 master_volume: 0.42,
697 is_active: false,
698 channels: vec![
699 channel_dto(4, "Clean", 1.25, 0.8, tone_stack(25.0, 0.45, 130.0), vec![]),
700 channel_dto(
701 7,
702 "Lead",
703 2.0,
704 0.65,
705 tone_stack(0.6, 80.0, -0.5),
706 vec![distortion_effect(11, "Drive", true, 0.33, 0.7, "#ff6600")],
707 ),
708 ],
709 current_channel: 7,
710 };
711
712 service.apply_amp_config(config);
713
714 let snapshot = AmpConfigDto::from_service(&service);
715 let clean = snapshot
716 .channels
717 .iter()
718 .find(|channel| channel.id == 4)
719 .unwrap();
720 let lead = snapshot
721 .channels
722 .iter()
723 .find(|channel| channel.id == 7)
724 .unwrap();
725
726 assert_eq!(snapshot.channels.len(), 2);
727 assert_eq!(snapshot.current_channel, 7);
728 assert!(!snapshot.is_active);
729 assert_eq!(service.next_channel_id, 8);
730 assert!((snapshot.master_volume - 0.42).abs() < f32::EPSILON);
731
732 assert_eq!(clean.name, "Clean");
733 assert!((clean.gain - 1.25).abs() < f32::EPSILON);
734 assert!((clean.volume - 0.8).abs() < f32::EPSILON);
735 assert!((clean.tone_stack.bass - 0.25).abs() < 1e-6);
736 assert!((clean.tone_stack.middle - 0.45).abs() < 1e-6);
737 assert!((clean.tone_stack.treble - 1.0).abs() < 1e-6);
738
739 assert_eq!(lead.name, "Lead");
740 assert!((lead.tone_stack.bass - 0.6).abs() < 1e-6);
741 assert!((lead.tone_stack.middle - 0.8).abs() < 1e-6);
742 assert!((lead.tone_stack.treble - 0.0).abs() < 1e-6);
743 assert_eq!(lead.effect_chain.len(), 1);
744 if let EffectDto::HCDistortion(dto) = &lead.effect_chain[0] {
747 assert_eq!(dto.id, 11);
748 assert_eq!(dto.name, "Drive");
749 assert!(dto.is_active);
750 assert_eq!(dto.color, "#ff6600");
751 assert!((dto.threshold - 0.33).abs() < 1e-6);
752 assert!((dto.level - 0.7).abs() < 1e-5);
753 } else {
754 panic!("Expected HCDistortion effect");
755 }
756 }
757
758 #[test]
759 fn apply_amp_config_restores_cabinet_effect_ir_file_path() {
760 let mut service = build_service(make_mock_handler());
761 let config = AmpConfigDto {
762 master_volume: 0.8,
763 is_active: false,
764 channels: vec![channel_dto(
765 2,
766 "Cab Channel",
767 1.0,
768 1.0,
769 tone_stack(0.5, 0.5, 0.5),
770 vec![cabinet_effect(9, "Cab", true, "#445566", "Vox-ac30.wav")],
771 )],
772 current_channel: 2,
773 };
774
775 service.apply_amp_config(config);
776
777 let snapshot = AmpConfigDto::from_service(&service);
778 assert_eq!(snapshot.channels.len(), 1);
779 assert_eq!(snapshot.channels[0].effect_chain.len(), 1);
780
781 if let EffectDto::Cabinet(dto) = &snapshot.channels[0].effect_chain[0] {
782 assert_eq!(dto.id, 9);
783 assert_eq!(dto.name, "Cab");
784 assert!(dto.is_active);
785 assert_eq!(dto.color, "#445566");
786 assert_eq!(dto.ir_file_path, "Vox-ac30.wav");
787 } else {
788 panic!("Expected Cabinet effect");
789 }
790 }
791
792 #[test]
793 fn apply_amp_config_clamps_non_positive_levels_and_falls_back_to_first_channel() {
794 let mut service = build_service(make_mock_handler());
795 let config = AmpConfigDto {
796 master_volume: 0.0,
797 is_active: false,
798 channels: vec![channel_dto(
799 4,
800 "Crunch",
801 -2.0,
802 0.0,
803 tone_stack(0.2, 0.4, 0.6),
804 vec![],
805 )],
806 current_channel: 999,
807 };
808
809 service.apply_amp_config(config);
810
811 let channel = service
812 .channels
813 .iter()
814 .find(|channel| channel.id() == 4)
815 .unwrap();
816
817 assert_eq!(service.channels.len(), 1);
818 assert_eq!(*service.current_channel_id(), 4);
819 assert_eq!(service.next_channel_id, 5);
820 assert!((channel.gain().load(Ordering::Relaxed) - 0.0001).abs() < 1e-6);
821 assert!((channel.volume().load(Ordering::Relaxed) - 0.0001).abs() < 1e-6);
822 assert!((service.master_volume().load(Ordering::Relaxed) - 0.0001).abs() < 1e-6);
823 }
824
825 #[test]
826 fn apply_amp_config_with_no_channels_creates_default_channel() {
827 let mut service = build_service(make_mock_handler());
828
829 service.apply_amp_config(AmpConfigDto {
830 master_volume: 0.75,
831 is_active: false,
832 channels: vec![],
833 current_channel: 321,
834 });
835
836 assert_eq!(service.channels.len(), 1);
837 assert_eq!(service.channels[0].id(), 0);
838 assert_eq!(service.channels[0].name(), "Default");
839 assert_eq!(*service.current_channel_id(), 0);
840 assert_eq!(service.next_channel_id, 1);
841 assert!((service.master_volume().load(Ordering::Relaxed) - 0.75).abs() < f32::EPSILON);
842 }
843
844 #[test]
845 fn apply_amp_config_with_active_flag_starts_loopback() {
846 let mut service = build_service(make_mock_handler());
847
848 service.apply_amp_config(AmpConfigDto {
849 master_volume: 0.9,
850 is_active: true,
851 channels: vec![channel_dto(
852 2,
853 "Loopback",
854 1.0,
855 1.0,
856 tone_stack(0.5, 0.5, 0.5),
857 vec![],
858 )],
859 current_channel: 2,
860 });
861
862 assert!(*service.is_active());
863
864 service.stop_loopback();
865
866 assert!(!*service.is_active());
867 }
868 }
869
870 #[cfg(test)]
871 mod failure_path {
872 use super::*;
873
874 #[test]
875 #[should_panic(expected = "Master volume must be positive")]
876 fn master_volume_set_to_negative_value_should_panic() {
877 let mock = MockAudioHandlerTrait::new();
878 let service = AudioService::new_with_handler(Arc::new(mock));
879 service.set_master_volume(-0.5);
880 }
881
882 #[test]
883 fn removing_default_channel_should_do_nothing() {
884 let mock = MockAudioHandlerTrait::new();
885 let mut service = AudioService::new_with_handler(Arc::new(mock));
886 service.remove_channel(0);
887
888 assert_eq!(service.channels.len(), 1);
889 }
890
891 #[test]
892 #[should_panic(expected = "Channel name must be 30 characters or less")]
893 fn add_channel_should_panic_with_to_long_name() {
894 let mock = MockAudioHandlerTrait::new();
895 let mut service = AudioService::new_with_handler(Arc::new(mock));
896 let _test_channel =
897 service.add_channel("Hippopotomonstrosesquippedaliophobia".to_string());
898 }
899 }
900}