AgentStack
SKILL verified MIT Self-run

Ffmpeg Audio Filter

skill-damionrashford-media-os-ffmpeg-audio-filter · by damionrashford

>

No reviews yet
0 installs
15 views
0.0% view→install

Install

$ agentstack add skill-damionrashford-media-os-ffmpeg-audio-filter

✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

Are you the author of Ffmpeg Audio Filter? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

Ffmpeg Audio Filter

Context: $ARGUMENTS

Quick start

  • Loudness normalize (YouTube, podcast, broadcast): 2-pass loudnorm → Step 1
  • Quick volume bump/cut: volume filter → Step 2
  • Change speed without pitch shift: atempo (chain if outside 0.5–2.0) → Step 2
  • Mix two tracks (voice over music): amix with aresample + aformat guards → Step 2
  • Stereo → mono / 5.1 → stereo: pan with explicit channel map → Step 2
  • Trim leading/trailing silence: silenceremove → Step 2
  • Verify result: ebur128 or astats → Step 3

When to use

  • Delivering audio to a loudness spec (YT -14, Spotify -14, Apple Pod -16, EBU R128 -23, ATSC A/85 -24 LKFS).
  • Fixing level mismatch across multiple source clips.
  • Changing playback speed, pitch, or sample rate without manual re-encode math.
  • Mixing/merging/splitting channels or remapping surround layouts.
  • Cleaning up: de-ess, denoise, compress, EQ, silence trim.

Step 1 — Pick your target

Loudness mastering (perceptual) → 2-pass loudnorm. Single-pass is dynamic and can pump; 2-pass uses measured stats for a clean linear gain.

Peak normalize (simple, not perceptual) → measure peak with astats or volumedetect, then apply volume=dB.

Speed/tempo change → atempo (keeps pitch; range 0.5–100 per instance, chain two atempo=0.5 for 0.25x). Pitch shift → asetrate+aresample or rubberband (rare build).

Mix/merge → amix (sums signals, ideal for music+voice); amerge (interleaves channels, e.g. L+R mono files → stereo).

Channel remap → pan with an explicit layout expression.

Verify → ebur128 filter (loudness meter) or astats (peak/RMS/DC).

Step 2 — Build the filter graph

Use -af when there is a single audio input. Use -filter_complex whenever more than one input feeds the graph (amix, amerge, sidechaincompress).

Loudnorm 2-pass (canonical, required for masters)

Pass 1 — measure (writes JSON to stderr):

ffmpeg -hide_banner -i in.wav \
  -af loudnorm=I=-16:LRA=11:TP=-1.5:print_format=json \
  -f null -

Parse input_i, input_lra, input_tp, input_thresh, target_offset from the JSON block at the end of stderr.

Pass 2 — apply (linear gain, clean):

ffmpeg -i in.wav -af \
  loudnorm=I=-16:LRA=11:TP=-1.5:\
measured_I=:measured_LRA=:\
measured_TP=:measured_thresh=:\
offset=:linear=true:print_format=summary \
  -ar 48000 out.wav

scripts/afilter.py --two-pass handles both passes and JSON parsing.

Single-pass dynaudnorm (fast, OK for rough jobs)

ffmpeg -i in.wav -af dynaudnorm=f=150:g=15:p=0.95 out.wav

Volume

-af volume=0.5           # linear (half amplitude)
-af volume=-6dB          # decibel
-af volume=0.8:precision=float

atempo (speed, pitch-preserving)

-af atempo=1.5                      # 1.5x
-af atempo=0.5,atempo=0.5           # 0.25x (chain; single-instance limit 0.5–100)

asetrate + aresample (speed AND pitch, like a tape)

-af asetrate=48000*1.06,aresample=48000,atempo=1/1.06
# pitch up ~1 semitone, keep duration

amix (two sources summed)

ffmpeg -i voice.wav -i music.wav -filter_complex \
  "[0]aresample=48000,aformat=channel_layouts=stereo[a0]; \
   [1]aresample=48000,aformat=channel_layouts=stereo,volume=0.25[a1]; \
   [a0][a1]amix=inputs=2:duration=longest:dropout_transition=0:normalize=0[aout]" \
  -map "[aout]" out.wav

normalize=0 is critical — default 1 scales by 1/N which is almost never what you want.

amerge (L+R mono → stereo)

ffmpeg -i L.wav -i R.wav -filter_complex \
  "[0][1]amerge=inputs=2,pan=stereo|c0=c0|c1=c1[aout]" \
  -map "[aout]" stereo.wav

pan (channel remap)

-af pan=mono|c0=0.5*c0+0.5*c1                     # stereo → mono
-af "pan=stereo|c0=c0|c1=c1"                      # passthrough stereo
-af "pan=stereo|c0=0.5*FL+0.707*FC+0.5*BL+0.5*SL| \
               c1=0.5*FR+0.707*FC+0.5*BR+0.5*SR"  # 5.1 → stereo downmix

silenceremove

-af silenceremove=start_periods=1:start_duration=0.5:start_threshold=-50dB: \
                  stop_periods=-1:stop_duration=0.5:stop_threshold=-50dB

Fade

-af "afade=t=in:st=0:d=1,afade=t=out:st=58:d=2"

EQ chain

-af "highpass=f=80,lowpass=f=16000,equalizer=f=3000:t=q:w=1.5:g=3,bass=g=2,treble=g=1"

Step 3 — Run + verify

After any loudness operation, re-measure:

ffmpeg -hide_banner -nostats -i out.wav -af ebur128=peak=true -f null - 2>&1 | tail -20

Look at I: (integrated LUFS), LRA: (loudness range), Peak: (dBTP). They should match the target within ~0.5 LU.

For peak/RMS/DC offset:

ffmpeg -i out.wav -af astats=metadata=1:reset=1 -f null - 2>&1 | grep -E "RMS|Peak|DC"

Channel layout / codec sanity:

ffprobe -v error -select_streams a -show_entries stream=codec_name,sample_rate,channels,channel_layout -of json out.wav

Available scripts

  • scripts/afilter.py — presets for loudnorm (yt/podcast/ebu), peak normalize, 2-input mix, speed, mono downmix, simple denoise. Implements the 2-pass loudnorm measure→apply flow (--two-pass).

Workflow

# 2-pass loudnorm to YouTube target
uv run ${CLAUDE_SKILL_DIR}/scripts/afilter.py loudnorm-yt \
  --input in.wav --output out.wav --two-pass --verbose

# mix voice over music
uv run ${CLAUDE_SKILL_DIR}/scripts/afilter.py mix-two \
  --inputs voice.wav music.wav --output mix.wav

# speed up 2x (chain-safe)
uv run ${CLAUDE_SKILL_DIR}/scripts/afilter.py speed \
  --input in.wav --output out.wav --factor 2.0

# preview command without running
uv run ${CLAUDE_SKILL_DIR}/scripts/afilter.py loudnorm-podcast \
  --input in.wav --output out.wav --two-pass --dry-run

Reference docs

  • Read [references/filters.md](references/filters.md) for full option tables (loudnorm, dynaudnorm, acompressor, compand, equalizer, aresample, amix, pan, silenceremove), channel-layout names, platform loudness targets, and recipes (ducking, de-ess, click removal).

Gotchas

  • loudnorm single-pass is dynamic, not linear. It can pump and distort transients. Always run 2-pass for masters — measure first, then apply with measured_* + linear=true.
  • linear=true only works when every measured_* value is supplied. Miss one → falls back to dynamic silently.
  • -af is single-input. Any graph with amix, amerge, or sidechaincompress must use -filter_complex and -map "[aout]".
  • amix vs amerge: amix sums samples (music + voice mix). amerge interleaves channels (two mono files → one stereo stream). Mixing these up produces silent outputs or wrong channel counts.
  • Sample-rate mismatch silently corrupts amix. Insert aresample=48000 on every input before mixing.
  • Channel-layout mismatch silently breaks amix. Insert aformat=channel_layouts=stereo (or mono) per input.
  • amix default normalize=1 scales output by 1/N. Almost always wrong — set normalize=0 and handle gain yourself.
  • volume is NOT loudness. Linear/dB gain only — perceptual targets need loudnorm.
  • atempo is capped 0.5–100.0 per instance. For 0.25x chain atempo=0.5,atempo=0.5; for 200x chain atempo=10,atempo=10,atempo=2.
  • asetrate alone changes pitch. If you want speed without pitch, use atempo. asetrate+aresample resets SR but pitch is still shifted — combine with atempo to compensate.
  • rubberband filter requires a custom ffmpeg build. Check ffmpeg -filters | grep -i rubberband. Most distro/Homebrew builds do not include it.
  • -ar on output ≠ aresample filter. -ar is a global output option; filter-graph resampling must use aresample explicitly inside the graph.
  • silenceremove uses stop_periods=-1 (negative) to remove every silent region, not just the first.
  • loudnorm writes JSON to stderr, mixed with progress lines. Parse the last {...} block, not the whole stream.
  • Loudness targets differ across platforms — check references/filters.md before delivery.

Examples

Example 1: Normalize a podcast episode to -16 LUFS

# Pass 1 — measure
ffmpeg -hide_banner -i ep01.wav \
  -af loudnorm=I=-16:LRA=11:TP=-1.5:print_format=json -f null - 2>measure.txt

# Pass 2 — apply (values plugged in from measure.txt JSON block)
ffmpeg -i ep01.wav -af \
"loudnorm=I=-16:LRA=11:TP=-1.5:measured_I=-21.3:measured_LRA=8.2:\
measured_TP=-4.1:measured_thresh=-31.5:offset=-0.7:linear=true:print_format=summary" \
  -ar 48000 -c:a pcm_s24le ep01_norm.wav

# Verify
ffmpeg -i ep01_norm.wav -af ebur128=peak=true -f null - 2>&1 | tail -10

Example 2: Sidechain-duck music under a voiceover

ffmpeg -i voice.wav -i music.wav -filter_complex \
"[0:a]asplit=2[vox][sc]; \
 [1:a][sc]sidechaincompress=threshold=-24dB:ratio=8:attack=5:release=250[ducked]; \
 [vox][ducked]amix=inputs=2:duration=longest:normalize=0[aout]" \
-map "[aout]" out.wav

Example 3: 5.1 Dolby → stereo downmix

ffmpeg -i surround.wav -af \
"pan=stereo|c0=0.5*FL+0.707*FC+0.5*BL+0.5*SL|\
c1=0.5*FR+0.707*FC+0.5*BR+0.5*SR" -c:a pcm_s16le stereo.wav

Example 4: Trim leading + trailing silence below -50 dB

ffmpeg -i raw.wav -af \
"silenceremove=start_periods=1:start_duration=0.3:start_threshold=-50dB:\
stop_periods=-1:stop_duration=0.5:stop_threshold=-50dB" trimmed.wav

Example 5: Speed to 0.25x (chain atempo)

ffmpeg -i in.wav -af atempo=0.5,atempo=0.5 slow.wav

Troubleshooting

Error: "Filter amix has an unconnected output" / "Impossible to convert between the formats"

Cause: inputs have different sample rates or channel layouts. Solution: pre-normalize each input:

[0]aresample=48000,aformat=channel_layouts=stereo[a0];
[1]aresample=48000,aformat=channel_layouts=stereo[a1];
[a0][a1]amix=inputs=2:normalize=0[aout]

Error: "Value 4.000000 for parameter 'tempo' out of range [0.500000 - 2.000000]"

Older ffmpeg (<5.1) capped atempo at 2.0. Chain multiple instances: atempo=2.0,atempo=2.0 for 4x. Modern builds go to 100.

Error: loudnorm output still sounds pumpy

Cause: single-pass mode (dynamic). Either you did not supply all measured_* values, or linear=true was omitted. Double-check every field from pass 1 JSON is present in pass 2.

Error: "No such filter: 'rubberband'"

Cause: ffmpeg built without librubberband. Use atempo (keeps pitch) or asetrate (changes pitch) instead, or install a build with rubberband (e.g. brew install ffmpeg usually does NOT include it; build from source with --enable-librubberband).

Error: output file has 0 channels / silent

Cause: pan expression referenced a channel that does not exist in the layout (e.g. BL on a stereo source). Use ffprobe to confirm input channel_layout before writing the pan map.

Error: amix output is half as loud as expected

Cause: default normalize=1 scales by 1/N. Add :normalize=0.

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet — be the first.

Versions

  • v0.1.0 Imported from the upstream source.