# How Meetily’s Audio Resampling Pipeline Maintains a Consistent 48kHz Sample Rate Across Capture Devices

> Meetily's audio resampling pipeline ensures a consistent 48kHz sample rate with a Rubato resampler. Learn how it buffers device audio to prevent energy blow-up and timing drift.

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

---

**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`](https://github.com/Zackriya-Solutions/meetily/blob/main/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`](https://github.com/Zackriya-Solutions/meetily/blob/main/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`](https://github.com/Zackriya-Solutions/meetily/blob/main/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, `AudioCapture` sets the `sample_rate` field on emitted `AudioChunk` structures to `48000`, ensuring `ContinuousVadProcessor` and Whisper receive expected rates without additional checks.

## Implementation Examples

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

```

```rust
// 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
}

```

```bash

# 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`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/audio/pipeline.rs)**: Contains `AudioCapture::new` (lines 1024–1052) for detection and resampler creation, and `process_audio_data` (lines 1048–1072) for buffering and conversion.
- **[`frontend/src-tauri/src/audio/audio_processing.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/audio/audio_processing.rs)**: Provides the fallback `resample_audio` function used when the persistent resampler cannot be instantiated.
- **`frontend/src-tauri/src/audio/devices/*.rs`**: Device discovery modules that report native sample rates to `AudioCapture`.
- **[`frontend/src-tauri/src/audio/vad.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/audio/vad.rs)**: `ContinuousVadProcessor` consumes the normalized 48kHz stream, internally down-sampling to 16kHz for VAD analysis.

## Summary

- Meetily enforces **48kHz** for all audio via the `AudioCapture` component in [`pipeline.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/pipeline.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.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/audio_processing.rs) when 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`](https://github.com/Zackriya-Solutions/meetily/blob/main/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.