fix(player): route recordings by source extension, not content sniff (#89)#100
Merged
Conversation
Library and Local playback failed for tracker modules ("Couldn't play
that track — pick another"); only Mod Archive worked. Reported at
issue #89.
Root cause: AudioPlayer.play() content-sniffed every ArrayBuffer via
mimeForBuffer to detect PCM recordings, and its deep MP3 frame-sync scan
(deepScanMp3, scanning 64 KB for >=3 0xFF-framesync patterns) false-
positives on a tracker module's raw 8-bit PCM sample data. A genuine
.mod/.xm/etc. was therefore classified as audio/mpeg and routed to the
<audio> element, which can't decode a module -> MEDIA_ERR (pcm code 4)
-> "Couldn't play". Mod Archive masked it: play-random silently skips a
failing track and advances, while a user-picked Library/Local row stays
failed. Confirmed live on prod (v0.5.11) and reproduced in the current
build; mimeForBuffer('4mat_VI.mod') returns 'audio/mpeg'.
Fix: recordings only ever arrive from Library/Local sources with a
recording extension (Mod Archive is always a module; AHX is caught by
reliable offset-0 magic; TFMX by object shape). So classify recordings
by the SOURCE's extension — the authoritative signal — and stop feeding
arbitrary module buffers into the MP3 sniffer:
- recording-magic.ts: new mimeForExtension(name); export MimeType.
- sources/index.ts: recordingMime(source) derives the MIME from the
Library path / Local filename (null for Mod Archive / TFMX).
- audio-player.ts: play(input, pcmMime?) takes the caller's recording
hint; the PCM arm fires on pcmMime instead of mimeForBuffer(input).
- Player.tsx + EmbedPlayer.tsx: pass recordingMime(source).
mimeForBuffer is unchanged and still used for download-time extension
guessing (Mod Archive, which has no filename). Verified live: a real
Library .mod plays through libopenmpt ("Playing from: library"), an .mp3
recording still routes to the PCM engine. +new tests (222 passing);
typecheck + lint clean.
Note: some Misc/ChipTune files on prod are 0 bytes on disk — a separate
data/sync issue, not addressed here.
Assisted-by: ClaudeCode:claude-opus-4-8
Signed-off-by: Ronny Trommer <ronny@no42.org>
Code-review follow-ups on PR #100: - lib/recording-magic.ts: introduce RECORDING_MIME_BY_EXTENSION and derive mimeForExtension from it instead of a hardcoded if-chain. - components/sources/index.ts: derive RECORDING_EXTENSIONS from the same map, so widening the allowlist (e.g. .opus) widens routing automatically — listing and playback dispatch cannot diverge. Fix the stale comment that still claimed engine dispatch is content-sniffed at AudioPlayer.play(). Add the missing SPDX header. Assisted-by: ClaudeCode:claude-fable-5 Signed-off-by: Ronny Trommer <ronny@no42.org>
This was referenced Jul 8, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes the core of #89: Library and Local playback fails for tracker modules ("Couldn't play that track — pick another"); only Mod Archive works.
Root cause
AudioPlayer.play()content-sniffed every buffer viamimeForBufferto detect PCM recordings. Its deep MP3 frame-sync scan (deepScanMp3— scans 64 KB for ≥30xFF-framesync byte patterns) false-positives on a tracker module's raw 8-bit PCM sample data. So a genuine.mod/.xm/etc. was classified asaudio/mpeg, routed to the<audio>element (which can't decode a module →MEDIA_ERR,[pcm] 4), and failed.Why only Mod Archive worked: its play-random silently skips a failing track and advances, masking the bug; a user-picked Library/Local row stays failed.
Confirmed live on prod (v0.5.11) and reproduced in the current build —
mimeForBuffer(4mat_VI.mod)returns"audio/mpeg".Fix — trust the source, don't sniff module bytes
Recordings only ever arrive from Library/Local sources with a recording extension (Mod Archive is always a module; AHX is caught by reliable offset-0 magic; TFMX by object shape). So classify recordings by the source's extension and stop feeding arbitrary module buffers into the MP3 sniffer:
recording-magic.ts: newmimeForExtension(name); exportMimeType.sources/index.ts:recordingMime(source)— MIME from Library path / Local filename,nullfor Mod Archive/TFMX.audio-player.ts:play(input, pcmMime?)takes the caller's recording hint; the PCM arm fires onpcmMimeinstead ofmimeForBuffer(input).Player.tsx+EmbedPlayer.tsx: passrecordingMime(source).mimeForBufferis unchanged and still used for download-time extension guessing (Mod Archive has no filename).Verification
Live (local dev against real files pulled from prod):
.modnow plays through libopenmpt ("Playing from: library", no error)..mp3recording still routes to the PCM engine and plays.mimeForExtensionandrecordingMime(incl..mod → null); 222 passing; typecheck + lint clean.Out of scope (separate issues from #89)
Misc/ChipTunefiles on prod are 0 bytes on disk — a data/sync issue on the server, not a code bug (/api/library/filefaithfully streamsstat.size). Worth a re-sync.TextDecodergap; needs its own confirmation from the browser console.🤖 Generated with Claude Code