Add Background Music to Video with FFmpeg, or One API Call

Your faceless-channel video already has a voiceover. You want a music bed under it that never buries the narration, and you want it applied to 40 clips tonight without opening an editor. Most "merge audio and video" guides stop at combining two tracks, which is the easy half.
Quick answer: Add background music to video with FFmpeg by mixing the music into the existing audio withamixafter lowering it withvolume, then ducking it under speech withsidechaincompress. A working one-liner isffmpeg -i video.mp4 -i music.mp3 -filter_complex "[1:a]volume=0.15[bg];[0:a][bg]amix=inputs=2:duration=first:normalize=0[a]" -map 0:v -map "[a]" -c:v copy -c:a aac -b:a 192k out.mp4. To mix audio into video via API instead, post the video URL, the music URL, and the filter graph as one job and download the rendered file.
A plain audio merge sounds wrong for a reason
Conventional wisdom says adding music is a merge: two audio streams in, one out. That's true mechanically, and it's why every tutorial hands you amix and stops. But the thing that makes a music bed sound professional isn't the merge, it's the level relationship between the music and the speech, and that relationship changes every time somebody talks.
Two specific failures come out of the naive command. First, amix defaults to normalize=1, which divides every input by the number of inputs. Mix two tracks and your voiceover drops about 6 dB, so the narration sounds further away than it did before you "added" anything. Second, a music bed set to one fixed volume is always wrong somewhere: loud enough to feel present in the gaps, loud enough to fight the voice during a sentence.
The fix isn't picking a better constant. It's letting the voice control the music level in real time.
The FFmpeg command for background music under a voiceover
Start with the fixed-volume version, because it's the right answer when your video has no dialogue at all (b-roll, product loops, slideshow content). Music at volume=0.15 is roughly -16.5 dB of gain, which sits under a normally recorded voiceover without disappearing.
ffmpeg -i video.mp4 -i music.mp3 -filter_complex \
"[1:a]volume=0.15[bg];\
[0:a][bg]amix=inputs=2:duration=first:normalize=0[aout]" \
-map 0:v -map "[aout]" \
-c:v copy -c:a aac -b:a 192k \
out.mp4
normalize=0 is the part most snippets omit. It keeps both inputs at their stated levels instead of scaling them by 1/n, and it's available in FFmpeg 4.4 and later. duration=first ends the mix when the video's own audio ends so a long music file doesn't extend your output. -c:v copy means you re-encode a few megabytes of audio instead of re-encoding the whole video stream, which is why this finishes in seconds on a clip that would take minutes to transcode.
Duck the music with sidechaincompress
Ducking is the mechanism that separates a music bed from noise. sidechaincompress uses the voiceover as a key signal: when speech is present, the music is pushed down automatically; when the speaker pauses, it comes back up. You split the voice track in two with asplit, send one copy to the compressor's sidechain input and keep the other for the final mix.
ffmpeg -i video.mp4 -i music.mp3 -filter_complex \
"[0:a]aformat=sample_rates=48000:channel_layouts=stereo,asplit=2[voice][key];\
[1:a]aformat=sample_rates=48000:channel_layouts=stereo,volume=0.4[music];\
[music][key]sidechaincompress=threshold=0.03:ratio=8:attack=5:release=400:makeup=1[ducked];\
[voice][ducked]amix=inputs=2:duration=first:normalize=0,loudnorm=I=-14:TP=-1.5:LRA=11[aout]" \
-map 0:v -map "[aout]" \
-c:v copy -c:a aac -b:a 192k \
out.mp4
Order matters in sidechaincompress: the first input is the signal being compressed (music), the second is the key (voice). An attack of 5 ms pulls the music down fast enough that you don't hear it clip the first syllable, and a release of 400 ms brings it back slowly enough that it doesn't pump between words. Raise the release to 800 ms if the music sounds like it's breathing.
The loudnorm=I=-14 at the end targets -14 LUFS, which is roughly where YouTube, Spotify, and TikTok normalize playback. Without it, every clip in a batch lands at a different perceived loudness because every source voiceover was recorded differently.
Note the aformat on both inputs. If your voiceover is 48 kHz mono and your royalty-free music is 44.1 kHz stereo, the filter graph will either error out or resample somewhere you didn't choose. Forcing both to the same rate and layout before they meet removes a whole class of intermittent batch failures.
Handle music that's shorter or longer than the video
Loop a short music file by putting -stream_loop -1 before its input, and fade it out at the end so it doesn't stop mid-phrase. For a 120-second video, a 5-second fade starting at 115 seconds is enough.
ffmpeg -i video.mp4 -stream_loop -1 -i music.mp3 -filter_complex \
"[1:a]volume=0.15,afade=t=out:st=115:d=5[bg];\
[0:a][bg]amix=inputs=2:duration=first:normalize=0[aout]" \
-map 0:v -map "[aout]" -c:v copy -c:a aac -shortest out.mp4
If you'd rather set levels by hand than trust a compressor, the volume filter supports timeline expressions: volume=0.05:enable='between(t,10,32)' drops the bed only between 10 and 32 seconds. That's fine for one hero video and unmaintainable for a channel, since it means knowing the speech timestamps for every clip.
Mix audio into video with one API call
Running that filter graph across a content pipeline means installing FFmpeg somewhere, keeping the build current enough to have normalize on amix, and giving the process a machine with disk and time. That's the part that breaks, not the filter. FFmpeg Micro takes the same job as an HTTP request: you pass the video URL, the music URL, and the work you want done, then poll for status or take a webhook and download the output. No servers to run, no FFmpeg to install, and it works from code, from n8n, Make, and Zapier, or from your AI agents over MCP.
curl -X POST https://api.ffmpeg-micro.com/v1/jobs \
-H "Authorization: Bearer $FFMPEG_MICRO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"inputs": [
"https://cdn.example.com/clips/ep-104.mp4",
"https://cdn.example.com/music/lofi-bed.mp3"
],
"filter_complex": "[0:a]asplit=2[voice][key];[1:a]volume=0.4[music];[music][key]sidechaincompress=threshold=0.03:ratio=8:attack=5:release=400[ducked];[voice][ducked]amix=inputs=2:duration=first:normalize=0[aout]",
"output": { "format": "mp4", "video_codec": "copy", "audio_codec": "aac" }
}'
Check the current job schema and field names in the docs before you wire it into production, since that's the source of truth for parameters. The job semantics are the stable part: submit, poll or webhook, download.
Manual FFmpeg versus a video API
The trade-off is not about capability. Both run the identical filter graph, because the API is FFmpeg. What differs is everything around the render.
| Local or self-hosted FFmpeg | FFmpeg Micro | |
|---|---|---|
| Setup | Install binary, pin version, match codec support | API key |
| Batch of 40 clips | Your box, serialized or hand-parallelized | Concurrent jobs, no queue to build |
| Long renders in n8n/Make | Blocks the workflow, hits timeouts | Webhook when done |
| Version drift | `normalize` missing on old builds | Managed toolkit |
| Cost model | Server running whether or not you render | Free tier, then usage-based |
If you're already fighting workflow timeouts on the automation side, the polling pattern in How to Fix n8n Timeout Errors When Processing Video applies here unchanged, and the full clip workflow post shows where the music step slots in.
Common pitfalls when adding a music bed
Most broken runs trace back to five things, and four of them are silent failures rather than errors.
- The video has no audio stream.
[0:a]then matches nothing and FFmpeg stops withStream specifier ':a' in filtergraph description matches no streams. Detect it withffprobe -show_streams -select_streams aand branch to a simpler graph that just attaches the music. - You forgot
-map. Without explicit-map 0:v -map "[aout]", FFmpeg picks default streams and may write the original untouched audio into the output. The render succeeds and sounds wrong. - You used
-c:a copywith a filter graph. Copy and filter are mutually exclusive on the same stream. Encode the audio, keep-c:v copy. - Sample rate mismatch between voice and music. Fix it with
aformaton both branches before they meet, as above. - Music licensed for personal use only. Batch-publishing 40 clips a week is commercial use on most royalty-free libraries. Check the license tier before it's 400 videos.
One more that only shows up at scale: if you swap the audio entirely rather than layering it, you want a different command. That case is covered in How to Replace Audio in Video with FFmpeg.
When not to bother with programmatic ducking
Sidechain compression is the wrong tool when the music matters more than the words. A music video, a performance clip, or a trailer where the bed carries emotional weight deserves an editor and manual automation curves. Nobody wins by scripting that.
It's also overkill for a single video. If you're making one clip and you already have DaVinci Resolve or Premiere open, the panel is faster than tuning a threshold. The break-even is somewhere around the fifth clip, or the first time you need the same treatment applied consistently across a back catalog.
FAQ
How do I add background music without removing the original audio?
Keeping the original audio means mixing rather than mapping. Use amix=inputs=2 with both [0:a] (the video's existing audio) and the music branch as inputs, and set normalize=0 so the original voiceover keeps its level. Mapping only the music input with -map 1:a is what replaces the audio instead of layering it.
Why does my voiceover get quieter after I add music?
The amix filter normalizes by default, dividing each input by the number of inputs, so a two-input mix cuts the voiceover by about 6 dB. Adding normalize=0 to the amix options stops that scaling and preserves the original speech level.
How loud should background music be under a voiceover?
Music under speech usually sits around volume=0.15 in FFmpeg, about -16.5 dB of gain, when levels are fixed. With sidechaincompress doing the ducking, start the music higher at volume=0.4 and let the compressor pull it down 8 to 12 dB whenever the speaker is talking.
What happens if the music file is shorter than the video?
A short music file leaves silence at the end unless you loop it. Put -stream_loop -1 immediately before the music input to repeat it indefinitely, then use amix=duration=first or -shortest so the output still ends with the video.
Can I do this in n8n or Make without running a server?
n8n and Make can both trigger the mix through an HTTP request node that posts the video URL, the music URL, and the filter graph to a video API, then continue when the webhook fires. Neither platform can run FFmpeg itself, and passing large media files through the workflow is what causes the size and timeout errors those tools throw.
The ducking graph above is the same one FFmpeg Micro runs, minus the install, the version pinning, and the machine sitting idle between renders. Sign up free and send your first mix job with the curl request in this post.
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.
You might also like

Normalize audio loudness across a video library
Normalize audio loudness across a video library with FFmpeg loudnorm two-pass, EBU R128 targets, and one API call. Real commands, LUFS targets, and pitfalls.

An FFmpeg audiogram is one filter. The rest is what breaks.
Build an ffmpeg audiogram from a podcast MP3 and cover art: showwaves vs showwavespic, copy-paste commands, vertical Reels sizing, and a one-call batch API.

Drawtext can't do karaoke captions. FFmpeg's ass filter can.
How to build TikTok-style karaoke captions with FFmpeg: Whisper word timestamps, an ASS file with \k tags, the ass filter, and the one-call API version.
Ready to process videos at scale?
Start using FFmpeg Micro's simple API today. No infrastructure required.
Get Started Free