# How Meetily Handles Concurrent Microphone and System Audio Mixing in Rust

> Discover how Meetily's Rust audio pipeline seamlessly mixes microphone and system audio using ring buffers and soft-clipping for clear, synchronized sound. Learn more.

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

---

**Meetily captures microphone and system audio simultaneously through separate `AudioCapture` instances, synchronizes them via a ring buffer, and mixes them using a professional soft-clipping mixer before sending to transcription and file recording.**

Meetily, an open-source meeting transcription tool by Zackriya-Solutions, implements a sophisticated Rust-based audio pipeline to combine microphone input with system audio in real-time. The core 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), where the application manages dual-stream capture, synchronization, and broadcast-quality mixing using a multi-stage architecture.

## The Three-Stage Audio Pipeline Architecture

### Stage 1: Device-Specific Capture and Preprocessing

Each audio stream originates from an `AudioCapture` instance that handles device-specific initialization and preprocessing. According to the source code in [`pipeline.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/pipeline.rs), the microphone stream undergoes a comprehensive enhancement chain in `AudioCapture::process_audio_data`:

- **High-pass filtering** to remove low-frequency rumble
- **RNNoise suppression** for real-time noise reduction
- **EBU-R128 loudness normalization** targeting -23 LUFS broadcast standard
- **Mono conversion** and resampling to the pipeline's native 48 kHz

System audio is captured simultaneously but bypasses the enhancement chain, proceeding directly to resampling when necessary. Both streams forward their processed chunks as `AudioChunk` structures to the shared pipeline state via `state.send_audio_chunk` (see [`pipeline.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/pipeline.rs) lines 11–20 and 86–118).

### Stage 2: Ring-Buffer Synchronization

The `AudioPipeline` receives asynchronous chunks from both devices through an unbounded Tokio channel. An `AudioMixerRingBuffer` stores microphone and system samples in independent `VecDeque` buffers until each side accumulates sufficient data for a fixed-size mixing window of approximately 50 ms.

The buffer implements three critical mechanisms in [`pipeline.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/pipeline.rs) (lines 16–23, 87–90, and 92–110):

- **Synchronization via `can_mix`**: Ensures both streams have adequate samples before extraction
- **Silence padding**: `extract_window` pads missing data with zeros rather than repeating samples, eliminating audible artifacts when one stream temporarily lags
- **Memory protection**: A `max_buffer_size` of 400 ms (8× the mixing window) prevents runaway memory growth during bursty system audio delivery, particularly common on macOS Core Audio

### Stage 3: Professional Mixing and Downstream Routing

Once the ring buffer reports readiness via `can_mix`, the `ProfessionalAudioMixer` combines the windows in `mix_window` (lines 45–53 and 59–78). The mixer applies two professional-grade techniques:

1. **Attenuation**: System audio is scaled down by 70% to prevent it from overpowering the microphone signal
2. **Soft-clipping**: When the sum of mic and system samples exceeds ±1.0, the mixer proportionally scales the result back into the unit range rather than applying hard clipping, preserving audio fidelity and preventing distortion

The `AudioPipeline::run` loop (lines 66–71, 176–185, and 187–204) orchestrates the mixed output to two destinations:
- A **VAD (Voice Activity Detection)** processor that extracts speech-only segments for Whisper transcription
- A **recording sender** that writes the final mixed WAV file containing both microphone and system audio

## Handling Sample Rate Mismatches

When `AudioCapture` detects a sample-rate mismatch between the device and the pipeline's 48 kHz native rate, it initializes a persistent **Rubato resampler**. As implemented in [`pipeline.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/pipeline.rs) (lines 24–34 and 44–62), the resampler buffers variable-size input chunks until accumulating 512 samples, then processes the block while preserving RMS energy across chunk boundaries.

## Key Design Highlights

### Latency and Jitter Management

The 400 ms maximum buffer size specifically targets jitter absorption on macOS Core Audio, where system audio delivery can be bursty. This ensures synchronization without dropping samples during temporary delivery delays.

### Separation of Concerns

Raw microphone and system streams are **never** written directly to disk. Only the professionally mixed output is saved, while speech segments (post-VAD) are dispatched to Whisper. This architecture prevents double-recording and echo artifacts in the transcription.

### Code Example: Starting the Pipeline

```rust
// In src/lib.rs – a Tauri command that begins recording
#[tauri::command]
async fn start_recording(
    app: tauri::AppHandle,
    mic_device_name: Option<String>,
    system_device_name: Option<String>,
    meeting_name: Option<String>,
) -> Result<(), String> {
    let state = RecordingState::new();                 // Shared state
    let (trans_tx, trans_rx) = mpsc::unbounded_channel(); // Whisper sender
    let recording_sender = Some(mpsc::unbounded_channel().0); // WAV writer

    // Build the manager and launch the pipeline
    let mut manager = AudioPipelineManager::new();
    manager.start(
        Arc::new(state),
        trans_tx,
        /*target_chunk_duration_ms=*/ 20,
        /*sample_rate=*/ 48_000,
        recording_sender,
        mic_device_name.unwrap_or_default(),
        detect_mic_kind(),
        system_device_name.unwrap_or_default(),
        detect_system_kind(),
    )?;
    Ok(())
}

```

### Code Example: Professional Mixing Logic

```rust
fn mix_window(&mut self, mic_window: &[f32], sys_window: &[f32]) -> Vec<f32> {
    let max_len = mic_window.len().max(sys_window.len());
    let mut mixed = Vec::with_capacity(max_len);
    for i in 0..max_len {
        let mic = mic_window.get(i).copied().unwrap_or(0.0);
        let sys = sys_window.get(i).copied().unwrap_or(0.0);
        // System is attenuated to 30% (70% reduction)
        let sum = mic + sys * 0.3;
        // Soft clipping – proportionally scale if > 1.0
        let mixed_sample = if sum.abs() > 1.0 { sum / sum.abs() } else { sum };
        mixed.push(mixed_sample);
    }
    mixed
}

```

## Summary

- **Dual capture**: Separate `AudioCapture` instances handle microphone (with enhancement) and system audio (direct), both converting to 48 kHz mono
- **Synchronization**: `AudioMixerRingBuffer` uses dual `VecDeque` structures with a 50 ms window and 400 ms maximum capacity to align asynchronous streams
- **Professional mixing**: `ProfessionalAudioMixer::mix_window` applies 70% system attenuation and soft-clipping to prevent distortion while preserving dynamic range
- **Downstream routing**: The mixed signal feeds both the VAD processor (for Whisper transcription) and the WAV file recorder, with raw streams kept separate to avoid echo

## Frequently Asked Questions

### How does Meetily prevent audio clipping when mixing loud system audio with microphone input?

Instead of hard clipping that introduces distortion, Meetily's `ProfessionalAudioMixer` implements soft-clipping in `mix_window`. When the combined sample exceeds the ±1.0 range, the algorithm proportionally scales the entire sum back into bounds, preserving the audio waveform's integrity while preventing the "radio-break" distortion characteristic of hard clipping.

### What happens if microphone and system audio have different sample rates?

The `AudioCapture` struct detects sample-rate mismatches and instantiates a persistent Rubato resampler that buffers variable-size chunks until 512 samples are available for processing. This resampling preserves RMS energy across chunk boundaries, ensuring consistent loudness levels even when converting from non-48 kHz sources to the pipeline's native rate.

### Why does Meetily use a ring buffer instead of mixing audio immediately?

The `AudioMixerRingBuffer` solves synchronization challenges between asynchronous capture devices. Since system audio (particularly on macOS Core Audio) can arrive in bursts while microphone input is more regular, the buffer stores samples in independent `VecDeque` structures until both streams have sufficient data for the 50 ms mixing window. This prevents alignment artifacts and allows silence padding when one stream temporarily stalls.

### How is the mixed audio separated for transcription versus recording?

After mixing in `AudioPipeline::run`, the combined stream splits into two paths: the full mixed signal goes to the recording sender for WAV file output, while the same signal passes through a VAD processor that extracts only speech segments. These speech-only segments are then dispatched to Whisper for transcription, ensuring the saved recording contains both voices and system sounds while the transcription receives only spoken content without background audio.