ffmpegvideo-editingapi

Trim Silence From Video Automatically with FFmpeg (API)

·Javid Jamae·11 min read
Trim Silence From Video Automatically with FFmpeg (API)

You have a 58-minute interview recording with six minutes of dead air scattered across it, and you need jump-cut clips out of it by tonight. The filter that sounds right, silenceremove, will hand you audio that's shorter than your video and lip sync that drifts further with every cut. The one that actually works doesn't remove anything.

Quick answer: To remove silence from video automatically, run FFmpeg's silencedetect filter to log the silent intervals (ffmpeg -i in.mp4 -vn -af silencedetect=noise=-32dB:d=0.4 -f null -), invert that list into the segments you want to keep, then cut and rejoin them in one pass with the select and aselect filters plus setpts/asetpts so audio and video stay locked together. silencedetect only reports timestamps; it never edits the file, which is exactly why it's the right tool for video.

Why silenceremove is the wrong filter for video

Conventional wisdom says use silenceremove, and for a bare WAV or MP3 that's correct. It works on audio samples. Point it at an MP4 and it happily drops 6 minutes of audio while leaving all 58 minutes of video frames untouched. By minute 20 the mouths are a few seconds ahead of the words.

There's no video-side equivalent. FFmpeg has no filter that shortens both streams based on loudness. So the mechanism isn't "find a better remove filter," it's detect, invert, trim, rejoin, in that order, with both streams cut at identical timestamps.

silencedetect is a measurement filter. It passes audio through unchanged and writes findings to stderr:

[silencedetect @ 0x7f8] silence_start: 412.336
[silencedetect @ 0x7f8] silence_end: 415.669 | silence_duration: 3.333

That's your edit decision list. Everything after this is parsing.

Step 1: Detect the silences

Run the detection pass with -vn so FFmpeg skips video decoding entirely:

ffmpeg -hide_banner -nostats -vn -i podcast.mp4 \
  -af silencedetect=noise=-32dB:d=0.4 \
  -f null - 2> silences.txt

On a 58-minute 1080p H.264 file, that pass finished in about 9 seconds on an M1. Drop -vn and it takes roughly 80 seconds, because FFmpeg decodes every frame for no reason.

The defaults are why people say it found nothing

silencedetect defaults to noise=-60dB and d=2. Both are wrong for real recordings.

A Shure SM7B in an untreated room sits at a noise floor around -45 dBFS. A USB condenser with a fan nearby is closer to -38 dBFS. Nothing in that file is ever quieter than -60 dB, so the filter reports zero silences and the internet concludes it's broken.

SourceTypical noise floorUse `noise=`Use `d=`
Treated studio, XLR mic-55 dBFS-45dB0.5
Home office, condenser mic-40 dBFS-32dB0.4
Zoom/Riverside remote guest-35 dBFS-28dB0.5
Synthetic TTS voiceovertrue digital zero-50dB0.25

Set d to the shortest pause you're willing to cut. Below about 0.25s you start clipping the natural gaps between words and the result sounds like a ransom note. For faceless-channel voiceover assembled from ElevenLabs output, 0.25s is safe because the silence is mathematically silent.

Check your work before you cut anything:

grep -c silence_start silences.txt

On that 58-minute file at -32dB/0.4s: 214 silences totaling 6m 12s. At the default -60dB/2s: zero.

Step 2: Invert the list into keep segments

silencedetect gives you what to throw away. You need the inverse, with a little padding so speech doesn't get chopped at the consonant.

import re, subprocess, sys

src = sys.argv[1]
PAD = 0.06  # seconds of breathing room on each side of a cut

log = subprocess.run(
    ["ffmpeg", "-hide_banner", "-nostats", "-vn", "-i", src,
     "-af", "silencedetect=noise=-32dB:d=0.4", "-f", "null", "-"],
    stderr=subprocess.PIPE, text=True).stderr

starts = [float(x) for x in re.findall(r"silence_start: (-?[\d.]+)", log)]
ends   = [float(x) for x in re.findall(r"silence_end: (-?[\d.]+)", log)]
ends  += [None] * (len(starts) - len(ends))   # file ends in silence

keep, cursor = [], 0.0
for s, e in zip(starts, ends):
    if s + PAD > cursor:
        keep.append((cursor, s + PAD))
    if e is None:
        cursor = None
        break
    cursor = max(0.0, e - PAD)

expr = "+".join(f"between(t,{a:.3f},{b:.3f})" for a, b in keep)
if cursor is not None:
    expr += f"+gte(t,{cursor:.3f})"
print(expr)

The ends += [None] * ... line matters. When a recording fades into room tone at the end, some FFmpeg builds emit a silence_start with no matching silence_end, and naive parsers that zip() the two lists silently drop the final segment or crash.

Step 3: Cut both streams in a single pass

Most tutorials tell you to export N clips and stitch them with the concat demuxer. That's N+1 encodes and N chances for a codec parameter mismatch. Do it in one command instead:

EXPR=$(python3 invert.py podcast.mp4)

ffmpeg -i podcast.mp4 \
  -vf "select='${EXPR}',setpts=N/FRAME_RATE/TB" \
  -af "aselect='${EXPR}',asetpts=N/SR/TB" \
  -c:v libx264 -crf 20 -preset medium \
  -c:a aac -b:a 128k \
  -fps_mode cfr \
  trimmed.mp4

select and aselect evaluate the same expression against the same timeline, so the streams can't drift. setpts=N/FRAME_RATE/TB and asetpts=N/SR/TB renumber presentation timestamps so there are no gaps where the dropped frames used to be. Without those two, the output keeps the original PTS values and most players stall on the holes.

That 58-minute file came out at 51m 48s and took 6m 40s to encode at -preset medium on 8 cores.

Two things that will bite you here. With 200+ segments the filter expression runs past 6 KB, which is fine on Linux but hits argument limits on some shells, so write it to a file and use -filter_complex_script. And you cannot use -c copy: stream copy can only cut on keyframes, and with a 250-frame GOP your cuts land up to 10 seconds off target.

The one-call version

The detect-parse-invert-encode chain is four moving parts, and the encode is the one that needs CPU you'd rather not rent by the hour. FFmpeg Micro runs the whole thing as a single job: submit the input, get a job ID, poll or take a webhook, download the output.

curl -X POST https://api.ffmpeg-micro.com/v1/jobs \
  -H "Authorization: Bearer $FFMPEG_MICRO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "input": "https://cdn.example.com/podcast.mp4",
    "operation": "trim_silence",
    "options": { "threshold_db": -32, "min_duration": 0.4, "padding": 0.06 }
  }'
{ "job_id": "job_8f2c41", "status": "queued" }

The exact parameter names for each operation live in the docs, and you can dial in a threshold against your own file in the playground before you wire anything up. Pricing is usage-based with a free tier to start: /pricing.

Self-hosted FFmpegFFmpeg Micro
Detection passyou script itincluded in the job
Timestamp inversionyour Pythonincluded
Encode computeyour box or containerno servers to run
60-min file, wall clock~7 min on 8 dedicated coresone API call, poll for done
Fails whendisk fills, container hits its memory capjob returns an error payload

If you've tried running this on serverless, you already know why the compute row matters. FFmpeg on AWS Lambda dies on the 15-minute ceiling long before a 60-minute re-encode finishes.

Wire it into n8n or Make for batch publishing

The batch shape for a repurposing pipeline is four nodes:

  1. Trigger on a new upload (Google Drive, Dropbox, or an S3 event).
  2. HTTP Request to submit the silence-trim job. Store the job_id.
  3. Wait for the webhook, don't poll in a loop. A 60-minute source file takes minutes to process, and n8n's default HTTP timeout is 300 seconds. The webhooks and polling recipe covers the resume pattern.
  4. Second job to slice the trimmed master into clips, then hand off to your publishing node.

One podcast repurposer I traded notes with was doing step 1 by hand in Descript, about 40 minutes of scrubbing per episode across three episodes a week. Two hours a week, gone, and the output is deterministic instead of dependent on how carefully they were watching the waveform that day.

Silence is only one way to find your cut points. If the source is screen-recorded or has hard visual transitions, scene detection gives you better boundaries, and the two combine well: cut silence first, split on scenes second. For the full long-form to vertical pipeline, see turning long-form into Shorts and Reels.

Common pitfalls

  • Zero silences detected. Your noise threshold is below the recording's noise floor. Run ffmpeg -i in.mp4 -af volumedetect -vn -f null - to read the actual mean_volume and max_volume, then set the threshold about 12 dB under the mean.
  • Everything gets detected. d is too low, or a music bed is sitting under the speech and confusing the gate. Detect on the isolated dialogue track with -map 0:a:1 if you have one.
  • Cuts land seconds off. You used -c copy. Stream copy snaps to keyframes. Re-encode, or force -g 30 on the original export so keyframes are dense enough to cut against.
  • Words clipped at the start of each segment. Increase PAD from 0.06 to 0.1. Speech onsets, especially plosives, sit below the threshold for a frame or two before they spike.
  • Multi-track source only gates one channel. silencedetect triggers when the mix is quiet, so one host breathing keeps the whole file "loud." Add mono=1 to evaluate channels independently.
  • Output plays back stuttering. You dropped setpts/asetpts, or your player dislikes the variable frame rate that select produces. Add -fps_mode cfr.

When not to auto-cut silence

Don't do this to a video that needs its pacing. Comedy timing, dramatic beats, and instructional demos where the viewer is meant to read something on screen all get ruined by aggressive gating. Educational content in particular: a 1.5-second pause after a key point is doing work.

Also skip it if the silence carries picture. B-roll, screen recordings of a slow build step, and anything with on-screen text that outlasts the narration will lose frames the viewer needed. In those cases, detect the silences, then review the list and cut selectively rather than trimming everything the filter flags.

And if you only need to fix the audio, not the video, silenceremove is genuinely the right filter. Extract the audio, gate it, and put it back with a track swap only if the runtime is unchanged.

FAQ

Can I remove silent parts of a video without re-encoding?

Not accurately. -c copy can only cut on keyframes, so with a typical 250-frame GOP each cut lands up to 10 seconds from where you asked. If you must stream-copy, re-export the source with -g 30 first so keyframes appear twice a second.

What silencedetect threshold should I use for a podcast?

Start at noise=-32dB:d=0.4 for home-office recordings and -45dB:d=0.5 for treated-room XLR audio. Confirm it against your file by running volumedetect to read the real noise floor, then set the threshold roughly 12 dB below the reported mean_volume.

Does silenceremove work on MP4 video files?

It runs, but it desyncs the result. silenceremove deletes audio samples and leaves every video frame in place, so the output has a shorter audio track than video track and the drift compounds with each cut. Use silencedetect plus select/aselect instead.

How do I auto cut pauses in a video from n8n?

Submit a silence-trim job with an HTTP Request node, then resume on a webhook instead of waiting inside the node. n8n's default HTTP timeout is 300 seconds and a 60-minute source file takes longer than that to process.

Will trimming silence break lip sync?

Not if both streams are cut with the same expression in the same pass. select and aselect evaluate against one shared timeline, and setpts=N/FRAME_RATE/TB with asetpts=N/SR/TB renumbers timestamps on both so there are no gaps to drift into.

Point a job at one of your own recordings and compare the runtime before and after. The free tier is enough to find your threshold on a real file, which is the only part of this that takes any judgment.

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