ffmpegaudioapi

Fixed music volume is a compromise. FFmpeg audio ducking isn't.

·Javid Jamae·10 min read
Fixed music volume is a compromise. FFmpeg audio ducking isn't.

You render a podcast clip or a faceless-channel video, drop a music bed under the voiceover at a fixed volume, and every take is a compromise. Loud enough to feel like a video, quiet enough that nobody misses a word. Broadcast engineers stopped picking that number decades ago: they let the voice push the music down and let it come back up on its own.

Quick answer: FFmpeg audio ducking is done with the sidechaincompress filter. Split the voiceover with asplit, feed one copy as the second input to sidechaincompress with the music as the first, then amix the compressed music back with the voice: ffmpeg -i music.mp3 -i voice.wav -filter_complex "[1:a]asplit=2[sc][vox];[0:a][sc]sidechaincompress=threshold=0.03:ratio=8:attack=20:release=500[duck];[duck][vox]amix=inputs=2:normalize=0[a]" -map "[a]" -c:a aac -b:a 192k out.m4a. If you'd rather not host an encoder to run that graph on every render, FFmpeg Micro takes the same filtergraph as one API call: https://www.ffmpeg-micro.com/ffmpeg-api

Conventional wisdom says ducking is a mixing job, so you need a mixing tool. That's half right. The gain curve really is the hard part, but you don't draw it. sidechaincompress is a compressor whose gain reduction is driven by a different signal than the one it's compressing, so the voice track computes the curve for you, sample by sample. The reason most people's first attempt does nothing isn't the parameters. It's that they wired the graph so the compressor never sees the voice at all.

FFmpeg audio ducking needs three filters, not one

Ducking in FFmpeg takes asplit, sidechaincompress, and amix together, because sidechaincompress outputs only its first input. The voice goes in as a control signal and comes back out nowhere. You get a nicely ducked music bed with silence on top of it, which is why so many mailing-list answers end with someone asking where their narration went.

asplit=2 duplicates the voice stream into two labeled outputs. One copy drives the compressor's detector, the other goes straight to the mixer. Here's the full command with video attached, which is what a real render looks like:

ffmpeg -i input.mp4 -i music.mp3 -i voice.wav \
  -filter_complex "\
[2:a]aformat=sample_fmts=fltp:sample_rates=48000:channel_layouts=stereo,asplit=2[sc][vox];\
[1:a]aformat=sample_fmts=fltp:sample_rates=48000:channel_layouts=stereo,apad[music];\
[music][sc]sidechaincompress=threshold=0.03:ratio=8:attack=20:release=500:makeup=1[duck];\
[duck][vox]amix=inputs=2:normalize=0:duration=first[mixed];\
[mixed]loudnorm=I=-14:TP=-1.5:LRA=11[a]" \
  -map 0:v -map "[a]" -c:v copy -c:a aac -b:a 192k -shortest output.mp4

Read the labels, not the filters. Voice splits into [sc] and [vox]. Music gets padded and compressed against [sc]. The ducked music mixes with [vox]. The mix gets normalized to -14 LUFS, which is where YouTube stops turning your audio down. Podcast platforms generally target -16 LUFS, so change I=-14 if that's your destination.

The sidechain has to be the second input, and both tracks need the same format

Input order in sidechaincompress is not symmetric: the first input is the signal that gets compressed and the second is the key that triggers it. Swap them and the command still runs, still produces audio, and ducks your voiceover under the music. That failure is quiet, which is what makes it expensive. You only catch it on playback.

Format mismatch is the other silent one. A mono 44.1 kHz voice recording and a stereo 48 kHz music file don't fail loudly. FFmpeg negotiates something, and what you get back rarely matches what you expected. Forcing aformat=sample_fmts=fltp:sample_rates=48000:channel_layouts=stereo on both branches before they meet costs nothing and removes the whole class of problem. Use it even when you think the files match, because a voice track that came out of a text-to-speech API is mono more often than not.

Release time is what makes ducking sound automatic

Release is the single parameter that separates a mix people describe as professional from one they describe as weird. It's how long the music takes to climb back after the voice stops. FFmpeg's default is 250 ms, which is fast enough that the bed audibly surges in the gaps between sentences. That's the pumping sound.

ParameterWhat it controlsVoice-over-music value
`threshold`Level the voice must exceed to trigger ducking (linear, not dB)`0.03` (about -30 dBFS)
`ratio`How hard the music is pushed down once triggered`6` to `10`
`attack`Milliseconds before the duck engages`5` to `20`
`release`Milliseconds to return to full volume`400` to `800`
`level_sc`Gain applied to the detector input only`1`, raise to `4`-`8` for a quiet voice
`makeup`Output gain after compression`1` (leave it)

The threshold is linear amplitude, not decibels, and that trips up everyone who's used a hardware compressor. FFmpeg's default of 0.125 is roughly -18 dBFS. A normal spoken track recorded with headroom peaks around -12 dBFS and averages closer to -26, so at the default threshold a lot of speech never crosses the line and the music never moves. Run ffmpeg -i voice.wav -af volumedetect -f null - first, look at mean_volume, and set your threshold below it.

Ratio behaves the way it does anywhere else. At ratio=8, with the voice sitting 12 dB over the threshold, the music drops about 10 dB. Attack should stay short enough that the duck lands before the first syllable, but 0 ms clips consonants, so 5 to 20 ms is the working range.

Common pitfalls that produce a technically correct, unusable mix

The one that catches almost everybody is amix. It normalizes by default, dividing every input by the number of inputs, so a two-input mix comes out roughly 6 dB quieter than either source. Your ducking works and the whole video sounds thin. Set normalize=0 and control levels yourself with volume filters on each branch.

Music shorter than the voiceover is the second one. A 90-second royalty-free loop under a 4-minute narration means sidechaincompress runs out of one of its inputs and the mix ends early. apad on the music branch pads it with silence indefinitely, and duration=first on amix plus -shortest on the output ties the final length to the voice track. If your music is a loop, -stream_loop -1 before that input repeats it instead of padding with silence.

Third: sidechaincompress can't separate a voice that's already mixed into the same file as the music. It's a compressor, not source separation. You need two discrete tracks. If your only asset is a finished mix, ducking is the wrong tool entirely.

Fourth, if the output has video but no audio, the problem is usually your -map arguments and not the filtergraph. That failure mode has its own tell, covered in why "stream map matches no streams" means no audio.

The same mix as one API call

This filtergraph is the last step of a render, which is exactly where it's least convenient to run. If the voiceover comes from ElevenLabs or OpenAI TTS inside an n8n or Make workflow, the ducked mix is the only step in that chain that needs an FFmpeg binary, a machine to run it on, and a timeout window long enough for a 40-minute podcast episode. FFmpeg Micro takes the same filter string over its REST API: submit the job with your input URLs, get a job ID back, poll it or take a webhook, download the output. No servers to run, no encoder to install, and the free tier is enough to test the parameters on a real episode before you wire it into anything. The exact request shape is in the docs, and you can try a graph against your own files in the playground first.

That's the same offload logic as the assembly step in AI video generators that stop at 8 seconds: the generation is a hosted API call already, and the muxing shouldn't be the only part you babysit.

When sidechain ducking is the wrong call

Sidechain ducking is a blunt instrument, and there are jobs it does badly. Narrative or documentary work where the score is supposed to swell under a pause needs a human drawing automation in a DAW, not a compressor reacting to level. Music that's already heavily compressed and dynamically flat ducks fine but comes back up with an obvious lurch, so you're better off just mixing it 18 dB down and leaving it there.

There's also a precision case. If you have a word-level transcript, you can duck to exact timestamps by driving the volume filter with sendcmd or enable expressions instead of reacting to amplitude. That's more setup, but it never misfires on a cough or a loud breath, and it's what you want for a video where captions and music cues need to line up. The same transcript that gives you those timestamps is the one you'd use for burned-in captions with Whisper, so the work isn't wasted.

FAQ

Why isn't sidechaincompress ducking anything?

Almost always the threshold is set above the voice track's actual level. sidechaincompress uses linear amplitude, so its default threshold=0.125 is about -18 dBFS, and a normally recorded voiceover averages well below that. Check with ffmpeg -i voice.wav -af volumedetect -f null -, then set the threshold below the reported mean_volume, or raise level_sc to boost the detector signal without changing the mix.

What threshold and ratio should I use to lower music volume during a voiceover?

For a typical voiceover over a music bed, threshold=0.03:ratio=8:attack=20:release=500 gets you roughly 10 dB of ducking that recovers smoothly between sentences. Raise the ratio toward 10 if the music still competes, and raise release toward 800 ms if the bed sounds like it's breathing between words.

Can FFmpeg duck music automatically without a separate voice file?

FFmpeg cannot duck music inside an already-mixed track, because sidechaincompress needs the voice as a separate input to use as its trigger. If you only have a finished mix, no FFmpeg filter will pull the two apart. You need the original stems, or a fresh render from whatever produced the mix.

How do I duck music in n8n or Make without installing FFmpeg?

Send the music URL, the voice URL, and the filtergraph to a hosted video API from an HTTP node, then continue the workflow when the job returns. FFmpeg Micro works this way with n8n, Make, and Zapier, and it also exposes an MCP server so an AI agent can run the same mix as a tool call.

Does ducking replace loudness normalization?

Ducking and loudness normalization solve different problems and you generally want both. Ducking sets the relationship between two tracks; loudnorm=I=-14:TP=-1.5:LRA=11 sets the absolute level of the finished mix so YouTube doesn't turn it down on playback. Run loudnorm last, after amix, never on the individual stems.

If your next render already has a voice track and a music bed sitting in a bucket somewhere, the fastest way to hear what those parameters actually do is to run the graph against the real files instead of a test tone. Sign up free and send the mix as one call.

About Javid Jamae

Founder & CEO at FFmpeg Micro

Javid is a software engineer, author, and entrepreneur with over 25 years of professional software development experience across enterprise, startup, and consulting environments. He founded FFmpeg Micro to make video processing accessible to developers through a simple, automation-first REST API.

Software EngineeringVideo ProcessingFFmpegCloud ArchitectureAPI DesignAutomation

Ready to process videos at scale?

Start using FFmpeg Micro's simple API today. No infrastructure required.

Get Started Free