use crate::audio_process::agc::{AgcConfig, AudioEnhancer};
use crate::audio_process::hushfilter::HushVaniFilter;
use crate::audio_process::noisefilter::RNNoiseFilter;
use crate::audio_process::resamplers::{ResamplerQuality, StreamResampler};
use crate::audio_process::StreamingDenoiser;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum NoiseBackend {
#[default]
Rnnoise,
HushVani,
}
#[derive(Debug, Clone)]
pub struct FrontendConfig {
pub target_sample_rate: u32,
pub noise_reduction: bool,
pub noise_backend: NoiseBackend,
pub agc: bool,
pub agc_config: AgcConfig,
}
impl Default for FrontendConfig {
fn default() -> Self {
Self {
target_sample_rate: 16_000,
noise_reduction: true,
noise_backend: NoiseBackend::default(),
agc: true,
agc_config: AgcConfig::default(),
}
}
}
pub struct AudioFrontend {
target_sample_rate: u32,
source_sample_rate: Option<u32>,
resampler: Option<StreamResampler>,
denoiser: Option<Box<dyn StreamingDenoiser>>,
enhancer: Option<AudioEnhancer>,
}
impl AudioFrontend {
pub fn new(config: &FrontendConfig) -> Self {
let rate = config.target_sample_rate;
let denoiser: Option<Box<dyn StreamingDenoiser>> = if config.noise_reduction {
Some(match config.noise_backend {
NoiseBackend::Rnnoise => {
log::info!("AudioFrontend: noise reduction — RNNoise (sample_rate={rate})");
Box::new(RNNoiseFilter::new(rate))
}
NoiseBackend::HushVani => match HushVaniFilter::new(rate) {
Ok(f) => {
log::info!(
"AudioFrontend: noise reduction — hush-vani (sample_rate={rate})"
);
Box::new(f) as Box<dyn StreamingDenoiser>
}
Err(e) => {
log::error!(
"AudioFrontend: hush-vani init failed ({e}); falling back to RNNoise"
);
Box::new(RNNoiseFilter::new(rate))
}
},
})
} else {
None
};
let enhancer = if config.agc {
log::info!("AudioFrontend: speech enhancement — HPF + AGC + limiter (sample_rate={rate})");
Some(AudioEnhancer::with_config(rate, config.agc_config.clone()))
} else {
None
};
Self {
target_sample_rate: rate,
source_sample_rate: None,
resampler: None,
denoiser,
enhancer,
}
}
pub fn target_sample_rate(&self) -> u32 {
self.target_sample_rate
}
pub fn process(&mut self, pcm: &[i16], source_rate: u32) -> Vec<i16> {
let pcm = self.resample(pcm, source_rate);
if pcm.is_empty() {
return Vec::new();
}
let mut out = match &mut self.enhancer {
Some(enh) => enh.pre_filter(&pcm),
None => pcm,
};
if let Some(d) = &mut self.denoiser {
out = d.filter(&out);
}
if !out.is_empty() {
if let Some(enh) = &mut self.enhancer {
out = enh.post_filter(&out);
}
}
out
}
pub fn flush(&mut self) -> Vec<i16> {
let mut tail: Vec<i16> = Vec::new();
if let Some(r) = &mut self.resampler {
let resampled = f32_to_i16(&r.flush());
if !resampled.is_empty() {
let pre = match &mut self.enhancer {
Some(enh) => enh.pre_filter(&resampled),
None => resampled,
};
match &mut self.denoiser {
Some(d) => tail.extend(d.filter(&pre)),
None => tail.extend(pre),
}
}
}
if let Some(d) = &mut self.denoiser {
tail.extend(d.flush());
}
if !tail.is_empty() {
if let Some(enh) = &mut self.enhancer {
tail = enh.post_filter(&tail);
}
}
tail
}
pub fn reset(&mut self) {
if let Some(r) = &mut self.resampler {
r.reset();
}
if let Some(d) = &mut self.denoiser {
d.reset();
}
if let Some(enh) = &mut self.enhancer {
enh.reset();
}
}
fn resample(&mut self, pcm: &[i16], source_rate: u32) -> Vec<i16> {
if source_rate == self.target_sample_rate {
if self.resampler.is_some() {
log::info!(
"AudioFrontend: input rate now matches target {}Hz — resampler dropped",
self.target_sample_rate
);
self.resampler = None;
}
self.source_sample_rate = Some(source_rate);
return pcm.to_vec();
}
if self.source_sample_rate != Some(source_rate) || self.resampler.is_none() {
log::info!(
"AudioFrontend: resampling input {}Hz → {}Hz",
source_rate, self.target_sample_rate
);
self.resampler = Some(StreamResampler::new(
source_rate,
self.target_sample_rate,
ResamplerQuality::Quick,
));
self.source_sample_rate = Some(source_rate);
}
let input: Vec<f32> = pcm.iter().map(|&s| s as f32).collect();
let resampled = self
.resampler
.as_mut()
.expect("resampler built above")
.process(&input);
f32_to_i16(&resampled)
}
}
fn f32_to_i16(samples: &[f32]) -> Vec<i16> {
samples
.iter()
.map(|&s| s.clamp(i16::MIN as f32, i16::MAX as f32) as i16)
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn tone(rate: u32, ms: u32) -> Vec<i16> {
let n = (rate as u64 * ms as u64 / 1000) as usize;
(0..n)
.map(|i| {
let t = i as f32 / rate as f32;
(8000.0 * (2.0 * std::f32::consts::PI * 300.0 * t).sin()) as i16
})
.collect()
}
fn plain(target: u32) -> FrontendConfig {
FrontendConfig {
target_sample_rate: target,
noise_reduction: false,
noise_backend: NoiseBackend::Rnnoise,
agc: false,
agc_config: AgcConfig::default(),
}
}
#[test]
fn passthrough_when_everything_disabled() {
let mut fe = AudioFrontend::new(&plain(16_000));
let input = tone(16_000, 20);
assert_eq!(fe.process(&input, 16_000), input);
assert!(fe.flush().is_empty());
}
fn assert_resampled_len(out: usize, expected: usize, target_rate: u32, from_rate: u32) {
let max_pad = (480.0 * target_rate as f64 / from_rate as f64).ceil() as usize;
assert!(
out + expected / 20 >= expected,
"resampling must not lose audio: expected ~{expected}, got {out}"
);
assert!(
out <= expected + max_pad,
"flush padding must stay under one output chunk ({max_pad}): \
expected ~{expected}, got {out}"
);
}
#[test]
fn upsamples_8k_to_16k_at_roughly_double_length() {
let mut fe = AudioFrontend::new(&plain(16_000));
let mut out = 0usize;
for _ in 0..10 {
out += fe.process(&tone(8_000, 20), 8_000).len();
}
out += fe.flush().len();
assert_resampled_len(out, 3_200, 16_000, 8_000);
}
#[test]
fn downsamples_48k_to_16k_at_roughly_a_third() {
let mut fe = AudioFrontend::new(&plain(16_000));
let mut out = 0usize;
for _ in 0..10 {
out += fe.process(&tone(48_000, 20), 48_000).len();
}
out += fe.flush().len();
assert_resampled_len(out, 3_200, 16_000, 48_000);
}
#[test]
fn matching_rate_is_not_resampled() {
let mut fe = AudioFrontend::new(&plain(16_000));
fe.process(&tone(16_000, 20), 16_000);
assert!(fe.resampler.is_none(), "no resampler should be built for a matching rate");
}
#[test]
fn rate_change_mid_stream_rebuilds_the_resampler() {
let mut fe = AudioFrontend::new(&plain(16_000));
fe.process(&tone(8_000, 20), 8_000);
assert_eq!(fe.source_sample_rate, Some(8_000));
assert!(fe.resampler.is_some());
fe.process(&tone(48_000, 20), 48_000);
assert_eq!(fe.source_sample_rate, Some(48_000));
assert!(fe.resampler.is_some());
fe.process(&tone(16_000, 20), 16_000);
assert!(fe.resampler.is_none());
}
#[test]
fn denoise_chain_preserves_length_over_an_utterance() {
let mut fe = AudioFrontend::new(&FrontendConfig {
target_sample_rate: 16_000,
noise_reduction: true,
noise_backend: NoiseBackend::Rnnoise,
agc: true,
agc_config: AgcConfig::default(),
});
let chunk = tone(16_000, 20); let mut sent = 0usize;
let mut out = 0usize;
for _ in 0..25 {
sent += chunk.len();
out += fe.process(&chunk, 16_000).len();
}
out += fe.flush().len();
let ratio = out as f64 / sent as f64;
assert!(
(0.9..=1.1).contains(&ratio),
"denoise chain must roughly preserve length: sent {sent}, got {out} (ratio {ratio:.3})"
);
}
#[test]
fn reset_clears_buffers_without_panicking() {
let mut fe = AudioFrontend::new(&FrontendConfig {
target_sample_rate: 16_000,
noise_reduction: true,
..Default::default()
});
fe.process(&tone(8_000, 20), 8_000);
fe.reset();
fe.process(&tone(8_000, 20), 8_000);
fe.flush();
}
}