Meetily Ring Buffer Implementation: Synchronizing Microphone and System Audio Streams

Meetily uses a custom AudioMixerRingBuffer struct in frontend/src-tauri/src/audio/pipeline.rs to align microphone and system audio streams before mixing, employing dual VecDeque<f32> buffers with a fixed 50 ms window and zero-padding to handle jitter.

Meetily solves the challenging problem of synchronizing independent audio capture sources through a specialized ring buffer implementation. When capturing both microphone input and system audio playback simultaneously, timing drift and OS-level jitter can cause misalignment. The AudioMixerRingBuffer struct in the Tauri-based Rust backend ensures temporal alignment by buffering streams until matching windows are available for mixing.

How the Ring Buffer Implementation Works in Meetily

The AudioMixerRingBuffer Structure

The ring buffer implementation centers on the AudioMixerRingBuffer struct defined at lines 16-23 in frontend/src-tauri/src/audio/pipeline.rs. This structure maintains two independent VecDeque<f32> buffers—mic_buffer and system_buffer—that accumulate samples from their respective audio sources. Each buffer is configured with a fixed window_size_samples representing approximately 50 ms of audio, and a max_buffer_size capped at eight windows (roughly 400 ms) to prevent memory overflow during jitter spikes, particularly on macOS Core Audio systems.

Buffer Management and Overflow Protection

Incoming audio chunks arrive asynchronously through the add_samples() method, which accepts a DeviceType enum (Microphone or System) and routes the Vec<f32> samples to the appropriate deque. The implementation includes automatic overflow protection: when either buffer exceeds max_buffer_size, the oldest samples are dropped from the front using pop_front(), and warnings are logged to alert developers of potential capture issues.

fn add_samples(&mut self, device_type: DeviceType, samples: Vec<f32>) {
    match device_type {
        DeviceType::Microphone => self.mic_buffer.extend(samples),
        DeviceType::System => self.system_buffer.extend(samples),
    }
    // Trim overflow, emit warnings
    while self.mic_buffer.len() > self.max_buffer_size {
        self.mic_buffer.pop_front();
    }
    while self.system_buffer.len() > self.max_buffer_size {
        self.system_buffer.pop_front();
    }
}

Synchronization Logic and Window Extraction

The can_mix() method determines readiness by checking if either buffer contains at least one full window of samples. When true, extract_window() drains a complete window from both buffers simultaneously. If one stream has insufficient data, the implementation applies zero-padding to prevent audio artifacts, ensuring both returned vectors contain exactly the same number of samples. This returns an Option<(Vec<f32>, Vec<f32>)> containing the synchronized microphone and system windows.

fn can_mix(&self) -> bool {
    self.mic_buffer.len() >= self.window_size_samples ||
    self.system_buffer.len() >= self.window_size_samples
}

fn extract_window(&mut self) -> Option<(Vec<f32>, Vec<f32>)> {
    if !self.can_mix() { return None; }
    // Drain a full window or zero-pad if insufficient data
    // ... returns (mic_window, sys_window) ...
}

Integrating the Ring Buffer with the Audio Pipeline

The AudioPipeline struct orchestrates the mixing process using the ring buffer. Within the run() method (lines 17-24), captured chunks are fed into the ring buffer via add_samples(). A loop continuously checks can_mix(), and upon success, extracts aligned windows and passes them to the ProfessionalAudioMixer. The mixed output then feeds into the Voice Activity Detection (VAD) pipeline and the recording manager, ensuring perfectly synchronized transcription of both audio sources.

// Inside AudioPipeline::run()
self.ring_buffer.add_samples(chunk.device_type.clone(), chunk.data);
while self.ring_buffer.can_mix() {
    if let Some((mic_window, sys_window)) = self.ring_buffer.extract_window() {
        let mixed = self.mixer.mix_window(&mic_window, &sys_window);
        // Send to VAD / recorder ...
    }
}

Practical Code Examples

Initializing the Ring Buffer

Create a new ring buffer instance by specifying the sample rate. The constructor automatically calculates the window size and maximum buffer capacity.

use meetily::audio::pipeline::AudioMixerRingBuffer;

// Initialize for 48kHz audio capture
let mut ring_buffer = AudioMixerRingBuffer::new(48_000);

Feeding Audio Streams

Push samples from capture callbacks into the appropriate buffer using the DeviceType enum to distinguish sources.

use meetily::audio::recording_state::DeviceType;

// Feed microphone samples
ring_buffer.add_samples(DeviceType::Microphone, mic_chunk);

// Feed system (desktop) samples
ring_buffer.add_samples(DeviceType::System, system_chunk);

Extracting and Mixing Synchronized Windows

Always check can_mix() before extraction. The returned windows are guaranteed to be the same length, with zero-padding applied to shorter streams.

if ring_buffer.can_mix() {
    if let Some((mic_win, sys_win)) = ring_buffer.extract_window() {
        // `mic_win` and `sys_win` now contain exactly the same number of samples
        let mixed = mixer.mix_window(&mic_win, &sys_win);
        // Process mixed audio for VAD or WAV recording...
    }
}

Summary

  • Dual Buffer Architecture: AudioMixerRingBuffer uses separate VecDeque<f32> buffers for microphone and system audio to handle asynchronous arrival times.
  • Jitter Absorption: The implementation enforces a 50 ms mixing window with an 800 ms maximum capacity (8 windows) to accommodate system jitter without losing data.
  • Automatic Overflow Handling: The add_samples() method routes data by DeviceType and automatically trims excess samples, logging warnings when buffers exceed safe limits.
  • Zero-Padding Synchronization: The extract_window() method ensures temporal alignment by zero-padding deficient streams, enabling artifact-free mixing in ProfessionalAudioMixer.
  • Pipeline Integration: Located in frontend/src-tauri/src/audio/pipeline.rs, this ring buffer implementation feeds the VAD-driven transcription pipeline and mixed-audio recorder.

Frequently Asked Questions

Why does Meetily use a ring buffer instead of simple timestamp matching?

Timestamp matching alone fails when audio drivers deliver chunks at irregular intervals or when system load causes jitter. The ring buffer implementation accumulates samples into fixed windows, allowing the extract_window() method to wait until both streams have sufficient data, effectively decoupling the capture timing from the processing logic according to the source code in pipeline.rs.

How does the ring buffer handle when one audio source stops producing data?

If one stream's buffer falls below window_size_samples while the other continues receiving data, can_mix() will eventually return true for the full buffer while the empty buffer remains short. The extract_window() method detects this imbalance and applies zero-padding to the deficient side, preventing pipeline stalls while maintaining audio continuity for the transcription workflow.

What is the performance impact of the 50ms buffering delay?

The 50ms window introduces minimal latency while providing enough samples for the Voice Activity Detection (VAD) algorithm to analyze audio context. As implemented in the Meetily source code, this trade-off ensures reliable synchronization without perceptible delay, as the 400ms maximum buffer safely absorbs typical macOS Core Audio jitter without blocking the capture threads.

Can the ring buffer size be adjusted for different latency requirements?

Yes. The new() constructor in frontend/src-tauri/src/audio/pipeline.rs calculates window_size_samples from the sample rate and window duration, and max_buffer_size is set to eight times that value. Developers can modify these constants to tune the buffer for lower latency or higher jitter tolerance, though the current defaults are optimized for typical meeting transcription scenarios.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →