How to Duck Music Under a Voice-Over in ffmpeg: sidechaincompress Settings, Measured
📅 September 29, 2026 · ⏱️ 14 min read
Feed the music into the sidechaincompress filter as its first input and the voice as its second, then mix the voice back on top with amix. This is the whole command, with settings chosen from the measurements further down:
ffmpeg -y -i voice.wav -i music.wav -filter_complex "
[1:a][0:a]sidechaincompress=threshold=0.05:ratio=8:attack=10:release=400[ducked];
[0:a][ducked]amix=inputs=2:duration=first:normalize=0[out]
" -map "[out]" mix.wav
Read the graph left to right. [1:a] is the music and [0:a] is the voice. sidechaincompress turns down its first input whenever its second input (the “sidechain”, or key) is loud, so the order of those two labels is the order that matters most on this page: swap them and you get a voice that ducks under the music. The voice is then mixed back over the ducked music with amix. normalize=0 stops amix from halving both inputs, which it does by default; duration=first makes the output as long as the voice.
Most answers to this question stop there, with the filter at its default settings. The rest of this page is what those defaults actually do, what each of the four settings that matter changes, and why the numbers in the command above are the ones to start from. Every figure below was produced by running the commands on this page with ffmpeg 4.4.2, the version Ubuntu 22.04 ships, in an empty directory.
Make test inputs you can measure
Real speech and real music are too irregular to show what one setting does, so the test uses two synthetic stand-ins that ffmpeg can generate itself, with no files to download:
# Stand-in "voice": a 220 Hz tone, 1.2 s on / 0.8 s off, for 12 s.
ffmpeg -y -f lavfi -i "aevalsrc='0.5*sin(2*PI*220*t)*lt(mod(t,2),1.2)':s=48000:d=12" voice.wav
# Stand-in "music": steady pink noise, fixed seed so every run is identical.
ffmpeg -y -f lavfi -i "anoisesrc=color=pink:amplitude=0.5:sample_rate=48000:seed=1:d=12" music.wav
The “voice” is six phrases of 1.2 seconds with 0.8-second pauses between them. Its level while it is sounding is -9.0 dBFS RMS, a hot, fully normalised level; a quieter voice ducks less, as a later section measures. The “music” is a continuous bed at -20.1 dBFS RMS, 11.1 dB under the voice. That is already a quieter bed than many people start with, and it is still too loud to talk over comfortably, which is why ducking exists.
With both files in place, the command at the top of the page runs as-is and writes mix.wav.
How to measure the music level during speech and in the gaps
To see what the compressor does, render the ducked music on its own (no voice mixed in) and measure its level separately over the stretches where the voice is talking and the stretches where it is silent. astats measures the level. asetnsamples=n=480 cuts the audio into 10 ms blocks (480 samples at 48 kHz), so every 2-second cycle is exactly 200 blocks, and aselect keeps only the blocks you ask for by their index n:
level() { # $1 = file, $2 = which 10 ms blocks to keep
ffmpeg -hide_banner -nostats -i "$1" -af "asetnsamples=n=480,aselect='$2',astats=measure_perchannel=none:measure_overall=RMS_level" -f null - 2>&1 | sed -n 's/.*RMS level dB: //p'
}
SPEECH='lt(mod(n,200),120)' # the 1.2 s phrase in every 2 s cycle
ONSET='lt(mod(n,200),5)' # the first 50 ms of each phrase
GAPS='gte(mod(n,200),120)' # the 0.8 s gap after it
echo "voice $(level voice.wav "$SPEECH")"
echo "undocked $(level music.wav "$SPEECH") $(level music.wav "$ONSET") $(level music.wav "$GAPS")"
for p in threshold=0.125 threshold=0.05 threshold=0.02 ratio=4 ratio=8 ratio=20 attack=5 attack=200 release=50 release=1000 threshold=0.05:ratio=8:attack=10:release=400; do
ffmpeg -hide_banner -loglevel error -y -i music.wav -i voice.wav -filter_complex "[0:a][1:a]sidechaincompress=$p[out]" -map "[out]" ducked.wav
echo "$p $(level ducked.wav "$SPEECH") $(level ducked.wav "$ONSET") $(level ducked.wav "$GAPS")"
done
threshold=0.125 is the filter’s own default, so that line is the plain sidechaincompress everyone copies. Each other line changes one setting and leaves the rest at their defaults, except the last, which is the command from the top of the page. The whole loop takes a few seconds. This is its output, word for word, and every value is an RMS level in dBFS:
voice -9.030648
undocked -20.133579 -20.302776 -20.151055
threshold=0.125 -25.717696 -24.964507 -20.556374
threshold=0.05 -29.684225 -28.691266 -21.103016
threshold=0.02 -33.646397 -32.352869 -21.835660
ratio=4 -28.496123 -27.118110 -20.677397
ratio=8 -29.879574 -28.134054 -20.725221
ratio=20 -30.707186 -28.721006 -20.750705
attack=5 -25.970720 -25.779918 -20.582397
attack=200 -24.598402 -22.206500 -20.452159
release=50 -25.161662 -24.690917 -20.219903
release=1000 -25.936138 -25.093055 -22.266223
threshold=0.05:ratio=8:attack=10:release=400 -37.157219 -34.892767 -22.468435
What threshold, ratio, attack and release each do
The same numbers as a table, rounded to 0.1 dB. “Ducked by” is how far the music sits below its undocked level (-20.1 dBFS) while the voice is talking; bigger means quieter music under speech. The last column is how far the music still sits below its undocked level (-20.2 dBFS) averaged over the 0.8-second pauses; smaller means the music comes back up faster between phrases.
| Setting (others at default) | Music during speech | Ducked by | First 50 ms of a phrase | Music in the gaps | Still down in the gaps |
|---|---|---|---|---|---|
| no ducking | -20.1 dBFS | 0.0 dB | -20.3 dBFS | -20.2 dBFS | 0.0 dB |
| defaults (threshold=0.125, ratio=2, attack=20, release=250) | -25.7 dBFS | 5.6 dB | -25.0 dBFS | -20.6 dBFS | 0.4 dB |
| threshold=0.05 | -29.7 dBFS | 9.6 dB | -28.7 dBFS | -21.1 dBFS | 1.0 dB |
| threshold=0.02 | -33.6 dBFS | 13.5 dB | -32.4 dBFS | -21.8 dBFS | 1.7 dB |
| ratio=4 | -28.5 dBFS | 8.4 dB | -27.1 dBFS | -20.7 dBFS | 0.5 dB |
| ratio=8 | -29.9 dBFS | 9.7 dB | -28.1 dBFS | -20.7 dBFS | 0.6 dB |
| ratio=20 | -30.7 dBFS | 10.6 dB | -28.7 dBFS | -20.8 dBFS | 0.6 dB |
| attack=5 | -26.0 dBFS | 5.8 dB | -25.8 dBFS | -20.6 dBFS | 0.4 dB |
| attack=200 | -24.6 dBFS | 4.5 dB | -22.2 dBFS | -20.5 dBFS | 0.3 dB |
| release=50 | -25.2 dBFS | 5.0 dB | -24.7 dBFS | -20.2 dBFS | 0.1 dB |
| release=1000 | -25.9 dBFS | 5.8 dB | -25.1 dBFS | -22.3 dBFS | 2.1 dB |
| recommended: threshold=0.05, ratio=8, attack=10, release=400 | -37.2 dBFS | 17.0 dB | -34.9 dBFS | -22.5 dBFS | 2.3 dB |
The defaults barely duck. At its defaults, sidechaincompress pulled the music down by only 5.6 dB while the voice was talking, and the music was back to within 0.4 dB of full level in the gaps. A 5.6 dB dip is audible if you listen for it, but the music is still only 16.7 dB under the voice, when the undocked bed was already 11.1 dB under it. If your music feels as if it is fighting the narration after you added sidechaincompress, this is why.
Threshold sets how deep the duck can go. threshold is a linear amplitude, not dB: the default 0.125 is -18.1 dBFS, 0.05 is -26.0 dBFS and 0.02 is -34.0 dBFS. The compressor only acts on the part of the voice that is louder than the threshold, so lowering it is the single biggest lever. Going from 0.125 to 0.02 took the duck from 5.6 dB to 13.5 dB with the ratio still at 2.
Ratio has a ceiling set by the threshold. Raising ratio from 2 to 20 at the default threshold only moved the duck from 5.6 dB to 10.6 dB. The reason is the arithmetic of a compressor: the most it can take off is roughly how far the voice exceeds the threshold, times (1 − 1/ratio). Here the voice is 9.0 dB over the default threshold, so no ratio can duck by much more than that. Worked for the recommended settings: (-9.0 − -26.0) × (1 − 1/8) = 17.0 × 0.875 = 14.9 dB estimated, against 17.0 dB measured. In all nine rows that change only the threshold, the ratio or the voice level, the measured duck came out deeper than that estimate by between 1.0 and 2.2 dB, so treat the formula as a floor, not a prediction.
Attack is how fast the music gets out of the way. It is in milliseconds. At attack=200, the first 50 ms of every phrase had the music only 1.9 dB below its undocked level, so the first syllable of each sentence lands on top of full-volume music. At the default 20 it was 4.7 dB below, and at attack=5 it was 5.5 dB below. Anything from 5 to 20 is fine for speech; above that you start to hear the music “lean back” a beat late.
Release is how fast the music comes back. Also in milliseconds. At release=50 the music was back to within 0.1 dB of full level across the gaps, which on real speech means the music swells up in every pause between words, the “pumping” people complain about. At release=1000 it was still 2.1 dB down averaged over each 0.8-second gap. A few hundred milliseconds keeps the music down through the short gaps between words and lets it come back during longer pauses.
A starting point, and how to adjust it
For a voice-over that has been normalised to a normal speaking level, start with:
sidechaincompress=threshold=0.05:ratio=8:attack=10:release=400
A threshold of 0.05, a ratio of 8, a 10 ms attack and a 400 ms release make a sound starting point for a voice-over that has been normalised to a normal speaking level. On the test inputs it ducked the music by 17.0 dB during speech, put the music 28.1 dB under the voice, and left it 2.3 dB below full level averaged over the pauses, so the bed rises between sentences without swelling between every word. Measured with ebur128, the integrated loudness of the music bed went from -20.7 LUFS undocked to -24.2 LUFS ducked.
Then adjust by ear, one setting at a time:
- Music still too loud under the voice: lower
threshold(0.03, then 0.02). Do this before touchingratio. - Music vanishes completely under the voice: raise
threshold(0.08, then 0.125). - The first word of each sentence is buried: lower
attack, to 5. - Music pumps up between words: raise
release, to 600 or 800. - Music takes too long to come back after the narrator stops: lower
release, to 250.
To drop these settings into a full vertical-video command for your own clip, use the Shorts ffmpeg command builder.
The duck depends on how loud the voice is
A compressor keys off the level of the sidechain, so the same settings duck a quiet voice far less than a loud one. Run this after the loop above, in the same shell, and it repeats the recommended settings with the voice at a half and a quarter of its amplitude (6 dB and 12 dB quieter):
for a in 0.25 0.125; do
ffmpeg -hide_banner -loglevel error -y -f lavfi -i "aevalsrc='$a*sin(2*PI*220*t)*lt(mod(t,2),1.2)':s=48000:d=12" quiet.wav
ffmpeg -hide_banner -loglevel error -y -i music.wav -i quiet.wav -filter_complex "[0:a][1:a]sidechaincompress=threshold=0.05:ratio=8:attack=10:release=400[out]" -map "[out]" ducked.wav
echo "voice x$a $(level quiet.wav "$SPEECH") $(level ducked.wav "$SPEECH") $(level ducked.wav "$GAPS")"
done
voice x0.25 -15.051235 -31.949669 -21.422798
voice x0.125 -21.071810 -26.711471 -20.678068
| Voice level during speech | Music during speech | Ducked by |
|---|---|---|
| -9.0 dBFS | -37.2 dBFS | 17.0 dB |
| -15.1 dBFS | -31.9 dBFS | 11.8 dB |
| -21.1 dBFS | -26.7 dBFS | 6.6 dB |
A voice 12 dB quieter got a duck of 6.6 dB instead of 17.0 dB. So normalise the voice first (for example with loudnorm) and set the ducking after, or copy settings from someone else’s command knowing they were tuned for their voice level, not yours. If you would rather not touch the voice, level_sc boosts only what the compressor hears from the sidechain, which has the same effect on the duck as a louder voice without changing the voice in the mix.
Gotcha: the output stops when the voice stops
sidechaincompress stops when its sidechain input stops, so a voice track shorter than the music cuts the whole output to the length of the voice. That matters if you want the music to keep playing after the narration, for an intro or an outro. Padding the voice with silence using apad fixes it:
ffmpeg -hide_banner -loglevel error -y -f lavfi -i "anoisesrc=color=pink:amplitude=0.5:sample_rate=48000:seed=1:d=20" long-music.wav
ffmpeg -hide_banner -loglevel error -y -i long-music.wav -i voice.wav -filter_complex "[0:a][1:a]sidechaincompress[out]" -map "[out]" cut.wav
ffmpeg -hide_banner -loglevel error -y -i long-music.wav -i voice.wav -filter_complex "[1:a]apad[key];[0:a][key]sidechaincompress[out]" -map "[out]" padded.wav
for f in long-music.wav cut.wav padded.wav; do echo "$f $(ffprobe -v error -show_entries format=duration -of csv=p=0 $f)"; done
long-music.wav 20.000000
cut.wav 12.000000
padded.wav 20.000000
With 20 seconds of music and a 12-second voice, the plain version came out at 12 seconds and the padded one at the full 20. The same applies to the full mix: amix also needs duration=longest instead of duration=first. Without apad, even duration=longest still came out at 12 seconds, because the ducked music itself ends there. The complete version for music that outlasts the narration:
ffmpeg -y -i voice.wav -i long-music.wav -filter_complex "
[0:a]apad[key];
[1:a][key]sidechaincompress=threshold=0.05:ratio=8:attack=10:release=400[ducked];
[0:a][ducked]amix=inputs=2:duration=longest:normalize=0[out]
" -map "[out]" long-mix.wav
ffprobe -v error -show_entries format=duration -of csv=p=0 long-mix.wav
It prints 20.000000: the whole music track, with the voice over the first 12 seconds.
The loudness figures above came from this block, which renders the recommended ducked bed and runs ebur128 on it and on the original:
ffmpeg -hide_banner -loglevel error -y -i music.wav -i voice.wav -filter_complex "[0:a][1:a]sidechaincompress=threshold=0.05:ratio=8:attack=10:release=400[out]" -map "[out]" ducked.wav
for f in music.wav ducked.wav; do
echo "$f $(ffmpeg -hide_banner -nostats -i $f -af ebur128 -f null - 2>&1 | sed -n 's/^ *I: *//p' | tail -1)"
done
music.wav -20.7 LUFS
ducked.wav -24.2 LUFS
What this test does not tell you
- A tone is not a voice. Real speech has syllables, consonants and breaths, and its level moves several dB inside a sentence. The default
detection=rmssmooths some of that out, but expect a real narration to duck less evenly than the steady tone here. Use the table for direction and size, then listen. - Pink noise is not music. A compressor keyed from the voice turns the music down by the same number of dB whatever the music is, so the “Ducked by” column carries over. How that duck sounds on a busy track with drums does not.
- Only ffmpeg 4.4.2 was tested. The numbers on this page were produced on that version only; check
ffmpeg -h filter=sidechaincompresson yours for the option names and defaults. mix,makeupandkneewere left at their defaults.makeupraises the whole ducked output, music included, so it works against ducking; there is rarely a reason to set it here.
Where this came from
The YouTube Shorts automation on a Raspberry Pi write-up includes a runnable ffmpeg example that ducks music under narration, and it says plainly that its sidechaincompress runs at the defaults because the pipeline it describes never published a threshold or ratio. This page measures what those defaults do: a 5.6 dB duck under a normal voice level. The starting point above is the one to try instead.
Follow new posts by RSS: paste dankdev.com/feed.xml into any feed reader. It is a plain XML file — no signup, no email address, no account. Posts land there the same day they go up, and there is nothing to unsubscribe from later.