A phone left recording overnight hands you a file far longer than anyone will listen to. Something made a noise at some point, and you want the timestamps, not the eight hours. ffmpeg's silencedetect filter logs where the audio goes quiet, so everything between those quiet stretches is when something happened.
I built two ten-minute test recordings with three known sounds in each and ran silencedetect against them at a range of settings. In short:
- In the quiet room it reported no events at its default threshold or at -40 dB, and that was wrong. It did not find the room quiet at any point. The background averaged -51 dB but peaked at -37.1 dB, and the threshold behaved like a test against those peaks: -38 dB gave 143 stretches of activity, -36 dB the correct three.
- An empty event list has two opposite meanings: the room was not quiet at any point, or it was quiet the whole time.
- A steady fan hum shrank the working range of thresholds from at least 16 dB to about 4. Two passes of a 300 Hz high-pass filter restored it.
- The duration setting decides what counts as one event. At 0.2 seconds a bark-like burst split into three; at 1 second it was one.
Disclosure: I make an Android camera app. Nothing here uses its code or describes how it records or stores audio. Everything below runs on a computer, against files the scripts create themselves, with ffmpeg 4.4.2.
The test recordings
Each file is ten minutes of mono 48 kHz audio, AAC at 128 kb/s, in an MP4 next to a black video track: roughly the shape of a phone camera recording. The background is random noise at -50 dB RMS. Three sounds sit at known times: three tonal bursts at 100 s peaking near -10 dB (a stand-in for barking), a 40 ms click at 250 s at -8 dB (a door latch), and 2 seconds of speech-band noise at 400 s at -34 dB RMS (someone talking quietly in the next room). The second file adds a constant 60 Hz hum at -26 dB, which is what a fan or a fridge compressor brings.
# Builds two 10-minute synthetic recordings (48 kHz mono, then AAC in MP4 with a black video track).
# room_a: noise floor only + 3 events. room_b: the same plus a constant 60 Hz fan hum.
import numpy as np, wave, subprocess
SR = 48000; N = 600 * SR
rng = np.random.default_rng(20261016)
def db(x): return 10 ** (x / 20)
floor = rng.standard_normal(N); floor *= db(-50) / np.sqrt(np.mean(floor ** 2))
t = np.arange(N) / SR
ev = np.zeros(N)
# event 1, t=100 s: three 0.15 s tonal bursts (700 Hz + harmonics), 0.25 s apart
for k in range(3):
s = int((100 + 0.4 * k) * SR); n = int(0.15 * SR); tt = np.arange(n) / SR
b = np.sin(2*np.pi*700*tt) + 0.5*np.sin(2*np.pi*1400*tt) + 0.25*np.sin(2*np.pi*2100*tt)
ev[s:s+n] += b / np.max(np.abs(b)) * db(-10) * np.hanning(n) ** 0.3
# event 2, t=250 s: a 40 ms broadband click
s = 250 * SR; n = int(0.04 * SR); c = rng.standard_normal(n); ev[s:s+n] += c / np.max(np.abs(c)) * db(-8)
# event 3, t=400 s: 2 s of quiet speech-band noise (300-3000 Hz), RMS -34 dBFS, 4 Hz syllable modulation
s = 400 * SR; n = 2 * SR; w = rng.standard_normal(n); F = np.fft.rfft(w); f = np.fft.rfftfreq(n, 1/SR)
F[(f < 300) | (f > 3000)] = 0; v = np.fft.irfft(F, n); v *= 0.6 + 0.4*np.sin(2*np.pi*4*np.arange(n)/SR)
v *= db(-34) / np.sqrt(np.mean(v ** 2)); ev[s:s+n] += v
hum = np.sin(2*np.pi*60*t) + 0.2*np.sin(2*np.pi*120*t); hum *= db(-26) / np.sqrt(np.mean(hum ** 2))
for name, sig in [("room_a", floor + ev), ("room_b", floor + ev + hum)]:
pcm = np.clip(sig * 32767, -32768, 32767).astype("<i2")
with wave.open(f"/tmp/vgf/{name}.wav", "wb") as wv:
wv.setnchannels(1); wv.setsampwidth(2); wv.setframerate(SR); wv.writeframes(pcm.tobytes())
subprocess.run(["ffmpeg", "-v", "error", "-y", "-f", "lavfi", "-i", "color=black:s=320x240:r=5",
"-i", f"/tmp/vgf/{name}.wav", "-shortest", "-c:v", "libx264", "-preset", "ultrafast",
"-c:a", "aac", "-b:a", "128k", f"/tmp/vgf/{name}.mp4"], check=True)
print(name, "written")
What silencedetect reports
volumedetect, the filter people reach for first, gives one mean and one maximum for the whole file, with no times. silencedetect logs a start and end for each quiet stretch. Its built-in help (ffmpeg -h filter=silencedetect) says "Detect silence." and lists the two settings that matter: noise, "set noise tolerance (from 0 to DBL_MAX) (default 0.001)", and duration, "set minimum duration in seconds (default 2)". An amplitude of 0.001 is -60 dB.
This script runs it at several thresholds and prints what you actually want: the stretches that are not silent.
#!/bin/bash
# Runs silencedetect over the audio track at several thresholds and prints what a
# person would read off it: the stretches that are NOT silent (start-end, seconds).
cd /tmp/vgf
loud() { # loud FILE FILTERCHAIN
ffmpeg -hide_banner -nostats -i "$1" -vn -af "$2" -f null - 2>&1 \
| awk '/silence_end/ {s = $5}
/silence_start/ {if (s != "") {n++; if (n <= 5) out = out sprintf(" %.2f-%.2f", s, $5)}; s = ""}
END {printf "%4d stretch(es)%s%s\n", n, out, (n > 5 ? " ..." : "")}'
}
echo "== volumedetect, whole file"
for f in room_a room_b; do
printf "%s: " $f
ffmpeg -hide_banner -nostats -i $f.mp4 -vn -af volumedetect -f null - 2>&1 \
| grep -oE "(mean|max)_volume: [-0-9.]+ dB" | tr '\n' ' '; echo
done
echo "== room_a (quiet room), silencedetect with its defaults"
printf "defaults "; loud room_a.mp4 silencedetect
echo "== room_a (quiet room), silencedetect d=1"
for n in -40 -38 -36 -30 -20; do printf "n=%sdB " $n; loud room_a.mp4 silencedetect=n=${n}dB:d=1; done
echo "== room_b (same room + fan hum), silencedetect d=1"
for n in -40 -22 -20 -16 -15; do printf "n=%sdB " $n; loud room_b.mp4 silencedetect=n=${n}dB:d=1; done
echo "== room_b, two highpass=f=300 passes first"
for n in -40 -36 -34 -30; do printf "n=%sdB " $n; loud room_b.mp4 highpass=f=300,highpass=f=300,silencedetect=n=${n}dB:d=1; done
echo "== room_a, n=-34dB, changing d"
for d in 0.2 1 5; do printf "d=%-4s " $d; loud room_a.mp4 silencedetect=n=-34dB:d=$d; done
== volumedetect, whole file
room_a: mean_volume: -46.0 dB max_volume: -7.1 dB
room_b: mean_volume: -26.0 dB max_volume: -7.2 dB
== room_a (quiet room), silencedetect with its defaults
defaults 0 stretch(es)
== room_a (quiet room), silencedetect d=1
n=-40dB 0 stretch(es)
n=-38dB 143 stretch(es) 5.60-6.51 9.12-9.12 13.42-14.17 15.36-15.36 20.61-22.01 ...
n=-36dB 3 stretch(es) 100.00-100.95 250.00-250.06 400.00-402.00
n=-30dB 3 stretch(es) 100.00-100.95 250.00-250.04 400.00-402.00
n=-20dB 3 stretch(es) 100.01-100.94 250.00-250.04 400.05-401.33
== room_b (same room + fan hum), silencedetect d=1
n=-40dB 0 stretch(es)
n=-22dB 0 stretch(es)
n=-20dB 3 stretch(es) 100.00-100.95 250.00-250.04 400.00-402.00
n=-16dB 3 stretch(es) 100.01-100.94 250.00-250.04 400.32-401.33
n=-15dB 2 stretch(es) 100.01-100.94 250.00-250.04
== room_b, two highpass=f=300 passes first
n=-40dB 0 stretch(es)
n=-36dB 4 stretch(es) 100.00-100.95 250.00-250.06 354.25-354.25 400.00-402.00
n=-34dB 3 stretch(es) 100.00-100.95 250.00-250.05 400.00-402.00
n=-30dB 3 stretch(es) 100.00-100.95 250.00-250.04 400.00-402.00
== room_a, n=-34dB, changing d
d=0.2 5 stretch(es) 100.00-100.15 100.40-100.55 100.80-100.95 250.00-250.06 400.00-402.00
d=1 3 stretch(es) 100.00-100.95 250.00-250.06 400.00-402.00
d=5 3 stretch(es) 100.00-100.95 250.00-250.06 400.00-402.00
- volumedetect saw a maximum near -7 dB in both files and a mean that rose 20 dB with the hum. Neither says when anything happened.
- Quiet room: the default and -40 dB gave zero stretches, -38 dB gave 143 (the background poking over the line), and -36 through -20 dB gave the right three. At -20 dB the talking was already trimmed at both ends.
- Hum room: -40 and -22 dB gave zero, -20 and -16 dB gave three, and -15 dB lost the talking.
- After two high-pass passes, -34 and -30 dB worked again. -36 dB added one stray zero-length stretch at 354.25 s.
- At 0.2 seconds of duration, the three bark bursts came out separately; at 1 and 5 seconds they merged, because the gaps between them were shorter than the duration.
Two notes from the high-pass filter's own help. It reads "Apply a high-pass filter with 3dB point frequency." and its frequency option defaults to 3000 Hz, so a bare highpass cuts most of the energy in a voice: set f yourself. It allows at most two poles, which is why I chained two passes against a 60 Hz hum.
A script that prints the list
The working threshold started just above the loudest moment of background, not its average. So this script measures that first: the astats peak of a stretch you say is quiet (the first 60 seconds by default), plus 3 dB. Then it prints each non-silent stretch with start, end and length.
#!/bin/bash
# events.sh FILE [QUIET_SECONDS] [FILTER]
# Measures the peak of a stretch you know is quiet, sets the threshold 3 dB above it,
# and prints each stretch that is NOT silent as: start end length (seconds).
set -euo pipefail
f="${1:?usage: events.sh FILE [QUIET_SECONDS] [FILTER]}"
quiet="${2:-60}"
pre="${3:-}"
[ -n "$pre" ] && pre="$pre,"
peak=$(ffmpeg -hide_banner -nostats -t "$quiet" -i "$f" -vn \
-af "${pre}astats=measure_perchannel=none:measure_overall=Peak_level" -f null - 2>&1 \
| awk '/Peak level dB/ {print $NF}')
thr=$(awk -v p="$peak" 'BEGIN {printf "%.1f", p + 3}')
echo "quiet-stretch peak ${peak} dB -> threshold ${thr} dB"
ffmpeg -hide_banner -nostats -i "$f" -vn -af "${pre}silencedetect=n=${thr}dB:d=1" -f null - 2>&1 \
| awk '/silence_end/ {s = $5}
/silence_start/ {if (s != "") printf "%10.2f %10.2f %7.2f\n", s, $5, $5 - s; s = ""}'
$ ./events.sh room_a.mp4
quiet-stretch peak -37.111415 dB -> threshold -34.1 dB
100.00 100.95 0.95
250.00 250.06 0.06
400.00 402.00 2.00
$ ./events.sh room_b.mp4
quiet-stretch peak -21.210052 dB -> threshold -18.2 dB
100.00 100.95 0.94
250.00 250.04 0.04
400.05 401.84 1.78
$ ./events.sh room_b.mp4 60 highpass=f=300,highpass=f=300
quiet-stretch peak -36.968463 dB -> threshold -34.0 dB
100.00 100.95 0.95
250.00 250.05 0.06
400.00 402.00 2.00
$ ./events.sh room_a.mp4 260 # "quiet" stretch that holds the first two events
quiet-stretch peak -7.079386 dB -> threshold -4.1 dB
exit 0
The measured peaks match the sweep: -37.1 dB in the quiet room, where -38 failed and -36 worked; -21.2 dB with the hum, where -22 failed and -20 worked; -37.0 dB once filtered. Without the filter, the hum file's threshold of -18.2 dB still caught the talking but trimmed it from 2.00 to 1.78 seconds.
The last run is the empty-list trap. I told the script that the first 260 seconds were quiet, and that stretch holds the bark and the click. The peak came back -7.1 dB, the threshold -4.1 dB, and the list was empty with exit status 0, indistinguishable from a night when nothing happened. Pick the quiet stretch by listening to it once, and check its peak is near the background, not near the loudest thing in the file.
With the list in hand, jump to each start time a second or two early. Cutting those moments into files has its own keyframe traps, covered in splitting a long recording with ffmpeg's segment muxer; finding them from the picture instead is in three ffmpeg filters on a still camera's view.
What this does not tell you
- The background here is steady noise and a pure tone. A real room changes overnight as heating or a fridge cycles, so a threshold from the first minute can be too low later. Untested.
- A phone may adjust microphone gain on its own, which could raise the background in quiet stretches. I tested no phone's recording path.
- Mono, one AAC bitrate, ffmpeg 4.4.2 only. silencedetect has a mono option that checks each channel of a stereo file separately; I did not test stereo.
- silencedetect finds loud, not important. A cough and a break-in look the same.
Before trusting an empty list
- Measure the peak of a known-quiet stretch with astats and set the threshold a few dB above it.
- If you get zero events, check whether the log printed any silence start at all. None means the threshold sits under the background.
- With a fan or fridge running, add
highpass=f=300before measuring. - Choose the duration by how far apart two sounds can be and still count as one event.
- Spot-check one listed time and one quiet time by ear.
Try it: Background Camera RemoteStream on Google Play -- record with the screen off and keep the footage on the phone. Setup notes and other write-ups are at superfunicular.com.
Sources. Quotes are from the built-in filter help of ffmpeg 4.4.2 (ffmpeg -h filter=silencedetect, ffmpeg -h filter=highpass); the ffmpeg filters documentation covers the same options. Test results come from the scripts above, run as shown. A list of loud moments is only as good as the quiet you measured it against.
Top comments (0)