# Meetily Rust Audio Capture: Threading Models and Async Boundaries Explained

> Explore Meetily's Rust audio capture. Discover how native threads, Tokio async, and MPSC channels handle real-time audio processing and disk writing.

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

---

**Meetily isolates real-time audio capture on native OS threads, bridges them to Tokio async tasks via unbounded MPSC channels, and processes mixed audio through an asynchronous pipeline before writing to disk.**

Meetily, an open-source meeting assistant by Zackriya-Solutions, implements a sophisticated audio pipeline in Rust to handle real-time microphone and system audio capture. Understanding the threading models and async boundaries used in Meetily's Rust audio capture modules is essential for contributors optimizing performance or debugging latency issues. The architecture deliberately separates time-sensitive capture operations from asynchronous processing to prevent audio dropouts while maintaining responsive UI interactions.

## Threading Architecture Overview

### Native OS Threads for Audio Capture

Meetily spawns dedicated native threads for low-latency audio acquisition to avoid interference from the async runtime's scheduling. In [`frontend/src-tauri/src/audio/capture/microphone.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/audio/capture/microphone.rs), the `MicrophoneCapture` struct leverages **CPAL** (Cross-Platform Audio Library) to create a platform-specific stream that runs on its own thread. The `start()` method initializes this stream, while `stop()` signals termination.

Similarly, [`frontend/src-tauri/src/audio/capture/system.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/audio/capture/system.rs) implements `SystemAudioCapture` using platform-specific APIs such as WASAPI loopback on Windows or ScreenCaptureKit on macOS. These implementations spawn dedicated threads to read system audio and push chunks into the pipeline.

### Tokio Async Tasks for Processing and I/O

While capture runs on native threads, downstream processing uses Tokio's async runtime. The `RecordingManager` in [`frontend/src-tauri/src/audio/recording_manager.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/audio/recording_manager.rs) orchestrates this transition. When `start()` is invoked, it spawns a Tokio task to run the `AudioPipeline` and another async task for the `RecordingSaver`. This separation ensures that heavy CPU operations like mixing and voice activity detection (VAD), as well as blocking file I/O, never stall the real-time capture threads.

## Async Boundaries and Channel Communication

### Capture-to-Pipeline Boundary (AudioChunk)

The primary async boundary exists between the native capture threads and the Tokio processing task. Both `MicrophoneCapture` and `SystemAudioCapture` hold a `tokio::sync::mpsc::UnboundedSender<AudioChunk>` to transmit raw PCM data without blocking the capture callback.

In [`microphone.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/microphone.rs), the capture callback constructs an `AudioChunk` and sends it via:

```rust
let chunk = AudioChunk {
    data: samples.to_vec(),
    sample_rate: self.sample_rate,
    timestamp: now(),
    chunk_id: id,
    device_type: DeviceType::Microphone,
};
self.sender.send(chunk).expect("pipeline receiver dropped");

```

The `RecordingManager::start()` method creates the corresponding `UnboundedReceiver` and spawns an async task that awaits these chunks:

```rust
let (sender, mut receiver) = mpsc::unbounded_channel::<AudioChunk>();
self.state.set_audio_sender(sender);
let pipeline = self.pipeline.clone();

tokio::spawn(async move {
    while let Some(chunk) = receiver.recv().await {
        pipeline.process_chunk(chunk);
    }
});

```

This unbounded channel ensures that the native OS thread never waits on the async runtime, eliminating jitter and dropouts during capture.

### Pipeline-to-Saver Boundary (ProcessedAudioChunk)

After mixing and VAD processing in [`frontend/src-tauri/src/audio/pipeline.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/audio/pipeline.rs), the `AudioPipeline` sends `ProcessedAudioChunk` structures to the `RecordingSaver` through a second MPSC channel. The `RecordingSaver` in [`frontend/src-tauri/src/audio/recording_saver.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/audio/recording_saver.rs) consumes these chunks asynchronously:

```rust
pub async fn start(&mut self) -> Result<()> {
    while let Some(chunk) = self.receiver.recv().await {
        self.write_chunk(&chunk).await?;
    }
    Ok(())
}

```

This boundary isolates blocking disk I/O from the CPU-intensive mixing logic, allowing both operations to proceed concurrently without interfering with capture latency.

### Control Flow Boundaries

The Tauri frontend invokes async commands defined in [`frontend/src-tauri/src/audio/recording_commands.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/audio/recording_commands.rs). These commands call `RecordingManager::start()` and `stop()`, which internally invoke the synchronous `start()` and `stop()` methods on the capture structs to control the native threads. This creates a clear boundary where async control logic meets synchronous thread management.

## Thread Safety Mechanisms

### Shared State Management

[`frontend/src-tauri/src/audio/recording_state.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/audio/recording_state.rs) maintains shared state using **Arc** and atomic types. The `RecordingState` struct wraps boolean flags like `is_recording` and `is_paused` in `Arc<AtomicBool>` to enable lock-free access from both capture threads and async tasks.

### Synchronization Primitives

- **Atomic counters**: Error tracking uses `AtomicU32` to avoid lock contention on hot paths.
- **Mutex for device handles**: Optional device references and the channel sender are protected by `std::sync::Mutex` to allow safe mutation across thread boundaries.
- **AudioBufferPool**: The `AudioBufferPool` passed between components uses `Arc`-wrapped internal buffers, enabling zero-copy reuse of memory across the native capture thread and async processing tasks.

## Implementation Details and Code Examples

The following patterns from the codebase demonstrate the threading and async integration:

**Native thread spawning in capture modules:**

```rust
// From capture/microphone.rs
pub fn start(&self) -> Result<()> {
    let host = cpal::default_host();
    let device = self.device.inner.clone();
    let mut config = device.default_input_config()?.config();
    config.sample_rate = cpal::SampleRate(self.sample_rate);
    
    // CPAL spawns a native thread for the audio callback
    let stream = device.build_input_stream(
        &config,
        move |data: &[f32], _: &cpal::InputCallbackInfo| {
            // Callback runs on native OS thread
            let chunk = // ... create AudioChunk
            let _ = sender.send(chunk);
        },
        err_fn,
    )?;
    stream.play()?;
    Ok(())
}

```

**Async pipeline processing:**

```rust
// From recording_manager.rs
pub async fn start(&mut self) -> Result<()> {
    self.mic_capture.start()?;
    self.system_capture.start()?;

    let (sender, mut receiver) = mpsc::unbounded_channel::<AudioChunk>();
    self.state.set_audio_sender(sender);
    let pipeline = self.pipeline.clone();

    tokio::spawn(async move {
        while let Some(chunk) = receiver.recv().await {
            pipeline.process_chunk(chunk);
        }
    });
    Ok(())
}

```

**Async file writing:**

```rust
// From recording_saver.rs
impl RecordingSaver {
    pub async fn start(&mut self) -> Result<()> {
        while let Some(chunk) = self.receiver.recv().await {
            // Async file operations
            self.file.write_all(&chunk.data).await?;
        }
        Ok(())
    }
}

```

## Summary

- **Native OS threads** handle real-time audio capture via CPAL and platform-specific APIs to ensure deterministic latency.
- **Tokio MPSC unbounded channels** bridge native threads to async tasks without blocking the capture callback.
- **Three distinct layers** exist: capture (native thread), pipeline (async task), and saver (async I/O), each communicating via dedicated channels.
- **Thread-safe state management** uses `Arc<AtomicBool>` and `Mutex` to coordinate between synchronous capture and asynchronous control flows.
- **Zero-copy buffer pooling** minimizes allocations across thread boundaries using `Arc`-wrapped buffers.

## Frequently Asked Questions

### Why does Meetily use native threads instead of Tokio tasks for audio capture?

Audio capture requires deterministic, low-latency execution that cannot tolerate the scheduling unpredictability of async runtimes. CPAL and platform APIs like WASAPI require callbacks that execute on dedicated native threads. By keeping capture on OS threads and using channels to communicate with Tokio tasks, Meetily guarantees that audio acquisition never pauses due to async runtime congestion.

### How does Meetily prevent audio dropouts during heavy processing?

The architecture uses **unbounded MPSC channels** to decouple capture speed from processing speed. The native capture thread immediately sends chunks to the channel without waiting for the pipeline to process them. While this theoretically allows memory growth under extreme load, it ensures that the capture callback never blocks, preventing dropouts at the source.

### What is the role of the unbounded channel in the audio pipeline?

The `tokio::sync::mpsc::unbounded_channel` serves as the critical async boundary between the synchronous capture world and the asynchronous processing world. It allows the native thread to fire-and-forget `AudioChunk` structures while the Tokio task consumes them at its own pace. This pattern appears in both the capture-to-pipeline and pipeline-to-saver handoffs.

### How is thread safety ensured when multiple capture sources run simultaneously?

`RecordingState` centralizes shared flags using `Arc<AtomicBool>` for lock-free reads and `Mutex` for device handles. Each capture source (microphone and system) owns its own `UnboundedSender`, but both send into the same pipeline receiver. The pipeline processes chunks sequentially in its async loop, eliminating race conditions during mixing. All buffer operations use `Arc`-pooled memory to avoid use-after-free across threads.