use crate::messages::{Complex, SeparatedBin};
#[derive(Debug, Clone)]
pub struct MixerConfig {
pub dry_wet: f32,
pub direct_gain: f32,
pub ambience_gain: f32,
pub output_gain: f32,
pub soft_clip_threshold: Option<f32>,
}
impl Default for MixerConfig {
fn default() -> Self {
Self {
dry_wet: 0.5,
direct_gain: 1.0,
ambience_gain: 1.0,
output_gain: 1.0,
soft_clip_threshold: Some(0.95),
}
}
}
impl MixerConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_dry_wet(mut self, dry_wet: f32) -> Self {
self.dry_wet = dry_wet.clamp(0.0, 1.0);
self
}
pub fn with_direct_gain(mut self, gain: f32) -> Self {
self.direct_gain = gain.max(0.0);
self
}
pub fn with_ambience_gain(mut self, gain: f32) -> Self {
self.ambience_gain = gain.max(0.0);
self
}
pub fn with_output_gain(mut self, gain: f32) -> Self {
self.output_gain = gain.max(0.0);
self
}
pub fn with_soft_clip(mut self, threshold: Option<f32>) -> Self {
self.soft_clip_threshold = threshold.map(|t| t.clamp(0.1, 1.0));
self
}
pub fn direct_only() -> Self {
Self {
dry_wet: 0.0,
direct_gain: 1.0,
ambience_gain: 0.0,
output_gain: 1.0,
soft_clip_threshold: Some(0.95),
}
}
pub fn ambience_only() -> Self {
Self {
dry_wet: 1.0,
direct_gain: 0.0,
ambience_gain: 1.0,
output_gain: 1.0,
soft_clip_threshold: Some(0.95),
}
}
pub fn balanced_with_boost(boost_db: f32) -> Self {
let linear_gain = 10.0_f32.powf(boost_db / 20.0);
Self {
dry_wet: 0.5,
direct_gain: 1.0,
ambience_gain: 1.0,
output_gain: linear_gain,
soft_clip_threshold: Some(0.95),
}
}
}
pub struct DryWetMixer {
config: MixerConfig,
direct_peak: f32,
ambience_peak: f32,
output_peak: f32,
}
impl DryWetMixer {
pub fn new() -> Self {
Self::with_config(MixerConfig::default())
}
pub fn with_config(config: MixerConfig) -> Self {
Self {
config,
direct_peak: 0.0,
ambience_peak: 0.0,
output_peak: 0.0,
}
}
pub fn config(&self) -> &MixerConfig {
&self.config
}
pub fn set_config(&mut self, config: MixerConfig) {
self.config = config;
}
pub fn set_dry_wet(&mut self, dry_wet: f32) {
self.config.dry_wet = dry_wet.clamp(0.0, 1.0);
}
pub fn set_direct_gain(&mut self, gain: f32) {
self.config.direct_gain = gain.max(0.0);
}
pub fn set_ambience_gain(&mut self, gain: f32) {
self.config.ambience_gain = gain.max(0.0);
}
pub fn set_output_gain(&mut self, gain: f32) {
self.config.output_gain = gain.max(0.0);
}
pub fn set_output_gain_db(&mut self, gain_db: f32) {
self.config.output_gain = 10.0_f32.powf(gain_db / 20.0);
}
pub fn mix_bin(&mut self, bin: &SeparatedBin) -> Complex {
let direct = bin.direct.scale(self.config.direct_gain);
let ambience = bin.ambience.scale(self.config.ambience_gain);
self.direct_peak = self.direct_peak.max(direct.magnitude());
self.ambience_peak = self.ambience_peak.max(ambience.magnitude());
let dry_amount = 1.0 - self.config.dry_wet;
let wet_amount = self.config.dry_wet;
let mixed = Complex {
re: direct.re * dry_amount + ambience.re * wet_amount,
im: direct.im * dry_amount + ambience.im * wet_amount,
};
let output = mixed.scale(self.config.output_gain);
self.output_peak = self.output_peak.max(output.magnitude());
if let Some(threshold) = self.config.soft_clip_threshold {
self.soft_clip(output, threshold)
} else {
output
}
}
pub fn mix_frame(&mut self, bins: &[SeparatedBin]) -> Vec<Complex> {
bins.iter().map(|bin| self.mix_bin(bin)).collect()
}
pub fn direct_only(&self, bin: &SeparatedBin) -> Complex {
bin.direct
.scale(self.config.direct_gain * self.config.output_gain)
}
pub fn ambience_only(&self, bin: &SeparatedBin) -> Complex {
bin.ambience
.scale(self.config.ambience_gain * self.config.output_gain)
}
pub fn extract_direct(&self, bins: &[SeparatedBin]) -> Vec<Complex> {
bins.iter().map(|bin| self.direct_only(bin)).collect()
}
pub fn extract_ambience(&self, bins: &[SeparatedBin]) -> Vec<Complex> {
bins.iter().map(|bin| self.ambience_only(bin)).collect()
}
fn soft_clip(&self, value: Complex, threshold: f32) -> Complex {
let magnitude = value.magnitude();
if magnitude <= threshold {
return value;
}
let overshoot = magnitude - threshold;
let compressed = threshold + overshoot.tanh() * (1.0 - threshold);
if magnitude > 1e-10 {
value.scale(compressed / magnitude)
} else {
value
}
}
pub fn peak_levels(&self) -> (f32, f32, f32) {
(self.direct_peak, self.ambience_peak, self.output_peak)
}
pub fn peak_levels_db(&self) -> (f32, f32, f32) {
let to_db = |level: f32| {
if level > 1e-10 {
20.0 * level.log10()
} else {
-200.0
}
};
(
to_db(self.direct_peak),
to_db(self.ambience_peak),
to_db(self.output_peak),
)
}
pub fn reset_peaks(&mut self) {
self.direct_peak = 0.0;
self.ambience_peak = 0.0;
self.output_peak = 0.0;
}
}
impl Default for DryWetMixer {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct MixedFrame {
pub bins: Vec<Complex>,
pub direct_bins: Vec<Complex>,
pub ambience_bins: Vec<Complex>,
pub frame_id: u64,
}
impl MixedFrame {
pub fn new(
bins: Vec<Complex>,
direct: Vec<Complex>,
ambience: Vec<Complex>,
frame_id: u64,
) -> Self {
Self {
bins,
direct_bins: direct,
ambience_bins: ambience,
frame_id,
}
}
}
pub struct FrameMixer {
mixer: DryWetMixer,
}
impl FrameMixer {
pub fn new(config: MixerConfig) -> Self {
Self {
mixer: DryWetMixer::with_config(config),
}
}
pub fn mixer(&self) -> &DryWetMixer {
&self.mixer
}
pub fn mixer_mut(&mut self) -> &mut DryWetMixer {
&mut self.mixer
}
pub fn process(&mut self, bins: &[SeparatedBin]) -> MixedFrame {
let frame_id = bins.first().map(|b| b.frame_id).unwrap_or(0);
let mixed = self.mixer.mix_frame(bins);
let direct = self.mixer.extract_direct(bins);
let ambience = self.mixer.extract_ambience(bins);
MixedFrame::new(mixed, direct, ambience, frame_id)
}
pub fn set_dry_wet(&mut self, dry_wet: f32) {
self.mixer.set_dry_wet(dry_wet);
}
pub fn set_gain_db(&mut self, gain_db: f32) {
self.mixer.set_output_gain_db(gain_db);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mixer_config() {
let config = MixerConfig::new()
.with_dry_wet(0.3)
.with_direct_gain(1.2)
.with_ambience_gain(0.8);
assert!((config.dry_wet - 0.3).abs() < 1e-6);
assert!((config.direct_gain - 1.2).abs() < 1e-6);
}
#[test]
fn test_dry_wet_mix() {
let config = MixerConfig::new().with_soft_clip(None);
let mut mixer = DryWetMixer::with_config(config);
let bin = SeparatedBin::new(
0,
0,
Complex::new(1.0, 0.0), Complex::new(0.5, 0.0), 0.6,
0.0,
);
mixer.set_dry_wet(0.0);
let result = mixer.mix_bin(&bin);
assert!((result.re - 1.0).abs() < 1e-6);
mixer.set_dry_wet(1.0);
let result = mixer.mix_bin(&bin);
assert!((result.re - 0.5).abs() < 1e-6);
mixer.set_dry_wet(0.5);
let result = mixer.mix_bin(&bin);
assert!((result.re - 0.75).abs() < 1e-6); }
#[test]
fn test_gain_application() {
let config = MixerConfig::new()
.with_dry_wet(0.0) .with_direct_gain(2.0)
.with_output_gain(0.5)
.with_soft_clip(None);
let mut mixer = DryWetMixer::with_config(config);
let bin = SeparatedBin::new(
0,
0,
Complex::new(1.0, 0.0),
Complex::new(0.0, 0.0),
1.0,
0.0,
);
let result = mixer.mix_bin(&bin);
assert!((result.re - 1.0).abs() < 1e-6);
}
#[test]
fn test_soft_clipping() {
let config = MixerConfig::new()
.with_output_gain(10.0) .with_soft_clip(Some(0.9));
let mut mixer = DryWetMixer::with_config(config);
let bin = SeparatedBin::new(
0,
0,
Complex::new(0.5, 0.0),
Complex::new(0.0, 0.0),
1.0,
0.0,
);
let result = mixer.mix_bin(&bin);
assert!(result.magnitude() < 1.0);
}
#[test]
fn test_peak_tracking() {
let mut mixer = DryWetMixer::new();
let bin = SeparatedBin::new(
0,
0,
Complex::new(0.8, 0.0),
Complex::new(0.3, 0.0),
0.5,
0.0,
);
mixer.mix_bin(&bin);
let (direct, ambience, _output) = mixer.peak_levels();
assert!((direct - 0.8).abs() < 1e-6);
assert!((ambience - 0.3).abs() < 1e-6);
mixer.reset_peaks();
let (direct, ambience, output) = mixer.peak_levels();
assert_eq!(direct, 0.0);
assert_eq!(ambience, 0.0);
assert_eq!(output, 0.0);
}
}