Skip to content

Update module github.com/llehouerou/go-mp3 to v1.5.0 - #1469

Merged
biglittlebigben merged 1 commit into
mainfrom
renovate/github.com-llehouerou-go-mp3-1.x
Oct 20, 2026
Merged

biglittlebigben merged 1 commit into
mainfrom
renovate/github.com-llehouerou-go-mp3-1.x

Conversation

@renovate

@renovate renovate Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

This PR contains the following updates:

Package Change Age Confidence
github.com/llehouerou/go-mp3 v1.2.0 → v1.5.0 age confidence

Release Notes

llehouerou/go-mp3 (github.com/llehouerou/go-mp3)

v1.5.0

Compare Source

Decoding is 1.6x faster than v1.4.0 and allocates almost nothing per frame. The decoder's internals were reshaped around the concepts they implement, with the output pinned by a golden hash and the mpg123 comparison at every step. Four edge-case behaviours changed; read that section before upgrading.

Measured

BenchmarkDecode, whole-file decode to io.Discard, benchstat n=10, 12-thread desktop:

file v1.4.0 v1.5.0
229 KB (classic_lame.mp3) 56.9 ms/op 35.5 ms/op 1.60x
11.4 MB (classic.mp3) 2.18 s/op 1.38 s/op 1.59x
v1.4.0 v1.5.0
bytes allocated, 11.4 MB file 336 MB/op 162 KB/op -99.95%
allocations, 11.4 MB file 122,600/op 13,750/op -89%
Frame.Decode (DSP only, one frame) 71.0 µs 66.2 µs 0 allocs, unchanged

Where it came from, in order of landing:

  • One Frame per stream, not per frame (#​15): the decoder state, including the bit reservoir and the synthesis history, is allocated once and reused. This is most of the allocation drop.
  • Bit reading (#​16): each read is one 8-byte load at the current position instead of a loop over bits.
  • Huffman decoding (#​17): a 10-bit lookup table resolves most codewords in one step.
  • Requantization (#​18): done a scalefactor band at a time, and the per-band gains are now computed once at parse time rather than per line.
  • IMDCT (#​20, #​22): the 36-point long-block transform runs as an 18-point DCT-IV over a 9-point FFT, in place on the subband's lines, with the frequency inversion folded into the overlap-add.
  • No error-path work on the hot path: the end-of-audio triage runs only when a frame actually fails.

Behaviour changes

None of these change a signature. All are covered by tests in this release.

  • Read stops at Length() in both gapless modes. With gapless disabled, a file whose Xing frame count under-reports the frames present used to play past Length() to EOF, so Progress() could exceed 1. The Xing count is now the length contract whether or not gapless is on.
  • Seeking past the real audio yields nothing. On a file whose Xing count over-promises by too little for the size check to catch, Seek past the last real frame used to land mid-way through that frame and hand out audio the position does not have. It now reads to EOF.
  • lameinfo.Parse rejects Layer I and Layer II headers, as the decoder itself does; a "Xing" tag where Layer III side info would end is not a Xing header in a Layer II frame.
  • NewDecoder reports an error from the gapless head skip instead of swallowing it. Only a source that fails with something other than EOF while discarding the encoder delay is affected.

Internals

The decoder is now organised around the concepts in CONTEXT.md, each behind a small interface with tests at that seam:

  • trim owns gapless trimming: raw/virtual position mapping, zero when gapless is off. The six lameInfo != nil branches in the decoder are gone, and Seek is one path instead of two.
  • source.nextFrame is the one place that decides the audio has ended (EOF, a frame cut short, trailing tags past the sync search).
  • internal/granule turns a frame's bitstream into finished granules: side info, reservoir, scalefactors, Huffman, gains. internal/sideinfo and internal/maindata, which passed a quartet of pointers between them and wrote into each other's structs, are deleted. internal/frame is the DSP stages only.
  • internal/bits is a pure bit reader. Its sticky error, which nothing ever read, is gone; the documented contract is that reads past the end yield zero, which the mid-stream reservoir join has always relied on.
  • lameinfo parses the frame header with internal/frameheader instead of its own copy of the bitrate and sample-rate tables. internal/consts is dissolved into the packages that use its pieces.

Output accuracy

As with v1.4.0, faster transforms reassociate floating-point additions, so decoded PCM is not bit-identical to v1.4.0. Measured on example/classic.mp3, 31.3M samples: 7,483 samples (one in 4,200) moved by a single LSB, 3,756 down and 3,727 up. Accuracy against the mpg123 reference is unchanged: RMS 0.7369 on that file, identical to v1.4.0, and 0.708–0.743 across the four test files including MPEG-2, within the pinned budget of RMS < 1.0 / MaxDiff <= 2 (ADR-0002).

The architecture commits themselves moved nothing: from the moment the golden hash landed, every refactoring commit decoded the 64-frame fixture to the same SHA-256 as the commit before it.

Verification

  • TestDecodeGolden pins the SHA-256 of 64 real LAME frames decoded end to end, and runs everywhere; the mpg123 comparison runs on the same synthesised file where mpg123 is installed, so re-pinning the hash after a legitimate float change is a measured decision.
  • internal/testaudio can now carry real encoded frames, so the synthesised corpus exercises the whole decoder, not just frame structure.
  • FuzzDecode mutates real frames, MPEG-1 and MPEG-2 files, and a copy of the real frames with intensity stereo set. Under go test -fuzz it reaches every DSP stage including mixed blocks and both intensity stereo paths, and 90% of internal/; ~7M inputs, no panic.
  • CI runs lint and tests on every push.

v1.4.0: - 2.5x faster decoding

Compare Source

Decoding is about 2.5x faster. Nothing in the API changed; the work is entirely in the transform and bit-reading paths, guided by profiles rather than guesses.

Measured

BenchmarkDecode, i7-8700K:

file v1.3.0 v1.4.0
229 KB 146.98 ms/op 58.57 ms/op 2.51x
11.4 MB 5.620 s/op 2.206 s/op 2.55x

Where it came from:

  • Polyphase synthesis (#​8): the 64x32 cosine matrix multiply — 2048 multiply-adds per sample, 46% of decode — is a DCT-II in disguise. Replaced with the Lee recursion.
  • Synthesis data movement (#​9): the sample history is a circular buffer now instead of a 960-float shift per sample, a 4 KB array copy per sample is gone, and the 512-entry U vector is summed as it is built rather than materialised.
  • IMDCT (#​10): its 36 outputs hold only 18 distinct values, so half the dot products were recomputing what the other half already knew; the cosine tables are stored output-major so the inner loops run contiguously.
  • Requantization (#​7): the gain table is precomputed instead of calling math.Pow per coefficient.

Output accuracy — read this before upgrading a golden-hash test

Faster transforms reassociate floating-point additions, so decoded PCM is not bit-identical to v1.3.0. Measured on example/classic.mp3, 31.3M samples:

differing: 7801 samples (0.0249%)   max difference: 1 LSB   distribution: -1: 3869, +1: 3932

One sample in four thousand moved by a single LSB, evenly split in both directions: dither-level noise at -96 dBFS, inaudible and non-accumulating. Accuracy against the mpg123 reference is unchanged (RMS 0.7369, ISO/IEC 11172-4 limited compliance, same as before).

If you assert an exact hash of decoder output in your own tests, that assertion will fail and needs re-recording. ADR 0002 now states the contract explicitly: output is bounded by an accuracy budget against a reference decoder, not promised stable bit-for-bit across versions.

Also

  • internal/imdct and the new DCT gained oracle tests that check each fast transform against the cosine matrix it replaced, on impulses, extremes and random inputs.
  • The compliance suite pins deviation from mpg123 at RMS < 1.0 / MaxDiff <= 2, roughly 6x tighter than the ISO limited ceiling it previously allowed.

v1.3.0: - Open without scanning the file

Compare Source

Opening a file no longer walks every frame header to learn its length. The frame count comes from the Xing/Info header, peeked out of the first frame before it is decoded; the scan survives only for headerless files, where it is deferred to the first call that needs it, and as the reference the header path is tested against.

Fixes #​1, found from a track played off an SMB mount pinning two kernel workers for its whole duration.

Measured

Open + Length(), counted at the io.Reader boundary:

file before after
11.4 MB, no Xing header 13 747 reads, 13 612 seeks 175 reads, 177 seeks
229 KB, Xing header 396 reads, 391 seeks 1 read, 2 seeks

Playing a file through now reads it exactly once instead of twice.

Added

  • ErrNotSeekable, returned by Seek, SeekToSample, SeekToTime and Skip on a plain io.Reader.
  • Length, Duration and Progress work on non-seekable streams that carry a Xing/Info header. Gapless trimming works there too, since the header is now parsed from bytes already read instead of by seeking back.

Changed

  • Reads are buffered in 64 KB blocks, with short forward seeks served from the buffer.
  • The frame index is built on the first seek instead of at open, so playback-only callers never build it.
  • A Xing frame count is checked against the file size before being trusted; a truncated download falls back to counting frames.
  • Fixed a long-standing position drift: bytes pushed back by Unread were subtracted from the stream position but never added back when re-read, leaving every recorded frame offset three bytes short.

Internal

New test corpus (internal/testaudio) synthesising CBR/VBR, Xing/Info/headerless, MPEG1/MPEG2, mono/stereo, tag-wrapped, truncated and lying-header files, plus a counting reader used to assert I/O budgets directly, and a differential test holding the header-derived length to what a full frame count produces — on synthetic files and on real LAME output.


Configuration

📅 Schedule: (UTC)

  • Branch creation
    • "on sunday"
  • Automerge
    • At any time (no schedule defined)

🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.

♻ Rebasing: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 Ignore: Close this PR and you won't be reminded about this update again.


  • If you want to rebase/retry this PR, check this box

This PR was generated by Mend Renovate. View the repository job log.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Devin Review

@github-actions

github-actions Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

AV-sync stats summary: view in run #39503353775

@renovate
renovate Bot force-pushed the renovate/github.com-llehouerou-go-mp3-1.x branch from bbdc9b7 to fb18a02 Compare October 20, 2026 21:48
@biglittlebigben
biglittlebigben merged commit 3a810a3 into main Oct 20, 2026
19 checks passed
@biglittlebigben
biglittlebigben deleted the renovate/github.com-llehouerou-go-mp3-1.x branch October 20, 2026 22:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant