1use crate::messages::{Complex, SeparatedBin};
7
8#[derive(Debug, Clone)]
10pub struct MixerConfig {
11 pub dry_wet: f32,
13 pub direct_gain: f32,
15 pub ambience_gain: f32,
17 pub output_gain: f32,
19 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 pub fn new() -> Self {
38 Self::default()
39 }
40
41 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 pub fn with_direct_gain(mut self, gain: f32) -> Self {
49 self.direct_gain = gain.max(0.0);
50 self
51 }
52
53 pub fn with_ambience_gain(mut self, gain: f32) -> Self {
55 self.ambience_gain = gain.max(0.0);
56 self
57 }
58
59 pub fn with_output_gain(mut self, gain: f32) -> Self {
61 self.output_gain = gain.max(0.0);
62 self
63 }
64
65 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 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 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 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
106pub struct DryWetMixer {
108 config: MixerConfig,
109 direct_peak: f32,
111 ambience_peak: f32,
113 output_peak: f32,
115}
116
117impl DryWetMixer {
118 pub fn new() -> Self {
120 Self::with_config(MixerConfig::default())
121 }
122
123 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 pub fn config(&self) -> &MixerConfig {
135 &self.config
136 }
137
138 pub fn set_config(&mut self, config: MixerConfig) {
140 self.config = config;
141 }
142
143 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 pub fn set_direct_gain(&mut self, gain: f32) {
150 self.config.direct_gain = gain.max(0.0);
151 }
152
153 pub fn set_ambience_gain(&mut self, gain: f32) {
155 self.config.ambience_gain = gain.max(0.0);
156 }
157
158 pub fn set_output_gain(&mut self, gain: f32) {
160 self.config.output_gain = gain.max(0.0);
161 }
162
163 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 pub fn mix_bin(&mut self, bin: &SeparatedBin) -> Complex {
170 let direct = bin.direct.scale(self.config.direct_gain);
172 let ambience = bin.ambience.scale(self.config.ambience_gain);
173
174 self.direct_peak = self.direct_peak.max(direct.magnitude());
176 self.ambience_peak = self.ambience_peak.max(ambience.magnitude());
177
178 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 let output = mixed.scale(self.config.output_gain);
189
190 self.output_peak = self.output_peak.max(output.magnitude());
192
193 if let Some(threshold) = self.config.soft_clip_threshold {
195 self.soft_clip(output, threshold)
196 } else {
197 output
198 }
199 }
200
201 pub fn mix_frame(&mut self, bins: &[SeparatedBin]) -> Vec<Complex> {
203 bins.iter().map(|bin| self.mix_bin(bin)).collect()
204 }
205
206 pub fn direct_only(&self, bin: &SeparatedBin) -> Complex {
208 bin.direct
209 .scale(self.config.direct_gain * self.config.output_gain)
210 }
211
212 pub fn ambience_only(&self, bin: &SeparatedBin) -> Complex {
214 bin.ambience
215 .scale(self.config.ambience_gain * self.config.output_gain)
216 }
217
218 pub fn extract_direct(&self, bins: &[SeparatedBin]) -> Vec<Complex> {
220 bins.iter().map(|bin| self.direct_only(bin)).collect()
221 }
222
223 pub fn extract_ambience(&self, bins: &[SeparatedBin]) -> Vec<Complex> {
225 bins.iter().map(|bin| self.ambience_only(bin)).collect()
226 }
227
228 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 let overshoot = magnitude - threshold;
238 let compressed = threshold + overshoot.tanh() * (1.0 - threshold);
239
240 if magnitude > 1e-10 {
242 value.scale(compressed / magnitude)
243 } else {
244 value
245 }
246 }
247
248 pub fn peak_levels(&self) -> (f32, f32, f32) {
250 (self.direct_peak, self.ambience_peak, self.output_peak)
251 }
252
253 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 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#[derive(Debug, Clone)]
286pub struct MixedFrame {
287 pub bins: Vec<Complex>,
289 pub direct_bins: Vec<Complex>,
291 pub ambience_bins: Vec<Complex>,
293 pub frame_id: u64,
295}
296
297impl MixedFrame {
298 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
314pub struct FrameMixer {
316 mixer: DryWetMixer,
317}
318
319impl FrameMixer {
320 pub fn new(config: MixerConfig) -> Self {
322 Self {
323 mixer: DryWetMixer::with_config(config),
324 }
325 }
326
327 pub fn mixer(&self) -> &DryWetMixer {
329 &self.mixer
330 }
331
332 pub fn mixer_mut(&mut self) -> &mut DryWetMixer {
334 &mut self.mixer
335 }
336
337 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 pub fn set_dry_wet(&mut self, dry_wet: f32) {
350 self.mixer.set_dry_wet(dry_wet);
351 }
352
353 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 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), Complex::new(0.5, 0.0), 0.6,
386 0.0,
387 );
388
389 mixer.set_dry_wet(0.0);
391 let result = mixer.mix_bin(&bin);
392 assert!((result.re - 1.0).abs() < 1e-6);
393
394 mixer.set_dry_wet(1.0);
396 let result = mixer.mix_bin(&bin);
397 assert!((result.re - 0.5).abs() < 1e-6);
398
399 mixer.set_dry_wet(0.5);
401 let result = mixer.mix_bin(&bin);
402 assert!((result.re - 0.75).abs() < 1e-6); }
404
405 #[test]
406 fn test_gain_application() {
407 let config = MixerConfig::new()
408 .with_dry_wet(0.0) .with_direct_gain(2.0)
410 .with_output_gain(0.5)
411 .with_soft_clip(None); 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 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) .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 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}