Skip to main content

audio_engine_core/processor/
adapters.rs

1//! Processor Adapters
2//!
3//! Wraps existing processors with the AudioProcessor trait, enabling
4//! lock-free parameter passing and unified DSP chain management.
5//!
6//! Each adapter:
7//! - Owns the actual processor (audio thread exclusive)
8//! - References lock-free parameters (shared with main thread)
9//! - Synchronizes parameters before processing
10
11use std::sync::atomic::{AtomicBool, Ordering};
12use std::sync::Arc;
13
14use arc_swap::ArcSwapOption;
15
16use super::convolver::FFTConvolver;
17use super::crossfeed::Crossfeed;
18use super::dsp::NoiseShaper;
19use super::dynamic_loudness::DynamicLoudness;
20use super::eq::Equalizer;
21use super::lockfree_params::*;
22use super::loudness::PeakLimiter;
23use super::saturation::Saturation;
24use super::traits::{AudioProcessor, ProcessResult};
25// ============================================================================
26// EQ Adapter
27// ============================================================================
28
29/// Equalizer processor adapter with lock-free parameters
30pub struct EqProcessor {
31    /// Internal EQ processor (audio thread exclusive)
32    eq: Equalizer,
33    /// Channel count for reinitialization
34    channels: usize,
35    /// Lock-free parameters reference
36    params: Arc<AtomicEqParams>,
37    cached_params: Arc<EqParamsSnapshot>,
38    cached_generation: u64,
39    /// Local parameter cache
40    cached: EqParamsSnapshot,
41    /// Sample rate for coefficient recalculation
42    sample_rate: f64,
43}
44
45impl EqProcessor {
46    /// Create new EQ processor with lock-free params
47    pub fn new(channels: usize, sample_rate: f64, params: Arc<AtomicEqParams>) -> Self {
48        let (cached_params, cached_generation) = params.load_with_generation();
49        let cached = *cached_params;
50        let mut eq = Equalizer::new(channels, sample_rate);
51        eq.set_all_bands(&cached.gains, sample_rate);
52        eq.set_enabled(cached.enabled);
53        Self {
54            eq,
55            channels,
56            params,
57            cached_params,
58            cached_generation,
59            cached,
60            sample_rate,
61        }
62    }
63
64    /// Synchronize parameters from lock-free storage
65    fn sync_params(&mut self) {
66        if let Some((current, generation)) =
67            self.params.load_if_changed_since(self.cached_generation)
68        {
69            self.cached = *current;
70            self.cached_params = current;
71            self.cached_generation = generation;
72
73            // Apply to internal EQ
74            self.eq.set_all_bands(&self.cached.gains, self.sample_rate);
75            self.eq.set_enabled(self.cached.enabled);
76        }
77    }
78}
79
80impl AudioProcessor for EqProcessor {
81    fn name(&self) -> &'static str {
82        "Equalizer"
83    }
84
85    fn process(&mut self, buffer: &mut [f64], _channels: usize) -> ProcessResult {
86        self.sync_params();
87
88        if !self.cached.enabled {
89            return ProcessResult::Bypassed;
90        }
91
92        self.eq.process(buffer);
93        ProcessResult::Ok
94    }
95
96    fn reset(&mut self) {
97        self.eq.reset();
98    }
99
100    fn is_enabled(&self) -> bool {
101        self.cached.enabled
102    }
103
104    fn set_enabled(&mut self, enabled: bool) {
105        self.params.set_enabled(enabled);
106    }
107
108    fn set_sample_rate(&mut self, sample_rate: f64) {
109        self.sample_rate = sample_rate;
110        self.eq = Equalizer::new(self.channels, sample_rate);
111        self.eq.set_all_bands(&self.cached.gains, sample_rate);
112        self.eq.set_enabled(self.cached.enabled);
113    }
114}
115
116// ============================================================================
117// Saturation Adapter
118// ============================================================================
119
120/// Saturation processor adapter
121pub struct SaturationProcessor {
122    saturation: Saturation,
123    params: Arc<AtomicSaturationParams>,
124    cached_params: Arc<SaturationParamsSnapshot>,
125    cached_generation: u64,
126    cached: SaturationParamsSnapshot,
127    sample_rate: f64,
128}
129
130impl SaturationProcessor {
131    pub fn new(channels: usize, params: Arc<AtomicSaturationParams>) -> Self {
132        let (cached_params, cached_generation) = params.load_with_generation();
133        let cached = *cached_params;
134        let mut saturation = Saturation::new();
135        // Pre-size per-channel HPF state off the audio thread so highpass-mode
136        // processing never resizes on the realtime thread.
137        saturation.set_channel_count(channels);
138        saturation.set_drive(cached.drive);
139        saturation.set_threshold(cached.threshold);
140        saturation.set_mix(cached.mix);
141        saturation.set_input_gain(cached.input_gain_db);
142        saturation.set_output_gain(cached.output_gain_db);
143        saturation.set_highpass_mode(cached.highpass_mode);
144        saturation.set_highpass_cutoff(cached.highpass_cutoff);
145        saturation.set_enabled(cached.enabled);
146        saturation.set_type(super::saturation::SaturationType::from(cached.sat_type));
147        Self {
148            saturation,
149            params,
150            cached_params,
151            cached_generation,
152            cached,
153            sample_rate: 44100.0,
154        }
155    }
156
157    fn sync_params(&mut self) {
158        if let Some((current, generation)) =
159            self.params.load_if_changed_since(self.cached_generation)
160        {
161            self.cached = *current;
162            self.cached_params = current;
163            self.cached_generation = generation;
164
165            // Apply to saturation processor
166            self.saturation.set_drive(self.cached.drive);
167            self.saturation.set_threshold(self.cached.threshold);
168            self.saturation.set_mix(self.cached.mix);
169            self.saturation.set_input_gain(self.cached.input_gain_db);
170            self.saturation.set_output_gain(self.cached.output_gain_db);
171            self.saturation.set_highpass_mode(self.cached.highpass_mode);
172            self.saturation
173                .set_highpass_cutoff(self.cached.highpass_cutoff);
174            self.saturation.set_enabled(self.cached.enabled);
175
176            // M-4 fix: use From trait for type-safe conversion
177            self.saturation
178                .set_type(super::saturation::SaturationType::from(
179                    self.cached.sat_type,
180                ));
181        }
182    }
183}
184
185impl AudioProcessor for SaturationProcessor {
186    fn name(&self) -> &'static str {
187        "Saturation"
188    }
189
190    fn process(&mut self, buffer: &mut [f64], channels: usize) -> ProcessResult {
191        self.sync_params();
192
193        if !self.cached.enabled {
194            return ProcessResult::Bypassed;
195        }
196
197        self.saturation.process_with_channels(buffer, channels);
198        ProcessResult::Ok
199    }
200
201    fn reset(&mut self) {
202        self.saturation.reset();
203    }
204
205    fn is_enabled(&self) -> bool {
206        self.cached.enabled
207    }
208
209    fn set_enabled(&mut self, enabled: bool) {
210        self.params.set_enabled(enabled);
211    }
212
213    fn set_sample_rate(&mut self, sample_rate: f64) {
214        self.sample_rate = sample_rate;
215        self.saturation.set_sample_rate(sample_rate);
216    }
217}
218
219// ============================================================================
220// Crossfeed Adapter
221// ============================================================================
222
223/// Crossfeed processor adapter
224pub struct CrossfeedProcessor {
225    crossfeed: Crossfeed,
226    params: Arc<AtomicCrossfeedParams>,
227    cached_params: Arc<CrossfeedParamsSnapshot>,
228    cached_generation: u64,
229    cached: CrossfeedParamsSnapshot,
230    sample_rate: f64,
231}
232
233impl CrossfeedProcessor {
234    pub fn new(sample_rate: f64, params: Arc<AtomicCrossfeedParams>) -> Self {
235        let (cached_params, cached_generation) = params.load_with_generation();
236        let cached = *cached_params;
237        let mut crossfeed = Crossfeed::new(sample_rate);
238        crossfeed.set_mix(cached.mix);
239        crossfeed.set_enabled(cached.enabled);
240        crossfeed.set_sample_rate(sample_rate, cached.cutoff_hz);
241        Self {
242            crossfeed,
243            params,
244            cached_params,
245            cached_generation,
246            cached,
247            sample_rate,
248        }
249    }
250
251    fn sync_params(&mut self) {
252        if let Some((current, generation)) =
253            self.params.load_if_changed_since(self.cached_generation)
254        {
255            self.cached = *current;
256            self.cached_params = current;
257            self.cached_generation = generation;
258            self.crossfeed.set_mix(self.cached.mix);
259            self.crossfeed.set_enabled(self.cached.enabled);
260            // Cutoff change requires sample rate update
261            self.crossfeed
262                .set_sample_rate(self.sample_rate, self.cached.cutoff_hz);
263        }
264    }
265}
266
267impl AudioProcessor for CrossfeedProcessor {
268    fn name(&self) -> &'static str {
269        "Crossfeed"
270    }
271
272    fn process(&mut self, buffer: &mut [f64], channels: usize) -> ProcessResult {
273        self.sync_params();
274
275        if !self.cached.enabled {
276            return ProcessResult::Bypassed;
277        }
278
279        self.crossfeed.process(buffer, channels);
280        ProcessResult::Ok
281    }
282
283    fn reset(&mut self) {
284        self.crossfeed.reset();
285    }
286
287    fn is_enabled(&self) -> bool {
288        self.cached.enabled
289    }
290
291    fn set_enabled(&mut self, enabled: bool) {
292        self.params.set_enabled(enabled);
293    }
294
295    fn set_sample_rate(&mut self, sample_rate: f64) {
296        self.sample_rate = sample_rate;
297        self.crossfeed
298            .set_sample_rate(sample_rate, self.cached.cutoff_hz);
299    }
300}
301
302// ============================================================================
303// Peak Limiter Adapter
304// ============================================================================
305
306/// Peak limiter processor adapter
307pub struct PeakLimiterProcessor {
308    limiter: PeakLimiter,
309    params: Arc<AtomicPeakLimiterParams>,
310    cached_params: Arc<PeakLimiterParamsSnapshot>,
311    cached_generation: u64,
312    cached: PeakLimiterParamsSnapshot,
313    sample_rate: u32,
314    channels: usize,
315}
316
317impl PeakLimiterProcessor {
318    pub fn new(channels: usize, sample_rate: u32, params: Arc<AtomicPeakLimiterParams>) -> Self {
319        let (cached_params, cached_generation) = params.load_with_generation();
320        let cached = *cached_params;
321        Self {
322            limiter: PeakLimiter::new(
323                channels,
324                sample_rate,
325                cached.threshold_db,
326                10.0,
327                cached.release_ms,
328            ),
329            params,
330            cached_params,
331            cached_generation,
332            cached,
333            sample_rate,
334            channels,
335        }
336    }
337
338    fn sync_params(&mut self) {
339        if let Some((current, generation)) =
340            self.params.load_if_changed_since(self.cached_generation)
341        {
342            self.cached = *current;
343            self.cached_params = current;
344            self.cached_generation = generation;
345
346            // In-place update — NO PeakLimiter::new(), NO heap allocation
347            self.limiter.set_threshold(self.cached.threshold_db);
348            self.limiter.set_release_ms(self.cached.release_ms);
349            // If enabled state changed, limiter reset may be needed
350            if self.cached.enabled != self.limiter.is_enabled() {
351                self.limiter.reset();
352            }
353        }
354    }
355}
356
357impl AudioProcessor for PeakLimiterProcessor {
358    fn name(&self) -> &'static str {
359        "PeakLimiter"
360    }
361
362    fn process(&mut self, buffer: &mut [f64], _channels: usize) -> ProcessResult {
363        self.sync_params();
364
365        if !self.cached.enabled {
366            return ProcessResult::Bypassed;
367        }
368
369        self.limiter.process(buffer);
370        ProcessResult::Ok
371    }
372
373    fn reset(&mut self) {
374        self.limiter.reset();
375    }
376
377    fn is_enabled(&self) -> bool {
378        self.cached.enabled
379    }
380
381    fn set_enabled(&mut self, enabled: bool) {
382        self.params.set_enabled(enabled);
383    }
384
385    fn set_sample_rate(&mut self, sample_rate: f64) {
386        self.sample_rate = sample_rate as u32;
387        self.limiter = PeakLimiter::new(
388            self.channels,
389            self.sample_rate,
390            self.cached.threshold_db,
391            10.0,
392            self.cached.release_ms,
393        );
394    }
395}
396
397// ============================================================================
398// Volume Adapter (P1-3 fix: anti-zipper smoothing)
399// ============================================================================
400
401/// Volume processor with exponential smoothing to prevent zipper noise.
402///
403/// Uses ~5ms smoothing time constant to ensure click-free volume transitions.
404/// Previous implementation directly multiplied buffer by target volume,
405/// causing audible clicks/zips on rapid volume changes.
406pub struct VolumeProcessor {
407    params: Arc<AtomicVolumeParams>,
408    cached_params: Arc<VolumeParamsSnapshot>,
409    cached_generation: u64,
410    cached: VolumeParamsSnapshot,
411    /// Current smoothed volume (exponentially approaches target)
412    current_volume: f64,
413    /// Smoothing coefficient per sample (calculated from sample rate)
414    smoothing_coeff: f64,
415    /// Cached `1.0 - smoothing_coeff`
416    one_minus_smoothing_coeff: f64,
417    /// Sample rate for smoothing calculation
418    sample_rate: f64,
419}
420
421impl VolumeProcessor {
422    const SETTLE_EPSILON: f64 = 1.0e-6;
423
424    pub fn new(params: Arc<AtomicVolumeParams>) -> Self {
425        let smoothing_coeff = Self::calc_smoothing_coeff(44100.0);
426        let one_minus_smoothing_coeff = 1.0 - smoothing_coeff;
427        let (cached_params, cached_generation) = params.load_with_generation();
428        let cached = *cached_params;
429        Self {
430            params,
431            cached_params,
432            cached_generation,
433            cached,
434            current_volume: 1.0,
435            smoothing_coeff,
436            one_minus_smoothing_coeff,
437            sample_rate: 44100.0,
438        }
439    }
440
441    /// Calculate smoothing coefficient for ~5ms time constant
442    fn calc_smoothing_coeff(sample_rate: f64) -> f64 {
443        let smoothing_time_ms = 5.0;
444        let smoothing_samples = (smoothing_time_ms / 1000.0) * sample_rate;
445        (-1.0 / smoothing_samples).exp()
446    }
447
448    fn sync_params(&mut self) {
449        if let Some((current, generation)) =
450            self.params.load_if_changed_since(self.cached_generation)
451        {
452            self.cached = *current;
453            self.cached_params = current;
454            self.cached_generation = generation;
455        }
456    }
457}
458
459impl AudioProcessor for VolumeProcessor {
460    fn name(&self) -> &'static str {
461        "Volume"
462    }
463
464    fn process(&mut self, buffer: &mut [f64], channels: usize) -> ProcessResult {
465        self.sync_params();
466
467        // Volume is always "enabled" - just applies gain
468        // Check for mute
469        if self.cached.muted {
470            // Smooth fade to zero to avoid click
471            let coeff = self.smoothing_coeff;
472            let mut current_volume = self.current_volume;
473            for sample in buffer.iter_mut() {
474                current_volume *= coeff;
475                *sample *= current_volume;
476            }
477            self.current_volume = current_volume;
478            return ProcessResult::Ok;
479        }
480
481        // Apply volume with anti-zipper smoothing
482        let target = self.cached.volume;
483        if self.current_volume == target {
484            if target != 1.0 {
485                for sample in buffer.iter_mut() {
486                    *sample *= target;
487                }
488            }
489            return ProcessResult::Ok;
490        }
491
492        let one_minus_coeff = self.one_minus_smoothing_coeff;
493        let mut current_volume = self.current_volume;
494        let frames = buffer.len() / channels;
495        let mut frame = 0;
496
497        while frame < frames {
498            if (target - current_volume).abs() <= Self::SETTLE_EPSILON {
499                current_volume = target;
500                break;
501            }
502
503            // Exponential smoothing: current = current + (target - current) * (1 - coeff)
504            current_volume += (target - current_volume) * one_minus_coeff;
505            for ch in 0..channels {
506                buffer[frame * channels + ch] *= current_volume;
507            }
508            frame += 1;
509        }
510
511        if frame < frames && target != 1.0 {
512            for sample in &mut buffer[(frame * channels)..] {
513                *sample *= target;
514            }
515        }
516        self.current_volume = current_volume;
517
518        ProcessResult::Ok
519    }
520
521    fn reset(&mut self) {
522        self.current_volume = self.cached.volume;
523    }
524
525    fn is_enabled(&self) -> bool {
526        true // Volume is always active
527    }
528
529    fn set_enabled(&mut self, _enabled: bool) {
530        // Use set_muted instead
531    }
532
533    fn set_sample_rate(&mut self, sample_rate: f64) {
534        if (self.sample_rate - sample_rate).abs() > 1.0 {
535            self.sample_rate = sample_rate;
536            self.smoothing_coeff = Self::calc_smoothing_coeff(sample_rate);
537            self.one_minus_smoothing_coeff = 1.0 - self.smoothing_coeff;
538        }
539    }
540}
541
542// ============================================================================
543// Noise Shaper Adapter
544// ============================================================================
545
546/// Noise shaper processor adapter
547pub struct NoiseShaperProcessor {
548    noise_shaper: NoiseShaper,
549    params: Arc<AtomicNoiseShaperParams>,
550    cached_params: Arc<NoiseShaperParamsSnapshot>,
551    cached_generation: u64,
552    cached: NoiseShaperParamsSnapshot,
553    sample_rate: u32,
554    channels: usize,
555}
556
557impl NoiseShaperProcessor {
558    pub fn new(channels: usize, sample_rate: u32, params: Arc<AtomicNoiseShaperParams>) -> Self {
559        let (cached_params, cached_generation) = params.load_with_generation();
560        let cached = *cached_params;
561        let mut noise_shaper = NoiseShaper::new(channels, sample_rate, cached.bits);
562        noise_shaper.set_enabled(cached.enabled);
563        noise_shaper.set_curve(cached.curve);
564
565        Self {
566            noise_shaper,
567            params,
568            cached_params,
569            cached_generation,
570            cached,
571            sample_rate,
572            channels,
573        }
574    }
575
576    pub fn refresh_is_enabled(&mut self) -> bool {
577        self.sync_params();
578        self.cached.enabled
579    }
580
581    pub fn process_cached(&mut self, buffer: &mut [f64], _channels: usize) -> ProcessResult {
582        if !self.cached.enabled {
583            return ProcessResult::Bypassed;
584        }
585
586        self.noise_shaper.process(buffer, self.channels);
587        ProcessResult::Ok
588    }
589
590    fn sync_params(&mut self) {
591        if let Some((current, generation)) =
592            self.params.load_if_changed_since(self.cached_generation)
593        {
594            self.cached = *current;
595            self.cached_params = current;
596            self.cached_generation = generation;
597            self.noise_shaper.set_enabled(self.cached.enabled);
598            self.noise_shaper.set_bits(self.cached.bits);
599            self.noise_shaper.set_curve(self.cached.curve);
600        }
601    }
602}
603
604impl AudioProcessor for NoiseShaperProcessor {
605    fn name(&self) -> &'static str {
606        "NoiseShaper"
607    }
608
609    fn process(&mut self, buffer: &mut [f64], _channels: usize) -> ProcessResult {
610        self.sync_params();
611
612        if !self.cached.enabled {
613            return ProcessResult::Bypassed;
614        }
615
616        self.noise_shaper.process(buffer, self.channels);
617        ProcessResult::Ok
618    }
619
620    fn reset(&mut self) {
621        self.noise_shaper.reset();
622    }
623
624    fn is_enabled(&self) -> bool {
625        self.cached.enabled
626    }
627
628    fn set_enabled(&mut self, enabled: bool) {
629        self.params.set_enabled(enabled);
630    }
631
632    fn set_sample_rate(&mut self, sample_rate: f64) {
633        self.sample_rate = sample_rate as u32;
634        self.noise_shaper = NoiseShaper::new(self.channels, self.sample_rate, self.cached.bits);
635        self.noise_shaper.set_enabled(self.cached.enabled);
636        self.noise_shaper.set_curve(self.cached.curve);
637    }
638}
639
640// ============================================================================
641// Dynamic Loudness Adapter
642// ============================================================================
643
644/// Dynamic loudness compensation processor
645pub struct DynamicLoudnessProcessor {
646    dynamic_loudness: DynamicLoudness,
647    params: Arc<AtomicDynamicLoudnessParams>,
648    telemetry: Arc<AtomicDynamicLoudnessTelemetry>,
649    cached_params: Arc<DynamicLoudnessParamsSnapshot>,
650    cached_generation: u64,
651    cached: DynamicLoudnessParamsSnapshot,
652    sample_rate: u32,
653    channels: usize,
654}
655
656impl DynamicLoudnessProcessor {
657    pub fn new(
658        channels: usize,
659        sample_rate: u32,
660        params: Arc<AtomicDynamicLoudnessParams>,
661        telemetry: Arc<AtomicDynamicLoudnessTelemetry>,
662    ) -> Self {
663        let (cached_params, cached_generation) = params.load_with_generation();
664        let cached = *cached_params;
665        let mut dynamic_loudness = DynamicLoudness::new(channels, sample_rate as f64);
666        dynamic_loudness.set_volume(cached.volume);
667        dynamic_loudness.set_strength(cached.strength);
668        Self {
669            dynamic_loudness,
670            params,
671            telemetry,
672            cached_params,
673            cached_generation,
674            cached,
675            sample_rate,
676            channels,
677        }
678    }
679
680    fn sync_params(&mut self) {
681        if let Some((current, generation)) =
682            self.params.load_if_changed_since(self.cached_generation)
683        {
684            self.cached = *current;
685            self.cached_params = current;
686            self.cached_generation = generation;
687            self.dynamic_loudness.set_volume(self.cached.volume);
688            self.dynamic_loudness.set_strength(self.cached.strength);
689        }
690    }
691}
692
693impl AudioProcessor for DynamicLoudnessProcessor {
694    fn name(&self) -> &'static str {
695        "DynamicLoudness"
696    }
697
698    fn process(&mut self, buffer: &mut [f64], _channels: usize) -> ProcessResult {
699        self.sync_params();
700
701        if !self.cached.enabled {
702            self.telemetry.update(0.0, [0.0; 7]);
703            return ProcessResult::Bypassed;
704        }
705
706        self.dynamic_loudness.process(buffer);
707        self.telemetry.update(
708            self.dynamic_loudness.loudness_factor(),
709            self.dynamic_loudness.get_band_gains(),
710        );
711        ProcessResult::Ok
712    }
713
714    fn reset(&mut self) {
715        self.dynamic_loudness.reset();
716    }
717
718    fn is_enabled(&self) -> bool {
719        self.cached.enabled
720    }
721
722    fn set_enabled(&mut self, enabled: bool) {
723        self.params.set_enabled(enabled);
724    }
725
726    fn set_sample_rate(&mut self, sample_rate: f64) {
727        self.sample_rate = sample_rate as u32;
728        self.dynamic_loudness = DynamicLoudness::new(self.channels, self.sample_rate as f64);
729    }
730}
731
732// ============================================================================
733// Convolver Adapter
734// ============================================================================
735
736/// FFT convolver processor with wait-free kernel swap-in.
737pub struct ConvolverProcessor {
738    owned: Option<FFTConvolver>,
739    swap: Arc<ArcSwapOption<FFTConvolver>>,
740    enabled: Arc<AtomicBool>,
741}
742
743impl ConvolverProcessor {
744    pub fn new(swap: Arc<ArcSwapOption<FFTConvolver>>, enabled: Arc<AtomicBool>) -> Self {
745        Self {
746            owned: None,
747            swap,
748            enabled,
749        }
750    }
751
752    fn sync_convolver(&mut self) {
753        if !self.enabled.load(Ordering::Acquire) {
754            self.owned = None;
755            let _ = self.swap.swap(None);
756            return;
757        }
758
759        let new_conv = self.swap.swap(None);
760        if let Some(arc_conv) = new_conv {
761            match Arc::try_unwrap(arc_conv) {
762                Ok(conv) => self.owned = Some(conv),
763                Err(arc) => self.owned = Some((*arc).clone()),
764            }
765        }
766    }
767}
768
769impl AudioProcessor for ConvolverProcessor {
770    fn name(&self) -> &'static str {
771        "Convolver"
772    }
773
774    fn process(&mut self, buffer: &mut [f64], _channels: usize) -> ProcessResult {
775        self.sync_convolver();
776
777        if let Some(ref mut convolver) = self.owned {
778            convolver.process_inplace(buffer);
779            ProcessResult::Ok
780        } else {
781            ProcessResult::Bypassed
782        }
783    }
784
785    fn reset(&mut self) {
786        if let Some(ref mut convolver) = self.owned {
787            convolver.reset();
788        }
789    }
790
791    fn is_enabled(&self) -> bool {
792        self.enabled.load(Ordering::Acquire)
793    }
794
795    fn set_enabled(&mut self, enabled: bool) {
796        if !enabled {
797            self.owned = None;
798        }
799        self.enabled.store(enabled, Ordering::Release);
800    }
801}
802
803#[cfg(test)]
804mod tests {
805    use super::*;
806
807    #[test]
808    fn test_convolver_processor_swaps_in_and_processes() {
809        let swap = Arc::new(ArcSwapOption::empty());
810        let enabled = Arc::new(AtomicBool::new(false));
811        let mut proc = ConvolverProcessor::new(Arc::clone(&swap), Arc::clone(&enabled));
812        let mut buffer = vec![1.0, 2.0, 3.0, 4.0];
813
814        assert_eq!(proc.process(&mut buffer, 1), ProcessResult::Bypassed);
815
816        swap.store(Some(Arc::new(FFTConvolver::new(&[0.5], 1))));
817        enabled.store(true, Ordering::Release);
818        assert_eq!(proc.process(&mut buffer, 1), ProcessResult::Ok);
819        assert_eq!(buffer, vec![0.5, 1.0, 1.5, 2.0]);
820    }
821
822    #[test]
823    fn test_convolver_processor_clear_disables_owned_convolver() {
824        let swap = Arc::new(ArcSwapOption::empty());
825        let enabled = Arc::new(AtomicBool::new(true));
826        let mut proc = ConvolverProcessor::new(Arc::clone(&swap), Arc::clone(&enabled));
827        let mut buffer = vec![1.0, 2.0, 3.0, 4.0];
828
829        swap.store(Some(Arc::new(FFTConvolver::new(&[0.5], 1))));
830        assert_eq!(proc.process(&mut buffer, 1), ProcessResult::Ok);
831
832        enabled.store(false, Ordering::Release);
833        let mut bypassed = vec![1.0, 2.0, 3.0, 4.0];
834        assert_eq!(proc.process(&mut bypassed, 1), ProcessResult::Bypassed);
835        assert_eq!(bypassed, vec![1.0, 2.0, 3.0, 4.0]);
836    }
837
838    #[test]
839    fn test_eq_processor() {
840        let params = Arc::new(AtomicEqParams::new());
841        let mut proc = EqProcessor::new(2, 44100.0, Arc::clone(&params));
842
843        // Set params from "main thread"
844        let gains = [2.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
845        params.write(&gains, true);
846
847        // Process from "audio thread"
848        let mut buffer = vec![0.5; 4096];
849        let result = proc.process(&mut buffer, 2);
850
851        assert_eq!(result, ProcessResult::Ok);
852        // EQ gain smoothing may not boost the very first sample, but the block should change.
853        assert!(buffer.iter().any(|&sample| (sample - 0.5).abs() > 1e-6));
854    }
855
856    #[test]
857    fn test_volume_processor_muted() {
858        let params = Arc::new(AtomicVolumeParams::new());
859        let mut proc = VolumeProcessor::new(Arc::clone(&params));
860
861        params.set_volume(0.5);
862        params.set_muted(true);
863
864        let mut buffer = vec![1.0; 4096];
865        proc.process(&mut buffer, 2);
866
867        // Muting uses a click-free exponential fade rather than an instant hard cut.
868        assert!(buffer[0] < 1.0);
869        assert!(buffer[buffer.len() - 1] < 0.001);
870    }
871
872    #[test]
873    fn test_volume_processor_writes_back_smoothed_volume() {
874        let params = Arc::new(AtomicVolumeParams::new());
875        let mut proc = VolumeProcessor::new(Arc::clone(&params));
876
877        params.set_volume(0.25);
878        let mut buffer = vec![1.0; 128];
879        proc.process(&mut buffer, 2);
880
881        let first_pass_volume = proc.current_volume;
882        assert!(first_pass_volume < 1.0);
883        assert!(first_pass_volume > 0.25);
884
885        proc.process(&mut buffer, 2);
886
887        assert!(proc.current_volume < first_pass_volume);
888        assert!(proc.current_volume > 0.25);
889    }
890
891    #[test]
892    fn test_volume_processor_steady_state_fast_path_preserves_unity() {
893        let params = Arc::new(AtomicVolumeParams::new());
894        let mut proc = VolumeProcessor::new(Arc::clone(&params));
895        proc.reset();
896
897        let mut buffer = vec![0.25, -0.5, 0.75, -1.0];
898        let original = buffer.clone();
899
900        assert_eq!(proc.process(&mut buffer, 2), ProcessResult::Ok);
901        assert_eq!(buffer, original);
902        assert_eq!(proc.current_volume, 1.0);
903    }
904
905    #[test]
906    fn test_volume_processor_steady_state_fast_path_applies_target() {
907        let params = Arc::new(AtomicVolumeParams::new());
908        params.set_volume(0.5);
909        let mut proc = VolumeProcessor::new(Arc::clone(&params));
910        proc.sync_params();
911        proc.reset();
912
913        let mut buffer = vec![0.25, -0.5, 0.75, -1.0];
914
915        assert_eq!(proc.process(&mut buffer, 2), ProcessResult::Ok);
916        assert_eq!(buffer, vec![0.125, -0.25, 0.375, -0.5]);
917        assert_eq!(proc.current_volume, 0.5);
918    }
919
920    #[test]
921    fn volume_lazy_settle_dc_null_residual_stays_below_snap_floor() {
922        let input = vec![0.8; 32_768 * 2];
923
924        assert_lazy_settle_residual_bounds("dc", &input, 2);
925    }
926
927    #[test]
928    fn volume_lazy_settle_sweep_null_residual_stays_below_snap_floor() {
929        let input = sweep_signal(32_768, 2);
930
931        assert_lazy_settle_residual_bounds("sweep", &input, 2);
932    }
933
934    #[test]
935    fn volume_lazy_settle_abrupt_step_null_residual_stays_below_snap_floor() {
936        let input = abrupt_step_signal(32_768, 2);
937
938        assert_lazy_settle_residual_bounds("abrupt_step", &input, 2);
939    }
940
941    #[test]
942    fn test_saturation_processor() {
943        let params = Arc::new(AtomicSaturationParams::new());
944        let mut proc = SaturationProcessor::new(2, Arc::clone(&params));
945
946        params.set_drive(1.0);
947        params.set_mix(1.0);
948        params.set_enabled(true);
949
950        let mut buffer = vec![0.9, 0.9];
951        proc.process(&mut buffer, 2);
952
953        // tanh(0.9 * 2) ≈ 0.96, less than input
954        assert!(buffer[0].abs() < 0.9 * 2.0);
955    }
956
957    fn assert_lazy_settle_residual_bounds(name: &str, input: &[f64], channels: usize) {
958        const RESIDUAL_DELTA_LIMIT: f64 = 2.0e-6;
959        const RESIDUAL_RMS_LIMIT: f64 = 2.0e-7;
960
961        let mut exact = input.to_vec();
962        let mut lazy = input.to_vec();
963        process_volume_exact_kernel(&mut exact, channels, 48_000.0, 0.25);
964        process_volume_lazy_settle_kernel(
965            &mut lazy,
966            channels,
967            48_000.0,
968            0.25,
969            VolumeProcessor::SETTLE_EPSILON,
970        );
971
972        let mut max_abs = 0.0_f64;
973        let mut sum_sq = 0.0_f64;
974        let mut max_delta = 0.0_f64;
975        let mut prev_residual = 0.0_f64;
976
977        for (idx, (left, right)) in lazy.iter().zip(&exact).enumerate() {
978            let residual = left - right;
979            max_abs = max_abs.max(residual.abs());
980            sum_sq += residual * residual;
981            if idx > 0 {
982                max_delta = max_delta.max((residual - prev_residual).abs());
983            }
984            prev_residual = residual;
985        }
986
987        let rms = (sum_sq / input.len() as f64).sqrt();
988        assert!(
989            max_abs <= VolumeProcessor::SETTLE_EPSILON,
990            "{name} lazy-settle max residual {max_abs:.3e} exceeds {:.3e}",
991            VolumeProcessor::SETTLE_EPSILON
992        );
993        assert!(
994            max_delta <= RESIDUAL_DELTA_LIMIT,
995            "{name} lazy-settle residual delta {max_delta:.3e} exceeds {RESIDUAL_DELTA_LIMIT:.3e}"
996        );
997        assert!(
998            rms <= RESIDUAL_RMS_LIMIT,
999            "{name} lazy-settle residual rms {rms:.3e} exceeds {RESIDUAL_RMS_LIMIT:.3e}"
1000        );
1001    }
1002
1003    fn process_volume_exact_kernel(
1004        buffer: &mut [f64],
1005        channels: usize,
1006        sample_rate: f64,
1007        target: f64,
1008    ) -> f64 {
1009        let smoothing_coeff = VolumeProcessor::calc_smoothing_coeff(sample_rate);
1010        let one_minus_coeff = 1.0 - smoothing_coeff;
1011        let mut current_volume = 1.0;
1012        let frames = buffer.len() / channels;
1013
1014        for frame in 0..frames {
1015            current_volume += (target - current_volume) * one_minus_coeff;
1016            for ch in 0..channels {
1017                buffer[frame * channels + ch] *= current_volume;
1018            }
1019        }
1020
1021        current_volume
1022    }
1023
1024    fn process_volume_lazy_settle_kernel(
1025        buffer: &mut [f64],
1026        channels: usize,
1027        sample_rate: f64,
1028        target: f64,
1029        settle_epsilon: f64,
1030    ) -> f64 {
1031        let smoothing_coeff = VolumeProcessor::calc_smoothing_coeff(sample_rate);
1032        let one_minus_coeff = 1.0 - smoothing_coeff;
1033        let mut current_volume = 1.0;
1034        let frames = buffer.len() / channels;
1035        let mut frame = 0;
1036
1037        while frame < frames {
1038            if (target - current_volume).abs() <= settle_epsilon {
1039                current_volume = target;
1040                break;
1041            }
1042
1043            current_volume += (target - current_volume) * one_minus_coeff;
1044            for ch in 0..channels {
1045                buffer[frame * channels + ch] *= current_volume;
1046            }
1047            frame += 1;
1048        }
1049
1050        if frame < frames && target != 1.0 {
1051            for sample in &mut buffer[(frame * channels)..] {
1052                *sample *= target;
1053            }
1054        }
1055
1056        current_volume
1057    }
1058
1059    fn sweep_signal(frames: usize, channels: usize) -> Vec<f64> {
1060        let mut out = Vec::with_capacity(frames * channels);
1061        let sample_rate = 48_000.0;
1062        let start_hz = 20.0_f64;
1063        let end_hz = 20_000.0_f64;
1064        let mut phase = 0.0_f64;
1065
1066        for frame in 0..frames {
1067            let progress = frame as f64 / frames.saturating_sub(1).max(1) as f64;
1068            let hz = start_hz * (end_hz / start_hz).powf(progress);
1069            phase += std::f64::consts::TAU * hz / sample_rate;
1070            let sample = phase.sin() * 0.9;
1071            for ch in 0..channels {
1072                out.push(sample * (1.0 - ch as f64 * 0.05));
1073            }
1074        }
1075
1076        out
1077    }
1078
1079    fn abrupt_step_signal(frames: usize, channels: usize) -> Vec<f64> {
1080        let mut out = Vec::with_capacity(frames * channels);
1081
1082        for frame in 0..frames {
1083            let sample = match frame * 4 / frames.max(1) {
1084                0 => 0.0,
1085                1 => 1.0,
1086                2 => -1.0,
1087                _ => {
1088                    if frame % 2 == 0 {
1089                        1.0
1090                    } else {
1091                        -1.0
1092                    }
1093                }
1094            };
1095            for _ in 0..channels {
1096                out.push(sample);
1097            }
1098        }
1099
1100        out
1101    }
1102}