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

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 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 Holds shared mutable state (is_recording flag, timestamps, error callbacks) and provides a channel for raw audio chunks.
AudioStreamManager 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 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 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 and 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:

  1. 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.

  2. Metadata Preparation – self.recording_saver.set_device_info(...) stores the device names in the meeting’s metadata.json before streaming begins.

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

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,
)?;
  1. Startup Delay – A short tokio::time::sleep(Duration::from_millis(50)).await ensures the pipeline’s internal tasks are ready before streams begin.

  2. 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.

  3. 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, updates metadata.json with the actual duration, and emits the recording-saved event to the frontend.

Implementation Examples

Creating a Manager and Starting Recording

// 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

// 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

// 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 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, 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 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. 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →