# Meetily AudioMixerRingBuffer and ProfessionalAudioMixer: Dual-Stream Audio Processing Explained

> Explore Meetily's AudioMixerRingBuffer and ProfessionalAudioMixer for synchronized audio streams. Achieve clean, distortion-free audio for Whisper transcription and recording.

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

---

**The AudioMixerRingBuffer and ProfessionalAudioMixer components in Meetily synchronize microphone and system audio streams using a lock-free ring buffer and soft-scaling mixer to produce clean, distortion-free audio for Whisper transcription and recording.**

Meetily captures two independent audio streams—microphone input and system audio—which arrive on separate threads with potentially different sample rates and timing characteristics. To deliver a single, temporally-aligned track to the Whisper transcription engine while preventing distortion, the application relies on two specialized components defined in [`frontend/src-tauri/src/audio/pipeline.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/audio/pipeline.rs): the **AudioMixerRingBuffer** for temporal alignment and the **ProfessionalAudioMixer** for amplitude-preserving mixing.

## AudioMixerRingBuffer: Lock-Free Synchronization

The **AudioMixerRingBuffer** serves as a FIFO buffer that aligns microphone and system audio samples into consistent time windows. It maintains two `VecDeque<f32>` queues—`mic_buffer` and `system_buffer`—defined at lines 19-21 of [`pipeline.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/pipeline.rs).

### Window Configuration and Buffer Sizing

The buffer calculates window sizes based on a 600-millisecond mixing window (`window_ms = 600.0`), yielding `window_size_samples` relative to the sample rate (lines 27-30). To accommodate jitter from system audio capture, the maximum buffer length is set to eight times the window size (lines 35-36), preventing overflow during temporary latency spikes while maintaining approximately 400ms of tolerance.

### Sample Ingestion and Health Monitoring

The `add_samples(device_type, samples)` method pushes incoming audio into the appropriate buffer, logging health metrics every 200 calls. If a buffer exceeds `max_buffer_size`, the oldest data is automatically dropped to prevent memory exhaustion (lines 49-85). Periodic `debug!` and `warn!` messages expose buffer sizes and overflow events, aiding diagnostics across macOS, Windows, and Linux platforms (lines 55-70).

### Temporal Alignment and Window Extraction

The `can_mix()` method returns `true` when either buffer contains at least `window_size_samples`, allowing the pipeline to proceed as soon as sufficient data is available (lines 87-90). The `extract_window()` method drains a full window from each buffer, zero-padding any missing samples to maintain identical lengths. This zero-padding strategy prevents artifacts that would arise from "hold-last-sample" approaches (lines 92-139).

## ProfessionalAudioMixer: Soft-Clipping Mix

The **ProfessionalAudioMixer** combines synchronized audio windows while preserving loudness and avoiding harsh clipping. Unlike simple truncation mixers, this component implements soft-scaling to maintain waveform integrity and broadcast-standard loudness.

### Soft-Scaling Algorithm

The `mix_window(mic_window, sys_window)` method iterates over the longer of the two input windows, applies preliminary scaling to the system audio (currently `sys_scaled = sys * 1.0`), and sums the microphone sample. When the absolute sum exceeds 1.0, the algorithm divides the value by `|sum|` to keep the output within the ±1.0 range without introducing distortion (lines 55-84). This approach prevents the "radio-break" sound characteristic of hard clipping, which could confuse the Voice Activity Detection (VAD) processor.

### Mixing Implementation Details

The mixer constructor `ProfessionalAudioMixer::new(_sample_rate)` operates as a stateless processor, requiring no internal buffers (lines 45-53). The method returns a `Vec<f32>` containing the mixed samples ready for transcription or disk writing (lines 86-89). A commented-out mic scaling (`_mic_scaled = mic * 0.8`) at line 62 indicates where future ducking algorithms can be inserted without breaking existing behavior.

## Integration in the Audio Pipeline

The `AudioPipeline` struct orchestrates these components, holding one instance of each (`ring_buffer` and `mixer`) at lines 92-95.

When `AudioChunk` data arrives from capture sources, `pipeline.run()` invokes `self.ring_buffer.add_samples(...)` to buffer the raw chunks (lines 217-219). Once `ring_buffer.can_mix()` returns true, the loop extracts aligned windows via `extract_window()`, processes them through `mixer.mix_window()`, and produces the clean mixed frame.

This mixed frame serves dual purposes: it feeds into `self.vad_processor.process_audio(&mixed_with_gain)` for speech segmentation (lines 226-236), and optionally forwards to `recording_sender_for_mixed` for persistent storage (lines 268-277).

## Practical Implementation Examples

### Initializing the Buffer and Mixer

```rust
let sample_rate = 48_000;
let mut ring_buffer = AudioMixerRingBuffer::new(sample_rate);
let mixer = ProfessionalAudioMixer::new(sample_rate);

```

### Feeding Audio Samples

```rust
// Simulate 10ms of audio at 48kHz (480 samples)
let mic_samples = vec![0.01_f32; 480];
let sys_samples = vec![0.005_f32; 480];

ring_buffer.add_samples(DeviceType::Microphone, mic_samples);
ring_buffer.add_samples(DeviceType::System, sys_samples);

```

### Executing the Mix

```rust
if ring_buffer.can_mix() {
    if let Some((mic_win, sys_win)) = ring_buffer.extract_window() {
        let mixed = mixer.mix_window(&mic_win, &sys_win);
        // mixed is now ready for VAD processing or WAV encoding
    }
}

```

### Pipeline Integration Context

```rust
// Simplified excerpt from AudioPipeline::run in pipeline.rs
while let Some(chunk) = self.receiver.recv().await {
    self.ring_buffer.add_samples(chunk.device_type, chunk.data);
    
    while self.ring_buffer.can_mix() {
        if let Some((mic, sys)) = self.ring_buffer.extract_window() {
            let clean = self.mixer.mix_window(&mic, &sys);
            self.vad_processor.process_audio(&clean)?;
        }
    }
}

```

## Summary

- **AudioMixerRingBuffer** provides lock-free FIFO buffering with two `VecDeque<f32>` queues, using a 600ms window and zero-padding for temporal alignment between asynchronous capture threads.
- **ProfessionalAudioMixer** implements soft-scaling to prevent distortion when combining streams, keeping output within ±1.0 through proportional amplitude reduction rather than hard clipping.
- Both components reside in [`frontend/src-tauri/src/audio/pipeline.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/audio/pipeline.rs) and operate within the `AudioPipeline::run` loop (lines 217-280) to feed clean audio to Whisper transcription and recording systems.
- The architecture handles independent arrival rates from different capture threads while maintaining broadcast-standard audio quality and preventing VAD confusion.

## Frequently Asked Questions

### Why does Meetily use a ring buffer for audio mixing instead of a simple vector?

The **AudioMixerRingBuffer** uses `VecDeque` structures because microphone and system audio arrive on separate threads with independent clock domains. A FIFO queue accommodates jitter and temporary latency differences between sources, automatically dropping old samples when buffers exceed the eight-window capacity. Simple vectors would require manual index management and cannot efficiently handle the producer-consumer pattern between capture threads and the mixing pipeline.

### How does ProfessionalAudioMixer prevent distortion when combining loud audio sources?

Instead of hard clipping values exceeding ±1.0, the `mix_window` method implements **soft-scaling**: when the absolute sum of microphone and system samples exceeds 1.0, the algorithm divides the value by its own magnitude. This preserves the waveform shape while reducing amplitude proportionally, preventing the harsh distortion that would degrade transcription accuracy and listening experience.

### What happens when microphone and system audio arrive at different rates?

The `extract_window()` method handles rate mismatches by zero-padding shorter buffers to match the window length of longer buffers. This temporal alignment ensures that when `can_mix()` returns true, both chunks represent the same time duration regardless of their original capture rates. The 600ms window size provides sufficient tolerance for Bluetooth microphones at 16kHz and system audio at 48kHz.

### Where can I modify the mixing levels for microphone vs. system audio?

In [`frontend/src-tauri/src/audio/pipeline.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/audio/pipeline.rs), the `ProfessionalAudioMixer::mix_window` method contains commented scaling logic at line 62 (`_mic_scaled = mic * 0.8`). Uncommenting and adjusting this coefficient allows you to implement ducking, where the microphone level automatically reduces when system audio is present. The system audio scaling is currently set at `sys_scaled = sys * 1.0` (line 68), which you can reduce to prioritize voice over desktop sounds.