How to Implement RMS-Based Audio Ducking in Meetily to Prevent System Audio from Drowning Out Microphone

TLDR: Meetily achieves RMS-based audio ducking by calculating the root-mean-square energy of microphone chunks in AudioMixer::mix and attenuating system audio to 60% volume (configurable via system_ducking) whenever the mic signal exceeds the SPEECH_THRESHOLD of 0.01, preventing background audio from overpowering speech.

Meetily is an open-source meeting assistant that captures parallel audio streams from both microphone and system sources. When recording meetings with shared media or background applications, implementing RMS-based audio ducking in Meetily ensures the microphone remains intelligible by automatically reducing system volume during speech. The ducking logic lives entirely within the Rust-based audio pipeline in the frontend/src-tauri/src/audio/ directory of the Zackriya-Solutions/meetily repository.

Understanding the Audio Pipeline Architecture

Meetily's audio processing relies on four interconnected components that manage buffering, mixing, and device-specific timing.

  • SourceBuffer: Located in frontend/src-tauri/src/audio/ffmpeg_mixer.rs (lines 78-110), this struct holds time-stamped audio chunks for each input source and applies adaptive timeouts based on whether the device is wired or Bluetooth.

  • AudioMixer: The core ducking implementation in frontend/src-tauri/src/audio/ffmpeg_mixer.rs (lines 55-85). This struct performs RMS calculations and applies dynamic gain scaling when adaptive_ducking is enabled.

  • FFmpegAudioMixer: A façade that orchestrates two SourceBuffer instances, calls AudioMixer::mix, and returns 50ms mixed windows. Defined in frontend/src-tauri/src/audio/ffmpeg_mixer.rs (lines 40-70).

  • AudioPipeline: A higher-level manager in frontend/src-tauri/src/audio/pipeline.rs (lines 45-90) that extracts windows and forwards them to the recorder. It falls back to ProfessionalAudioMixer when adaptive ducking is disabled.

How RMS-Based Ducking Works

The ducking algorithm follows a five-step process that evaluates microphone energy and adjusts system gain in real-time:

  1. Capture: AudioCapture feeds microphone and system audio into their respective SourceBuffer instances.

  2. Ready Check: FFmpegAudioMixer::has_data_ready verifies both buffers contain sufficient data according to device-specific adaptive timeouts.

  3. RMS Calculation: Inside AudioMixer::mix, the system computes the root-mean-square energy of the microphone chunk using calculate_rms. The code compares this against a calibrated threshold:

let mic_rms = calculate_rms(mic);
const SPEECH_THRESHOLD: f32 = 0.01;   // calibrated for meetings
let is_speech = mic_rms > SPEECH_THRESHOLD;

This logic appears in frontend/src-tauri/src/audio/ffmpeg_mixer.rs at lines 88-95.

  1. Dynamic Gain Application: When speech is detected, the system audio gain is set to the system_ducking factor (default 0.60, representing 40% reduction). Otherwise, system gain remains at 1.0 (full volume).

  2. Mixing and Clipping: Samples are mixed using the formula mic * mic_ducking + system * system_gain and clamped to [-1.0, 1.0] to prevent digital clipping (lines 103-108 in ffmpeg_mixer.rs).

Implementing the Ducking Logic

Basic Setup with FFmpegAudioMixer

To enable RMS-based ducking in your Meetily implementation, instantiate FFmpegAudioMixer with adaptive processing enabled. The following example configures the mixer for 48kHz audio with default ducking parameters:

use meetily::audio::ffmpeg_mixer::FFmpegAudioMixer;
use meetily::audio::device_detection::InputDeviceKind;

// Create the adaptive mixer (adaptive ducking enabled by default)
let mut mixer = FFmpegAudioMixer::new(
    "Built‑in Mic".into(),
    InputDeviceKind::Wired,
    "BlackHole 2ch".into(),
    InputDeviceKind::Wired,
    48_000,
);

// Feed raw chunks as they arrive from capture callbacks
mixer.push_mic(mic_chunk);
mixer.push_system(sys_chunk);

// Retrieve mixed 50ms windows when both buffers have data
if let Some(mixed) = mixer.pop_mixed() {
    // mixed is a Vec<f32> containing interleaved stereo samples
    // Forward to Whisper transcription or file recorder
    transcribe_audio(mixed);
}

The constructor is defined at FFmpegAudioMixer::new (lines 40-70) and the mixing retrieval at FFmpegAudioMixer::pop_mixed (lines 94-106).

Customizing Ducking Parameters

For fine-grained control over the ducking behavior, construct a custom AudioMixer struct with modified gain values. This example reduces system audio to 40% during speech instead of the default 60%:

use meetily::audio::ffmpeg_mixer::{AudioMixer, FFmpegAudioMixer};
use meetily::audio::device_detection::InputDeviceKind;

// Configure custom AudioMixer with aggressive ducking
let custom_mixer = AudioMixer {
    mic_ducking: 1.0,           // maintain full microphone volume
    system_ducking: 0.4,        // reduce system to 40% during speech
    adaptive_ducking: true,     // enable RMS-based speech detection
};

// Construct FFmpegAudioMixer with custom configuration
let mut mixer = FFmpegAudioMixer {
    mic_buffer: SourceBuffer::new("Mic".into(), InputDeviceKind::Wired, 48_000),
    system_buffer: SourceBuffer::new("System".into(), InputDeviceKind::Wired, 48_000),
    mixer: custom_mixer,
    sample_rate: 48_000,
    mixing_window_samples: 2400, // 50ms at 48kHz
    windows_mixed: 0,
};

// Process audio as normal - custom system_ducking applies automatically
mixer.push_mic(microphone_data);
mixer.push_system(system_data);

The AudioMixer struct fields are defined at lines 57-63 in ffmpeg_mixer.rs, with the default constructor implementation at lines 68-74.

Configuring Speech Detection Thresholds

The sensitivity of the ducking trigger depends on two configurable constants in the AudioMixer implementation:

  • SPEECH_THRESHOLD: Set to 0.01f32 by default (lines 88-95). Lower values trigger ducking more aggressively for quiet speech; higher values reduce false positives from background noise.

  • system_ducking: Controls the attenuation depth. Values range from 0.0 (complete mute) to 1.0 (no ducking). The default 0.60 provides a balance where system audio remains audible but subordinate to microphone input.

To adjust the threshold for different acoustic environments, modify the constant in frontend/src-tauri/src/audio/ffmpeg_mixer.rs before compilation, or extend the AudioMixer struct to accept runtime threshold configuration.

Summary

Meetily's RMS-based audio ducking provides automatic gain control that preserves microphone clarity during system audio playback:

  • The AudioMixer in frontend/src-tauri/src/audio/ffmpeg_mixer.rs calculates microphone RMS energy and attenuates system audio by 40% when SPEECH_THRESHOLD is exceeded.
  • FFmpegAudioMixer orchestrates the dual-buffer architecture, delivering 50ms mixed windows at 48kHz sample rate.
  • Ducking behavior is controlled via the system_ducking parameter (0.0-1.0) and the adaptive_ducking boolean flag.
  • The algorithm prevents clipping by clamping mixed output to [-1.0, 1.0] using the formula mic * mic_ducking + system * system_gain.

Frequently Asked Questions

How does Meetily detect when to apply audio ducking?

Meetily detects speech by calculating the root-mean-square (RMS) energy of each microphone audio chunk in AudioMixer::mix. When the calculated mic_rms value exceeds the SPEECH_THRESHOLD constant of 0.01, the system identifies active speech and triggers the ducking mechanism to reduce system audio volume according to the system_ducking parameter.

Can I disable RMS-based ducking and use simple mixing instead?

Yes. Set the adaptive_ducking parameter to false when constructing the AudioMixer, or use the ProfessionalAudioMixer fallback located in frontend/src-tauri/src/audio/pipeline.rs. When disabled, the mixer sums both audio sources at full volume without RMS analysis or dynamic gain adjustments.

What is the latency impact of the RMS calculation?

The RMS calculation adds negligible latency because it operates on 50ms windows (2400 samples at 48kHz) using a single pass through the buffer. The FFmpegAudioMixer maintains separate SourceBuffer instances for microphone and system audio, ensuring that timing synchronization occurs before mixing rather than during the RMS computation itself.

How do I tune the ducking for very quiet speakers?

Decrease the SPEECH_THRESHOLD constant in frontend/src-tauri/src/audio/ffmpeg_mixer.rs below the default 0.01 value, or increase the system_ducking attenuation to 0.3 or 0.2 for more aggressive background suppression. For extremely quiet environments, you may also need to adjust your microphone gain upstream before it reaches the Meetily pipeline, as the RMS calculation depends on sufficient signal-to-noise ratio in the input buffer.

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 →