use crate::{
filter::{FilterMode, StateVariableConfig, StateVariableFilter},
utils::EnvelopeFollower,
AudioEffect,
};
#[derive(Debug, Clone)]
pub struct VocoderConfig {
pub bands: usize,
pub attack_ms: f32,
pub release_ms: f32,
pub min_freq: f32,
pub max_freq: f32,
}
impl Default for VocoderConfig {
fn default() -> Self {
Self {
bands: 32,
attack_ms: 5.0,
release_ms: 50.0,
min_freq: 80.0,
max_freq: 18_000.0,
}
}
}
struct VocoderBand {
modulator_filter: StateVariableFilter,
carrier_filter: StateVariableFilter,
envelope: EnvelopeFollower,
}
pub struct Vocoder {
bands: Vec<VocoderBand>,
#[allow(dead_code)]
config: VocoderConfig,
}
impl Vocoder {
#[must_use]
pub fn new(config: VocoderConfig, sample_rate: f32) -> Self {
let num_bands = config.bands.clamp(4, 64);
let min_freq = config.min_freq.clamp(20.0, 2000.0);
let nyquist_safe = sample_rate * 0.45;
let max_freq = config.max_freq.min(nyquist_safe).max(min_freq * 2.0);
#[allow(clippy::cast_precision_loss)]
let resonance = (1.5_f32 + num_bands as f32 / 16.0).min(12.0);
let bands: Vec<VocoderBand> = (0..num_bands)
.map(|i| {
#[allow(clippy::cast_precision_loss)]
let ratio = if num_bands > 1 {
i as f32 / (num_bands - 1) as f32
} else {
0.5
};
let frequency = min_freq * (max_freq / min_freq).powf(ratio);
let filter_config = StateVariableConfig {
frequency,
resonance,
mode: FilterMode::BandPass,
};
VocoderBand {
modulator_filter: StateVariableFilter::new(filter_config.clone(), sample_rate),
carrier_filter: StateVariableFilter::new(filter_config, sample_rate),
envelope: EnvelopeFollower::new(
config.attack_ms,
config.release_ms,
sample_rate,
),
}
})
.collect();
Self { bands, config }
}
#[must_use]
pub fn num_bands(&self) -> usize {
self.bands.len()
}
pub fn process(&mut self, modulator: f32, carrier: f32) -> f32 {
let mut output = 0.0_f32;
for band in &mut self.bands {
let mod_filtered = band.modulator_filter.process_sample(modulator);
let envelope = band.envelope.process(mod_filtered);
let car_filtered = band.carrier_filter.process_sample(carrier);
output += car_filtered * envelope;
}
#[allow(clippy::cast_precision_loss)]
let scale = 1.0 / self.bands.len() as f32;
output * scale
}
}
impl AudioEffect for Vocoder {
fn process_sample(&mut self, input: f32) -> f32 {
self.process(input, input)
}
fn reset(&mut self) {
for band in &mut self.bands {
band.modulator_filter.reset();
band.carrier_filter.reset();
band.envelope.reset();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_vocoder_default_process() {
let config = VocoderConfig::default();
let mut vocoder = Vocoder::new(config, 48000.0);
let output = vocoder.process(0.5, 0.3);
assert!(output.is_finite(), "output must be finite: {output}");
}
#[test]
fn test_vocoder_default_band_count() {
let config = VocoderConfig::default();
let vocoder = Vocoder::new(config, 48000.0);
assert_eq!(vocoder.num_bands(), 32, "default should be 32 bands");
}
#[test]
fn test_vocoder_32_bands() {
let config = VocoderConfig {
bands: 32,
..Default::default()
};
let vocoder = Vocoder::new(config, 48000.0);
assert_eq!(vocoder.num_bands(), 32);
}
#[test]
fn test_vocoder_64_bands() {
let config = VocoderConfig {
bands: 64,
..Default::default()
};
let mut vocoder = Vocoder::new(config, 48000.0);
assert_eq!(vocoder.num_bands(), 64);
let out = vocoder.process(0.3, 0.7);
assert!(out.is_finite(), "64-band output must be finite: {out}");
}
#[test]
fn test_vocoder_clamp_max() {
let config = VocoderConfig {
bands: 128,
..Default::default()
};
let vocoder = Vocoder::new(config, 48000.0);
assert_eq!(vocoder.num_bands(), 64, "bands must clamp at 64");
}
#[test]
fn test_vocoder_clamp_min() {
let config = VocoderConfig {
bands: 1,
..Default::default()
};
let vocoder = Vocoder::new(config, 48000.0);
assert_eq!(vocoder.num_bands(), 4, "bands must clamp to minimum 4");
}
#[test]
fn test_vocoder_reset() {
let config = VocoderConfig {
bands: 16,
..Default::default()
};
let mut vocoder = Vocoder::new(config, 48000.0);
for _ in 0..1000 {
vocoder.process(0.9, 0.9);
}
vocoder.reset();
let out = vocoder.process(0.0, 0.0);
assert!(
out.abs() < 1e-6,
"output after reset on silence must be ~0: {out}"
);
}
#[test]
fn test_vocoder_mono_process_sample() {
let config = VocoderConfig::default();
let mut vocoder = Vocoder::new(config, 48000.0);
let out = vocoder.process_sample(0.5);
assert!(out.is_finite());
}
#[test]
fn test_vocoder_output_finite_bulk() {
let config = VocoderConfig {
bands: 32,
..Default::default()
};
let mut vocoder = Vocoder::new(config, 48000.0);
use std::f32::consts::TAU;
for i in 0..4800 {
let mod_s = (i as f32 * TAU * 300.0 / 48000.0).sin() * 0.5;
let car_s = (i as f32 * TAU * 440.0 / 48000.0).sin() * 0.8;
let out = vocoder.process(mod_s, car_s);
assert!(out.is_finite(), "output at sample {i} not finite: {out}");
}
}
}