How to Implement Platform-Specific Audio Capture (ALSA/PulseAudio) in Meetily
Meetily implements platform-specific audio capture through a modular trait-based architecture in frontend/src-tauri/src/audio/devices/platform/linux.rs, allowing developers to choose between ALSA and PulseAudio backends at runtime by implementing the AudioCapture trait and configuring the backend preference in the Tauri command layer.
According to the Meetily source code, the audio pipeline uses a platform-agnostic abstraction layer to handle system audio capture across Linux, macOS, and Windows. For Linux environments, the application supports both ALSA and PulseAudio through dedicated implementations in the Rust backend. This guide explains how to implement and configure these platform-specific audio capture backends to control microphone and system audio ingestion.
Architecture Overview
The audio subsystem separates device discovery from platform-specific capture logic using a trait-based contract. The AudioDevice trait defined in frontend/src-tauri/src/audio/devices/mod.rs serves as the common interface, while OS-specific modules handle the actual stream initialization.
| Layer | Responsibility | Key Files |
|---|---|---|
| Device Discovery | Enumerates input devices and selects default sources | frontend/src-tauri/src/audio/devices/discovery.rs |
| Platform Implementations | Provides concrete ALSA/PulseAudio backends | frontend/src-tauri/src/audio/devices/platform/linux.rs |
| Capture Stream | Wraps low-level devices into async streams | frontend/src-tauri/src/audio/system_audio_stream.rs |
| Audio Pipeline | Mixes streams and runs VAD/Whisper transcription | frontend/src-tauri/src/audio/pipeline.rs |
Implementing the Linux Audio Capture Backend
The Linux implementation lives in frontend/src-tauri/src/audio/devices/platform/linux.rs. It currently uses ALSA (via the cpal crate) and falls back to PulseAudio when available.
The AudioCapture Trait Contract
The platform layer implements the AudioCapture trait defined in frontend/src-tauri/src/audio/devices/mod.rs. Concrete implementations must:
- Request a 48 kHz, 16-bit stereo stream to match the pipeline requirements
- Yield
AudioChunkstructs consumed by the mixing pipeline - Expose
start()andstop()methods for stream control
ALSA Implementation Example
To add explicit ALSA support, implement the AudioCapture trait using cpal with the ALSA host:
// frontend/src-tauri/src/audio/devices/platform/linux.rs
use crate::audio::devices::AudioCapture;
use cpal::traits::{DeviceTrait, HostTrait};
pub struct AlsaCapture {
stream: cpal::Stream,
}
impl AlsaCapture {
pub fn new(device_name: &str) -> anyhow::Result<Self> {
// Obtain the ALSA host
let host = cpal::host_from_id(cpal::HostId::Alsa)?;
// Find device by name or fallback to default
let device = host
.devices()?
.find(|d| d.name().ok().as_deref() == Some(device_name))
.or_else(|| host.default_input_device())
.ok_or_else(|| anyhow!("No ALSA input device found"))?;
// Configure 48kHz, 16-bit format
let config = device.default_input_config()?.into();
// Build input stream
let stream = device.build_input_stream(
&config,
move |data: &[i16], _: &cpal::InputCallbackInfo| {
let chunk = AudioChunk::from_i16(data);
AudioCapture::push_chunk(chunk);
},
move |err| log::error!("ALSA capture error: {}", err),
)?;
stream.play()?;
Ok(Self { stream })
}
}
impl AudioCapture for AlsaCapture {
fn start(&self) -> anyhow::Result<()> { Ok(()) }
fn stop(&self) -> anyhow::Result<()> {
self.stream.pause()?;
Ok(())
}
}
PulseAudio Fallback Logic
The module automatically detects the runtime environment. When libpulse-simple is available, it constructs a PulseAudioCapture object; otherwise, it creates an AlsaCapture. This selection happens during device initialization in frontend/src-tauri/src/audio/devices/platform/linux.rs.
Configuring Backend Selection at Runtime
To force ALSA instead of PulseAudio, modify the select_backend function:
fn select_backend(preferred: BackendPreference) -> Box<dyn AudioCapture> {
match preferred {
BackendPreference::Alsa => Box::new(AlsaCapture::new("default").unwrap()),
BackendPreference::Pulse => Box::new(PulseAudioCapture::new(...).unwrap()),
_ => Box::new(PulseAudioCapture::new(...).unwrap_or_else(|_| AlsaCapture::new("default").unwrap())),
}
}
Expose this preference through the Tauri command layer in frontend/src-tauri/src/audio/recording_commands.rs:
TypeScript frontend:
await invoke('start_recording', {
mic_device_name: "Built-in Microphone",
system_device_name: "BlackHole 2ch",
meeting_name: "Team Sync",
linux_backend: "alsa"
});
Rust backend:
#[tauri::command]
async fn start_recording(
mic_device_name: Option<String>,
system_device_name: Option<String>,
meeting_name: Option<String>,
#[serde(default)] linux_backend: Option<String>,
) -> Result<(), String> {
let backend_pref = match linux_backend.as_deref() {
Some("alsa") => BackendPreference::Alsa,
Some("pulse") => BackendPreference::Pulse,
_ => BackendPreference::Auto,
};
audio::devices::platform::linux::set_preference(backend_pref);
// Continue with recording flow
Ok(())
}
Testing the Linux Capture Implementation
-
Run with verbose logging:
RUST_LOG=frontend::audio=debug ./clean_run.sh -
Verify backend selection in console output:
[audio::devices::platform::linux] Selected backend: ALSA -
Inspect audio metrics using the UI panel (
Cmd+I) to confirm real-time RMS data from the Linux pipeline. -
Run automated tests:
cargo test --package meetily --test linux_capture
Choosing Between ALSA and PulseAudio
| Situation | Recommended Backend |
|---|---|
| Default desktop Linux (GNOME, KDE) | PulseAudio – better mixing and virtual device support |
| Low-latency or headless servers | ALSA – fewer layers, lower overhead |
| PulseAudio disabled | ALSA (automatic fallback) |
| Virtual audio sink capture | PulseAudio – provides virtual source support |
Troubleshooting Common Pitfalls
- Mismatched sample rates: If the ALSA device cannot configure to 48 kHz,
cpalreturns an error. Use theresamplerinaudio/ffmpeg.rsas a fallback. - Permission errors: Users must belong to the
audiogroup on some distributions. Check logs forEACCESwarnings. - Concurrent access: Only one capture stream may own an ALSA device at a time. Ensure
fallback.rsreleases previous streams before re-initializing.
Summary
- Meetily isolates platform-specific audio capture in
frontend/src-tauri/src/audio/devices/platform/linux.rsusing theAudioCapturetrait - Implement the trait to add custom ALSA or PulseAudio backends with 48 kHz, 16-bit PCM output
- Force specific backends at runtime by passing preference parameters through Tauri commands
- Verify implementations using
RUST_LOG=debugand the built-in audio metrics panel - Handle sample rate mismatches and permissions errors to ensure stable capture
Frequently Asked Questions
How do I force Meetily to use ALSA instead of PulseAudio?
Modify the select_backend function in frontend/src-tauri/src/audio/devices/platform/linux.rs to return AlsaCapture explicitly, or pass a backend preference parameter through the Tauri command layer to override the auto-detection logic.
What sample rate does Meetily require for audio capture?
The pipeline expects 48 kHz, 16-bit PCM audio. If your ALSA device cannot natively support this rate, the system falls back to the resampler in audio/ffmpeg.rs to ensure compatibility with the Whisper transcription engine.
Where is the platform-specific audio capture trait defined?
The AudioCapture trait is defined in frontend/src-tauri/src/audio/devices/mod.rs and implemented by platform-specific modules including linux.rs, macos.rs, and windows.rs to provide consistent cross-platform behavior.
How do I test audio capture without a real microphone?
Run the unit tests in frontend/src-tauri/src/audio/devices/platform/linux_tests.rs which spawn mock ALSA devices, or use virtual audio sinks like PulseAudio null sinks to simulate system audio input.
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 →