aec3 0.1.8

An acoustic echo canceller written in rust based on the WebRTC aec3 project
Documentation
use aec3::voip::VoipAec3;
use anyhow::Result;
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use crossbeam_channel::{Receiver, Sender, bounded};
use std::collections::VecDeque;
use std::thread;
use std::time::{Duration, Instant};

/// Example like `karaoke_loopback` but introduces a fixed delay before
/// forwarding loopback (render) frames into the AEC render path. This lets
/// you simulate remote playback latency and observe how the AEC delay
/// estimator reacts.

fn processing_thread(
    rx_in: Receiver<Vec<f32>>,
    rx_render: Receiver<Vec<f32>>,
    tx_out: Sender<Vec<f32>>,
    sample_rate: usize,
    channels: usize,
) {
    let builder = VoipAec3::builder(sample_rate, channels, channels).initial_delay_ms(0);

    // Enables debug dumping (WAL + WAV files) when the crate is built with
    // `--features diagnostics`.
    let builder = {
        #[cfg(feature = "diagnostics")]
        {
            builder.enable_diagnostics(true)
        }
        #[cfg(not(feature = "diagnostics"))]
        {
            builder
        }
    };

    let mut pipeline = builder.build().expect("failed to create AEC pipeline");

    let mut last_metrics = Instant::now();
    let metrics_interval = Duration::from_secs(5);
    let target_delay = Duration::from_millis(20);
    let mut capture_queue: VecDeque<(Instant, Vec<f32>)> = VecDeque::new();

    while let Ok(frame) = rx_in.recv() {
        capture_queue.push_back((Instant::now(), frame));

        loop {
            let ready = match capture_queue.front() {
                Some((ts, _)) => Instant::now().saturating_duration_since(*ts) >= target_delay,
                None => false,
            };

            if !ready {
                break;
            }

            let (_, capture_frame) = capture_queue
                .pop_front()
                .expect("queue signaled ready but was empty");

            while let Ok(render_frame) = rx_render.try_recv() {
                if let Err(err) = pipeline.handle_render_frame(&render_frame) {
                    eprintln!("handle_render_frame error: {err}");
                }
            }

            let mut output = vec![0f32; capture_frame.len()];
            match pipeline.process_capture_frame(&capture_frame, false, &mut output) {
                Ok(metrics) => {
                    if last_metrics.elapsed() >= metrics_interval {
                        println!("AEC metrics: {:?}", metrics);
                        last_metrics = Instant::now();
                    }
                    let _ = tx_out.try_send(output);
                }
                Err(err) => eprintln!("AEC processing error: {err}"),
            }
        }
    }
}

fn main() -> Result<()> {
    let host = cpal::default_host();

    let input_device = host
        .default_input_device()
        .expect("No input device available");
    let output_device = host
        .default_output_device()
        .expect("No output device available");

    let loopback_supported = output_device.default_output_config()?;
    let loopback_stream_config: cpal::StreamConfig = loopback_supported.clone().into();
    let sample_rate_hz = loopback_stream_config.sample_rate.0 as usize;
    let channels = loopback_stream_config.channels as usize;
    let frames_per_buffer = (sample_rate_hz / 100) as usize; // 10 ms

    // Channels between audio callbacks and the processing thread
    let (tx_in, rx_in) = bounded::<Vec<f32>>(32);
    let (tx_out, rx_out) = bounded::<Vec<f32>>(32);
    let (tx_render, rx_render) = bounded::<Vec<f32>>(32);

    // Spawn processing thread that owns the AEC instance.
    thread::spawn(move || processing_thread(rx_in, rx_render, tx_out, sample_rate_hz, channels));

    // Build microphone input stream using the same sample rate and channels as loopback.
    let in_config = cpal::StreamConfig {
        channels: channels as u16,
        sample_rate: cpal::SampleRate(sample_rate_hz as u32),
        buffer_size: cpal::BufferSize::Fixed(frames_per_buffer as u32),
    };

    let tx_in_clone = tx_in.clone();
    let input_stream = input_device.build_input_stream(
        &in_config,
        move |data: &[f32], _| {
            let vec = data.to_vec();
            let _ = tx_in_clone.try_send(vec);
        },
        move |err| eprintln!("input stream error: {:?}", err),
        None,
    )?;

    // Build a loopback input stream (captures what is played on the system output)
    let tx_render_clone = tx_render.clone();
    let loopback_input_stream = output_device.build_input_stream(
        &loopback_stream_config,
        move |data: &[f32], _| {
            // Forward the loopback (render) frames immediately to the AEC.
            let vec = data.to_vec();
            let _ = tx_render_clone.try_send(vec);
        },
        move |err| eprintln!("loopback input stream error: {:?}", err),
        None,
    )?;

    // Build output stream (plays processed audio)
    let out_config = cpal::StreamConfig {
        channels: channels as u16,
        sample_rate: cpal::SampleRate(sample_rate_hz as u32),
        buffer_size: cpal::BufferSize::Fixed(frames_per_buffer as u32),
    };

    // Playback delay queue to accumulate ~3 seconds before playing back processed audio
    let mut delay_queue: VecDeque<Vec<f32>> = VecDeque::new();
    let delay_buffers = std::cmp::max(1usize, (sample_rate_hz * 3) / frames_per_buffer);

    let output_stream = output_device.build_output_stream(
        &out_config,
        move |out: &mut [f32], _| {
            while let Ok(frame) = rx_out.try_recv() {
                delay_queue.push_back(frame);
            }

            if delay_queue.len() > delay_buffers {
                if let Some(frame_to_play) = delay_queue.pop_front() {
                    let len = out.len().min(frame_to_play.len());
                    out[..len].copy_from_slice(&frame_to_play[..len]);
                    if len < out.len() {
                        for s in out[len..].iter_mut() {
                            *s = 0.0;
                        }
                    }
                } else {
                    for s in out.iter_mut() {
                        *s = 0.0;
                    }
                }
            } else {
                for s in out.iter_mut() {
                    *s = 0.0;
                }
            }
        },
        move |err| eprintln!("output stream error: {:?}", err),
        None,
    )?;

    input_stream.play()?;
    loopback_input_stream.play()?;
    output_stream.play()?;

    println!("Running delayed karaoke loopback — press Ctrl+C to exit (capture delay ≈ 20ms)");
    #[cfg(feature = "diagnostics")]
    {
        println!("Diagnostics enabled (feature=diagnostics): writing WAL + WAV files to ./aec3_diagnostics");
        println!("Inspect WAL: cargo run --features diagnostics --bin parse-diagnostics");
    }

    loop {
        thread::sleep(Duration::from_millis(100));
    }
}