# How Meetily’s RecordingManager Coordinates Audio Capture, Processing, and Saving

> Discover how Meetily’s RecordingManager coordinates audio capture processing and saving. Learn about its async channel architecture for mixing transcribing and saving audio files.

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

---

**Meetily’s RecordingManager orchestrates audio recording by coordinating four components—RecordingState, AudioStreamManager, AudioPipelineManager, and RecordingSaver—through async channels to capture raw audio, mix streams, transcribe speech, and save finalized files.**

The open-source Meetily meeting assistant (Zackriya-Solutions/meetily) implements a privacy-first recording pipeline in Rust. At its core, 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) acts as a high-level façade that wires together capture, processing, and persistence layers while guaranteeing that raw microphone and system streams are mixed and processed before ever touching disk.

## Architecture Overview

The RecordingManager delegates work to four specialized components that communicate via asynchronous channels:

| Component | Source File | Role |
|-----------|-------------|------|
| **RecordingState** | [`recording_state.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/recording_state.rs) | Holds shared mutable state (`is_recording` flag, timestamps, error callbacks) and provides a channel for raw audio chunks. |
| **AudioStreamManager** | [`stream.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/stream.rs) | Creates audio streams for each selected device using CPAL or Core Audio back-ends. Streams feed raw, unmixed audio chunks into the `RecordingState`. |
| **AudioPipelineManager** | [`pipeline.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/pipeline.rs) | Consumes raw chunks, mixes microphone and system audio, runs Voice Activity Detection (VAD), and forwards speech-only segments to Whisper for transcription. It also sends the *pre-mixed* audio to a recording channel. |
| **RecordingSaver** | [`recording_saver.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/recording_saver.rs) | Receives the pre-mixed audio, optionally resamples it to 48 kHz, and writes incremental checkpoint files. At the end of a session, it finalizes the audio file, writes [`metadata.json`](https://github.com/Zackriya-Solutions/meetily/blob/main/metadata.json) and [`transcripts.json`](https://github.com/Zackriya-Solutions/meetily/blob/main/transcripts.json), and emits a `recording-saved` Tauri event. |

## Step-by-Step Coordination Flow

### Initialization and Channel Setup

When `start_recording_with_defaults_and_auto_save()` is invoked, the manager first establishes the communication backbone:

1. **Shared State Creation** – `RecordingState::new()` creates an `Arc` that can be safely shared between async tasks.

2. **Transcription Channel** – `mpsc::unbounded_channel()` yields `transcription_sender` and `transcription_receiver`. The receiver is returned to the caller so the UI can listen for Whisper chunks in real time.

3. **Recording Channel** – `self.recording_saver.start_accumulation(auto_save)` returns a `recording_sender`. This sender will receive the *mixed* audio that the pipeline produces.

4. **State Activation** – `self.state.start_recording()` sets the running flag and records the start timestamp.

### Pipeline and Stream Activation

Once channels are established, the manager configures the processing pipeline:

5. **Device Detection** – For each device (microphone, system), the manager calls `InputDeviceKind::detect` to determine if the source is Bluetooth or wired. This information is used by the pipeline for adaptive buffering to handle Bluetooth jitter.

6. **Metadata Preparation** – `self.recording_saver.set_device_info(...)` stores the device names in the meeting’s [`metadata.json`](https://github.com/Zackriya-Solutions/meetily/blob/main/metadata.json) before streaming begins.

7. **Pipeline Launch** – The manager invokes `AudioPipelineManager::start()` with the shared state, transcription sender, and the recording sender:

```rust
self.pipeline_manager.start(
    self.state.clone(),
    transcription_sender,
    0,                     // target chunk size (ignored – pipeline uses dynamic sizing)
    48_000,                // target sample rate
    Some(recording_sender),// critical: where mixed audio is sent for saving
    mic_name,
    mic_kind,
    sys_name,
    sys_kind,
)?;

```

8. **Startup Delay** – A short `tokio::time::sleep(Duration::from_millis(50)).await` ensures the pipeline’s internal tasks are ready before streams begin.

9. **Stream Creation** – `self.stream_manager.start_streams(mic_dev, sys_dev, None).await?` creates an `AudioStream` for each device. Each stream calls `AudioCapture::process_audio_data`, which forwards the raw chunk to `RecordingState.send_audio_chunk`.

10. **Device Monitoring** – On macOS, an `AudioDeviceMonitor` watches for disconnects and reconnects to handle hardware changes gracefully.

## Stopping and Finalizing Sessions

When `stop_recording()` or `stop_streams_and_force_flush()` is called, the manager executes a strict teardown sequence to prevent data loss:

- **Stop Device Monitoring** – Stops the `AudioDeviceMonitor` first to avoid long Windows WASAPI enumeration delays.
- **Clear State** – `self.state.stop_recording()` clears device references and stops the recording flag.
- **Halt Streams** – `AudioStreamManager::stop_streams()` terminates the CPAL/Core Audio streams.
- **Flush Pipeline** – `AudioPipelineManager::force_flush_and_stop()` processes any buffered audio remaining in the mixing stage.
- **Finalize Persistence** – `self.recording_saver.stop_and_save(app, duration)` finalizes the incremental audio saver, writes the final [`transcripts.json`](https://github.com/Zackriya-Solutions/meetily/blob/main/transcripts.json), updates [`metadata.json`](https://github.com/Zackriya-Solutions/meetily/blob/main/metadata.json) with the actual duration, and emits the `recording-saved` event to the frontend.

## Implementation Examples

### Creating a Manager and Starting Recording

```rust
// Create a manager and start a recording session with default devices
let mut manager = RecordingManager::default();
let transcription_rx = manager
    .start_recording_with_defaults_and_auto_save(true)
    .await?;   // `true` => audio checkpoints will be saved

// Listen for Whisper transcription chunks in the UI (frontend)
use tauri::invoke;
let _ = invoke::<_, AudioChunk>("listen_transcription", {})
    .await
    .map(|chunk| println!("Got transcript chunk: {}", chunk.text));

// Stop and save the recording (e.g. when the user clicks “End Meeting”)
manager.stop_streams_and_force_flush().await?;
manager.save_recording_only(&app_handle).await?;

```

### Inside the RecordingManager Start Method

```rust
// Inside RecordingManager::start_recording (simplified)
let (transcription_sender, transcription_receiver) = mpsc::unbounded_channel();
let recording_sender = self.recording_saver.start_accumulation(auto_save);
self.state.start_recording()?;                     // set is_recording flag
self.pipeline_manager.start(
    self.state.clone(),
    transcription_sender,
    0,
    48_000,
    Some(recording_sender),                         // mixed audio -> saver
    mic_name,
    mic_kind,
    sys_name,
    sys_kind,
)?;
self.stream_manager.start_streams(mic_dev, sys_dev, None).await?;

```

### Finalizing the Recording File

```rust
// RecordingSaver::stop_and_save – finalises audio and writes metadata
pub async fn stop_and_save<R: Runtime>(
    &mut self,
    app: &AppHandle<R>,
    recording_duration: Option<f64>,
) -> Result<Option<String>, String> {
    // Signal accumulation task to finish
    *self.is_saving.lock().unwrap() = false;
    // Give the task a moment to process any remaining chunks
    tokio::time::sleep(Duration::from_millis(200)).await;

    // Finalise the IncrementalAudioSaver (merge checkpoints → final .mp4)
    let final_path = self.incremental_saver.as_ref()
        .expect("auto_save disabled")
        .lock()
        .await
        .finalize()
        .await?;
    // Write transcripts.json and update metadata.json…
    // Emit Tauri event so the UI can show “Saved!”
    app.emit("recording-saved", &save_event)?;
    Ok(Some(final_path.to_string_lossy().to_string()))
}

```

## Summary

- **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) provides a thin façade that keeps all components loosely coupled via async channels.
- **AudioPipelineManager** guarantees that mixed audio (microphone + system) is produced exactly once, while raw streams are consumed only for transcription.
- **RecordingSaver** handles incremental checkpoint files during recording and finalizes them into [`metadata.json`](https://github.com/Zackriya-Solutions/meetily/blob/main/metadata.json), [`transcripts.json`](https://github.com/Zackriya-Solutions/meetily/blob/main/transcripts.json), and the audio file at session end.
- Device-specific quirks (Bluetooth jitter, macOS Core Audio, Windows WASAPI) are abstracted through `InputDeviceKind` detection and adaptive buffering.

## Frequently Asked Questions

### How does Meetily separate transcription data from the saved recording?

The **AudioPipelineManager** forks the audio stream: it sends speech-only segments to the Whisper transcription channel while forwarding the pre-mixed audio to the `RecordingSaver` channel. This ensures that raw microphone or system streams never persist to disk—only the mixed, processed audio is saved.

### What prevents audio loss if a Bluetooth device disconnects during recording?

The `AudioDeviceMonitor` in [`frontend/src-tauri/src/audio/device_monitor.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/audio/device_monitor.rs) watches for device disconnect events. When detected, the manager triggers `force_flush_and_stop()` on the pipeline to process buffered chunks before the stream terminates, preventing data loss during hardware changes.

### Where does the actual audio file writing happen in the codebase?

The physical file I/O occurs 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). The `RecordingSaver` uses an `IncrementalAudioSaver` to write checkpoint chunks during the session and calls `finalize()` at the end to merge checkpoints into the final MP4 file.

### Why does the RecordingManager wait 50ms after starting the pipeline?

The 50-millisecond delay (`sleep(Duration::from_millis(50))`) ensures that the `AudioPipelineManager`’s internal async tasks and channels are fully initialized before the `AudioStreamManager` begins pushing raw audio data. This prevents race conditions where early audio chunks might arrive before the pipeline is ready to receive them.