Ffmpeg audio spatial
Spatial and binaural audio with ffmpeg: headphone (HRTF binaural from multichannel), sofalizer (custom HRTF via SOFA files), surround (mono/stereo → 5.1/7.1 upmix), extrastereo, stereotools, stereowiden, earwax, crossfeed, channelsplit, channelmap, join. Use when the user asks to create binaural headphone audio, apply HRTF, convert stereo to 5.1, upmix mono to surround, widen stereo image, split channels to separate tracks, route channels, or fix mobile-device listening fatigue.From its SKILL.md
npx -y skills add damionrashford/media-os --skill ffmpeg-audio-spatialAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 17 stars17 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
12.4 KB, as published. Nobody here has run it
Ffmpeg Audio Spatial
Context: $ARGUMENTS
Quick start
- 5.1/7.1 → binaural headphones: Step 1 (probe layout) → Step 2 (pick
headphoneorsofalizer) → Step 3 (build filter, ClockWise channel order) - Stereo → 5.1 upmix: Step 1 (probe) → Step 2 (pick
surround) → Step 3 (chl_out=5.1) - Stereo widen / crossfeed / earwax: Step 2 (pick stereo FX) → Step 3 (one-liner
-af) - Split / remap / join channels: Step 2 (pick
channelsplit/pan/amerge) → Step 3 (build graph)
When to use
- You need a binaural headphone downmix from a 5.1/7.1 film or game mix.
- You are given a stereo master and asked to "make it wider," "add space," "reduce ping-pong fatigue on earbuds," or "trick a stereo file into 5.1."
- You need to split a multichannel master into one-mono-per-channel files (stem prep).
- You need to remap, reroute, or merge separate mono tracks into a laid-out multichannel bus.
Step 1 — Identify the channel layout
Every spatial op depends on the input layout. Probe first:
ffprobe -v error -select_streams a:0 \
-show_entries stream=channels,channel_layout,sample_rate \
-of default=nw=1 INPUT
Interpret:
channels=2 channel_layout=stereo→ stereo-only operations (widen, earwax, crossfeed, upmix).channels=6 channel_layout=5.1(or5.1(side)) → binaural, downmix, or split.channels=8 channel_layout=7.1→ binaural (extend channel map) or split.channels=1 channel_layout=mono→ duplicate to stereo first, or upmix.- Layout missing? Use
scripts/spatial.py detect— it falls back to FFmpeg's layout table and warns when the layout is unset so you can force it with-channel_layouton input.
Step 2 — Pick the operation
| Goal | Filter | Notes |
|---|---|---|
| 5.1/7.1 → binaural (built-in HRTF) | headphone | Channels must be listed in ClockWise order (see Gotchas). |
| 5.1/7.1 → binaural (custom HRTF) | sofalizer | Needs --enable-libmysofa build flag + a .sofa file. |
| Stereo → 5.1/7.1 upmix | surround | Invents center/surrounds — sounds plausible, not true 5.1. |
| Stereo widen | stereowiden | Delay+feedback — physical widening. |
| Boost stereo difference | extrastereo | Amplifies L-R, can exaggerate artifacts. |
| M/S stereo tools | stereotools | Mid/Side level, phase, mute-L/mute-R. |
| Headphone listening fatigue | crossfeed | Blends L/R back — opposite of extrastereo. |
| 2-ch CD → headphone cues | earwax | Fixed 44.1 kHz stereo only. |
| Split channels to streams | channelsplit | Produces N mono pads. |
| Remap channel layout | channelmap or pan | pan is expression-based. |
| Join N monos into a layout | amerge + pan | amerge interleaves; pan assigns names. |
Step 3 — Build the filter
5.1 → binaural stereo (built-in HRTF)
ffmpeg -i in.mkv -af "headphone=FL|FR|FC|LFE|BL|BR" -c:a libmp3lame -q:a 2 out.mp3
7.1 → binaural (ClockWise: FL FR FC LFE BL BR SL SR)
ffmpeg -i in.mkv -af "headphone=FL|FR|FC|LFE|BL|BR|SL|SR" -c:a aac -b:a 192k out.m4a
5.1 → binaural with custom SOFA
ffmpeg -i in.mkv -af "sofalizer=sofa=mit_kemar.sofa:type=freq:radius=1" out.flac
Stereo → 5.1 upmix
ffmpeg -i stereo.wav -af "surround=chl_out=5.1" -c:a ac3 -b:a 448k surround.ac3
Stereo widen
ffmpeg -i in.wav -af "stereowiden=delay=20:feedback=0.3:crossfeed=0.3:drymix=0.8" out.wav
Extra-stereo (L-R amplification)
ffmpeg -i in.wav -af "extrastereo=m=2.5" out.wav
Crossfeed (headphone fatigue reducer)
ffmpeg -i in.wav -af "crossfeed=strength=0.5:range=0.5" out.wav
Earwax (CD-stereo → headphone cues)
ffmpeg -i in.wav -af "earwax" out.wav
Stereotools M/S matrix
ffmpeg -i in.wav -af "stereotools=mlev=0.8:slev=1.2" out.wav
Split 5.1 into six mono WAVs
ffmpeg -i in.mkv -filter_complex \
"[0:a]channelsplit=channel_layout=5.1[FL][FR][FC][LFE][BL][BR]" \
-map "[FL]" FL.wav -map "[FR]" FR.wav -map "[FC]" FC.wav \
-map "[LFE]" LFE.wav -map "[BL]" BL.wav -map "[BR]" BR.wav
Remap — stereo → mono (left only)
ffmpeg -i in.wav -af "pan=mono|c0=c0" left.wav
# equivalent via channelmap:
ffmpeg -i in.wav -af "channelmap=channel_layout=mono:map=FL-FC" left.wav
Remap — mono → stereo (duplicate)
ffmpeg -i in.wav -af "pan=stereo|c0=c0|c1=c0" stereo.wav
# equivalent via channelmap:
ffmpeg -i in.wav -af "channelmap=channel_layout=stereo:map=0-FL|0-FR" stereo.wav
Join two mono files into stereo
ffmpeg -i L.wav -i R.wav -filter_complex \
"[0:a][1:a]amerge=inputs=2,pan=stereo|c0=c0|c1=c1[a]" \
-map "[a]" stereo.wav
Step 4 — Verify the output layout
Always re-probe the result:
ffprobe -v error -select_streams a:0 \
-show_entries stream=channels,channel_layout -of default=nw=1 OUT
- Binaural should report
channels=2 channel_layout=stereo. - Upmix should report
channels=6 channel_layout=5.1. - If
channel_layout=unknown, add-channel_layouton input or useaformat=channel_layouts=...before encoding.
Available scripts
scripts/spatial.py— Stdlib-only helper withdetect,binaural,upmix,widen,split,remap,joinsubcommands. Prints the exact ffmpeg command, supports--dry-runand--verbose.
Examples:
uv run ${CLAUDE_SKILL_DIR}/scripts/spatial.py detect --input movie.mkv
uv run ${CLAUDE_SKILL_DIR}/scripts/spatial.py binaural --input movie.mkv --output bin.m4a
uv run ${CLAUDE_SKILL_DIR}/scripts/spatial.py binaural --input movie.mkv --output bin.flac --sofa mit_kemar.sofa
uv run ${CLAUDE_SKILL_DIR}/scripts/spatial.py upmix --input stereo.wav --output surround.ac3 --layout 5.1
uv run ${CLAUDE_SKILL_DIR}/scripts/spatial.py widen --input in.wav --output wide.wav --strength 0.5
uv run ${CLAUDE_SKILL_DIR}/scripts/spatial.py split --input in.mkv --output-pattern "ch_%d.wav"
uv run ${CLAUDE_SKILL_DIR}/scripts/spatial.py remap --input in.wav --output out.wav --map "mono:c0=0.5*c0+0.5*c1"
uv run ${CLAUDE_SKILL_DIR}/scripts/spatial.py join --inputs L.wav R.wav --output stereo.wav --layout stereo
Reference docs
- Read
references/filters.mdfor per-filter option tables, channel-layout names/codes, ClockWise diagrams, SOFA sources,pangrammar, and a recipe book.
Gotchas
headphoneClockWise order is mandatory. 5.1 =FL|FR|FC|LFE|BL|BR; 7.1 =FL|FR|FC|LFE|BL|BR|SL|SR. Wrong order produces a spatially scrambled binaural mix that still "sounds like audio" — easy to miss on a casual listen.headphoneuses a generic built-in HRTF. Acceptable for previews; for delivery usesofalizerwith a measured SOFA file.sofalizerneeds--enable-libmysofa. Verify withffmpeg -filters | grep sofalizer. If absent, rebuild FFmpeg or fall back toheadphone.- SOFA file sources: SADIE II (University of York), MIT KEMAR, CIPIC HRTF database, ARI HRTF. Free for research. Per-subject impulse responses — pick one close to your listener population or use the "generic" subject.
surroundupmix invents channels. It is NOT Dolby Pro Logic / Pro Logic II decoding. For delivery masters, upmix stereo → 5.1 is generally discouraged unless the source was originally multichannel and got downmixed somewhere.- Channel names (FFmpeg):
FL, FR, FC, LFE, BL, BR, SL, SR, BC, FLC, FRC, TC, TFL, TFC, TFR, TBL, TBC, TBR, DL, DR, WL, WR. Some older builds useSL/SRwhere newer builds useBL/BRfor 5.1 — checkffmpeg -layouts. channel_layoutstrings:mono, stereo, 2.1, 3.0, 3.0(back), 4.0, quad, quad(side), 3.1, 5.0, 5.0(side), 4.1, 5.1, 5.1(side), 6.0, 6.0(front), hexagonal, 6.1, 6.1(back), 6.1(front), 7.0, 7.0(front), 7.1, 7.1(wide), 7.1(wide-side), octagonal, hexadecagonal, downmix.pansyntax:pan=LAYOUT|CHAN=EXPR|CHAN=EXPR.EXPRusesc0..cNfor the input channels, or named channels likeFL,FR. Weights are linear (not dB).amergeinterleaves,amixsums. Useamergeto combine separate monos into one multichannel stream; follow it withpanto assign a layout. Useamixto sum multiple stereo buses into one.- After
amerge=inputs=N: the layout is unset. Always chain,pan=stereo|c0=c0|c1=c1(or whatever layout you want) so downstream encoders know what to do. - Binaural output is stereo. Always deliver binaural as stereo AAC/Opus/FLAC — not mono, not 5.1.
- Crossfeed is for hard-panned masters. Old stereo records (pre-1970s pop, some jazz) sound exhausting on earbuds because they hard-pan instruments.
crossfeed=strength=0.4:range=0.6is a good default. - Sample rate matters for HRTF.
sofalizersupports multiple rates but resampling during HRTF adds artifacts. If your source varies, insertaresample=48000before the binaural filter and keep downstream consistent. - Container capacity: MP4+AAC up to 7.1 is fine. For exotic layouts (hexadecagonal, 22.2) use MKV/MOV + FLAC or PCM. Some MP4 players silently downmix anything >5.1.
Examples
Example 1: Theater 5.1 → binaural preview for editor review
ffmpeg -i reel.mov -vn \
-af "aresample=48000,headphone=FL|FR|FC|LFE|BL|BR" \
-c:a libopus -b:a 160k reel_binaural.opus
Result: stereo Opus file that carries spatial cues on headphones.
Example 2: Podcast — tighten host-guest stereo and reduce earbud fatigue
ffmpeg -i podcast.wav \
-af "crossfeed=strength=0.4:range=0.5,stereotools=mlev=1.0:slev=0.9" \
-c:a libopus -b:a 96k podcast_cf.opus
Example 3: Extract center-channel dialogue from a 5.1 film
ffmpeg -i film.mkv -filter_complex \
"[0:a]channelsplit=channel_layout=5.1:channels=FC[dlg]" \
-map "[dlg]" -c:a flac dialogue.flac
Example 4: Build a 5.1 master from six mono stems
ffmpeg -i FL.wav -i FR.wav -i FC.wav -i LFE.wav -i BL.wav -i BR.wav \
-filter_complex "[0][1][2][3][4][5]amerge=inputs=6,pan=5.1|c0=c0|c1=c1|c2=c2|c3=c3|c4=c4|c5=c5[a]" \
-map "[a]" -c:a flac master_5_1.flac
Example 5: Stereo widen a boom-mic narration for a VR piece
ffmpeg -i vo.wav \
-af "stereowiden=delay=18:feedback=0.2:crossfeed=0.2:drymix=0.9" \
-c:a flac vo_wide.flac
Troubleshooting
Error: Option 'map' not found or channel names look wrong in channelmap
Cause: using map=0-FL|0-FR on an input whose layout is unknown.
Solution: force the input layout with -channel_layout stereo (on the input) or use pan with c0/c1 indices instead of names.
Error: Unknown filter 'sofalizer'
Cause: FFmpeg not built with --enable-libmysofa.
Solution: rebuild FFmpeg, brew install ffmpeg with the --with-libmysofa formula option if available, or fall back to headphone.
Binaural sounds "inside your head" / lifeless
Cause: input wasn't a true multichannel mix (stereo fed into headphone does nothing useful); or channel order was wrong.
Solution: confirm source channel count with ffprobe; re-order the map to strict ClockWise FL|FR|FC|LFE|BL|BR.
surround upmix has no center / no surrounds
Cause: input was already mono or the chl_out layout wasn't set.
Solution: ensure input is stereo, then set chl_out=5.1 explicitly: -af "surround=chl_out=5.1". For mono sources duplicate to stereo first with pan=stereo|c0=c0|c1=c0.
Output reports channel_layout=unknown
Cause: amerge produced an unlabeled multichannel stream; encoder refuses to tag it.
Solution: chain ,pan=LAYOUT|... or ,aformat=channel_layouts=5.1 before the encoder.
Encoder rejects 7.1 output
Cause: codec/container mismatch (MP3 max 2ch, some AAC profiles max 5.1, older MP4 mux).
Solution: switch to FLAC/Opus/AC-3/E-AC-3 and MKV/MOV; verify with ffprobe that channel_layout=7.1 survived the mux.
extrastereo makes vocals disappear
Cause: m too high — vocals often sit in the M (mono) component and get cancelled.
Solution: lower m (try 1.5–2.0 max); or use stereotools with a small slev boost instead.