Skip to content

fix(worklets): polyfill TextDecoder for Firefox AudioWorkletGlobalScope (#89)#101

Merged
indigo423 merged 2 commits into
mainfrom
fix/worklet-textdecoder-firefox
Jul 8, 2026
Merged

fix(worklets): polyfill TextDecoder for Firefox AudioWorkletGlobalScope (#89)#101
indigo423 merged 2 commits into
mainfrom
fix/worklet-textdecoder-firefox

Conversation

@indigo423

@indigo423 indigo423 commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

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)

[tfmx-processor] init failed ReferenceError: TextDecoder is not defined
    createLibtfmx https://mods.amiga.fans/libtfmx.worklet.js:1

The WHATWG Encoding spec exposes TextDecoder as [Exposed=(Window,Worker)]worklets are not included. Firefox follows the spec, so AudioWorkletGlobalScope has no TextDecoder; Chrome exposes it there as a non-standard extension, which is how this went unnoticed during development.

The Emscripten glue in both libtfmx.worklet.js and libahx.worklet.js contains an unguarded var UTF8Decoder = new TextDecoder inside the module factory (modern Emscripten dropped the typeof guard 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 shipped libopenmpt.worklet.js bundle carries its own TextDecoder replacement, which is exactly why libopenmpt works everywhere — this fix follows that precedent.

Fix

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. 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 AudioWorkletGlobalScope also lacks TextDecoder, making it a faithful stand-in for Firefox:

  • Before: a Library .fc failed with the exact ReferenceError from the screenshot.
  • After: a Library .fc (TFMX single-file) and a Library .ahx both play with no errors (scripts/run-driver.mjs).
  • make verify green: 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-byte Misc/ChipTune.* files need a data re-sync.

indigo423 added 2 commits July 8, 2026 23:35
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>
@indigo423 indigo423 merged commit 7727484 into main Jul 8, 2026
3 checks passed
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