Repository navigation
Every render with audio re-encodes AAC 2-3 times (duration check counts priming packets; true-peak guard re-encodes silently), which grows ffmpeg AAC encoder bursts #6871
Description
Related: #5669 (render audio differs run to run; the -1 dBTP guard flips), #5556 (configurable audio bitrate).
Environment
- hyperframes 0.8.143
- ffmpeg 9.0.2 (Homebrew, built with Apple clang 21.0.0), macOS arm64
- Not tested on 0.8.186 or later; it may already encode from PCM.
What happens
The audio of a render goes through the built-in ffmpeg AAC encoder (-c:a aac -b:a 192k) more than once.
(a) Duration check counts priming/flush packets, so pad/trim always re-encodes. chunk-33RZPZ4K.js:8304-8323 sets the AAC duration to nb_read_packets * 1024 / sampleRate. That count includes the encoder-delay and flush packets, so the file always looks longer than the video by about a packet or two. padOrTrimAudioToVideoFrameCount (called at chunk-6XMFEICO.js:10272) then takes the trim branch (chunk-6XMFEICO.js:9894-9920: atrim + -t + -c:a aac -b:a 192k), a second encode, on every render with audio.
(b) True-peak guard re-encodes with a gain change and logs only at info level. enforceAacTruePeak (chunk-6XMFEICO.js:10036-10095) measures the true peak of the AAC and, if above the ceiling, re-encodes with volume=(-1.5-TP)dB (a third encode). The only trace is Audio lowered by X dB to stay under ... (chunk-6XMFEICO.js:10236), at info level, so --quiet hides it. The final level differs from the mix with no visible warning.
(c) Effect. The built-in AAC encoder (default twoloop coder) writes a 10-20 ms one-channel error burst at loud, L/R-different low-frequency onsets, and each further encode makes it larger (ffmpeg report to follow; standalone ffmpeg-only repro). With HyperFrames' 2-3 encodes, a first-frame beat over decorrelated room tone went from a 0.25 peak error on L after one encode to 0.69 after the pipeline, and the guard then took about 1.5 dB off. A mix that is linear under loudnorm after one encode came out dynamic (+0.5 dBTP over target) once it had been through HyperFrames' encodes. Checking only a single encode would not show this.
Repro
- Make a composition with an
<audio>clip whose source starts loud at t=0 with different L/R content (a low-frequency hit over decorrelated noise; see the ffmpeg-only generator in the ffmpeg report). - Render it (default settings, audio on).
- Extract the audio from the MP4 and compare it with the source WAV over the first 50 ms: peak of (decoded - source) per channel. Expect a burst at about 10-15 ms on one channel, well above the error elsewhere in the file, and a level lower than the source by the guard's gain if the true peak was above the ceiling.
- Count encodes:
ffprobeon the render's audio shows a single AAC stream, but running the render with logs on shows the pad/trim step and, when the guard fires, theAudio loweredline.
Verified here with ffmpeg alone: replaying the same three commands HyperFrames uses (encode; atrim=duration=T,asetpts=PTS-STARTPTS -t T re-encode at 192k; volume= re-encode) on a standalone source gave a byte-identical AAC stream and the same bursts. I did not re-run a full render with a minimal composition for this draft.
Suggested fixes
- Count the priming (and flush) samples in the duration check, or measure the decoded sample count, so a correctly sized file is stream-copied, not re-encoded.
- Encode once, from PCM: mix to PCM, apply pad/trim and any true-peak gain on the PCM, then run the AAC encoder a single time.
- Log the guard's gain change at warn level so
--quietdoes not hide a level change.
Activity
miguel-heygen commentedon Oct 31, 2026
PR: #6873 (not merged yet).
I checked current main/0.8.223 rather than assuming the 0.8.143 path is unchanged. Upgrading from 0.8.143 includes #6089, which fits PCM to the video frame duration before AAC, and #6312, which makes true-peak corrections from retained PCM rather than decoding an earlier AAC. Normal frame-fitted 30/1 and 30000/1001-fps media keeps identical AAC packets through duration normalization in the new real-media tests.
This PR fixes the remaining local-render disclosure bug: the actual gain reduction is a warning, so
--quietno longer hides it. Its regression fails on current main and passes with the log-level change. 39 Vitest and 40 Bun tests pass, as do normal lint/format/typecheck/audit commit hooks.This does not claim a reproduction of the macOS FFmpeg9.0.2 onset artifact or a literal single encoder invocation. The decoded-AAC ceiling check remains; it can create several candidates from the original PCM. Legacy/external AAC needing a real duration correction can still be transcoded, and distributed planning has a separate existing attenuation-disclosure gap. The PR uses Refs rather than closing this issue.
budprat commentedon Oct 31, 2026
Thanks for checking against current main rather than 0.8.143. We pinned 0.8.143 (Oct 8), so we were missing #6089 and #6312. We'll test 0.8.223 on our real renders (30 fps; AAC packets through duration normalization, the peak guard's log, and our onset-burst scan) and report back here. Agreed that the onset bursts are FFmpeg's built-in aac encoder, not HyperFrames; we're reporting that to FFmpeg separately. Refs is right; we'll confirm here once we've tested 0.8.223.