1use std::sync::Arc;
25
26use anyhow::{Context, Result};
27use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
28use cpal::{Device, Stream, StreamConfig};
29use wavecraft_processors::{
30 OscilloscopeFrameConsumer, OscilloscopeTap, Waveform, create_oscilloscope_channel,
31 generate_waveform_sample,
32};
33use wavecraft_protocol::MeterUpdateNotification;
34
35use super::atomic_params::AtomicParameterBridge;
36use super::ffi_processor::DevAudioProcessor;
37
38const GAIN_MULTIPLIER_MIN: f32 = 0.0;
39const GAIN_MULTIPLIER_MAX: f32 = 2.0;
40const INPUT_GAIN_PARAM_ID: &str = "input_gain_level";
42const OUTPUT_GAIN_PARAM_ID: &str = "output_gain_level";
43const OSCILLATOR_WAVEFORM_PARAM_ID: &str = "oscillator_waveform";
44
45#[derive(Debug, Clone)]
47pub struct AudioConfig {
48 pub sample_rate: f32,
50 pub buffer_size: u32,
52}
53
54pub struct AudioHandle {
57 _input_stream: Stream,
58 _output_stream: Option<Stream>,
59}
60
61pub struct AudioServer {
64 processor: Box<dyn DevAudioProcessor>,
65 config: AudioConfig,
66 input_device: Device,
67 output_device: Device,
68 input_config: StreamConfig,
69 output_config: StreamConfig,
70 param_bridge: Arc<AtomicParameterBridge>,
71}
72
73impl AudioServer {
74 pub fn new(
77 processor: Box<dyn DevAudioProcessor>,
78 config: AudioConfig,
79 param_bridge: Arc<AtomicParameterBridge>,
80 ) -> Result<Self> {
81 let host = cpal::default_host();
82
83 let input_device = host
85 .default_input_device()
86 .context("No input device available")?;
87 tracing::info!("Using input device: {}", input_device.name()?);
88
89 let supported_input = input_device
90 .default_input_config()
91 .context("Failed to get default input config")?;
92 let input_sample_rate = supported_input.sample_rate().0;
93 tracing::info!("Input sample rate: {} Hz", input_sample_rate);
94 let input_config: StreamConfig = supported_input.into();
95
96 let output_device = host
98 .default_output_device()
99 .context("No output device available")?;
100
101 match output_device.name() {
102 Ok(name) => tracing::info!("Using output device: {}", name),
103 Err(_) => tracing::info!("Using output device: (unnamed)"),
104 }
105
106 let supported_output = output_device
107 .default_output_config()
108 .context("Failed to get default output config")?;
109 let output_sr = supported_output.sample_rate().0;
110 tracing::info!("Output sample rate: {} Hz", output_sr);
111 if output_sr != input_sample_rate {
112 tracing::warn!(
113 "Input/output sample rate mismatch ({} vs {}). \
114 Processing at input rate; output device may resample.",
115 input_sample_rate,
116 output_sr
117 );
118 }
119 let output_config: StreamConfig = supported_output.into();
120
121 Ok(Self {
122 processor,
123 config,
124 input_device,
125 output_device,
126 input_config,
127 output_config,
128 param_bridge,
129 })
130 }
131
132 pub fn start(
140 mut self,
141 ) -> Result<(
142 AudioHandle,
143 rtrb::Consumer<MeterUpdateNotification>,
144 OscilloscopeFrameConsumer,
145 )> {
146 let actual_sample_rate = self.input_config.sample_rate.0 as f32;
148 self.processor.set_sample_rate(actual_sample_rate);
149
150 let mut processor = self.processor;
151 let buffer_size = self.config.buffer_size as usize;
152 let input_channels = self.input_config.channels as usize;
153 let output_channels = self.output_config.channels as usize;
154 let param_bridge = Arc::clone(&self.param_bridge);
155
156 let ring_capacity = buffer_size * 2 * 4;
160 let (mut ring_producer, mut ring_consumer) = rtrb::RingBuffer::new(ring_capacity);
161
162 let (mut meter_producer, meter_consumer) =
167 rtrb::RingBuffer::<MeterUpdateNotification>::new(64);
168 let (oscilloscope_producer, oscilloscope_consumer) = create_oscilloscope_channel(8);
169 let mut oscilloscope_tap = OscilloscopeTap::with_output(oscilloscope_producer);
170 oscilloscope_tap.set_sample_rate_hz(actual_sample_rate);
171
172 let mut frame_counter = 0u64;
173 let mut oscillator_phase = 0.0_f32;
174
175 let mut left_buf = vec![0.0f32; buffer_size];
179 let mut right_buf = vec![0.0f32; buffer_size];
180
181 let mut interleave_buf = vec![0.0f32; buffer_size * 2];
183
184 let input_stream = self
185 .input_device
186 .build_input_stream(
187 &self.input_config,
188 move |data: &[f32], _: &cpal::InputCallbackInfo| {
189 frame_counter += 1;
190
191 let num_samples = data.len() / input_channels.max(1);
192 if num_samples == 0 || input_channels == 0 {
193 return;
194 }
195
196 let actual_samples = num_samples.min(left_buf.len());
197 let left = &mut left_buf[..actual_samples];
198 let right = &mut right_buf[..actual_samples];
199
200 left.fill(0.0);
202 right.fill(0.0);
203
204 for i in 0..actual_samples {
205 left[i] = data[i * input_channels];
206 if input_channels > 1 {
207 right[i] = data[i * input_channels + 1];
208 } else {
209 right[i] = left[i];
210 }
211 }
212
213 {
215 let mut channels: [&mut [f32]; 2] = [left, right];
216 processor.process(&mut channels);
217 }
218
219 apply_output_modifiers(
223 left,
224 right,
225 ¶m_bridge,
226 &mut oscillator_phase,
227 actual_sample_rate,
228 );
229
230 let left = &left_buf[..actual_samples];
232 let right = &right_buf[..actual_samples];
233
234 oscilloscope_tap.capture_stereo(left, right);
236
237 let (peak_left, rms_left) = compute_peak_and_rms(left);
239 let (peak_right, rms_right) = compute_peak_and_rms(right);
240
241 if frame_counter.is_multiple_of(2) {
246 let notification = MeterUpdateNotification {
247 timestamp_us: frame_counter,
248 left_peak: peak_left,
249 left_rms: rms_left,
250 right_peak: peak_right,
251 right_rms: rms_right,
252 };
253 let _ = meter_producer.push(notification);
257 }
258
259 let interleave = &mut interleave_buf[..actual_samples * 2];
263 for i in 0..actual_samples {
264 interleave[i * 2] = left[i];
265 interleave[i * 2 + 1] = right[i];
266 }
267
268 for &sample in interleave.iter() {
270 if ring_producer.push(sample).is_err() {
271 break;
272 }
273 }
274 },
275 |err| {
276 tracing::error!("Audio input stream error: {}", err);
277 },
278 None,
279 )
280 .context("Failed to build input stream")?;
281
282 input_stream
283 .play()
284 .context("Failed to start input stream")?;
285 tracing::info!("Input stream started");
286
287 let output_stream = self
289 .output_device
290 .build_output_stream(
291 &self.output_config,
292 move |data: &mut [f32], _: &cpal::OutputCallbackInfo| {
293 if output_channels == 0 {
294 data.fill(0.0);
295 return;
296 }
297
298 for frame in data.chunks_mut(output_channels) {
301 let left = ring_consumer.pop().unwrap_or(0.0);
302 let right = ring_consumer.pop().unwrap_or(0.0);
303
304 if output_channels == 1 {
305 frame[0] = 0.5 * (left + right);
306 continue;
307 }
308
309 frame[0] = left;
310 frame[1] = right;
311
312 for channel in frame.iter_mut().skip(2) {
313 *channel = 0.0;
314 }
315 }
316 },
317 |err| {
318 tracing::error!("Audio output stream error: {}", err);
319 },
320 None,
321 )
322 .context("Failed to build output stream")?;
323
324 output_stream
325 .play()
326 .context("Failed to start output stream")?;
327 tracing::info!("Output stream started");
328
329 tracing::info!("Audio server started in full-duplex (input + output) mode");
330
331 Ok((
332 AudioHandle {
333 _input_stream: input_stream,
334 _output_stream: Some(output_stream),
335 },
336 meter_consumer,
337 oscilloscope_consumer,
338 ))
339 }
340
341 pub fn has_output(&self) -> bool {
343 true
344 }
345}
346
347fn apply_output_modifiers(
348 left: &mut [f32],
349 right: &mut [f32],
350 param_bridge: &AtomicParameterBridge,
351 oscillator_phase: &mut f32,
352 sample_rate: f32,
353) {
354 let input_gain = read_gain_multiplier(param_bridge, INPUT_GAIN_PARAM_ID);
355 let output_gain = read_gain_multiplier(param_bridge, OUTPUT_GAIN_PARAM_ID);
356 let combined_gain = input_gain * output_gain;
357
358 if let Some(enabled) = param_bridge.read("oscillator_enabled")
361 && enabled < 0.5
362 {
363 left.fill(0.0);
364 right.fill(0.0);
365 apply_gain(left, right, combined_gain);
366 return;
367 }
368
369 let oscillator_frequency = param_bridge.read("oscillator_frequency");
372 let oscillator_level = param_bridge.read("oscillator_level");
373 let oscillator_waveform = param_bridge
374 .read(OSCILLATOR_WAVEFORM_PARAM_ID)
375 .unwrap_or(0.0);
376
377 if let (Some(frequency), Some(level)) = (oscillator_frequency, oscillator_level) {
378 if !sample_rate.is_finite() || sample_rate <= 0.0 {
379 apply_gain(left, right, combined_gain);
380 return;
381 }
382
383 let clamped_frequency = if frequency.is_finite() {
384 frequency.clamp(20.0, 5000.0)
385 } else {
386 440.0
387 };
388 let clamped_level = if level.is_finite() {
389 level.clamp(0.0, 1.0)
390 } else {
391 0.0
392 };
393
394 let phase_delta = clamped_frequency / sample_rate;
395 let mut phase = if oscillator_phase.is_finite() {
396 *oscillator_phase
397 } else {
398 0.0
399 };
400 let waveform = Waveform::from_index(oscillator_waveform);
401
402 for (left_sample, right_sample) in left.iter_mut().zip(right.iter_mut()) {
403 let sample = generate_waveform_sample(waveform, phase) * clamped_level;
404 *left_sample = sample;
405 *right_sample = sample;
406
407 phase += phase_delta;
408 if phase >= 1.0 {
409 phase -= phase.floor();
410 }
411 }
412
413 *oscillator_phase = phase;
414 }
415
416 apply_gain(left, right, combined_gain);
417}
418
419fn read_gain_multiplier(param_bridge: &AtomicParameterBridge, id: &str) -> f32 {
420 if let Some(value) = param_bridge.read(id)
421 && value.is_finite()
422 {
423 return value.clamp(GAIN_MULTIPLIER_MIN, GAIN_MULTIPLIER_MAX);
424 }
425
426 1.0
427}
428
429fn compute_peak_and_rms(samples: &[f32]) -> (f32, f32) {
430 let peak = samples
431 .iter()
432 .copied()
433 .fold(0.0f32, |acc, sample| acc.max(sample.abs()));
434 let rms =
435 (samples.iter().map(|sample| sample * sample).sum::<f32>() / samples.len() as f32).sqrt();
436
437 (peak, rms)
438}
439
440fn apply_gain(left: &mut [f32], right: &mut [f32], gain: f32) {
441 if (gain - 1.0).abs() <= f32::EPSILON {
442 return;
443 }
444
445 for (left_sample, right_sample) in left.iter_mut().zip(right.iter_mut()) {
446 *left_sample *= gain;
447 *right_sample *= gain;
448 }
449}
450
451#[cfg(test)]
452mod tests {
453 use super::apply_output_modifiers;
454 use crate::audio::atomic_params::AtomicParameterBridge;
455 use wavecraft_protocol::{ParameterInfo, ParameterType};
456
457 fn bridge_with_enabled(default_value: f32) -> AtomicParameterBridge {
458 AtomicParameterBridge::new(&[ParameterInfo {
459 id: "oscillator_enabled".to_string(),
460 name: "Enabled".to_string(),
461 param_type: ParameterType::Float,
462 value: default_value,
463 default: default_value,
464 unit: Some("%".to_string()),
465 min: 0.0,
466 max: 1.0,
467 group: Some("Oscillator".to_string()),
468 variants: None,
469 }])
470 }
471
472 #[test]
473 fn output_modifiers_mute_when_oscillator_disabled() {
474 let bridge = bridge_with_enabled(1.0);
475 bridge.write("oscillator_enabled", 0.0);
476
477 let mut left = [0.25_f32, -0.5, 0.75];
478 let mut right = [0.2_f32, -0.4, 0.6];
479 let mut phase = 0.0;
480 apply_output_modifiers(&mut left, &mut right, &bridge, &mut phase, 48_000.0);
481
482 assert!(left.iter().all(|s| s.abs() <= f32::EPSILON));
483 assert!(right.iter().all(|s| s.abs() <= f32::EPSILON));
484 }
485
486 #[test]
487 fn output_modifiers_keep_signal_when_oscillator_enabled() {
488 let bridge = bridge_with_enabled(1.0);
489
490 let mut left = [0.25_f32, -0.5, 0.75];
491 let mut right = [0.2_f32, -0.4, 0.6];
492 let mut phase = 0.0;
493 apply_output_modifiers(&mut left, &mut right, &bridge, &mut phase, 48_000.0);
494
495 assert_eq!(left, [0.25, -0.5, 0.75]);
496 assert_eq!(right, [0.2, -0.4, 0.6]);
497 }
498
499 fn oscillator_bridge(
500 frequency: f32,
501 level: f32,
502 waveform: f32,
503 enabled: f32,
504 input_gain_level: f32,
505 output_gain_level: f32,
506 ) -> AtomicParameterBridge {
507 AtomicParameterBridge::new(&[
508 ParameterInfo {
509 id: "oscillator_enabled".to_string(),
510 name: "Enabled".to_string(),
511 param_type: ParameterType::Float,
512 value: enabled,
513 default: enabled,
514 unit: Some("%".to_string()),
515 min: 0.0,
516 max: 1.0,
517 group: Some("Oscillator".to_string()),
518 variants: None,
519 },
520 ParameterInfo {
521 id: "oscillator_frequency".to_string(),
522 name: "Frequency".to_string(),
523 param_type: ParameterType::Float,
524 value: frequency,
525 default: frequency,
526 min: 20.0,
527 max: 5_000.0,
528 unit: Some("Hz".to_string()),
529 group: Some("Oscillator".to_string()),
530 variants: None,
531 },
532 ParameterInfo {
533 id: "oscillator_waveform".to_string(),
534 name: "Waveform".to_string(),
535 param_type: ParameterType::Enum,
536 value: waveform,
537 default: waveform,
538 min: 0.0,
539 max: 3.0,
540 unit: None,
541 group: Some("Oscillator".to_string()),
542 variants: Some(vec![
543 "Sine".to_string(),
544 "Square".to_string(),
545 "Saw".to_string(),
546 "Triangle".to_string(),
547 ]),
548 },
549 ParameterInfo {
550 id: "oscillator_level".to_string(),
551 name: "Level".to_string(),
552 param_type: ParameterType::Float,
553 value: level,
554 default: level,
555 unit: Some("%".to_string()),
556 min: 0.0,
557 max: 1.0,
558 group: Some("Oscillator".to_string()),
559 variants: None,
560 },
561 ParameterInfo {
562 id: "input_gain_level".to_string(),
563 name: "Level".to_string(),
564 param_type: ParameterType::Float,
565 value: input_gain_level,
566 default: input_gain_level,
567 unit: Some("x".to_string()),
568 min: 0.0,
569 max: 2.0,
570 group: Some("InputGain".to_string()),
571 variants: None,
572 },
573 ParameterInfo {
574 id: "output_gain_level".to_string(),
575 name: "Level".to_string(),
576 param_type: ParameterType::Float,
577 value: output_gain_level,
578 default: output_gain_level,
579 unit: Some("x".to_string()),
580 min: 0.0,
581 max: 2.0,
582 group: Some("OutputGain".to_string()),
583 variants: None,
584 },
585 ])
586 }
587
588 #[test]
589 fn output_modifiers_generate_runtime_oscillator_from_frequency_and_level() {
590 let bridge = oscillator_bridge(880.0, 0.75, 0.0, 1.0, 1.0, 1.0);
591 let mut left = [0.0_f32; 128];
592 let mut right = [0.0_f32; 128];
593 let mut phase = 0.0;
594
595 apply_output_modifiers(&mut left, &mut right, &bridge, &mut phase, 48_000.0);
596
597 let peak_left = left
598 .iter()
599 .fold(0.0_f32, |acc, sample| acc.max(sample.abs()));
600 let peak_right = right
601 .iter()
602 .fold(0.0_f32, |acc, sample| acc.max(sample.abs()));
603
604 assert!(peak_left > 0.2, "expected audible generated oscillator");
605 assert!(peak_right > 0.2, "expected audible generated oscillator");
606 assert_eq!(left, right, "expected in-phase stereo oscillator output");
607 assert!(phase > 0.0, "phase should advance after generation");
608 }
609
610 #[test]
611 fn output_modifiers_level_zero_produces_silence() {
612 let bridge = oscillator_bridge(440.0, 0.0, 0.0, 1.0, 1.0, 1.0);
613 let mut left = [0.1_f32; 64];
614 let mut right = [0.1_f32; 64];
615 let mut phase = 0.0;
616
617 apply_output_modifiers(&mut left, &mut right, &bridge, &mut phase, 48_000.0);
618
619 assert!(left.iter().all(|s| s.abs() <= f32::EPSILON));
620 assert!(right.iter().all(|s| s.abs() <= f32::EPSILON));
621 }
622
623 #[test]
624 fn output_modifiers_frequency_change_changes_waveform() {
625 let low_freq_bridge = oscillator_bridge(220.0, 0.5, 0.0, 1.0, 1.0, 1.0);
626 let high_freq_bridge = oscillator_bridge(1760.0, 0.5, 0.0, 1.0, 1.0, 1.0);
627
628 let mut low_left = [0.0_f32; 256];
629 let mut low_right = [0.0_f32; 256];
630 let mut high_left = [0.0_f32; 256];
631 let mut high_right = [0.0_f32; 256];
632
633 let mut low_phase = 0.0;
634 let mut high_phase = 0.0;
635
636 apply_output_modifiers(
637 &mut low_left,
638 &mut low_right,
639 &low_freq_bridge,
640 &mut low_phase,
641 48_000.0,
642 );
643 apply_output_modifiers(
644 &mut high_left,
645 &mut high_right,
646 &high_freq_bridge,
647 &mut high_phase,
648 48_000.0,
649 );
650
651 assert_ne!(
652 low_left, high_left,
653 "frequency updates should alter waveform"
654 );
655 assert_eq!(low_left, low_right);
656 assert_eq!(high_left, high_right);
657 }
658
659 #[test]
660 fn output_modifiers_apply_input_and_output_gain_levels() {
661 let unity_bridge = oscillator_bridge(880.0, 0.5, 0.0, 1.0, 1.0, 1.0);
662 let boosted_bridge = oscillator_bridge(880.0, 0.5, 0.0, 1.0, 1.5, 2.0);
663
664 let mut unity_left = [0.0_f32; 256];
665 let mut unity_right = [0.0_f32; 256];
666 let mut boosted_left = [0.0_f32; 256];
667 let mut boosted_right = [0.0_f32; 256];
668
669 let mut unity_phase = 0.0;
670 let mut boosted_phase = 0.0;
671
672 apply_output_modifiers(
673 &mut unity_left,
674 &mut unity_right,
675 &unity_bridge,
676 &mut unity_phase,
677 48_000.0,
678 );
679 apply_output_modifiers(
680 &mut boosted_left,
681 &mut boosted_right,
682 &boosted_bridge,
683 &mut boosted_phase,
684 48_000.0,
685 );
686
687 let unity_peak = unity_left
688 .iter()
689 .fold(0.0_f32, |acc, sample| acc.max(sample.abs()));
690 let boosted_peak = boosted_left
691 .iter()
692 .fold(0.0_f32, |acc, sample| acc.max(sample.abs()));
693
694 assert!(boosted_peak > unity_peak * 2.5);
695 assert_eq!(boosted_left, boosted_right);
696 assert_eq!(unity_left, unity_right);
697 }
698
699 #[test]
700 fn output_modifiers_waveform_change_changes_shape() {
701 let sine_bridge = oscillator_bridge(440.0, 0.5, 0.0, 1.0, 1.0, 1.0);
702 let saw_bridge = oscillator_bridge(440.0, 0.5, 2.0, 1.0, 1.0, 1.0);
703
704 let mut sine_left = [0.0_f32; 256];
705 let mut sine_right = [0.0_f32; 256];
706 let mut saw_left = [0.0_f32; 256];
707 let mut saw_right = [0.0_f32; 256];
708
709 let mut sine_phase = 0.0;
710 let mut saw_phase = 0.0;
711
712 apply_output_modifiers(
713 &mut sine_left,
714 &mut sine_right,
715 &sine_bridge,
716 &mut sine_phase,
717 48_000.0,
718 );
719 apply_output_modifiers(
720 &mut saw_left,
721 &mut saw_right,
722 &saw_bridge,
723 &mut saw_phase,
724 48_000.0,
725 );
726
727 assert_ne!(
728 sine_left, saw_left,
729 "waveform selection should change output shape"
730 );
731 assert_eq!(sine_left, sine_right);
732 assert_eq!(saw_left, saw_right);
733 }
734
735 #[test]
736 fn output_modifiers_apply_gain_without_oscillator_params() {
737 let bridge = AtomicParameterBridge::new(&[
738 ParameterInfo {
739 id: "input_gain_level".to_string(),
740 name: "Level".to_string(),
741 param_type: ParameterType::Float,
742 value: 1.5,
743 default: 1.5,
744 unit: Some("x".to_string()),
745 min: 0.0,
746 max: 2.0,
747 group: Some("InputGain".to_string()),
748 variants: None,
749 },
750 ParameterInfo {
751 id: "output_gain_level".to_string(),
752 name: "Level".to_string(),
753 param_type: ParameterType::Float,
754 value: 1.2,
755 default: 1.2,
756 unit: Some("x".to_string()),
757 min: 0.0,
758 max: 2.0,
759 group: Some("OutputGain".to_string()),
760 variants: None,
761 },
762 ]);
763
764 let mut left = [0.25_f32, -0.5, 0.75];
765 let mut right = [0.2_f32, -0.4, 0.6];
766 let mut phase = 0.0;
767
768 apply_output_modifiers(&mut left, &mut right, &bridge, &mut phase, 48_000.0);
769
770 let expected_gain = 1.5 * 1.2;
771 assert_eq!(
772 left,
773 [
774 0.25 * expected_gain,
775 -0.5 * expected_gain,
776 0.75 * expected_gain
777 ]
778 );
779 assert_eq!(
780 right,
781 [
782 0.2 * expected_gain,
783 -0.4 * expected_gain,
784 0.6 * expected_gain
785 ]
786 );
787 }
788
789 #[test]
790 fn output_modifiers_ignore_compact_legacy_gain_ids() {
791 let bridge = AtomicParameterBridge::new(&[
792 ParameterInfo {
793 id: "inputgain_level".to_string(),
794 name: "Level".to_string(),
795 param_type: ParameterType::Float,
796 value: 0.2,
797 default: 0.2,
798 unit: Some("x".to_string()),
799 min: 0.0,
800 max: 2.0,
801 group: Some("InputGain".to_string()),
802 variants: None,
803 },
804 ParameterInfo {
805 id: "outputgain_level".to_string(),
806 name: "Level".to_string(),
807 param_type: ParameterType::Float,
808 value: 0.2,
809 default: 0.2,
810 unit: Some("x".to_string()),
811 min: 0.0,
812 max: 2.0,
813 group: Some("OutputGain".to_string()),
814 variants: None,
815 },
816 ]);
817
818 let mut left = [0.5_f32; 16];
819 let mut right = [0.5_f32; 16];
820 let mut phase = 0.0;
821
822 apply_output_modifiers(&mut left, &mut right, &bridge, &mut phase, 48_000.0);
823
824 let expected = 0.5;
826 assert!(left.iter().all(|sample| (*sample - expected).abs() < 1e-6));
827 assert!(right.iter().all(|sample| (*sample - expected).abs() < 1e-6));
828 }
829
830 #[test]
831 fn output_modifiers_ignore_legacy_snake_case_gain_suffix_ids() {
832 let bridge = AtomicParameterBridge::new(&[
833 ParameterInfo {
834 id: "input_gain_gain".to_string(),
835 name: "Gain".to_string(),
836 param_type: ParameterType::Float,
837 value: 0.2,
838 default: 0.2,
839 unit: Some("x".to_string()),
840 min: 0.0,
841 max: 2.0,
842 group: Some("InputGain".to_string()),
843 variants: None,
844 },
845 ParameterInfo {
846 id: "output_gain_gain".to_string(),
847 name: "Gain".to_string(),
848 param_type: ParameterType::Float,
849 value: 0.2,
850 default: 0.2,
851 unit: Some("x".to_string()),
852 min: 0.0,
853 max: 2.0,
854 group: Some("OutputGain".to_string()),
855 variants: None,
856 },
857 ]);
858
859 let mut left = [0.5_f32; 16];
860 let mut right = [0.5_f32; 16];
861 let mut phase = 0.0;
862
863 apply_output_modifiers(&mut left, &mut right, &bridge, &mut phase, 48_000.0);
864
865 let expected = 0.5;
867 assert!(left.iter().all(|sample| (*sample - expected).abs() < 1e-6));
868 assert!(right.iter().all(|sample| (*sample - expected).abs() < 1e-6));
869 }
870
871 #[test]
872 fn output_modifiers_use_canonical_ids_even_when_legacy_variants_exist() {
873 let bridge = AtomicParameterBridge::new(&[
874 ParameterInfo {
875 id: "input_gain_level".to_string(),
876 name: "Level".to_string(),
877 param_type: ParameterType::Float,
878 value: 1.6,
879 default: 1.6,
880 unit: Some("x".to_string()),
881 min: 0.0,
882 max: 2.0,
883 group: Some("InputGain".to_string()),
884 variants: None,
885 },
886 ParameterInfo {
887 id: "inputgain_level".to_string(),
888 name: "Level".to_string(),
889 param_type: ParameterType::Float,
890 value: 0.4,
891 default: 0.4,
892 unit: Some("x".to_string()),
893 min: 0.0,
894 max: 2.0,
895 group: Some("InputGain".to_string()),
896 variants: None,
897 },
898 ParameterInfo {
899 id: "output_gain_level".to_string(),
900 name: "Level".to_string(),
901 param_type: ParameterType::Float,
902 value: 1.0,
903 default: 1.0,
904 unit: Some("x".to_string()),
905 min: 0.0,
906 max: 2.0,
907 group: Some("OutputGain".to_string()),
908 variants: None,
909 },
910 ]);
911
912 let mut left = [0.5_f32; 8];
913 let mut right = [0.5_f32; 8];
914 let mut phase = 0.0;
915
916 apply_output_modifiers(&mut left, &mut right, &bridge, &mut phase, 48_000.0);
917
918 let expected = 0.5 * 1.6;
920 assert!(left.iter().all(|sample| (*sample - expected).abs() < 1e-6));
921 assert!(right.iter().all(|sample| (*sample - expected).abs() < 1e-6));
922 }
923}