Skip to content

fix(v2.4): honor per-frame unsynchronisation flag#665

Draft
joust wants to merge 1 commit into
eidoriantan:mainfrom
joust:fix/v24-per-frame-unsync
Draft

fix(v2.4): honor per-frame unsynchronisation flag#665
joust wants to merge 1 commit into
eidoriantan:mainfrom
joust:fix/v24-per-frame-unsync

Conversation

@joust
Copy link
Copy Markdown
Contributor

@joust joust commented Apr 15, 2026

Summary

ID3v2.4 §4.1.2 makes unsynchronisation a per-frame flag; the tag-level bit in v2.4 is only a hint. The decoder had the right intent but compared version === 4 where version is the [major, revision] array — always false. The branch was dead code; per-frame unsync was silently ignored.

Standards-compliant v2.4 tags (unsync only at frame level, no tag-level bit) could not be decoded. The previous behavior happened to work only because mp3tag.js also sets the tag-level bit when writing v2.4 — see the related v2.3 tag-level unsync PR.

Changes

  • src/id3v2/index.mjs: version === 4version[0] === 4, plus spec-citing comment
  • test/id3v2/index.cjs: new regression test — writes v2.4 GEOB with 0xFF bytes and unsync, clears the tag-level bit in the buffer, then asserts the reader still decodes the frame correctly via the per-frame flag

Relationship to #666

PR #666 (v2.3 tag-level unsync) touches the same line and restructures decodeFrame to drop the tag-level fallback entirely. The one-line fix in this PR is effectively included in #666 as part of that restructure, and the regression test here has been mirrored into #666 so that merging #666 first does not lose the coverage.

Merge options:

Test plan

  • npm test — 79 passing (78 → 79)
  • Test fails on old code (version === 4), passes on new (version[0] === 4)

ID3v2.4 §4.1.2 moves unsynchronisation from a tag-level flag (as in
v2.3 §5) to a per-frame flag. The tag-level bit in v2.4 is only a
hint meaning "at least one frame is unsynchronised"; the actual
decision of whether to un-unsynchronise a given frame must consult
that frame's own flag byte.

The decoder had the right intent but an incorrect check:

    if (version === 4) unsynchedData = frame.flags.unsynchronisation

`version` at this scope is the [major, revision] array returned from
the ID3 header, so `version === 4` is always false. The branch was
dead code and the per-frame unsync flag was silently ignored. Tags
produced by standards-compliant v2.4 writers — where unsync is set
only at the frame level — would be read as raw unsynchronised bytes.

The previous behavior "worked" only when the tag-level unsync bit
was also set, which mp3tag.js itself does (see the separate v2.3
tag-level unsync issue).

The new test writes a v2.4 tag with unsync enabled, then clears the
tag-level unsync bit in the buffer to simulate a conforming writer,
and verifies the reader still decodes correctly via the per-frame
flag. Fails on the old code, passes on the new.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant