How Meetily Implements RMS-Based Ducking for Professional Audio Mixing

Meetily's audio mixer uses RMS (Root-Mean-Square) calculations on 50ms microphone windows to detect speech activity, automatically reducing system audio gain to 0.60 (a 40% reduction) whenever voice is detected, ensuring clear dialogue prominence while preventing clipping through intelligent gain staging.

Meetily is an open-source meeting recording tool that synchronizes microphone and system audio streams in real-time. The RMS-based ducking algorithm implemented in its Rust audio pipeline ensures that voice content remains audible above background music or notification sounds, producing broadcast-quality mixed recordings without manual level adjustments.

Audio Pipeline Architecture

The ducking system operates within a multi-stage pipeline that synchronizes asynchronous audio sources before applying dynamic gain control.

Ring Buffer Synchronization

The AudioMixerRingBuffer in frontend/src-tauri/src/audio/pipeline.rs manages time-alignment between microphone and system audio streams. This lock-free VecDeque implementation buffers incoming samples until both sources contain sufficient data for a complete mixing window.

The ring buffer maintains a 400ms safety limit, dropping old samples when buffers exceed this threshold to prevent memory bloat. When both buffers contain at least window_size_samples (approximately 50ms), the extract_window method returns aligned slices (mic_window, sys_window), zero-padding any missing data to prevent artifacts.

Mixer Components

Meetily provides two implementations of the ducking algorithm:

  • ProfessionalAudioMixer: Located in pipeline.rs, this simpler mixer handles basic RMS detection and applies fixed ducking factors.
  • FFmpegAudioMixer: Found in frontend/src-tauri/src/audio/ffmpeg_mixer.rs, this full-featured variant includes adaptive buffering and specialized gap-handling for Bluetooth devices while implementing the identical RMS-based ducking logic.

The RMS-Based Ducking Algorithm

The ducking mechanism follows a six-step process to analyze microphone energy and adjust system audio levels dynamically.

RMS Calculation and Speech Detection

For each 50ms window, the mixer calculates the microphone's RMS level to determine speech presence. The implementation in pipeline.rs uses the standard RMS formula:

fn calculate_rms(samples: &[f32]) -> f32 {
    let sum_squares: f32 = samples.iter().map(|s| s * s).sum();
    (sum_squares / samples.len() as f32).sqrt()
}

The algorithm compares this value against a hard-coded SPEECH_THRESHOLD = 0.01. When the calculated RMS exceeds this threshold, the mixer identifies active speech and triggers the ducking mechanism.

Dynamic Gain Adjustment

Upon speech detection, the mixer applies the system_ducking factor (default 0.60) to the system audio stream, reducing its volume by 40%. When the microphone is silent, system audio plays at full gain (1.0).

The gain calculation occurs in the mix_window method:

let mic_rms = calculate_rms(mic);
let is_speech = mic_rms > 0.01;
let system_gain = if is_speech { self.system_ducking } else { 1.0 };

Mixing and Clipping Protection

The final mixing stage combines both streams with clamping to prevent digital distortion. The mixer computes each output sample as:

let mixed = (mic_sample * self.mic_ducking) + (system_sample * system_gain);
result.push(mixed.clamp(-1.0, 1.0));

This clamping ensures the output remains within the valid -1.0 to +1.0 range. The FFmpegAudioMixer implementation adds proportional soft-scaling for any over-range sums that might occur during high-amplitude moments.

Code Implementation Examples

Core Ducking Logic in pipeline.rs

The following excerpt from frontend/src-tauri/src/audio/pipeline.rs demonstrates the complete ducking algorithm:

fn mix(&mut self, mic: &[f32], system: &[f32]) -> Vec<f32> {
    let mic_rms = calculate_rms(mic);
    let is_speech = mic_rms > 0.01;               // speech threshold
    let system_gain = if is_speech { self.system_ducking } else { 1.0 };

    mic.iter()
        .zip(system.iter())
        .map(|(&m, &s)| (m * self.mic_ducking + s * system_gain).clamp(-1.0, 1.0))
        .collect()
}

FFmpeg-Style Mixer Usage

For custom tooling or advanced use cases, the FFmpegAudioMixer provides a configurable interface:

use meetily::frontend::src_tauri::src::audio::ffmpeg_mixer::FFmpegAudioMixer;
use meetily::frontend::src_tauri::src::audio::device_detection::InputDeviceKind;

let mut mixer = FFmpegAudioMixer::new(
    "Built‑in Microphone".into(),
    InputDeviceKind::Wired,
    "BlackHole".into(),
    InputDeviceKind::Wired,
    48_000,
);

// Push raw chunks as they arrive
mixer.push_mic(mic_chunk);
mixer.push_system(system_chunk);

// Extract processed window with ducking applied
if let Some(mixed) = mixer.pop_mixed() {
    send_to_transcriber(mixed);
}

Pipeline Initialization

The ducking system initializes through the AudioPipelineManager, which coordinates the ring buffer, mixer, and VAD components:

let mut manager = AudioPipelineManager::default();
manager.start(
    state.clone(),
    transcription_sender,
    0,                      // target_chunk_duration_ms
    48000,                  // sample_rate
    Some(recording_sender), // receive mixed audio for WAV files
    mic_device_name,
    mic_device_kind,
    system_device_name,
    system_device_kind,
)?;

Summary

  • RMS-based ducking in Meetily analyzes microphone energy using 50ms windows to detect speech activity in real-time.
  • The ProfessionalAudioMixer and FFmpegAudioMixer both implement a threshold of 0.01 RMS with a default ducking factor of 0.60 (40% attenuation).
  • The AudioMixerRingBuffer maintains synchronization between asynchronous mic and system streams using a 400ms safety buffer.
  • All mixed output undergoes clamping to -1.0…+1.0 to prevent hard clipping, with optional soft-scaling in the FFmpeg implementation.
  • The complete implementation resides in frontend/src-tauri/src/audio/pipeline.rs and frontend/src-tauri/src/audio/ffmpeg_mixer.rs.

Frequently Asked Questions

What is RMS-based ducking and why does Meetily use it?

RMS-based ducking measures the Root-Mean-Square energy of audio windows to objectively quantify loudness. Meetily uses this method rather than simple peak detection because RMS better correlates with perceived loudness, ensuring that brief audio spikes don't trigger unnecessary volume reductions while genuine speech consistently activates the ducking mechanism.

How does Meetily prevent audio clipping during mixing?

The mixer applies a clamp(-1.0, 1.0) operation to every output sample immediately after summing the mic and system channels. The FFmpegAudioMixer additionally implements soft-scaling that proportionally reduces the entire window when the sum exceeds unity, preserving the audio's dynamic character while eliminating hard digital distortion.

What is the difference between ProfessionalAudioMixer and FFmpegAudioMixer?

The ProfessionalAudioMixer in pipeline.rs provides a straightforward implementation suitable for standard wired audio devices, while FFmpegAudioMixer in ffmpeg_mixer.rs offers enhanced buffering logic specifically designed to handle Bluetooth audio gaps and variable latency. Both implement identical RMS-based ducking algorithms and produce identical gain reduction behavior.

Can the ducking threshold or gain values be customized?

Currently, the SPEECH_THRESHOLD (0.01) and system_ducking factor (0.60) are hard-coded constants in the Rust source. To modify these values, you must edit the constants in frontend/src-tauri/src/audio/pipeline.rs (for the simple mixer) or ffmpeg_mixer.rs (for the adaptive mixer) and recompile the application. Future versions may expose these parameters through the Tauri configuration interface.

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 →