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'ssilencedetectfilter 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 theselectandaselectfilters plussetpts/asetptsso audio and video stay locked together.silencedetectonly 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.
| Source | Typical noise floor | Use `noise=` | Use `d=` |
|---|---|---|---|
| Treated studio, XLR mic | -55 dBFS | -45dB | 0.5 |
| Home office, condenser mic | -40 dBFS | -32dB | 0.4 |
| Zoom/Riverside remote guest | -35 dBFS | -28dB | 0.5 |
| Synthetic TTS voiceover | true digital zero | -50dB | 0.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 FFmpeg | FFmpeg Micro | |
|---|---|---|
| Detection pass | you script it | included in the job |
| Timestamp inversion | your Python | included |
| Encode compute | your box or container | no servers to run |
| 60-min file, wall clock | ~7 min on 8 dedicated cores | one API call, poll for done |
| Fails when | disk fills, container hits its memory cap | job 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:
- Trigger on a new upload (Google Drive, Dropbox, or an S3 event).
- HTTP Request to submit the silence-trim job. Store the
job_id. - 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.
- 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
noisethreshold is below the recording's noise floor. Runffmpeg -i in.mp4 -af volumedetect -vn -f null -to read the actualmean_volumeandmax_volume, then set the threshold about 12 dB under the mean. - Everything gets detected.
dis 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:1if you have one. - Cuts land seconds off. You used
-c copy. Stream copy snaps to keyframes. Re-encode, or force-g 30on the original export so keyframes are dense enough to cut against. - Words clipped at the start of each segment. Increase
PADfrom 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.
silencedetecttriggers when the mix is quiet, so one host breathing keeps the whole file "loud." Addmono=1to evaluate channels independently. - Output plays back stuttering. You dropped
setpts/asetpts, or your player dislikes the variable frame rate thatselectproduces. 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.
You might also like

How to Replace Audio in Video with FFmpeg (Track Swap API Guide)
Replace audio in video with FFmpeg by mapping the video and new audio streams, or run the same track swap as one API call with FFmpeg Micro's hosted API.

FFmpeg Scene Detection: Auto-Split a Long Video at Scene Changes
FFmpeg scene detection with select='gt(scene,0.4)',showinfo finds cut points automatically, then split a long video into clips by scene, no hand-marked timestamps.

Turn long-form video into Shorts/Reels/TikToks automatically
Turn long form video into shorts automatically by splitting moment-picking from clip-making, then calling a video API to cut, crop to 9:16, and caption.
Ready to process videos at scale?
Start using FFmpeg Micro's simple API today. No infrastructure required.
Get Started Free