use std::collections::VecDeque;
const TARGET_FILL_FRAMES: usize = 4800;
const LOOP_TAU_SECS: f64 = 30.0;
const CONTROLLER_GAIN: f64 = TARGET_FILL_FRAMES as f64 / (48_000.0 * LOOP_TAU_SECS);
const FILL_SMOOTHING: f64 = 0.002;
const INTEGRAL_TAU_SECS: f64 = 150.0;
const MAX_RATIO_DEVIATION: f64 = 0.01;
const LIMITER_THRESHOLD: f32 = 0.85;
pub struct Mixer {
channels: usize,
mic: VecDeque<f32>,
read_pos: f64,
ratio: f64,
smoothed_fill: f64,
primed: bool,
integral: f64,
pub clamp_hits: u64,
pub mic_overruns: u64,
pub mic_underruns: u64,
pub frames_out: u64,
pub limited: u64,
}
impl Mixer {
pub fn new(channels: u16) -> Self {
Self {
channels: channels.max(1) as usize,
mic: VecDeque::with_capacity(TARGET_FILL_FRAMES * 4 * channels.max(1) as usize),
read_pos: 0.0,
ratio: 1.0,
smoothed_fill: TARGET_FILL_FRAMES as f64,
primed: false,
integral: 0.0,
clamp_hits: 0,
mic_overruns: 0,
mic_underruns: 0,
frames_out: 0,
limited: 0,
}
}
pub fn drift_ppm(&self) -> f64 {
(self.ratio - 1.0) * 1.0e6
}
pub fn buffered_frames(&self) -> usize {
self.mic.len() / self.channels
}
pub fn smoothed_frames(&self) -> f64 {
self.smoothed_fill
}
pub fn integral_frames(&self) -> f64 {
self.integral
}
pub fn push_mic(&mut self, samples: &[f32]) {
self.mic.extend(samples.iter().copied());
let cap = TARGET_FILL_FRAMES * 8;
while self.buffered_frames() > cap {
for _ in 0..self.channels {
self.mic.pop_front();
}
self.read_pos = (self.read_pos - 1.0).max(0.0);
self.mic_overruns += 1;
}
}
pub fn mix(&mut self, discord: &[f32]) -> Vec<f32> {
let frames = discord.len() / self.channels;
self.steer_ratio(frames as f64 / 48_000.0);
let mut out = Vec::with_capacity(discord.len());
for f in 0..frames {
for c in 0..self.channels {
let remote = discord[f * self.channels + c];
let local = self.sample_mic(c);
out.push(self.limit(remote + local));
}
self.read_pos += self.ratio;
}
self.consume_read();
self.frames_out += frames as u64;
out
}
fn sample_mic(&mut self, channel: usize) -> f32 {
let base = self.read_pos.floor() as usize;
let frac = (self.read_pos - self.read_pos.floor()) as f32;
let a = self.mic.get(base * self.channels + channel).copied();
let b = self.mic.get((base + 1) * self.channels + channel).copied();
match (a, b) {
(Some(a), Some(b)) => a + (b - a) * frac,
(Some(a), None) => a,
_ => {
if channel == 0 && self.primed {
self.mic_underruns += 1;
}
0.0
}
}
}
fn consume_read(&mut self) {
let consumed = self.read_pos.floor() as usize;
if consumed == 0 {
return;
}
let drain = (consumed * self.channels).min(self.mic.len());
self.mic.drain(..drain);
self.read_pos -= consumed as f64;
}
fn steer_ratio(&mut self, dt: f64) {
let fill = self.buffered_frames() as f64;
let target = TARGET_FILL_FRAMES as f64;
if !self.primed && fill >= target {
self.primed = true;
}
self.smoothed_fill += (fill - self.smoothed_fill) * FILL_SMOOTHING;
let error = (self.smoothed_fill - target) / target;
if self.primed {
self.integral += error * dt;
let limit = INTEGRAL_TAU_SECS;
self.integral = self.integral.clamp(-limit, limit);
}
let wanted = 1.0 + CONTROLLER_GAIN * (error + self.integral / INTEGRAL_TAU_SECS);
self.ratio = wanted.clamp(1.0 - MAX_RATIO_DEVIATION, 1.0 + MAX_RATIO_DEVIATION);
if (wanted - self.ratio).abs() > f64::EPSILON {
self.clamp_hits += 1;
}
}
fn limit(&mut self, x: f32) -> f32 {
let mag = x.abs();
if mag <= LIMITER_THRESHOLD {
return x;
}
self.limited += 1;
let over = (mag - LIMITER_THRESHOLD) / (1.0 - LIMITER_THRESHOLD);
x.signum() * (LIMITER_THRESHOLD + (1.0 - LIMITER_THRESHOLD) * over.tanh())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn block(n: usize, v: f32) -> Vec<f32> {
vec![v; n * 2]
}
#[test]
fn output_length_always_matches_discord() {
let mut m = Mixer::new(2);
m.push_mic(&block(1000, 0.1));
for _ in 0..10 {
let out = m.mix(&block(480, 0.2));
assert_eq!(out.len(), 960, "Discord defines the output timeline");
}
}
#[test]
fn missing_mic_audio_does_not_stall_the_mix() {
let mut m = Mixer::new(2);
let out = m.mix(&block(480, 0.3));
assert_eq!(out.len(), 960, "output must not stall on a starved mic");
assert!((out[0] - 0.3).abs() < 1e-6);
}
#[test]
fn startup_starvation_is_not_reported_as_a_fault() {
let mut m = Mixer::new(2);
for _ in 0..50 {
m.mix(&block(480, 0.3));
}
assert_eq!(m.mic_underruns, 0, "startup should not count as underrun");
}
#[test]
fn starvation_after_priming_is_reported() {
let mut m = Mixer::new(2);
m.push_mic(&block(TARGET_FILL_FRAMES + 480, 0.1));
m.mix(&block(480, 0.1));
for _ in 0..200 {
m.mix(&block(480, 0.1));
}
assert!(
m.mic_underruns > 0,
"a genuine underrun after priming must be counted, not hidden"
);
}
#[test]
fn both_sources_are_summed() {
let mut m = Mixer::new(2);
m.push_mic(&block(5000, 0.25));
let out = m.mix(&block(480, 0.25));
assert!(
out[0] > 0.4,
"expected both sides in the sum, got {}",
out[0]
);
}
#[test]
fn limiter_prevents_full_scale_clipping() {
let mut m = Mixer::new(2);
m.push_mic(&block(5000, 0.9));
let out = m.mix(&block(480, 0.9));
assert!(
out.iter().all(|s| s.abs() < 1.0),
"limiter must hold below full scale"
);
assert!(m.limited > 0);
}
#[test]
fn a_sustained_backlog_speeds_the_microphone_up() {
let mut m = Mixer::new(2);
for _ in 0..2000 {
m.push_mic(&block(600, 0.1));
m.mix(&block(480, 0.1));
}
assert!(
m.drift_ppm() > 0.0,
"ratio should rise to drain a sustained backlog, got {} ppm",
m.drift_ppm()
);
}
#[test]
fn a_single_burst_does_not_move_the_ratio() {
let mut m = Mixer::new(2);
m.push_mic(&block(TARGET_FILL_FRAMES * 4, 0.1));
m.mix(&block(480, 0.1));
assert!(
m.drift_ppm().abs() < 500.0,
"one burst should barely move the ratio, got {} ppm",
m.drift_ppm()
);
}
#[test]
fn control_loop_is_slower_than_its_own_filter() {
let filter_tau = 0.010 / FILL_SMOOTHING;
let loop_tau = TARGET_FILL_FRAMES as f64 / (48_000.0 * CONTROLLER_GAIN);
assert!(
loop_tau >= filter_tau * 3.0,
"loop tau {loop_tau:.1}s must be well above filter tau {filter_tau:.1}s, \
or the loop oscillates"
);
}
#[test]
fn buffer_tolerates_far_more_drift_than_hardware_produces() {
let loop_tau = TARGET_FILL_FRAMES as f64 / (48_000.0 * CONTROLLER_GAIN);
let max_drift_fps = TARGET_FILL_FRAMES as f64 / loop_tau;
let max_drift_ppm = max_drift_fps / 48_000.0 * 1.0e6;
assert!(
max_drift_ppm > 2000.0,
"only tolerates {max_drift_ppm:.0} ppm; measured hardware drift is ~240 ppm \
and headroom this thin will underrun"
);
}
#[test]
fn integral_returns_the_buffer_to_target_under_constant_drift() {
let mut m = Mixer::new(2);
m.push_mic(&block(TARGET_FILL_FRAMES + 960, 0.1));
let mut owed = 0.0f64;
for _ in 0..60_000 {
owed += 480.0 * (1.0 + 240.0e-6);
let whole = owed.floor() as usize;
owed -= whole as f64;
m.push_mic(&block(whole, 0.1));
m.mix(&block(480, 0.1));
}
let target = TARGET_FILL_FRAMES as f64;
let offset = (m.smoothed_frames() - target).abs();
assert!(
offset < target * 0.10,
"buffer parked {offset:.0} frames from target ({target:.0}); \
the integral term is not nulling the standing offset"
);
assert_eq!(m.clamp_hits, 0, "controller should not saturate at 240 ppm");
assert_eq!(m.mic_underruns, 0, "buffer should never run dry at 240 ppm");
}
#[test]
fn ratio_never_leaves_sane_bounds() {
let mut m = Mixer::new(2);
for _ in 0..5000 {
m.push_mic(&block(2000, 0.1));
m.mix(&block(480, 0.1));
}
assert!(m.ratio <= 1.0 + MAX_RATIO_DEVIATION);
assert!(m.ratio >= 1.0 - MAX_RATIO_DEVIATION);
}
}