From video-editing
Apply ffmpeg loudnorm (two-pass EBU R128) to a video or audio file, hitting the user's preferred LUFS / true-peak target (or one specified inline). Use when the user says "normalize this", "fix the levels", "make this -14 LUFS", "loudnorm this", "normalize for YouTube/podcast/broadcast". Pairs with audio-analysis (analyze recommends, this skill applies). Re-muxes video stream-copy when the input is video.
npx claudepluginhub danielrosehill/claude-code-plugins --plugin video-editingThis skill is limited to using the following tools:
Two-pass `loudnorm` to hit a precise integrated LUFS + true-peak target. Reads the user's default target from `preferences.json` if present; otherwise accepts an inline target.
Mandates invoking relevant skills via tools before any response in coding sessions. Covers access, priorities, and adaptations for Claude Code, Copilot CLI, Gemini CLI.
Share bugs, ideas, or general feedback.
Two-pass loudnorm to hit a precise integrated LUFS + true-peak target. Reads the user's default target from preferences.json if present; otherwise accepts an inline target.
This is the apply step that pairs with the read-only audio-analysis skill.
| Field | Default |
|---|---|
| Source | required (video or audio) |
| Target | from preferences.json (loudness.target); else ask. Choices: youtube (-14/-1), spotify (-14/-1), apple-music (-16/-1), podcast (-16/-1), broadcast (-23/-2) |
| Output | sibling <basename>.normalized.<ext> (or project's working/ if inside an index project) |
Resolve target → I (integrated LUFS), TP (true peak), LRA (default 11).
DATA_DIR="${CLAUDE_USER_DATA:-${XDG_DATA_HOME:-$HOME/.local/share}/claude-plugins}/video-editing"
PREFS="$DATA_DIR/preferences.json"
if [ -f "$PREFS" ] && [ -z "$TARGET" ]; then
TARGET=$(jq -r '.loudness.target // empty' "$PREFS")
fi
case "$TARGET" in
youtube|spotify) I=-14; TP=-1; LRA=11 ;;
apple-music|podcast) I=-16; TP=-1; LRA=11 ;;
broadcast) I=-23; TP=-2; LRA=11 ;;
custom) ;; # caller sets I/TP/LRA
*) echo "Unknown target: $TARGET"; exit 1 ;;
esac
PASS1=$(ffmpeg -hide_banner -nostats -i "$SRC" \
-af "loudnorm=I=$I:TP=$TP:LRA=$LRA:print_format=json" \
-f null - 2>&1 | sed -n '/^{/,/^}/p')
MI=$(echo "$PASS1" | jq -r '.input_i')
MTP=$(echo "$PASS1" | jq -r '.input_tp')
MLRA=$(echo "$PASS1"| jq -r '.input_lra')
MTHRESH=$(echo "$PASS1" | jq -r '.input_thresh')
OFFSET=$(echo "$PASS1" | jq -r '.target_offset')
Sanity-check: if any of these are "-inf" or null, the source has silence-only or non-finite measurements. Fall back to single-pass loudnorm and warn.
LOUDNORM="loudnorm=I=$I:TP=$TP:LRA=$LRA:measured_I=$MI:measured_TP=$MTP:measured_LRA=$MLRA:measured_thresh=$MTHRESH:offset=$OFFSET:linear=true:print_format=summary"
Stream-copy video, re-encode audio only:
ffmpeg -hide_banner -y -i "$SRC" \
-map 0:v -map 0:a \
-c:v copy \
-af "$LOUDNORM" \
-c:a aac -b:a 192k \
-movflags +faststart \
"$OUT"
If the source has multiple audio tracks, normalize only a:0 and stream-copy the rest unless the user asked otherwise.
Pick the output codec from the input extension; default to wav for lossless intermediates, mp3/aac for delivery:
ffmpeg -hide_banner -y -i "$SRC" -af "$LOUDNORM" "$OUT"
Re-run a quick ebur128 pass on the output and confirm integrated is within ±0.5 LU of target and true-peak is at or below the ceiling:
ffmpeg -hide_banner -nostats -i "$OUT" -af 'ebur128=peak=true:framelog=quiet' -f null - 2>&1 | tail -25
Print a one-table summary:
Source : path/to/in.mp4
Target : YouTube (-14 LUFS / -1 dBTP)
Before : I=-19.2 LUFS, TP=-2.1 dBTP, LRA=8.3 LU
After : I=-14.0 LUFS, TP=-1.0 dBTP, LRA=7.9 LU
Output : path/to/in.normalized.mp4
linear=true keeps dynamics intact when the source already fits within the target LRA. Drop it (or omit) if you want loudnorm to compress to fit.working/ directory.