# How Meetily's Audio Ring Buffer Manages Synchronization Between Mic and System Streams

> Discover how Meetily's audio ring buffer synchronizes mic and system streams using separate buffers and fixed-size mixing windows for time-aligned output.

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

---

**Meetily's `AudioMixerRingBuffer` synchronizes independent microphone and system audio streams by maintaining separate `VecDeque<f32>` buffers for each source, extracting fixed-size mixing windows when either stream reaches the readiness threshold, and zero-padding any deficit to maintain time-aligned output for transcription and recording.**

Meetily is an open-source meeting transcription application that mixes microphone input with system audio output into a single synchronized track. The audio ring buffer that manages synchronization between mic and system streams is implemented in Rust within [`frontend/src-tauri/src/audio/pipeline.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/audio/pipeline.rs), where it handles asynchronous capture rates across separate threads while preventing drift and audio artifacts.

## Architecture of the AudioMixerRingBuffer

### Dual-Buffer Design

The `AudioMixerRingBuffer` struct defined at lines 16-23 in [`frontend/src-tauri/src/audio/pipeline.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/audio/pipeline.rs) allocates two independent `VecDeque<f32>` buffers: one for microphone samples and one for system audio samples. This separation allows each stream to accumulate data at its own rate while providing a unified interface for synchronized extraction.

### Timing Parameters

The constructor `AudioMixerRingBuffer::new(sample_rate)` (lines 27-36) calculates two critical timing thresholds based on the sample rate:

1. **Mixing window**: A fixed duration of approximately 50ms, converted to a sample count. This determines the chunk size for each mixing operation.
2. **Maximum buffer size**: Approximately 400ms of audio data. This upper bound prevents unbounded memory growth during network jitter or processing delays while tolerating temporary arrival rate mismatches.

## The Synchronization Pipeline

### Ingesting Samples with add_samples()

Incoming audio chunks from capture threads arrive via `add_samples(device_type, samples)` (lines 49-84). This method:

- Appends the floating-point samples to the appropriate internal buffer based on the `DeviceType::Microphone` or `DeviceType::System` discriminator
- Automatically trims the buffer tail if the total length exceeds the 400ms maximum, emitting overflow warnings when data is discarded
- Maintains chronological order without blocking, allowing capture threads to continue acquisition regardless of mixing backpressure

### Triggering Mix with can_mix()

The `can_mix()` method (lines 87-90) implements the synchronization trigger logic. It returns `true` when **either** buffer contains at least one full 50ms mixing window. This design choice permits the mixing loop to proceed as soon as one stream is ready, rather than waiting for both, which minimizes latency while allowing the lagging stream to catch up in subsequent iterations.

### Window Extraction and Zero-Padding

`extract_window()` (lines 92-139) handles the critical task of creating time-aligned audio slices:

- Pulls exactly one mixing window (50ms) from each buffer
- If a buffer lacks sufficient samples, the function **zero-pads** the missing positions with silence (0.0f32) rather than waiting or reusing old samples
- Returns a tuple of `Vec<f32>` windows guaranteed to be identical in length, ensuring the downstream mixer receives matched pairs

This zero-padding strategy prevents temporal drift and avoids the echo artifacts or pitch distortions that would result from sample stretching or repetition.

### Professional Mixing

The extracted windows are passed to `ProfessionalAudioMixer::mix_window`, which performs simple additive mixing followed by soft scaling. If the summed samples exceed the ±1.0 range, the mixer scales the entire window down proportionally to prevent hard clipping while preserving the relative dynamics of both sources.

## Continuous Processing in AudioPipeline::run

Inside `AudioPipeline::run` (lines 817-830), a tight processing loop maintains synchronization:

```rust
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);
        // Mixed audio is sent to VAD for transcription and WAV recorder
    }
}

```

This loop continuously drains the buffer whenever a full window is available, processing all ready data before yielding. It handles scenarios where the microphone delivers chunks every 10ms while system audio arrives every 20ms, ensuring the output stream remains regular regardless of input irregularity.

## Why This Design Prevents Audio Drift

**Asynchronous Stream Handling**: Microphone and system audio capture operate on separate OS threads with independent callbacks. The ring buffer decouples these arrival patterns from the processing timeline, allowing each stream to "catch up" during high-load periods without dropping data.

**Jitter Absorption**: The 400ms maximum buffer capacity absorbs temporary bursts from the OS audio stack or garbage collection pauses. Without this headroom, overflow would force sample dropping, creating audible glitches in the recording.

**Zero-Padding Strategy**: When one source temporarily stalls—such as when system audio mutes during silence—the insertion of calculated silence maintains the temporal alignment of the mixed track. This ensures that when audio resumes, it remains synchronized with the microphone stream rather than drifting out of phase.

**Soft Scaling**: The mixer's approach to preventing clipping preserves the integrity of both streams without introducing compression artifacts, maintaining transcription accuracy for speech while retaining background audio context.

## Implementation Example

To initialize the pipeline with the ring buffer:

```rust
let mut manager = AudioPipelineManager::new();
manager.start(
    recording_state.clone(),
    transcription_sender,
    target_chunk_duration_ms,
    sample_rate,
    Some(recording_sender),
    mic_device_name,
    mic_device_kind,
    system_device_name,
    system_device_kind,
)?;

```

Feeding captured data into the ring buffer:

```rust
// Inside the audio capture callback
let chunk = AudioChunk {
    data: mono_data,
    sample_rate,
    timestamp,
    chunk_id,
    device_type: DeviceType::Microphone, // or DeviceType::System
};
state.send_audio_chunk(chunk)?;

```

The `AudioPipeline` routes these chunks to the buffer:

```rust
self.ring_buffer.add_samples(chunk.device_type.clone(), chunk.data);

```

## Summary

- **Dual `VecDeque<f32>` buffers** in `AudioMixerRingBuffer` isolate microphone and system streams while maintaining temporal alignment through independent ingestion.
- **50ms mixing windows** with **zero-padding** ensure both streams contribute equally-sized slices to the mixer, preventing phase drift when streams arrive at different rates.
- **400ms maximum capacity** absorbs OS-level jitter without audio drops, balancing latency tolerance against memory constraints.
- **Soft-scaled additive mixing** in `ProfessionalAudioMixer::mix_window` prevents clipping when combining streams while preserving dynamic range.
- **Continuous while-loop** in `AudioPipeline::run` processes windows as soon as either stream reaches the threshold, minimizing end-to-end latency for transcription.

## Frequently Asked Questions

### What happens if one audio stream stops sending data?

The `extract_window()` method detects buffer underflow and zero-pads the missing samples to maintain the fixed 50ms window size. This inserts silence rather than creating artifacts or repeating old audio, allowing the active stream to continue recording while preserving synchronization for when the stalled stream resumes.

### How does the buffer handle different sample rates?

The `AudioMixerRingBuffer` initializes with a specific `sample_rate` parameter used to calculate window sizes in sample counts. The pipeline expects both input streams to match this rate; resampling occurs upstream in the capture devices before chunks enter the ring buffer.

### What is the maximum latency introduced by the ring buffer?

The buffer introduces between 50ms and 400ms of latency depending on fill levels. The minimum latency equals one mixing window (~50ms), while the 400ms maximum represents the overflow threshold where oldest samples are discarded. Typical operation maintains 50-100ms of buffering during steady-state capture.

### How does zero-padding affect transcription accuracy?

Zero-padding inserts calculated silence rather than interpolating or repeating audio content, ensuring the Voice Activity Detection (VAD) processor receives clean, time-aligned data. This prevents false transcriptions that would occur if the buffer duplicated microphone samples to fill gaps in system audio, maintaining accurate speaker diarization and timing.