ffmpegaudioautomation

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

·Javid Jamae·10 min read
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 with amix after lowering it with volume, then ducking it under speech with sidechaincompress. A working one-liner is 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[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 FFmpegFFmpeg Micro
SetupInstall binary, pin version, match codec supportAPI key
Batch of 40 clipsYour box, serialized or hand-parallelizedConcurrent jobs, no queue to build
Long renders in n8n/MakeBlocks the workflow, hits timeoutsWebhook when done
Version drift`normalize` missing on old buildsManaged toolkit
Cost modelServer running whether or not you renderFree 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 with Stream specifier ':a' in filtergraph description matches no streams. Detect it with ffprobe -show_streams -select_streams a and 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 copy with 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 aformat on 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.

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