# How Meetily Synchronizes Asynchronous Audio Streams Using a Ring Buffer Mechanism

> Discover how Meetily synchronizes asynchronous audio streams using a lock-free ring buffer mechanism. Learn to align and mix audio samples efficiently for seamless real-time communication.

- Repository: [Zackriya Solutions/meetily](https://github.com/Zackriya-Solutions/meetily)
- Tags: internals
- Published: 2026-07-21

---

**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`](https://github.com/Zackriya-Solutions/meetily/blob/main/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.

```rust
// 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`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/audio/capture/microphone.rs) and [`frontend/src-tauri/src/audio/capture/system.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/audio/capture/system.rs) while maintaining lock-free operation.

```rust
// 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`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/audio/vad.rs) for transcription analysis, and optionally to a recording sink for file output.