use async_trait::async_trait;
use crate::error::Result;
use crate::processor::frame::{AudioFrame, Frame, StartParams};
use crate::processor::{Envelope, FrameProcessor, Link, ProcessorSetup};
pub trait AudioMixer: Send {
fn name(&self) -> &str;
fn start(&mut self, _sample_rate: u32) -> Result<()> {
Ok(())
}
fn mix(&mut self, pcm: &[i16]) -> Vec<i16>;
}
#[derive(Debug, Default, Clone)]
pub struct SilenceMixer;
impl AudioMixer for SilenceMixer {
fn name(&self) -> &str {
"SilenceMixer"
}
fn mix(&mut self, pcm: &[i16]) -> Vec<i16> {
pcm.to_vec()
}
}
#[derive(Debug, Clone)]
pub struct SoundfileMixer {
source: Vec<i16>,
source_rate: u32,
pos: usize,
volume: f32,
looping: bool,
sample_rate: u32,
ready: bool,
}
impl SoundfileMixer {
pub fn new(source: Vec<i16>, source_rate: u32, volume: f32, looping: bool) -> Self {
Self {
source,
source_rate,
pos: 0,
volume: volume.clamp(0.0, 1.0),
looping,
sample_rate: source_rate,
ready: false,
}
}
pub fn set_volume(&mut self, volume: f32) {
self.volume = volume.clamp(0.0, 1.0);
}
}
impl AudioMixer for SoundfileMixer {
fn name(&self) -> &str {
"SoundfileMixer"
}
fn start(&mut self, sample_rate: u32) -> Result<()> {
self.sample_rate = sample_rate;
self.ready = self.source_rate == sample_rate && !self.source.is_empty();
if !self.ready && !self.source.is_empty() {
tracing::warn!(
"SoundfileMixer: source rate {} != output rate {} — mixing disabled",
self.source_rate,
sample_rate
);
}
Ok(())
}
fn mix(&mut self, pcm: &[i16]) -> Vec<i16> {
if !self.ready || self.volume == 0.0 {
return pcm.to_vec();
}
let mut out = Vec::with_capacity(pcm.len());
for &s in pcm {
if self.pos >= self.source.len() {
if self.looping {
self.pos = 0;
} else {
out.push(s);
continue;
}
}
let mixed = s as f32 + self.source[self.pos] as f32 * self.volume;
out.push(mixed.round().clamp(i16::MIN as f32, i16::MAX as f32) as i16);
self.pos += 1;
}
out
}
}
pub struct MixerProcessor<M: AudioMixer> {
mixer: M,
enabled: bool,
sample_rate: u32,
}
impl<M: AudioMixer> MixerProcessor<M> {
pub fn new(mixer: M) -> Self {
Self {
mixer,
enabled: true,
sample_rate: 24_000,
}
}
pub fn disabled(mixer: M) -> Self {
Self {
mixer,
enabled: false,
sample_rate: 24_000,
}
}
}
#[async_trait]
impl<M: AudioMixer + 'static> FrameProcessor for MixerProcessor<M> {
fn name(&self) -> &str {
self.mixer.name()
}
async fn start(&mut self, _setup: &ProcessorSetup, params: &StartParams) -> Result<()> {
self.sample_rate = params.audio_out_sample_rate;
self.mixer.start(self.sample_rate)
}
async fn process_frame(&mut self, env: Envelope, link: &Link) -> Result<()> {
match &env.frame {
Frame::MixerEnable(on) => {
self.enabled = *on;
link.push(env.meta, env.frame, env.direction).await;
return Ok(());
}
Frame::OutputAudio(audio) if self.enabled => {
let mixed = self.mixer.mix(&audio.pcm);
let out = AudioFrame::mono(mixed, audio.sample_rate);
link.push_down(Frame::OutputAudio(std::sync::Arc::new(out)))
.await;
return Ok(());
}
Frame::TtsAudio { audio, context_id } if self.enabled => {
let mixed = self.mixer.mix(&audio.pcm);
let out = AudioFrame::mono(mixed, audio.sample_rate);
link.push_down(Frame::TtsAudio {
audio: std::sync::Arc::new(out),
context_id: context_id.clone(),
})
.await;
return Ok(());
}
_ => {}
}
link.push(env.meta, env.frame, env.direction).await;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::observer::{FrameEvent, FrameObserver};
use crate::pipeline::{Pipeline, PipelineTask, PipelineTaskParams};
use std::sync::{Arc, Mutex};
#[test]
fn silence_mixer_passes_through() {
let mut m = SilenceMixer;
let pcm = vec![1i16, 2, 3];
assert_eq!(m.mix(&pcm), pcm);
}
#[test]
fn soundfile_mixer_blends_at_volume() {
let mut m = SoundfileMixer::new(vec![1000i16; 4], 24_000, 0.5, true);
m.start(24_000).unwrap();
let out = m.mix(&[2000i16; 4]);
assert_eq!(out, vec![2500i16; 4], "2000 + 1000*0.5 = 2500");
}
#[test]
fn soundfile_mixer_loops_source() {
let mut m = SoundfileMixer::new(vec![100i16, 200], 8_000, 1.0, true);
m.start(8_000).unwrap();
let out = m.mix(&[0i16; 4]);
assert_eq!(out, vec![100, 200, 100, 200]);
}
#[test]
fn soundfile_mixer_clamps_on_overflow() {
let mut m = SoundfileMixer::new(vec![30000i16; 2], 8_000, 1.0, true);
m.start(8_000).unwrap();
let out = m.mix(&[30000i16; 2]);
assert!(
out.iter().all(|&s| s == i16::MAX),
"60000 saturates to i16::MAX"
);
}
#[test]
fn soundfile_mixer_rate_mismatch_disables_mixing() {
let mut m = SoundfileMixer::new(vec![1000i16; 4], 8_000, 0.5, true);
m.start(24_000).unwrap(); assert_eq!(m.mix(&[2000i16; 4]), vec![2000i16; 4]);
}
#[derive(Default)]
struct OutTap {
first_samples: Mutex<Vec<i16>>,
}
#[async_trait]
impl FrameObserver for OutTap {
async fn on_process(&self, e: &FrameEvent<'_>) {
if let Frame::OutputAudio(a) = e.frame {
if let Some(&s) = a.pcm.first() {
self.first_samples.lock().unwrap().push(s);
}
}
}
}
#[tokio::test]
async fn mixer_processor_mixes_output_audio() {
let mixer = SoundfileMixer::new(vec![1000i16; 8], 24_000, 1.0, true);
let proc = MixerProcessor::new(mixer);
let pipeline = Pipeline::new(vec![Box::new(proc)]);
let tap = Arc::new(OutTap::default());
let task = PipelineTask::new(
pipeline,
PipelineTaskParams::default(),
vec![tap.clone() as Arc<dyn FrameObserver>],
);
task.queue_frame(Frame::OutputAudio(Arc::new(AudioFrame::mono(
vec![2000i16; 4],
24_000,
))))
.await;
task.stop_when_done().await;
tokio::time::timeout(std::time::Duration::from_secs(5), task.run())
.await
.expect("timed out")
.expect("run ok");
let firsts = tap.first_samples.lock().unwrap().clone();
assert!(
firsts.contains(&3000),
"mixed OutputAudio (2000+1000) must appear; saw {firsts:?}"
);
}
#[tokio::test]
async fn mixer_processor_toggle_bypasses_when_disabled() {
let mixer = SoundfileMixer::new(vec![1000i16; 8], 24_000, 1.0, true);
let proc = MixerProcessor::new(mixer);
let pipeline = Pipeline::new(vec![Box::new(proc)]);
let tap = Arc::new(OutTap::default());
let task = PipelineTask::new(
pipeline,
PipelineTaskParams::default(),
vec![tap.clone() as Arc<dyn FrameObserver>],
);
task.queue_frame(Frame::MixerEnable(false)).await;
task.queue_frame(Frame::OutputAudio(Arc::new(AudioFrame::mono(
vec![2000i16; 4],
24_000,
))))
.await;
task.stop_when_done().await;
tokio::time::timeout(std::time::Duration::from_secs(5), task.run())
.await
.expect("timed out")
.expect("run ok");
let firsts = tap.first_samples.lock().unwrap().clone();
assert!(
firsts.iter().all(|&s| s == 2000),
"disabled mixer must pass audio through unchanged; saw {firsts:?}"
);
}
}