feat(tfmx): play single-file Hippel TFMX and Future Composer formats#99
Conversation
The libtfmx backend already decodes single-file formats (Jochen Hippel TFMX/MCMD: .hip/.hipc/.hip7/.mcmd; Future Composer: .fc/.fc3/.fc4/.fc13/ .fc14/.smod), but the app only ever handed it two-file Huelsbeck pairs, so those files were rejected at every layer. Surfaces mschwendt's ask in issue #89 ("why reject Hippel TFMX / Future Composer / MCMD?"). Single-file formats now play from Library browse/search and Local drop alongside the existing pairs: - New TFMX_SINGLE_EXTENSIONS allowlist + isTfmxSingleFile/tfmxSingleExt; deliberately kept out of isModuleFile (distinct engine) and excluding the ambiguous .mdat/.tfm/.tfmx names (a lone .mdat could be an orphan Huelsbeck half OR a complete Hippel file — deferred). - New tfmx-single-{local,library} Source variants (single buffer, no sample half) wired through getBuffer / play dispatch / drawer state. - Worklet loads a lone music-data file: skips the .sam write and names the MEMFS file with the real extension (e.g. song.fc) so libtfmx's sidecar-guessing doesn't hunt for a phantom .sam; tfx_load then content-detects the format. - Server: byte-server perimeter allows single-file extensions with no partner check (orphan-half rejection preserved for pair formats); listing + search surface singles; tfmx-random left pair-only. Verified end to end: Library serves .fc/.hipc without a partner (200), orphan .tfx/.mdat still 404, and both a Future Composer and a Hippel file decode and play through the tfmx-single-library engine path. Adds scripts/run-driver.mjs (Playwright driver behind the local run-coolmodfiles skill) for driving the player headless. OpenSpec change: add-tfmx-single-file-formats. Assisted-by: ClaudeCode:claude-opus-4-8 Signed-off-by: Ronny Trommer <ronny@no42.org>
…; pair-half precedence Code-review follow-ups on the single-file TFMX feature (PR #99). A) Auto-advance / error recovery for single-file sources. pickRandomNext returned null for both tfmx-single-* arms, so on a getBuffer fetch failure (deleted/renamed library file, network blip) the retry path treated the source family as empty and teleported the user onto a random Mod Archive track — exactly what the surrounding code comments warn against for user-picked sources, and inconsistent with library/tfmx-library which retry in-family. Now: - tfmx-single-local mirrors tfmx-local (random-pick from the dropped set), so local singles auto-advance and retry in-family; - tfmx-single-library does a sequential folder walk (pickNextInFolder now understands the listing's singles[]) and, since single formats have no tfmx-random endpoint, falls back to /api/library/random — staying in the library instead of jumping to Mod Archive. B) Single-vs-pair-half precedence. Single-file detection classified files by extension alone, so a prefix-Amiga pair whose base ends in a single-file token (mdat.fc + smpl.fc, base "fc") was mis-split into bogus singles on local drop and double-listed (pair + phantom singles) by /api/library — while the search endpoint already got it right. detectSinglesInDir, the local detectTfmxPairs, and the search single-arm now all require parseHalfName()===null: a file that is a pair half is never a single. Pair detection takes precedence uniformly; orphan halves remain 404'd. Verified live: mdat.fc+smpl.fc lists as one pair (not singles); genuine .fc/.hipc still list and serve; orphan mdat.fc → 404. +2 regression tests (234 passing); typecheck + lint clean. Assisted-by: ClaudeCode:claude-opus-4-8 Signed-off-by: Ronny Trommer <ronny@no42.org>
|
Applied max-effort code-review follow-ups in A — single-file auto-advance / error recovery (the reachable one): B — single-vs-pair-half precedence: detection classified by extension alone, so a prefix-Amiga pair whose base ends in a single-file token ( Verified live: Not changed: the latent worklet |
…ternary Code-review follow-ups on PR #99: - lib/tfmx/pairs.ts: new parseSingleName(name) — the single definition of the "single-file with pair-half precedence" invariant (a name that is also a pair half, e.g. mdat.fc, is never a single). Previously the rule was repeated verbatim at three sites (detectSinglesInDir, the Local-drop detector, the search endpoint); an edit to one could silently desynchronize listing, search, and Local drop. All three now call the shared parser. - components/Player.tsx: collapse pickNextInFolder's currentPath ternary — the library and tfmx-single-library arms were identical. - components/sources/index.ts: add the missing SPDX header (identical to the one added on fix/mod-misrouted-as-recording, so the branches still merge cleanly). No behavior change; the mdat.fc-precedence regression tests now pin the shared parser (234 passing). Assisted-by: ClaudeCode:claude-fable-5 Signed-off-by: Ronny Trommer <ronny@no42.org>
Why
The vendored libtfmx backend already decodes single-file Amiga formats — Jochen Hippel TFMX/MCMD (
.hip.hipc.hip7.mcmd) and Future Composer (.fc.fc3.fc4.fc13.fc14.smod) — but the app only ever handed it two-file Huelsbeck pairs, so those files were rejected at every layer. This addresses issue #89, where the library author (mschwendt) asked "why do you reject the other file types? Like Jochen Hippel's TFMX files? Like Future Composer? MCMD?" — and the maintainer agreed we shouldn't.What changes
Single-file formats now play from Library browse/search and Local drop, alongside the existing pairs:
TFMX_SINGLE_EXTENSIONS+isTfmxSingleFile/tfmxSingleExt. Deliberately kept out ofisModuleFile(distinct engine) and excluding the ambiguous.mdat/.tfm/.tfmxnames — a lone.mdatcould be an orphan Huelsbeck half or a complete Hippel file, so it stays pair-only (deferred).Sourcevariantstfmx-single-local/tfmx-single-library(single buffer, no sample half), threaded throughgetBuffer, theplay()dispatch, and the drawer picked-state..samwrite and names the MEMFS file with the real extension (e.g.song.fc) so libtfmx's sidecar-guessing doesn't hunt for a phantom.sam;tfx_loadthen content-detects the format.tfmx-randomleft pair-only.No vendored-library or WASM change — the engine already plays these formats.
Verification
Driven end-to-end against a running server (via
scripts/run-driver.mjs):/api/library/fileserves.fc/.hipcwithout a partner → 200; orphan.tfx/.mdatstill → 404 (perimeter intact);tfmx-random→ 404 (singles excluded).tfmx-single-libraryengine path (screenshot: "Playing from: tfmx-single-library").make verifygreen (lint 0 errors, typecheck, 232 unit tests incl. 16 new, build).Notes / follow-ups
chambers-balance.fc) rather than a synthesized(FC)/(Hippel)label — the extension carries the format identity. Easy to add a family tag if wanted.scripts/run-driver.mjs, a Playwright driver for headless player smoke-tests (documented in the localrun-coolmodfilesskill). Heads-up: TFMX decode fails under Playwright's bundled Chromium because itsAudioWorkletGlobalScopelacksTextDecoder— an env gap, not a product bug; real browsers have it.OpenSpec change:
add-tfmx-single-file-formats(gitignored).🤖 Generated with Claude Code