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:
-
Shared State Creation –
RecordingState::new()creates anArcthat can be safely shared between async tasks. -
Transcription Channel –
mpsc::unbounded_channel()yieldstranscription_senderandtranscription_receiver. The receiver is returned to the caller so the UI can listen for Whisper chunks in real time. -
Recording Channel –
self.recording_saver.start_accumulation(auto_save)returns arecording_sender. This sender will receive the mixed audio that the pipeline produces. -
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:
-
Device Detection – For each device (microphone, system), the manager calls
InputDeviceKind::detectto determine if the source is Bluetooth or wired. This information is used by the pipeline for adaptive buffering to handle Bluetooth jitter. -
Metadata Preparation –
self.recording_saver.set_device_info(...)stores the device names in the meeting’smetadata.jsonbefore streaming begins. -
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,
)?;
-
Startup Delay – A short
tokio::time::sleep(Duration::from_millis(50)).awaitensures the pipeline’s internal tasks are ready before streams begin. -
Stream Creation –
self.stream_manager.start_streams(mic_dev, sys_dev, None).await?creates anAudioStreamfor each device. Each stream callsAudioCapture::process_audio_data, which forwards the raw chunk toRecordingState.send_audio_chunk. -
Device Monitoring – On macOS, an
AudioDeviceMonitorwatches 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
AudioDeviceMonitorfirst 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 finaltranscripts.json, updatesmetadata.jsonwith the actual duration, and emits therecording-savedevent 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.rsprovides 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
InputDeviceKinddetection 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →