How Meetily Synchronizes Microphone and System Audio Using Ring Buffers
Meetily uses a dual ring-buffer architecture (AudioMixerRingBuffer) to align microphone and system audio streams into fixed-size windows, zero-padding shorter buffers to prevent drift while maintaining deterministic mixing for Whisper transcription.
Meetily is an open-source meeting assistant written in Rust that captures both microphone input and system audio simultaneously. Because these two streams arrive at slightly different rates and suffer from platform-specific jitter, the pipeline implements a sophisticated ring-buffer-based synchronizer in frontend/src-tauri/src/audio/pipeline.rs to align them before mixing and transcription.
The Ring Buffer Architecture
The core synchronization primitive is the AudioMixerRingBuffer struct defined at lines 16-24 of pipeline.rs.
struct AudioMixerRingBuffer {
mic_buffer: VecDeque<f32>,
system_buffer: VecDeque<f32>,
window_size_samples: usize, // 50ms worth of samples
max_buffer_size: usize, // Safety limit (~400ms)
}
Key fields:
mic_buffer/system_buffer–VecDeque<f32>instances that hold raw PCM samples for each source in FIFO order.window_size_samples– Computed from the device sample rate and a fixed 50ms mixing interval (see thenewconstructor).max_buffer_size– Caps total buffer growth to approximately 400ms, a limit specifically added to tolerate macOS Core Audio jitter without unbounded memory growth.
Feeding Samples into the Buffer
When audio chunks arrive from either capture device, the pipeline invokes add_samples (lines 49-64) to push data into the appropriate queue.
match device_type {
DeviceType::Microphone => self.mic_buffer.extend(samples),
DeviceType::System => self.system_buffer.extend(samples),
}
This method performs two critical maintenance tasks:
- Health logging – Periodically reports buffer depths for diagnostic monitoring.
- Overflow protection – Automatically trims samples that exceed
max_buffer_size, emitting warning logs (lines 66-76) to alert developers of potential capture latency issues.
Detecting When to Mix
Before extracting audio, the pipeline checks can_mix (lines 87-90) to determine if at least one full window is available:
fn can_mix(&self) -> bool {
self.mic_buffer.len() >= self.window_size_samples ||
self.system_buffer.len() >= self.window_size_samples
}
This boolean gate ensures the mixer never operates on partial windows, maintaining consistent chunk sizes required by downstream Whisper transcription.
Extracting Synchronized Windows
The extract_window method (lines 92-140) pulls exactly window_size_samples from both buffers, handling temporal drift through zero-padding:
- If a buffer lacks sufficient samples, the remaining space is filled with silence (zeros) rather than repeating the last sample.
- When both buffers are empty, a full silence window is returned to keep the pipeline flowing.
The function returns Option<(Vec<f32>, Vec<f32>)> containing aligned microphone and system windows ready for mixing.
The Mixing Stage
Once windows are extracted, ProfessionalAudioMixer combines them via mix_window (lines 54-84).
Mixing characteristics:
- System audio receives light scaling (factor 1.0) while microphone audio remains at full level.
- Soft scaling logic prevents hard clipping by normalizing summed samples to the [-1.0, +1.0] range (see the
sum_abscalculation at lines 77-84).
The resulting mixed buffer is then forwarded to RecordingSaver for disk persistence and to the Whisper engine for real-time transcription.
End-to-End Pipeline Flow
- Capture –
AudioCapturestreams microphone and system audio concurrently fromcapture/microphone.rsandcapture/system.rs. - Push – Each chunk calls
ring_buffer.add_samples(device_type, samples)inrecording_manager.rs. - Window Ready? – The manager loops, polling
ring_buffer.can_mix(). - Grab Window –
extract_window()returns aligned vectors, zero-padding when one source lags. - Mix –
ProfessionalAudioMixer::mix_windowproduces a single PCM buffer. - Emit – Mixed audio routes to
recording_saver.rsand the transcription pipeline.
Because the ring buffer preserves the most recent samples (FIFO), drift automatically corrects: excess microphone samples drop, missing system samples fill with silence, and the mixing window size remains constant.
Implementation Example
Creating and using the synchronizer requires minimal boilerplate:
use meetily::audio::pipeline::{AudioMixerRingBuffer, ProfessionalAudioMixer};
use meetily::audio::recording_state::DeviceType;
// Initialize for 48kHz capture
let mut ring = AudioMixerRingBuffer::new(48_000);
let mut mixer = ProfessionalAudioMixer::new(48_000);
// Feed incoming chunks
ring.add_samples(DeviceType::Microphone, mic_chunk);
ring.add_samples(DeviceType::System, system_chunk);
// Process when ready
if let Some((mic_win, sys_win)) = ring.extract_window() {
let mixed = mixer.mix_window(&mic_win, &sys_win);
// Forward to file or transcription
}
For continuous streaming, integrate within an async loop:
async fn process_audio(
mut mic_stream: impl Stream<Item = Vec<f32>>,
mut sys_stream: impl Stream<Item = Vec<f32>>,
) {
let mut ring = AudioMixerRingBuffer::new(48_000);
let mut mixer = ProfessionalAudioMixer::new(48_000);
loop {
if let Some(mic) = mic_stream.next().await {
ring.add_samples(DeviceType::Microphone, mic);
}
if let Some(sys) = sys_stream.next().await {
ring.add_samples(DeviceType::System, sys);
}
while let Some((mic_w, sys_w)) = ring.extract_window() {
let mixed = mixer.mix_window(&mic_w, &sys_w);
recording_saver.save_chunk(mixed).await;
}
}
}
Summary
- Dual VecDeque buffers isolate microphone and system audio streams while maintaining FIFO ordering.
- Zero-padding strategy prevents audible artifacts when one source under-runs, avoiding the "repeating sample" distortion common in naive synchronizers.
- Fixed 50ms windows provide deterministic chunk sizes required by Whisper transcription models.
- Soft clipping protection in
ProfessionalAudioMixerpreserves audio quality during mixing. - 400ms safety limits absorb macOS Core Audio jitter without unbounded memory growth.
Frequently Asked Questions
Why does Meetily use ring buffers instead of simple vectors?
Ring buffers (implemented via VecDeque) provide O(1) insertion and removal at both ends with automatic FIFO ordering. According to the Meetily source code, this allows the extract_window method to efficiently drop old samples when buffers exceed max_buffer_size, preventing memory leaks during long recording sessions while preserving the most recent audio data.
What happens if the microphone source drops while system audio continues?
The extract_window method detects the underrun and zero-pads the microphone buffer instead of repeating the last sample. This produces silence for the missing microphone data while preserving the system audio timeline, preventing the "stuck audio" artifacts that occur when buffers repeat stale samples.
How does the 50ms window size benefit transcription accuracy?
Whisper and similar transcription engines process audio in fixed temporal chunks. By enforcing exactly 50ms windows (2400 samples at 48kHz) through the window_size_samples parameter, Meetily ensures the transcription pipeline receives uniform buffer sizes regardless of capture jitter, eliminating alignment errors that could degrade recognition accuracy.
Can developers adjust the buffer safety limits for different platforms?
Yes. The max_buffer_size is calculated in the new constructor based on the sample rate, but the underlying structure supports custom limits. Developers can modify the initialization logic in pipeline.rs to increase the 400ms ceiling for platforms with extreme scheduler jitter, or decrease it for real-time constrained environments where latency is critical.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →