Hypernatural

TechniqueBy HypernaturalVersion 1, Oct 21, 2026

ffmpeg-normalize-loudness

Bring a voice or dialogue track to a target loudness (EBU R128, -16 LUFS integrated, -1.5 dBTP true peak) with ffmpeg loudnorm in two passes and linear gain, so dynamics and timing are untouched and line timings stay valid. Use on a finished, assembled track before delivery or lip-sync, never per line.

SKILL.md

Normalize loudness

Give it an assembled track (a dialogue track, a narration, a final mix) and you get the same audio at -16 LUFS integrated with true peaks under -1.5 dBTP, the usual web and streaming level. Two passes: the first measures, the second applies one linear gain from those measurements, so nothing is compressed and every sample stays where it was (track.json line times remain true). Run it once, after the last edit to the track.

Inputs

  • The track (track.wav, ideally lossless: pcm_48000 from ElevenLabs).
  • Its sample rate, from ffprobe (loudnorm resamples to 192 kHz internally, so the second pass resamples back to it).

Run it

Keep the unnormalized file beside the result: rename track.wav to track_raw.wav first, and normalize from it.

  1. Sample rate:
exec ffprobe -v error -select_streams a:0 -show_entries stream=sample_rate -of csv=p=0 track_raw.wav
  1. Measure (pass 1). The JSON block is at the end of stderr; read input_i, input_tp, input_lra, input_thresh and target_offset from it.
exec ffmpeg -hide_banner -nostats -i track_raw.wav \
  -af loudnorm=I=-16:TP=-1.5:LRA=11:print_format=json -f null -
  1. Apply (pass 2), with the measured values and the source rate:
exec ffmpeg -v error -y -i track_raw.wav \
  -af "loudnorm=I=-16:TP=-1.5:LRA=11:linear=true:measured_I=<input_i>:measured_TP=<input_tp>:measured_LRA=<input_lra>:measured_thresh=<input_thresh>:offset=<target_offset>,aresample=<rate>" \
  track.wav
  1. Re-encode the MP3 from the normalized WAV:
exec ffmpeg -v error -y -i track.wav -b:a 192k track.mp3

Record the change where the track's metadata lives, e.g. "loudness": "-16 LUFS (from -22.4)" in track.json.

What you get

track.wav at -16 LUFS, track.mp3 from it, and track_raw.wav untouched. Durations and line times are unchanged.

Tips and traps

  • Normalize the assembled track, not each line or passage. Per-line normalization pumps the level from line to line and has to be redone after every edit.
  • linear=true falls back to dynamic (compressing) mode when one gain can't reach the target without passing the true-peak ceiling. Add :print_format=json to the pass 2 filter to see normalization_type; if it says dynamic, the track has peaks far above its speech. Accept it for voice, or lower the target (I=-18) to stay linear.
  • Replacing one piece inside a normalized track (a re-voiced chunk or line): don't normalize the piece on its own. Match its speech level to the piece it replaces, limit it with alimiter=limit=0.84:attack=2:release=50:level=false (-1.5 dBFS), and put it through the same post chain (EQ) as the rest of the track.
  • Loudness doesn't fix a thin voice. A voice that sounds like a telephone is usually band-limited at the source; see elevenlabs-tts-v4.

Check it

Run pass 1 on track.wav: input_i within 0.5 of -16 and input_tp at or under -1.5. ffprobe gives the same duration and sample rate as track_raw.wav.

Go make something cool.

Install the app, open your agent, describe the video. That's the whole onboarding.

Sticker HypernaturalSticker BannerSticker TVBoy