# How Meetily Synchronizes Microphone and System Audio Using Ring Buffers

> Discover how Meetily synchronizes microphone and system audio using ring buffers for precise Whisper transcription. Learn about zero-padding and deterministic mixing.

- Repository: [Zackriya Solutions/meetily](https://github.com/Zackriya-Solutions/meetily)
- Tags: deep-dive
- Published: 2026-07-12

---

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

```rust
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 the **[`new`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/audio/pipeline.rs#L26-L34)** constructor).
- **`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.

```rust
match device_type {
    DeviceType::Microphone => self.mic_buffer.extend(samples),
    DeviceType::System => self.system_buffer.extend(samples),
}

```

This method performs two critical maintenance tasks:
1. **Health logging** – Periodically reports buffer depths for diagnostic monitoring.
2. **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:

```rust
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_abs` calculation 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

1. **Capture** – `AudioCapture` streams microphone and system audio concurrently from [`capture/microphone.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/capture/microphone.rs) and [`capture/system.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/capture/system.rs).
2. **Push** – Each chunk calls `ring_buffer.add_samples(device_type, samples)` in [`recording_manager.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/recording_manager.rs).
3. **Window Ready?** – The manager loops, polling `ring_buffer.can_mix()`.
4. **Grab Window** – `extract_window()` returns aligned vectors, zero-padding when one source lags.
5. **Mix** – `ProfessionalAudioMixer::mix_window` produces a single PCM buffer.
6. **Emit** – Mixed audio routes to [`recording_saver.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/recording_saver.rs) and 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:

```rust
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:

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