# How Meetily Synchronizes Microphone and System Audio Using Ring Buffers

> Learn how Meetily synchronizes microphone and system audio using ring buffers. Discover its efficient audio pipeline for perfect synchronization before mixing.

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

---

**Meetily aligns asynchronous microphone and system audio streams by buffering them in separate ring buffers, extracting fixed-size windows when either buffer fills, and zero-padding the shorter stream to maintain perfect synchronization before mixing.**

Meetily is an open-source meeting transcription tool that captures both microphone input and system audio simultaneously. Because these two hardware sources operate on independent clocks and suffer from network or OS-level jitter, the application implements a sophisticated ring-buffer-based synchronizer in its Rust audio pipeline. This system ensures that Whisper receives perfectly aligned audio chunks regardless of timing drift between capture devices.

## Ring Buffer Architecture in [`pipeline.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/pipeline.rs)

The synchronization core is the `AudioMixerRingBuffer` struct defined in [[`frontend/src-tauri/src/audio/pipeline.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/audio/pipeline.rs)](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/audio/pipeline.rs#L16-L24). This dual-channel buffer maintains separate FIFO queues for each audio source:

```rust
struct AudioMixerRingBuffer {
    mic_buffer: VecDeque<f32>,
    system_buffer: VecDeque<f32>,
    window_size_samples: usize,   // e.g., 50 ms worth of samples
    max_buffer_size: usize,         // safety limit (≈ 400 ms)
}

```

**`window_size_samples`** is calculated based on the device sample rate and a target 50 ms mixing window in the `new` constructor ([`pipeline.rs#L26-34`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/audio/pipeline.rs#L26-L34)). The **`max_buffer_size`** caps each buffer at approximately 400 ms to absorb temporary jitter—particularly important for macOS Core Audio latency fluctuations—while preventing unbounded memory growth.

## Feeding Samples from Multiple Sources

When audio chunks arrive from the capture threads, the pipeline invokes `add_samples` ([`pipeline.rs#L49-64`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/audio/pipeline.rs#L49-L64)) to append them to the appropriate buffer:

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

```

This method performs periodic buffer health logging and enforces the `max_buffer_size` limit by trimming excess samples. If overflow occurs, the system emits diagnostic warnings via `warn` and `error` macros ([`pipeline.rs#L66-76`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/audio/pipeline.rs#L66-L76)), allowing developers to detect capture device anomalies without crashing the recording session.

## Window-Based Synchronization Logic

The pipeline determines readiness for mixing through the `can_mix` method ([`pipeline.rs#L87-90`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/audio/pipeline.rs#L87-L90)):

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

```

When `can_mix` returns true, `extract_window` ([`pipeline.rs#L92-140`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/audio/pipeline.rs#L92-L140)) pulls exactly `window_size_samples` from each channel. If one buffer lacks sufficient data, the remaining samples are **zero-padded** (filled with silence) rather than repeating the last sample. This approach eliminates audible artifacts like clicking or echoing when one source lags behind the other.

## The Mixing Stage

Aligned windows pass to `ProfessionalAudioMixer::mix_window` ([`pipeline.rs#L54-84`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/audio/pipeline.rs#L54-L84)), which combines the streams into a single output buffer:

- **System audio** receives a configurable scaling factor (currently 1.0) to balance against microphone levels.
- **Soft scaling** prevents hard clipping by checking the absolute sum of samples and applying gain reduction when the mixed signal approaches the -1.0 to +1.0 bounds ([`pipeline.rs#L77-84`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/audio/pipeline.rs#L77-L84)).

The resulting mixed PCM buffer is then forwarded to `RecordingSaver` for disk persistence and/or passed to the Whisper transcription engine.

## End-to-End Pipeline Flow

1. **Capture** – `AudioCapture` spawns concurrent threads for microphone ([[`capture/microphone.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/capture/microphone.rs)](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/audio/capture/microphone.rs)) and system audio ([[`capture/system.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/capture/system.rs)](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/audio/capture/system.rs)).
2. **Buffer** – Each chunk calls `ring_buffer.add_samples(device_type, samples)` to feed the ring buffers.
3. **Sync** – The recording manager polls `can_mix()` to detect when a full window is available.
4. **Extract** – `extract_window()` returns aligned `(mic_window, system_window)` tuples, zero-padding the shorter stream.
5. **Mix** – `ProfessionalAudioMixer` combines the windows with clipping protection.
6. **Emit** – The final buffer routes to [`recording_saver.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/recording_saver.rs) for file writing and transcription processing.

This architecture automatically corrects clock drift: if the microphone produces samples faster than system audio, excess mic samples remain in the ring buffer for the next window, while missing system samples are replaced with silence, maintaining constant output latency.

## Summary

- **Dual-ring architecture** – Separate `VecDeque<f32>` buffers isolate microphone and system audio streams.
- **Deterministic windows** – Fixed 50 ms mixing intervals ensure uniform chunk sizes for Whisper transcription.
- **Zero-padding strategy** – Silence injection prevents artifacts when sources drift out of sync.
- **Soft clipping protection** – Dynamic scaling in `ProfessionalAudioMixer` preserves audio quality during loud passages.
- **Jitter resilience** – 400 ms `max_buffer_size` accommodates OS-level latency fluctuations without data loss.

## Frequently Asked Questions

### What happens if one audio source stops producing samples?

If either the microphone or system audio stream stalls, `extract_window` continues to output mixed audio by zero-padding the missing source. This prevents the transcription pipeline from blocking and eliminates repetitive artifacts that would occur if the last valid sample were repeated.

### Why does Meetily use ring buffers instead of simple vectors?

Ring buffers (implemented via `VecDeque`) provide O(1) push and pop operations from both ends, allowing efficient FIFO management. According to the Meetily source code, this is critical for real-time performance when handling two asynchronous streams with different hardware latency characteristics.

### How does the system handle sample rate mismatches between devices?

The `AudioMixerRingBuffer` assumes both streams operate at the same sample rate specified during instantiation (typically 48 kHz). The capture modules in [`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) resample hardware input to match this target rate before calling `add_samples`, ensuring the ring buffer receives homogenous PCM data.

### Where is the mixing logic located in the codebase?

The core mixing implementation resides in [`frontend/src-tauri/src/audio/pipeline.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/audio/pipeline.rs), specifically within the `ProfessionalAudioMixer` struct and its `mix_window` method. The orchestration logic that coordinates capture threads and triggers mixing is found in [[`recording_manager.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/recording_manager.rs)](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/audio/recording_manager.rs).