Hold the true-peak ceiling on delivered renditions; ffmpeg 9 in production #20

Open
arne wants to merge 1 commit from truepeak-limiter into main
Owner

Problem

No production rendition respected its −1 dBTP ceiling, and every file came out 1 LU louder than its target. Measured across all 11 production masters with ffmpeg 9 loudnorm; the full before/after table is in docs/data/truepeak-2026-09-28.tsv.

Target Before (served) After (main-v2 / spotify-v3)
main AAC 128k −16 LUFS, ≤ −1 dBTP −15.05 LUFS, +0.44 to +4.20 dBTP −16.07 to −16.90 LUFS, −1.01 to −2.84 dBTP
spotify Vorbis q6 −14 LUFS, ≤ −1 dBTP −13.0 LUFS, +0.35 to +1.25 dBTP −14.05 to −14.29 LUFS, −1.72 to −2.10 dBTP

Causes

  1. alimiter's auto-level (level=1 is the default) applies 1/limit makeup gain after limiting. That puts the ceiling back at 0 dBFS and adds +1 LU to everything.
  2. alimiter limits sample peaks, not true peaks.
  3. Codec overshoot. Vorbis adds about 0.5–1 dB. ffmpeg's native AAC at 128k adds up to about 3 dB on specific transients.
  4. Production was running Debian's ffmpeg 5.1, while development used 9.x.

Changes

  • The limiter runs 4× oversampled (192 kHz) with level=0:latency=1.
  • The pre-encode limiter sits CodecHeadroomDB (1.5 dB) below the profile ceiling. TranscodeMeasured checks the encoded file and re-encodes with a lower ceiling if it's still over (up to 3 times; the step doubles if the peak doesn't respond). It keeps the lowest-peak attempt.
  • Profile bumps: main v1→v2 and spotify v2→v3, so a re-transcode replaces the served files (ADR-0020 rule).
  • deploy.sh installs BtbN's static ffmpeg-n9.0 build into /opt/ffmpeg, and the unit points there. Startup refuses ffmpeg < 9. Smoke-tested: the binary runs in the production container.
  • ADR-0022 and FINDINGS §9 record the measurements and the rejected alternatives: two-pass loudnorm silently switches to dynamic/AGC mode, a high-pass made one file worse, and -aac_coder fast didn't fix the worst case.

Trade-off

The four main renditions that needed retries land 0.6–0.9 LU under −16 LUFS. Apple and Overcast normalize at playback, so slightly quiet is harmless, whereas over 0 dBTP clips.

Testing

  • go build, go vet, go test ./... pass.
  • New TestTranscodeHoldsTruePeakCeiling encodes a hot pink-noise master through both profiles. It fails on the old chain (+0.6/+1.0 dBTP, 1.5–2 LU too loud) and passes now.
  • Ran the real TranscodeMeasured over all 11 production masters × both profiles: 22/22 at or under −1.0 dBTP.

After merging

  1. ./deploy/deploy.sh oink kokosbananas installs ffmpeg 9 and restarts.
  2. Re-transcode each episode to produce main-v2 / spotify-v3, then regenerate the feeds.

🤖 Generated with Claude Code

## Problem No production rendition respected its −1 dBTP ceiling, and every file came out 1 LU louder than its target. Measured across all 11 production masters with ffmpeg 9 `loudnorm`; the full before/after table is in `docs/data/truepeak-2026-09-28.tsv`. | | Target | Before (served) | After (`main-v2` / `spotify-v3`) | |---|---|---|---| | `main` AAC 128k | −16 LUFS, ≤ −1 dBTP | −15.05 LUFS, **+0.44 to +4.20 dBTP** | −16.07 to −16.90 LUFS, −1.01 to −2.84 dBTP | | `spotify` Vorbis q6 | −14 LUFS, ≤ −1 dBTP | −13.0 LUFS, **+0.35 to +1.25 dBTP** | −14.05 to −14.29 LUFS, −1.72 to −2.10 dBTP | ## Causes 1. **alimiter's auto-level** (`level=1` is the default) applies `1/limit` makeup gain after limiting. That puts the ceiling back at 0 dBFS and adds +1 LU to everything. 2. **alimiter limits sample peaks, not true peaks.** 3. **Codec overshoot.** Vorbis adds about 0.5–1 dB. ffmpeg's native AAC at 128k adds up to about 3 dB on specific transients. 4. Production was running **Debian's ffmpeg 5.1**, while development used 9.x. ## Changes - The limiter runs 4× oversampled (192 kHz) with `level=0:latency=1`. - The pre-encode limiter sits `CodecHeadroomDB` (1.5 dB) below the profile ceiling. `TranscodeMeasured` checks the encoded file and re-encodes with a lower ceiling if it's still over (up to 3 times; the step doubles if the peak doesn't respond). It keeps the lowest-peak attempt. - **Profile bumps:** `main` v1→v2 and `spotify` v2→v3, so a re-transcode replaces the served files (ADR-0020 rule). - `deploy.sh` installs BtbN's static `ffmpeg-n9.0` build into `/opt/ffmpeg`, and the unit points there. Startup refuses ffmpeg < 9. Smoke-tested: the binary runs in the production container. - ADR-0022 and FINDINGS §9 record the measurements and the rejected alternatives: two-pass `loudnorm` silently switches to dynamic/AGC mode, a high-pass made one file worse, and `-aac_coder fast` didn't fix the worst case. ## Trade-off The four `main` renditions that needed retries land 0.6–0.9 LU under −16 LUFS. Apple and Overcast normalize at playback, so slightly quiet is harmless, whereas over 0 dBTP clips. ## Testing - `go build`, `go vet`, `go test ./...` pass. - New `TestTranscodeHoldsTruePeakCeiling` encodes a hot pink-noise master through both profiles. It fails on the old chain (+0.6/+1.0 dBTP, 1.5–2 LU too loud) and passes now. - Ran the real `TranscodeMeasured` over all 11 production masters × both profiles: 22/22 at or under −1.0 dBTP. ## After merging 1. `./deploy/deploy.sh oink kokosbananas` installs ffmpeg 9 and restarts. 2. Re-transcode each episode to produce `main-v2` / `spotify-v3`, then regenerate the feeds. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
No production rendition respected its -1 dBTP ceiling. Measured with ffmpeg 9
loudnorm across all 11 production masters (full table in
docs/data/truepeak-2026-09-28.tsv):

  main    AAC 128k   before: -15.03..-15.07 LUFS, +0.44..+4.20 dBTP
                     after:  -16.07..-16.90 LUFS, -1.01..-2.84 dBTP
  spotify Vorbis q6  before: -12.98..-13.09 LUFS, +0.35..+1.25 dBTP
                     after:  -14.05..-14.29 LUFS, -1.72..-2.10 dBTP

Causes:
- alimiter's default auto-level (level=1) applies 1/limit makeup gain after
  limiting, putting the ceiling back at 0 dBFS and adding +1 LU to every file.
- alimiter limits sample peaks, not true peaks.
- Lossy encoders overshoot the limited signal; ffmpeg's native AAC encoder
  at 128k overshoots by up to ~3 dB on specific transients.

Changes:
- The limiter runs 4x oversampled with level=0 and latency=1.
- Profiles limit CodecHeadroomDB (1.5 dB) below their ceiling, and
  TranscodeMeasured re-encodes with a lower ceiling (up to 3 times) if the
  encoded file still measures over it.
- Bump main v1->v2 and spotify v2->v3 so the back catalog is re-encoded
  (ADR-0020 rule).
- deploy.sh installs a static ffmpeg n9.0 build into /opt/ffmpeg, and startup
  refuses ffmpeg < 9 (production was on Debian's 5.1).
- ADR-0022 and FINDINGS section 9 record the measurements and the rejected
  alternatives (loudnorm falls back to dynamic mode; high-pass; aac_coder fast).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
arne force-pushed truepeak-limiter from a9f5ddc870 to 9900b769c4 2026-10-06 10:12:16 +02:00 Compare
This pull request can be merged automatically.
This branch is out-of-date with the base branch
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin truepeak-limiter:truepeak-limiter
git switch truepeak-limiter

Merge

Merge the changes and update on Forgejo.
git switch main
git merge --no-ff truepeak-limiter
git switch truepeak-limiter
git rebase main
git switch main
git merge --ff-only truepeak-limiter
git switch truepeak-limiter
git rebase main
git switch main
git merge --no-ff truepeak-limiter
git switch main
git merge --squash truepeak-limiter
git switch main
git merge --ff-only truepeak-limiter
git switch main
git merge truepeak-limiter
git push origin main
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
tjue/kokosbananas!20
No description provided.