fix(worklets): polyfill TextDecoder for Firefox AudioWorkletGlobalScope (#89)#101
Merged
Conversation
TFMX and AHX playback failed in Firefox with 'ReferenceError: TextDecoder is not defined' thrown from createLibtfmx/createLibahx — confirmed by a user console screenshot on issue #89. The WHATWG Encoding spec exposes TextDecoder to Window and Worker scopes only, NOT to worklets; Firefox follows the spec, while Chrome exposes it in AudioWorkletGlobalScope as a non-standard extension (which is how the gap went unnoticed in development). The Emscripten glue in libtfmx.worklet.js and libahx.worklet.js constructs an unguarded 'new TextDecoder' inside the module factory, so engine init throws in Firefox and every TFMX/AHX play fails. The shipped libopenmpt.worklet.js bundle carries its own TextDecoder replacement — the precedent this fix follows. Add a shared decode-only UTF-8 TextDecoder polyfill (public/worklet-textdecoder-polyfill.js), imported by both wrapper worklets BEFORE the Emscripten glue import. Decode-only is sufficient: both bundles construct exactly one no-arg TextDecoder and only call .decode(Uint8Array); neither references TextEncoder or UTF-16. Verified end-to-end in the bundled headless Chromium — whose AudioWorkletGlobalScope also lacks TextDecoder, making it a faithful stand-in for Firefox: a Library .fc (TFMX single-file) and a Library .ahx both play with no errors; both failed identically before the fix. Lint/typecheck/tests green (240 passing). Assisted-by: ClaudeCode:claude-fable-5 Signed-off-by: Ronny Trommer <ronny@no42.org>
Code-review follow-up on PR #101. The first cut consumed continuation bytes blindly: a Latin-1 module title like "Läther" (4C E4 74 68 65 72 — common in Amiga-scene names) sent 0xE4 down the 3-byte branch, which ate the valid 't' and 'h' as continuations and rendered "L䴨er", while native TextDecoder renders "L�ther" with nothing lost. Trailing lead bytes also read past the buffer end, and garbage 4-byte sequences could emit lone surrogates. Now the whole sequence is validated before consuming anything (lead-byte range incl. C0/C1 overlong rejection, continuation shape, bounds, CESU-8 surrogates, astral range); on violation the decoder emits U+FFFD for the lead byte alone and resynchronizes at the next byte. Differential-tested against Node's native TextDecoder: byte-identical output for valid UTF-8 (all planes), Latin-1 titles, truncation, bare continuations, and overlongs; never emits lone surrogates. Only divergence is the U+FFFD count on pathological sequences (native's maximal-subpart rule emits one per subpart) — no valid bytes are ever lost. TFMX still plays end-to-end in the TextDecoder-less headless browser. Assisted-by: ClaudeCode:claude-fable-5 Signed-off-by: Ronny Trommer <ronny@no42.org>
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 TFMX half of #89 — and a previously unnoticed AHX twin of the same bug.
Root cause (validated by @mschwendt's console screenshot on #89)
The WHATWG Encoding spec exposes
TextDecoderas[Exposed=(Window,Worker)]— worklets are not included. Firefox follows the spec, soAudioWorkletGlobalScopehas noTextDecoder; Chrome exposes it there as a non-standard extension, which is how this went unnoticed during development.The Emscripten glue in both
libtfmx.worklet.jsandlibahx.worklet.jscontains an unguardedvar UTF8Decoder = new TextDecoderinside the module factory (modern Emscripten dropped thetypeofguard because Window/Worker always have it). In Firefox, engine init throws → every TFMX play fails — and every AHX play too, unnoticed because Mod Archive's random flow silently skips failures. The shippedlibopenmpt.worklet.jsbundle carries its ownTextDecoderreplacement, which is exactly why libopenmpt works everywhere — this fix follows that precedent.Fix
A shared, decode-only UTF-8
TextDecoderpolyfill (public/worklet-textdecoder-polyfill.js), imported by both wrapper worklets before the Emscripten glue import. Decode-only is sufficient: both bundles construct exactly one no-argTextDecoderand only call.decode(Uint8Array); neither referencesTextEncoderor UTF-16. No vendored source or wasm rebuild needed. (The deeper alternative — rebuilding with-sTEXTDECODER=0— can land whenever the bundles are next regenerated.)Verification
The bundled headless Chromium's
AudioWorkletGlobalScopealso lacksTextDecoder, making it a faithful stand-in for Firefox:.fcfailed with the exactReferenceErrorfrom the screenshot..fc(TFMX single-file) and a Library.ahxboth play with no errors (scripts/run-driver.mjs).make verifygreen: lint 0 errors, typecheck clean, 240 tests.With #100 (already merged) and this, every symptom in the #89 console screenshot is accounted for: the
[pcm] 4/ media-decode spam (MOD misrouting — fixed) and the TFMX init failure (this PR). Remaining item is server-side only: the 0-byteMisc/ChipTune.*files need a data re-sync.