Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ rubato = "0.16"
smallvec = "1.11"
symphonia = { version = "0.5", default-features = false }
vecmath = "1.0"
aec-rs = "1.0.0"
lazy_static = "1.5.0"

[target.'cfg(any(target_arch = "x86", target_arch = "x86_64", target_arch = "aarch64"))'.dependencies]
no_denormals = "0.2.0"
Expand Down
50 changes: 44 additions & 6 deletions examples/microphone.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ use web_audio_api::context::{
use web_audio_api::media_devices;
use web_audio_api::media_devices::{enumerate_devices_sync, MediaDeviceInfo, MediaDeviceInfoKind};
use web_audio_api::media_devices::{MediaStreamConstraints, MediaTrackConstraints};
use web_audio_api::media_recorder::{MediaRecorder, MediaRecorderOptions};
use web_audio_api::node::AudioNode;
use std::fs::File;
use std::io::Write;

// Pipe microphone stream into audio context
//
Expand Down Expand Up @@ -73,18 +76,53 @@ fn main() {
let mut constraints = MediaTrackConstraints::default();
constraints.device_id = source_id;
// constraints.channel_count = Some(2);
constraints.echo_cancellation = Some(false); // Enable echo cancellation
let stream_constraints = MediaStreamConstraints::AudioWithConstraints(constraints);
let mic = media_devices::get_user_media_sync(stream_constraints);

println!("\n✓ Microphone stream created with echo cancellation enabled");
println!("You should be able to speak without hearing feedback/echo.\n");

// create media stream source node with mic stream
let stream_source = context.create_media_stream_source(&mic);

// Create a media stream destination to capture audio
let dest = context.create_media_stream_destination();
stream_source.connect(&dest);

// Also connect to speakers so you can hear yourself
stream_source.connect(&context.destination());

loop {
std::thread::sleep(std::time::Duration::from_secs(1));
}
// Create media recorder
let options = MediaRecorderOptions::default(); // default to audio/wav
let recorder = MediaRecorder::new(dest.stream(), options);

// Create a file to write the recording
let mut file = File::create("recording.wav").expect("Failed to create file");

recorder.set_ondataavailable(move |event| {
eprintln!(
"Recording... timecode {:.3}s, chunk size {} bytes",
event.timecode,
event.blob.size()
);
file.write_all(&event.blob.data).unwrap();
});

// Start recording
recorder.start();
println!("Recording to 'recording.wav' for 10 seconds...");

// Record for 10 seconds
std::thread::sleep(std::time::Duration::from_secs(10));

// Stop recording and wait for final data
let (send, recv) = crossbeam_channel::bounded(1);
recorder.set_onstop(move |_| {
let _ = send.send(());
});
recorder.stop();
let _ = recv.recv();

// println!("Closing microphone");
// mic.get_tracks()[0].close();
// std::thread::sleep(std::time::Duration::from_secs(2));
println!("Recording saved successfully!");
}
145 changes: 145 additions & 0 deletions src/io/echo_cancellation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
use crate::buffer::AudioBuffer;
use std::collections::VecDeque;
use std::sync::{Arc, Mutex};
use super::echo_reference::ECHO_REFERENCE_MANAGER;
use std::cell::RefCell;

// Wrapper to make EchoCanceller Send + Sync
pub struct EchoCanceller {
inner: RefCell<EchoCancellerInner>,
reference_buffer: Arc<Mutex<VecDeque<f32>>>,
}

struct EchoCancellerInner {
aec: aec_rs::Aec,
config: aec_rs::AecConfig,
frame_buffer: Vec<f32>,
reference_frame_buffer: Vec<f32>,
input_buffer: VecDeque<f32>,
output_buffer: VecDeque<f32>,
}

// SAFETY: EchoCanceller is only used from the audio thread
unsafe impl Send for EchoCanceller {}
unsafe impl Sync for EchoCanceller {}

impl EchoCanceller {
pub fn new(sample_rate: f32, frame_size: usize) -> Self {
let config = aec_rs::AecConfig {
sample_rate: sample_rate as u32,
filter_length: (sample_rate * 0.4) as i32, // 200ms filter (typical for room echo)
frame_size,
enable_preprocess: true, // Disable preprocessing to reduce artifacts
};

let aec = aec_rs::Aec::new(&config);

let reference_buffer = Arc::new(Mutex::new(VecDeque::new()));

// Register this buffer with the global echo reference manager
ECHO_REFERENCE_MANAGER.register_buffer(Arc::downgrade(&reference_buffer));

let inner = EchoCancellerInner {
aec,
config,
frame_buffer: vec![0.0; frame_size],
reference_frame_buffer: vec![0.0; frame_size],
input_buffer: VecDeque::new(),
output_buffer: VecDeque::new(),
};

Self {
inner: RefCell::new(inner),
reference_buffer,
}
}

pub fn get_reference_buffer(&self) -> Arc<Mutex<VecDeque<f32>>> {
Arc::clone(&self.reference_buffer)
}

pub fn process(&self, input: &[f32]) -> Vec<f32> {
let mut output = Vec::with_capacity(input.len());
let mut inner = self.inner.borrow_mut();

// Add input to buffer
inner.input_buffer.extend(input);

// Process complete frames
while inner.input_buffer.len() >= inner.config.frame_size {
// Fill frame buffer from input buffer
for i in 0..inner.config.frame_size {
inner.frame_buffer[i] = inner.input_buffer.pop_front().unwrap();
}

// Get reference samples from the buffer
{
let mut ref_buffer = self.reference_buffer.lock().unwrap();
for i in 0..inner.config.frame_size {
inner.reference_frame_buffer[i] = ref_buffer.pop_front().unwrap_or(0.0);
}
}

// Convert f32 to i16 for AEC processing
let mut input_i16: Vec<i16> = inner.frame_buffer
.iter()
.map(|&x| (x * 32767.0).clamp(-32768.0, 32767.0) as i16)
.collect();

let mut reference_i16: Vec<i16> = inner.reference_frame_buffer
.iter()
.map(|&x| (x * 32767.0).clamp(-32768.0, 32767.0) as i16)
.collect();

let mut output_i16 = vec![0i16; inner.config.frame_size];

// Apply echo cancellation
inner.aec.cancel_echo(&mut input_i16, &mut reference_i16, &mut output_i16);

// Convert back to f32 and add to output buffer
for &sample in output_i16.iter() {
inner.output_buffer.push_back(sample as f32 / 32767.0);
}
}

// Return available output, maintaining input size
for _ in 0..input.len() {
if let Some(sample) = inner.output_buffer.pop_front() {
output.push(sample);
} else {
// If we don't have enough output yet, return the input unprocessed
// This happens during initial buffering
let idx = output.len();
if idx < input.len() {
output.push(input[idx]);
}
}
}

output
}

pub fn add_reference_audio(&self, audio: &AudioBuffer) {
let mut ref_buffer = self.reference_buffer.lock().unwrap();
let inner = self.inner.borrow();

// Mix all channels to mono for reference
let num_samples = audio.length();
let num_channels = audio.number_of_channels();

for sample_idx in 0..num_samples {
let mut mixed_sample = 0.0;
for ch in 0..num_channels {
mixed_sample += audio.get_channel_data(ch)[sample_idx];
}
mixed_sample /= num_channels as f32;

ref_buffer.push_back(mixed_sample);

// Keep buffer size reasonable (max 1 second)
if ref_buffer.len() > inner.config.sample_rate as usize {
ref_buffer.pop_front();
}
}
}
}
62 changes: 62 additions & 0 deletions src/io/echo_reference.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
use std::collections::VecDeque;
use std::sync::{Arc, Mutex, Weak};

/// Global echo reference manager to share audio output with echo cancellers
pub struct EchoReferenceManager {
reference_buffers: Arc<Mutex<Vec<Weak<Mutex<VecDeque<f32>>>>>>,
}

impl EchoReferenceManager {
pub fn new() -> Self {
Self {
reference_buffers: Arc::new(Mutex::new(Vec::new())),
}
}

/// Register a reference buffer for echo cancellation
pub fn register_buffer(&self, buffer: Weak<Mutex<VecDeque<f32>>>) {
let mut buffers = self.reference_buffers.lock().unwrap();
// Clean up any dead weak references
buffers.retain(|b| b.upgrade().is_some());
buffers.push(buffer);
}

/// Send reference audio to all registered echo cancellers
pub fn send_reference(&self, audio: &[f32], channels: usize) {
let mut buffers = self.reference_buffers.lock().unwrap();

// Clean up dead references and send to alive ones
buffers.retain(|weak_buffer| {
if let Some(buffer) = weak_buffer.upgrade() {
if let Ok(mut buf) = buffer.lock() {
// Mix to mono if needed
if channels > 1 {
for frame in audio.chunks(channels) {
let mono_sample: f32 = frame.iter().sum::<f32>() / channels as f32;
buf.push_back(mono_sample);

// Keep buffer size reasonable (max 1 second at 48kHz)
if buf.len() > 48000 {
buf.pop_front();
}
}
} else {
buf.extend(audio);
// Keep buffer size reasonable
while buf.len() > 48000 {
buf.pop_front();
}
}
}
true
} else {
false
}
});
}
}

// Global instance
lazy_static::lazy_static! {
pub static ref ECHO_REFERENCE_MANAGER: EchoReferenceManager = EchoReferenceManager::new();
}
42 changes: 41 additions & 1 deletion src/io/microphone.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
use std::error::Error;
use std::sync::{Arc, Mutex};

use crate::buffer::{AudioBuffer, AudioBufferOptions};
use crate::io::AudioBackendManager;
use crate::RENDER_QUANTUM_SIZE;

use crossbeam_channel::{Receiver, Sender, TryRecvError};

use super::echo_cancellation::EchoCanceller;

pub(crate) struct MicrophoneStream {
receiver: Receiver<AudioBuffer>,
number_of_channels: usize,
sample_rate: f32,
stream: Box<dyn AudioBackendManager>,
echo_canceller: Option<Arc<Mutex<EchoCanceller>>>,
}

impl MicrophoneStream {
Expand All @@ -23,8 +27,27 @@ impl MicrophoneStream {
number_of_channels: backend.number_of_channels(),
sample_rate: backend.sample_rate(),
stream: backend,
echo_canceller: None,
}
}

pub(crate) fn with_echo_canceller(
receiver: Receiver<AudioBuffer>,
backend: Box<dyn AudioBackendManager>,
echo_canceller: Arc<Mutex<EchoCanceller>>,
) -> Self {
Self {
receiver,
number_of_channels: backend.number_of_channels(),
sample_rate: backend.sample_rate(),
stream: backend,
echo_canceller: Some(echo_canceller),
}
}

pub(crate) fn echo_canceller(&self) -> Option<&Arc<Mutex<EchoCanceller>>> {
self.echo_canceller.as_ref()
}
}

impl Drop for MicrophoneStream {
Expand All @@ -38,7 +61,7 @@ impl Iterator for MicrophoneStream {
type Item = Result<AudioBuffer, Box<dyn Error + Send + Sync>>;

fn next(&mut self) -> Option<Self::Item> {
let next = match self.receiver.try_recv() {
let mut next = match self.receiver.try_recv() {
Ok(buffer) => {
// new frame was ready
buffer
Expand All @@ -60,6 +83,23 @@ impl Iterator for MicrophoneStream {
return None;
}
};

// Apply echo cancellation if enabled
if let Some(echo_canceller) = &self.echo_canceller {
let canceller = echo_canceller.lock().unwrap();

// Process each channel through echo cancellation
let mut processed_channels = Vec::with_capacity(self.number_of_channels);

for ch in 0..self.number_of_channels {
let input_data = next.get_channel_data(ch);
let processed = canceller.process(input_data);
processed_channels.push(processed);
}

// Create new buffer with processed audio
next = AudioBuffer::from(processed_channels, self.sample_rate);
}

Some(Ok(next))
}
Expand Down
Loading