How Meetily Synchronizes Asynchronous Audio Streams Using a Ring Buffer Mechanism

Meetily employs a lock-free AudioMixerRingBuffer with independent VecDeque<f32> queues for microphone and system audio to accumulate samples, align them into fixed 50-millisecond windows, and emit synchronized pairs for mixing regardless of asynchronous arrival times.

Meetily captures audio from two independent sources—microphone input and system output—that produce samples at different rates and timings. To combine these streams for transcription and recording without glitches, the application implements a deterministic ring buffer mechanism in the Zackriya-Solutions/meetily repository. This approach ensures temporal coherence even when underlying capture tasks run on separate asynchronous threads.

AudioMixerRingBuffer Architecture

The core synchronization logic resides in frontend/src-tauri/src/audio/pipeline.rs, which defines the AudioMixerRingBuffer struct. This structure maintains two independent buffers: mic_buffer and system_buffer, both implemented as VecDeque<f32>.

The ring buffer operates with two critical size constraints:

  • window_size_samples: Calculated from the sample rate (typically 48 kHz) to represent approximately 50 milliseconds of audio
  • max_buffer_size: Caps each queue at roughly 400 milliseconds to prevent unbounded memory growth

By isolating each stream into its own queue, the system can handle variable latency and jitter from either capture source without blocking the other.

Buffering Samples from Independent Streams

When audio chunks arrive from the capture tasks, the add_samples method (lines 49-84) appends data to the appropriate queue based on a DeviceType flag. This method handles both DeviceType::Microphone and DeviceType::System variants.

The implementation performs automatic overflow protection. If a buffer exceeds max_buffer_size, the method trims excess samples from the front of the queue. This safeguard ensures that temporary stalls in downstream processing do not cause distortion-inducing buffer overruns.

// Simulating incoming audio chunks from capture tasks
let mic_chunk = vec![0.1_f32; 2400]; // 50ms at 48kHz
let sys_chunk = vec![0.05_f32; 2400];

// Add to respective buffers
ring.add_samples(DeviceType::Microphone, mic_chunk);
ring.add_samples(DeviceType::System, sys_chunk);

Window Alignment and Synchronization

The ring buffer determines when sufficient data exists for mixing through the can_mix method (lines 87-90). This method returns true when either queue contains enough samples for a full window. This asynchronous design prevents pipeline stalls if one stream temporarily lags.

The extract_window method (lines 92-141) retrieves synchronized pairs:

  1. Pulls window_size_samples from each queue
  2. Applies zero-padding (silence) if one stream lacks sufficient data
  3. Returns a (mic_window, sys_window) tuple of equal length

Zero-padding preserves audio continuity and prevents audible artifacts when one source temporarily stalls, ensuring the mixed output maintains consistent timing.

Integration with the Audio Pipeline

In the main processing loop within AudioPipeline::run (lines 818-828), the pipeline orchestrates the mixing workflow:

  1. Receives AudioChunk structures from capture tasks
  2. Routes chunks to self.ring_buffer.add_samples(chunk.device_type.clone(), chunk.data)
  3. Checks while self.ring_buffer.can_mix() to process ready windows
  4. Mixes extracted windows using self.mixer.mix_window
  5. Forwards mixed audio to VAD processing and optional recording sinks

This loop runs continuously, handling asynchronous arrivals from frontend/src-tauri/src/audio/capture/microphone.rs and frontend/src-tauri/src/audio/capture/system.rs while maintaining lock-free operation.

// Main pipeline loop (simplified)
while let Ok(chunk) = audio_receiver.recv() {
    // Buffer the incoming chunk
    self.ring_buffer.add_samples(chunk.device_type, chunk.data);
    
    // Process all ready windows
    while self.ring_buffer.can_mix() {
        if let Some((mic_win, sys_win)) = self.ring_buffer.extract_window() {
            let mixed = self.mixer.mix_window(&mic_win, &sys_win);
            // Forward to VAD/transcription
            self.process_mixed_audio(mixed);
        }
    }
}

Summary

  • Meetily uses an AudioMixerRingBuffer with dual VecDeque<f32> queues to isolate microphone and system audio streams.
  • The buffer aligns asynchronous inputs into fixed 50-millisecond windows using can_mix and extract_window methods.
  • Zero-padding handles temporary stalls, ensuring continuous output when one stream lags behind the other.
  • Automatic overflow protection via max_buffer_size prevents distortion from buffer overruns.
  • Lock-free operation in the main pipeline loop enables real-time mixing without blocking either capture task.

Frequently Asked Questions

How does Meetily handle audio streams that arrive at different speeds?

Meetily buffers each stream independently in separate VecDeque queues within the AudioMixerRingBuffer. The can_mix method checks if either queue has enough samples for a full window, allowing the pipeline to continue processing the faster stream while waiting for the slower one to catch up. When extracting windows, extract_window zero-pads any missing samples to maintain temporal alignment.

What prevents the ring buffer from consuming unlimited memory?

The add_samples method enforces a max_buffer_size limit (approximately 400 milliseconds) on each queue. When a buffer exceeds this threshold, the method automatically trims excess samples from the front of the queue. This prevents unbounded growth during temporary downstream stalls or when one capture task significantly outpaces the other.

Why does Meetily use a ring buffer instead of simple thread synchronization?

A ring buffer provides non-blocking, lock-free accumulation suitable for real-time audio processing. Because microphone and system capture run on separate async tasks with different latencies and jitter characteristics, blocking one stream to wait for the other would introduce unacceptable latency. The ring buffer decouples capture timing from mixing, allowing each task to run at full speed while the pipeline consumes aligned windows deterministically.

Where is the mixed audio sent after extraction from the ring buffer?

After extract_window retrieves synchronized pairs, the AudioPipeline::run method passes them to self.mixer.mix_window for combination. The resulting mixed audio flows to the Voice Activity Detection (VAD) processor in frontend/src-tauri/src/audio/vad.rs for transcription analysis, and optionally to a recording sink for file output.

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 →