Skip to main content

ringkernel_audio_fft/
mixer.rs

1//! Dry/wet mixer with gain control.
2//!
3//! This module provides mixing functionality to combine the separated
4//! direct and ambient signals with user-controllable parameters.
5
6use crate::messages::{Complex, SeparatedBin};
7
8/// Configuration for the mixer.
9#[derive(Debug, Clone)]
10pub struct MixerConfig {
11    /// Dry/wet mix (0.0 = all direct, 1.0 = all ambience).
12    pub dry_wet: f32,
13    /// Direct signal gain (1.0 = unity).
14    pub direct_gain: f32,
15    /// Ambience signal gain (1.0 = unity).
16    pub ambience_gain: f32,
17    /// Output gain (applied to final mix).
18    pub output_gain: f32,
19    /// Soft clip threshold (None = no limiting).
20    pub soft_clip_threshold: Option<f32>,
21}
22
23impl Default for MixerConfig {
24    fn default() -> Self {
25        Self {
26            dry_wet: 0.5,
27            direct_gain: 1.0,
28            ambience_gain: 1.0,
29            output_gain: 1.0,
30            soft_clip_threshold: Some(0.95),
31        }
32    }
33}
34
35impl MixerConfig {
36    /// Create a new mixer configuration.
37    pub fn new() -> Self {
38        Self::default()
39    }
40
41    /// Set the dry/wet mix.
42    pub fn with_dry_wet(mut self, dry_wet: f32) -> Self {
43        self.dry_wet = dry_wet.clamp(0.0, 1.0);
44        self
45    }
46
47    /// Set the direct gain.
48    pub fn with_direct_gain(mut self, gain: f32) -> Self {
49        self.direct_gain = gain.max(0.0);
50        self
51    }
52
53    /// Set the ambience gain.
54    pub fn with_ambience_gain(mut self, gain: f32) -> Self {
55        self.ambience_gain = gain.max(0.0);
56        self
57    }
58
59    /// Set the output gain.
60    pub fn with_output_gain(mut self, gain: f32) -> Self {
61        self.output_gain = gain.max(0.0);
62        self
63    }
64
65    /// Set soft clip threshold.
66    pub fn with_soft_clip(mut self, threshold: Option<f32>) -> Self {
67        self.soft_clip_threshold = threshold.map(|t| t.clamp(0.1, 1.0));
68        self
69    }
70
71    /// Preset for direct-only output.
72    pub fn direct_only() -> Self {
73        Self {
74            dry_wet: 0.0,
75            direct_gain: 1.0,
76            ambience_gain: 0.0,
77            output_gain: 1.0,
78            soft_clip_threshold: Some(0.95),
79        }
80    }
81
82    /// Preset for ambience-only output.
83    pub fn ambience_only() -> Self {
84        Self {
85            dry_wet: 1.0,
86            direct_gain: 0.0,
87            ambience_gain: 1.0,
88            output_gain: 1.0,
89            soft_clip_threshold: Some(0.95),
90        }
91    }
92
93    /// Preset for balanced mix with boost.
94    pub fn balanced_with_boost(boost_db: f32) -> Self {
95        let linear_gain = 10.0_f32.powf(boost_db / 20.0);
96        Self {
97            dry_wet: 0.5,
98            direct_gain: 1.0,
99            ambience_gain: 1.0,
100            output_gain: linear_gain,
101            soft_clip_threshold: Some(0.95),
102        }
103    }
104}
105
106/// Dry/wet mixer for separated signals.
107pub struct DryWetMixer {
108    config: MixerConfig,
109    /// Peak level tracking for direct signal.
110    direct_peak: f32,
111    /// Peak level tracking for ambience signal.
112    ambience_peak: f32,
113    /// Peak level tracking for output.
114    output_peak: f32,
115}
116
117impl DryWetMixer {
118    /// Create a new mixer with default configuration.
119    pub fn new() -> Self {
120        Self::with_config(MixerConfig::default())
121    }
122
123    /// Create a new mixer with specific configuration.
124    pub fn with_config(config: MixerConfig) -> Self {
125        Self {
126            config,
127            direct_peak: 0.0,
128            ambience_peak: 0.0,
129            output_peak: 0.0,
130        }
131    }
132
133    /// Get the current configuration.
134    pub fn config(&self) -> &MixerConfig {
135        &self.config
136    }
137
138    /// Update the configuration.
139    pub fn set_config(&mut self, config: MixerConfig) {
140        self.config = config;
141    }
142
143    /// Set dry/wet mix (0.0 = all direct, 1.0 = all ambience).
144    pub fn set_dry_wet(&mut self, dry_wet: f32) {
145        self.config.dry_wet = dry_wet.clamp(0.0, 1.0);
146    }
147
148    /// Set direct signal gain.
149    pub fn set_direct_gain(&mut self, gain: f32) {
150        self.config.direct_gain = gain.max(0.0);
151    }
152
153    /// Set ambience signal gain.
154    pub fn set_ambience_gain(&mut self, gain: f32) {
155        self.config.ambience_gain = gain.max(0.0);
156    }
157
158    /// Set output gain.
159    pub fn set_output_gain(&mut self, gain: f32) {
160        self.config.output_gain = gain.max(0.0);
161    }
162
163    /// Set output gain in dB.
164    pub fn set_output_gain_db(&mut self, gain_db: f32) {
165        self.config.output_gain = 10.0_f32.powf(gain_db / 20.0);
166    }
167
168    /// Mix a separated bin and return the combined value.
169    pub fn mix_bin(&mut self, bin: &SeparatedBin) -> Complex {
170        // Apply gains to each component
171        let direct = bin.direct.scale(self.config.direct_gain);
172        let ambience = bin.ambience.scale(self.config.ambience_gain);
173
174        // Track peaks
175        self.direct_peak = self.direct_peak.max(direct.magnitude());
176        self.ambience_peak = self.ambience_peak.max(ambience.magnitude());
177
178        // Mix based on dry/wet
179        let dry_amount = 1.0 - self.config.dry_wet;
180        let wet_amount = self.config.dry_wet;
181
182        let mixed = Complex {
183            re: direct.re * dry_amount + ambience.re * wet_amount,
184            im: direct.im * dry_amount + ambience.im * wet_amount,
185        };
186
187        // Apply output gain
188        let output = mixed.scale(self.config.output_gain);
189
190        // Track output peak
191        self.output_peak = self.output_peak.max(output.magnitude());
192
193        // Apply soft clipping if enabled
194        if let Some(threshold) = self.config.soft_clip_threshold {
195            self.soft_clip(output, threshold)
196        } else {
197            output
198        }
199    }
200
201    /// Mix multiple separated bins.
202    pub fn mix_frame(&mut self, bins: &[SeparatedBin]) -> Vec<Complex> {
203        bins.iter().map(|bin| self.mix_bin(bin)).collect()
204    }
205
206    /// Get only the direct component (with gain).
207    pub fn direct_only(&self, bin: &SeparatedBin) -> Complex {
208        bin.direct
209            .scale(self.config.direct_gain * self.config.output_gain)
210    }
211
212    /// Get only the ambience component (with gain).
213    pub fn ambience_only(&self, bin: &SeparatedBin) -> Complex {
214        bin.ambience
215            .scale(self.config.ambience_gain * self.config.output_gain)
216    }
217
218    /// Extract direct bins from separated data.
219    pub fn extract_direct(&self, bins: &[SeparatedBin]) -> Vec<Complex> {
220        bins.iter().map(|bin| self.direct_only(bin)).collect()
221    }
222
223    /// Extract ambience bins from separated data.
224    pub fn extract_ambience(&self, bins: &[SeparatedBin]) -> Vec<Complex> {
225        bins.iter().map(|bin| self.ambience_only(bin)).collect()
226    }
227
228    /// Apply soft clipping to a complex value.
229    fn soft_clip(&self, value: Complex, threshold: f32) -> Complex {
230        let magnitude = value.magnitude();
231
232        if magnitude <= threshold {
233            return value;
234        }
235
236        // Soft knee compression above threshold
237        let overshoot = magnitude - threshold;
238        let compressed = threshold + overshoot.tanh() * (1.0 - threshold);
239
240        // Scale to new magnitude while preserving phase
241        if magnitude > 1e-10 {
242            value.scale(compressed / magnitude)
243        } else {
244            value
245        }
246    }
247
248    /// Get peak levels (direct, ambience, output).
249    pub fn peak_levels(&self) -> (f32, f32, f32) {
250        (self.direct_peak, self.ambience_peak, self.output_peak)
251    }
252
253    /// Get peak levels in dB.
254    pub fn peak_levels_db(&self) -> (f32, f32, f32) {
255        let to_db = |level: f32| {
256            if level > 1e-10 {
257                20.0 * level.log10()
258            } else {
259                -200.0
260            }
261        };
262
263        (
264            to_db(self.direct_peak),
265            to_db(self.ambience_peak),
266            to_db(self.output_peak),
267        )
268    }
269
270    /// Reset peak tracking.
271    pub fn reset_peaks(&mut self) {
272        self.direct_peak = 0.0;
273        self.ambience_peak = 0.0;
274        self.output_peak = 0.0;
275    }
276}
277
278impl Default for DryWetMixer {
279    fn default() -> Self {
280        Self::new()
281    }
282}
283
284/// Result of mixing a frame.
285#[derive(Debug, Clone)]
286pub struct MixedFrame {
287    /// Mixed frequency bins.
288    pub bins: Vec<Complex>,
289    /// Direct component only.
290    pub direct_bins: Vec<Complex>,
291    /// Ambience component only.
292    pub ambience_bins: Vec<Complex>,
293    /// Frame ID.
294    pub frame_id: u64,
295}
296
297impl MixedFrame {
298    /// Create a new mixed frame.
299    pub fn new(
300        bins: Vec<Complex>,
301        direct: Vec<Complex>,
302        ambience: Vec<Complex>,
303        frame_id: u64,
304    ) -> Self {
305        Self {
306            bins,
307            direct_bins: direct,
308            ambience_bins: ambience,
309            frame_id,
310        }
311    }
312}
313
314/// Full frame mixer that produces all output variants.
315pub struct FrameMixer {
316    mixer: DryWetMixer,
317}
318
319impl FrameMixer {
320    /// Create a new frame mixer.
321    pub fn new(config: MixerConfig) -> Self {
322        Self {
323            mixer: DryWetMixer::with_config(config),
324        }
325    }
326
327    /// Get the underlying mixer.
328    pub fn mixer(&self) -> &DryWetMixer {
329        &self.mixer
330    }
331
332    /// Get mutable reference to the mixer.
333    pub fn mixer_mut(&mut self) -> &mut DryWetMixer {
334        &mut self.mixer
335    }
336
337    /// Process a frame of separated bins.
338    pub fn process(&mut self, bins: &[SeparatedBin]) -> MixedFrame {
339        let frame_id = bins.first().map(|b| b.frame_id).unwrap_or(0);
340
341        let mixed = self.mixer.mix_frame(bins);
342        let direct = self.mixer.extract_direct(bins);
343        let ambience = self.mixer.extract_ambience(bins);
344
345        MixedFrame::new(mixed, direct, ambience, frame_id)
346    }
347
348    /// Update the dry/wet mix.
349    pub fn set_dry_wet(&mut self, dry_wet: f32) {
350        self.mixer.set_dry_wet(dry_wet);
351    }
352
353    /// Update the output gain in dB.
354    pub fn set_gain_db(&mut self, gain_db: f32) {
355        self.mixer.set_output_gain_db(gain_db);
356    }
357}
358
359#[cfg(test)]
360mod tests {
361    use super::*;
362
363    #[test]
364    fn test_mixer_config() {
365        let config = MixerConfig::new()
366            .with_dry_wet(0.3)
367            .with_direct_gain(1.2)
368            .with_ambience_gain(0.8);
369
370        assert!((config.dry_wet - 0.3).abs() < 1e-6);
371        assert!((config.direct_gain - 1.2).abs() < 1e-6);
372    }
373
374    #[test]
375    fn test_dry_wet_mix() {
376        // Disable soft clipping for predictable test results
377        let config = MixerConfig::new().with_soft_clip(None);
378        let mut mixer = DryWetMixer::with_config(config);
379
380        let bin = SeparatedBin::new(
381            0,
382            0,
383            Complex::new(1.0, 0.0), // Direct
384            Complex::new(0.5, 0.0), // Ambience
385            0.6,
386            0.0,
387        );
388
389        // All direct (dry = 1.0)
390        mixer.set_dry_wet(0.0);
391        let result = mixer.mix_bin(&bin);
392        assert!((result.re - 1.0).abs() < 1e-6);
393
394        // All ambience (wet = 1.0)
395        mixer.set_dry_wet(1.0);
396        let result = mixer.mix_bin(&bin);
397        assert!((result.re - 0.5).abs() < 1e-6);
398
399        // 50/50 mix
400        mixer.set_dry_wet(0.5);
401        let result = mixer.mix_bin(&bin);
402        assert!((result.re - 0.75).abs() < 1e-6); // 0.5 * 1.0 + 0.5 * 0.5
403    }
404
405    #[test]
406    fn test_gain_application() {
407        let config = MixerConfig::new()
408            .with_dry_wet(0.0) // All direct
409            .with_direct_gain(2.0)
410            .with_output_gain(0.5)
411            .with_soft_clip(None); // Disable soft clipping
412
413        let mut mixer = DryWetMixer::with_config(config);
414
415        let bin = SeparatedBin::new(
416            0,
417            0,
418            Complex::new(1.0, 0.0),
419            Complex::new(0.0, 0.0),
420            1.0,
421            0.0,
422        );
423
424        let result = mixer.mix_bin(&bin);
425        // 1.0 * 2.0 (direct gain) * 0.5 (output gain) = 1.0
426        assert!((result.re - 1.0).abs() < 1e-6);
427    }
428
429    #[test]
430    fn test_soft_clipping() {
431        let config = MixerConfig::new()
432            .with_output_gain(10.0) // High gain to trigger clipping
433            .with_soft_clip(Some(0.9));
434
435        let mut mixer = DryWetMixer::with_config(config);
436
437        let bin = SeparatedBin::new(
438            0,
439            0,
440            Complex::new(0.5, 0.0),
441            Complex::new(0.0, 0.0),
442            1.0,
443            0.0,
444        );
445
446        let result = mixer.mix_bin(&bin);
447        // Should be soft-clipped below 1.0
448        assert!(result.magnitude() < 1.0);
449    }
450
451    #[test]
452    fn test_peak_tracking() {
453        let mut mixer = DryWetMixer::new();
454
455        let bin = SeparatedBin::new(
456            0,
457            0,
458            Complex::new(0.8, 0.0),
459            Complex::new(0.3, 0.0),
460            0.5,
461            0.0,
462        );
463
464        mixer.mix_bin(&bin);
465        let (direct, ambience, _output) = mixer.peak_levels();
466
467        assert!((direct - 0.8).abs() < 1e-6);
468        assert!((ambience - 0.3).abs() < 1e-6);
469
470        mixer.reset_peaks();
471        let (direct, ambience, output) = mixer.peak_levels();
472        assert_eq!(direct, 0.0);
473        assert_eq!(ambience, 0.0);
474        assert_eq!(output, 0.0);
475    }
476}