How Meetily’s Audio Resampling Pipeline Maintains a Consistent 48kHz Sample Rate Across Capture Devices
Meetily normalizes all incoming audio to 48kHz using a persistent Rubato resampler inside the AudioCapture component, buffering variable-sized device buffers into fixed 512-sample chunks to prevent energy blow-up and timing drift.
Meetily is an open-source meeting transcription tool that standardizes audio processing by enforcing a 48kHz sample rate across all capture devices. The audio resampling pipeline automatically detects mismatched input rates—from Bluetooth headsets at 16kHz to system loops at 44.1kHz—and converts them on the fly using a high-quality sinc resampler. This ensures that downstream components like the ContinuousVadProcessor and Whisper transcription engine receive consistent, synchronized audio streams regardless of hardware differences.
The 48kHz Target and Why It Matters
The pipeline defines a constant TARGET_SAMPLE_RATE = 48_000 in frontend/src-tauri/src/audio/pipeline.rs that all audio must meet before entering the processing stage. This standardization eliminates timing bugs—such as audio playing at 3× speed when Bluetooth headphones supply 16kHz data—and simplifies mixing logic in ProfessionalAudioMixer. By unifying sample rates at the capture boundary, the system avoids complex resampling logic in the VAD and transcription stages.
Three-Step Resampling Flow
Step 1: Detect Sample-Rate Mismatch
In AudioCapture::new (lines 1024–1035 of pipeline.rs), the constructor compares the device’s reported sample_rate against the 48kHz target. When they differ, it sets a needs_resampling flag and emits a warning log: ⚠️ SAMPLE RATE MISMATCH DETECTED. This detection occurs once per device initialization, ensuring the pipeline knows immediately whether conversion is required.
Step 2: Build a Persistent Rubato Resampler
When resampling is needed, the code instantiates a rubato::SincFixedIn<f32> resampler configured for the ratio TARGET_SAMPLE_RATE / device_sample_rate (lines 1039–1052). The resampler’s parameters—including sinc_len, interpolation type, and oversampling factor—adapt based on the conversion ratio. For up-sampling ratios of 2× or greater, the pipeline selects high-quality settings (512-tap sinc with cubic interpolation); leaner configurations handle modest ratios to conserve CPU. This resampler persists for the entire capture session to avoid the energy artifacts that occur when recreating resamplers per chunk.
Step 3: Process Incoming Audio with Fixed-Size Chunking
The AudioCapture::process_audio_data method (lines 1048–1072) handles variable-sized callbacks from CPAL. It accumulates samples in a resampler_input_buffer until reaching exactly 512 samples, then drains the buffer through the Rubato resampler. The resampled output replaces the original mono_data before downstream transmission. If the persistent resampler fails to initialize, the system falls back to audio_processing::resample_audio from audio_processing.rs.
Key Technical Characteristics
- Energy Preservation: Reusing one resampler instance prevents the 173% RMS energy increase observed when recreating resamplers per chunk, a critical stability fix noted in the source comments.
- Variable Input Smoothing: The 512-sample buffer accommodates devices emitting 320, 512, or 1024-sample frames, smoothing asynchronous callback sizes into a steady stream.
- Adaptive Quality: The ratio-based switch automatically selects higher-quality sinc lengths for larger up-sampling factors while minimizing latency for modest conversions.
- Transparent Pipeline Integration: After conversion,
AudioCapturesets thesample_ratefield on emittedAudioChunkstructures to48000, ensuringContinuousVadProcessorand Whisper receive expected rates without additional checks.
Implementation Examples
// Creating an AudioCapture instance for a non-48kHz device
// Located in frontend/src-tauri/src/audio/pipeline.rs
let mic_capture = AudioCapture::new(
device.clone(),
recording_state.clone(),
device.sample_rate, // e.g., 16000 Hz from Bluetooth headset
device.channels,
DeviceType::Microphone,
Some(audio_sender.clone()),
);
// The constructor automatically logs a warning and allocates
// the persistent Rubato resampler if device.sample_rate != 48_000
// Processing raw audio from CPAL callbacks
// Called for every audio frame from the OS
fn on_audio_callback(data: &[f32]) {
mic_capture.process_audio_data(data);
// Internally:
// - Converts to mono if needed
// - Buffers samples until 512 samples accumulated
// - Runs Rubato resampler (e.g., 16kHz → 48kHz)
// - Sends resampled chunk downstream for VAD/mixing
}
# Inspecting resampling behavior in debug builds
RUST_LOG=debug ./clean_run.sh
# Expected output when capturing from 16kHz device:
# ⚠️ SAMPLE RATE MISMATCH DETECTED
# 🔄 [Microphone] Audio device 'AirPods Pro' reports 16000 Hz (pipeline expects 48000 Hz)
# 🔄 Automatic resampling will be applied: 16000 Hz → 48000 Hz
# Resampling strategy: High-quality upsampling (sinc_len=512, Cubic interpolation)
Core Files in the Resampling Pipeline
frontend/src-tauri/src/audio/pipeline.rs: ContainsAudioCapture::new(lines 1024–1052) for detection and resampler creation, andprocess_audio_data(lines 1048–1072) for buffering and conversion.frontend/src-tauri/src/audio/audio_processing.rs: Provides the fallbackresample_audiofunction used when the persistent resampler cannot be instantiated.frontend/src-tauri/src/audio/devices/*.rs: Device discovery modules that report native sample rates toAudioCapture.frontend/src-tauri/src/audio/vad.rs:ContinuousVadProcessorconsumes the normalized 48kHz stream, internally down-sampling to 16kHz for VAD analysis.
Summary
- Meetily enforces 48kHz for all audio via the
AudioCapturecomponent inpipeline.rs. - The Rubato library performs high-quality sinc resampling with adaptive parameters based on the conversion ratio.
- A 512-sample buffer smooths variable device callback sizes, feeding fixed-size chunks to the resampler.
- Persistent resampler instances prevent energy blow-up (avoiding 173% RMS increase) and maintain signal integrity across long capture sessions.
- The pipeline supports fallback resampling via
audio_processing.rswhen primary initialization fails.
Frequently Asked Questions
Why does Meetily require 48kHz instead of using the device's native rate?
Downstream components like the Whisper transcription engine and ProfessionalAudioMixer expect synchronized 48kHz audio to avoid resampling artifacts during mixing and to maintain consistent timing for VAD segmentation. Standardizing at capture prevents "sped up" playback bugs common with Bluetooth headsets operating at 16kHz, as noted in the AudioCapture implementation.
What happens if the Rubato resampler fails to initialize?
If AudioCapture::new cannot create the persistent rubato::SincFixedIn<f32> instance, the system falls back to the one-shot audio_processing::resample_audio function in audio_processing.rs. This fallback handles the conversion but may exhibit higher CPU usage or slightly different latency characteristics compared to the persistent approach.
How does the pipeline handle devices with variable callback sizes?
The resampler_input_buffer accumulates incoming samples from CPAL callbacks until reaching exactly 512 samples. If a callback delivers 320 samples, they queue until the next callback fills the buffer. This decouples variable device buffer sizes from the fixed-size requirements of the Rubato resampler, ensuring consistent output regardless of hardware buffer configurations.
Does resampling affect audio quality or introduce latency?
The pipeline uses high-quality sinc interpolation with parameters tuned to the conversion ratio (e.g., 512-tap filters for 2× up-sampling). While this adds minimal latency equivalent to the 512-sample buffer duration, it preserves energy and frequency content better than linear interpolation, preventing the 173% RMS increase observed in per-chunk resampling approaches.
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 →