Skip to content

feat(player): AHX/THX Sound pane + download polish (Chunk β)#38

Merged
indigo423 merged 4 commits into
mainfrom
feat/add-ahx-playback-chunk-b
May 17, 2026
Merged

feat(player): AHX/THX Sound pane + download polish (Chunk β)#38
indigo423 merged 4 commits into
mainfrom
feat/add-ahx-playback-chunk-b

Conversation

@indigo423
Copy link
Copy Markdown
Collaborator

Chunk β of the add-ahx-playback OpenSpec change. Follows
#37 (Chunk α — engine

  • facade). Chunk γ (HELP.md / README + release tag) is the final piece.

Summary

Two threads, both flowing from the Phase 0 spike's findings.

1. Per-control Sound pane gating (D9 amendment)

The Sound pane's two controls now gate independently per the design.md
D9 matrix:

Engine Amiga emulation Stereo separation
libopenmpt enabled enabled
tfmx disabled disabled (different scale, not wired up)
ahx disabled enabled (ahx2play accepts 0..100 natively)

The "Sound settings only affect classic MOD files" banner now only
shows when ALL controls are disabled — i.e. TFMX is active. For AHX
the slider is live, so the all-disabled banner would mislead.

Two string-equality sites migrated together to the facade's new
activeEngine: EngineKind getter:

  • components/SoundPane.tsx — gating predicate.
  • components/Player.tsxm-key keyboard handler for Amiga emulation.

Plus a new activeEngine React state in Player.tsx, set
synchronously after each player.play() call so the Sound pane
re-renders immediately on engine switch (without waiting for the new
worklet's meta postback).

2. Mod Archive download extension preserved

downloadTrack and downloadFavoriteMods no longer hardcode .mod
for Mod Archive downloads. Both now sniff the first 4 bytes of the
fetched Blob and pick .ahx for AHX/THX content, falling back to
.mod otherwise. The shared sniff helper mirrors looksLikeAhx's
gate (3-letter prefix + version byte ∈ {0x00, 0x01}).

This deviates from D13's "use ModItem.filename" decision in favour
of byte sniffing because the filename isn't always available at
download time (random walks, permalink loads, and favorites all bypass
the chart context). Byte sniffing is universal, costs ~4 bytes per
download, and keeps the FavoriteTrack localStorage shape unchanged
(no migration needed).

3. Refactor: looksLikeAhx extracted + unit tested

lib/audio-player.ts depends on the ambient ChiptuneJsPlayer global
(registered via public/chiptune3.js script tag) and can't be
imported in a Node test runtime. The 4-byte sniff lived inside that
file in Chunk α; moved to a pure-utility module at lib/ahx-magic.ts
so vitest can exercise it.

24 new test cases cover the spec scenarios: canonical AHX magic,
legacy THX magic (the bulk of modarchive's .ahx corpus), mismatched
version bytes, unrelated prefixes (XM, IT, MED, HTML/WAF response),
undersized buffers, and non-ArrayBuffer inputs. 33 / 33 tests pass.

Test plan

CI:

  • make verify green locally (lint + typecheck + audit + build)
  • npm test green locally (33/33 — 9 existing resolveSafe + 24 new looksLikeAhx)
  • CI verify + e2e + tests green on the PR

Manual smoke (pending human — needs browser):

  • Play a MOD → set stereo slider to 30% → switch to an AHX track:
    slider stays enabled at 30%, AHX renders with stereo separation applied.
  • During AHX playback, move slider to 80%: audio widens within one
    render block.
  • Switch AHX → TFMX: slider becomes disabled but retains 80% visually.
  • Switch TFMX → MOD: slider re-enables, 80% applies.
  • During AHX playback, Sound pane shows: Amiga toggle disabled,
    stereo slider enabled, NO "Sound settings only affect classic MOD
    files" banner.
  • During TFMX playback, banner appears.
  • Keyboard m key during AHX: toast "Amiga emulation only affects
    MOD tracks", no cycle. During MOD: cycles A1200 → A500 → Off.
  • Click download on a Mod Archive AHX track: saved file ends in
    .ahx, not .mod. Regression check: MOD downloads still .mod.
  • Favorite a Mod Archive AHX track → download favorites zip:
    that entry inside the zip ends in .ahx. MOD entries still .mod.

What's NOT in this PR

Chunk γ (final follow-up):

  • HELP.md / README updates calling out AHX/THX support and the
    modarchive papercut fix.
  • package.json bump to 0.5.0.
  • Signed tag + GitHub release.

The deferred-work items from the Chunk α code review (cross-engine
handshake gap, ackslot reuse, worklet-Set leak, generation counter
lockstep) remain pre-existing TFMX behaviour and are recorded for
joint follow-up — out of scope for this PR.

Refs

  • OpenSpec: openspec/changes/add-ahx-playback/ (local artifact;
    openspec/ is gitignored by convention)
  • Design: design.md D9, D12, D13, D14 + Phase 0 Resolved memo
  • Spec: specs/ahx-playback/spec.md requirements
    "Sound pane gating is per-control during AHX playback",
    "AHX worklet reports a stable meta.type value",
    "Mod Archive AHX downloads preserve the upstream extension",
    "AudioPlayer.load(url) is libopenmpt-only by design".

indigo423 added 4 commits May 18, 2026 00:21
…ownload extension

Two threads from Chunk β of add-ahx-playback, tightly coupled through
Player.tsx so committed together:

1. Per-control Sound pane gating (D9 amendment, post-Phase-0 spike).
   The Sound pane's two controls now gate independently by the
   AudioPlayer facade's activeEngine:
     - Amiga emulation toggle: enabled only for libopenmpt.
     - Stereo separation slider: enabled for libopenmpt AND ahx
       (ahx2play accepts stereo separation at the same 0..100 scale);
       disabled for tfmx only.
   The all-disabled banner ("Sound settings only affect classic MOD
   files") now appears only when both controls are disabled, i.e.
   when TFMX is active. For AHX the slider is live, so the banner
   would be misleading.

   Implementation:
     - SoundPane reads a new `activeEngine?: EngineKind` prop.
     - SourceDrawer's SoundProps mirrors the new prop.
     - Player.tsx tracks activeEngine in React state, set
       synchronously after each player.play() call so the gating
       updates immediately rather than waiting for the new worklet's
       meta postback.
     - Player.tsx's `m`-key handler migrates from
       `metaData.type.toLowerCase() !== "mod"` to the
       activeEngine-based predicate. The two sites must move
       together — otherwise the shortcut and the UI disagree.

2. Mod Archive download extension preserved for AHX/THX.
   downloadTrack and downloadFavoriteMods both fetch a Blob from
   api.modarchive.org/downloads.php. Both used to write `<title>.mod`
   regardless of the underlying format — AHX/THX files downloaded
   from Mod Archive would mislabel as `.mod`, and a user sharing
   such a file would propagate the wrong extension to recipients.

   New helper `sniffDownloadExtension(blob)` reads the first 4 bytes
   and returns `.ahx` for AHX/THX content (per the same 4-byte gate
   as `looksLikeAhx` — 3-letter prefix + version byte ∈ {0x00, 0x01}),
   else null. Both download paths fall back to `.mod` on null,
   preserving today's behaviour for all libopenmpt formats.

   This deviates from D13's "use ModItem.filename" decision in favour
   of byte sniffing because the filename isn't always available at
   download time (random walks, permalink loads, and favorites all
   bypass the chart context). Byte sniffing is universal, costs ~4
   bytes per download, and keeps the FavoriteTrack shape unchanged
   (no storage migration). The bytes are the canonical source of
   truth anyway — a `.mod`-named file with AHX magic is still AHX.

make verify green.

Assisted-by: ClaudeCode:claude-opus-4-7
…overage

Moves the magic-byte sniff from lib/audio-player.ts into a tiny pure-
utility module so it can be unit-tested in isolation. audio-player.ts
depends on the ChiptuneJsPlayer ambient global (registered via the
public/chiptune3.js script tag) and can't be imported in a Node test
runtime.

The 24 new test cases mirror the spec scenarios in
openspec/changes/add-ahx-playback/specs/ahx-playback/spec.md:
  - Canonical AHX magic (AHX\0, AHX\1)
  - Legacy THX magic (THX\0, THX\1) — the bulk of modarchive's `.ahx`
    corpus carries this magic, validating D4's dual-prefix design
  - Mismatched version bytes (0x02, 0xFF, 0x10, 0x20) fall through
  - Unrelated prefixes (IT IMPM, XM "Exte", MED MMD0, HTML "<!DO")
    fall through — the WAF/HTML scenario from design.md's false-
    positive analysis
  - Undersized buffers (0, 3 bytes) return false; exactly 4 bytes
    matching AHX v1 is the inclusive boundary
  - Non-ArrayBuffer inputs (Uint8Array view, plain object,
    TfmxPair-shaped, null/undefined/string/number) all return false

Tests run via `npm test` (vitest). Not gated by `make verify` today
but covered by `npm run test:ci` in the CI workflow.

Assisted-by: ClaudeCode:claude-opus-4-7
… hint, npm test gate

Three fixes from the adversarial review of PR #38 (Chunk β).

1. THX-magic files download as `.thx`, not `.ahx` (A2).
   sniffDownloadExtension now distinguishes the two prefixes:
   AHX-magic files return ".ahx", THX-magic files return ".thx".
   Honours the spec scenario "Legacy THX modarchive track downloads
   as `.thx`" — the bytes ARE the upstream identity, so a THX-magic
   file round-trips with the legacy extension that matches what the
   user would see on Modarchive. The format-engine routing is
   unchanged (both still play via ahx2play through looksLikeAhx);
   the distinction is purely cosmetic round-trip fidelity.

2. AHX-active Amiga-toggle hint in the Sound pane (B5).
   When an AHX file is playing only the Amiga emulation toggle is
   disabled (the stereo separation slider is live per the D9
   matrix), so the all-disabled "Sound settings only affect classic
   MOD files" banner doesn't fire. Without an explanation the
   greyed-out toggle was silent — users had no hint why it
   wouldn't respond. New one-line note next to the Amiga heading
   when activeEngine === "ahx": "Amiga emulation has no effect for
   AHX tracks — they render through ahx2play's built-in Paula
   model."

3. `npm test` added to `make verify` (E1).
   The previous Makefile target ran lint + typecheck + audit +
   build and explicitly omitted `npm test`. The 24 new unit tests
   for looksLikeAhx (and the 9 pre-existing resolveSafe tests)
   would not block a CI regression. Now `make verify` runs
   `npm test` between typecheck and audit. Pre-existing gap, but
   added in this PR because the same PR adds 145 lines of test
   scaffold that depend on something CI never invoked.

The remaining review findings (sticky activeEngine after stop,
m-key useEffect deps closure-staleness, banner string hardcoded
"TFMX", test label "AHX v2" ambiguity, HVL silent-reject) are
recorded in _bmad-output/implementation-artifacts/deferred-work.md
for joint follow-up.

make verify green (now including npm test — 33/33 tests pass).

Assisted-by: ClaudeCode:claude-opus-4-7
The Local tab's drop-zone label read "Drop MOD or TFMX files here"
and listed only MOD-family + TFMX extensions. AHX/THX support
landed in Chunk α and accepts .ahx and .thx files, but the hint
text wasn't updated — users dropping AHX files had no visual
confirmation the catalogue would accept them.

Updated to "Drop MOD, AHX, or TFMX files here" with the extension
hint extended to include `.ahx .thx` between the MOD-family list
and the TFMX pair patterns.

The file input itself has no `accept=` filter (it uses
webkitdirectory for folder selection), so the OS-level picker
already accepts AHX files — only the UI hint was stale.

Assisted-by: ClaudeCode:claude-opus-4-7
@indigo423 indigo423 merged commit 44416bd into main May 17, 2026
4 checks passed
indigo423 added a commit that referenced this pull request May 17, 2026
… hint, npm test gate

Three fixes from the adversarial review of PR #38 (Chunk β).

1. THX-magic files download as `.thx`, not `.ahx` (A2).
   sniffDownloadExtension now distinguishes the two prefixes:
   AHX-magic files return ".ahx", THX-magic files return ".thx".
   Honours the spec scenario "Legacy THX modarchive track downloads
   as `.thx`" — the bytes ARE the upstream identity, so a THX-magic
   file round-trips with the legacy extension that matches what the
   user would see on Modarchive. The format-engine routing is
   unchanged (both still play via ahx2play through looksLikeAhx);
   the distinction is purely cosmetic round-trip fidelity.

2. AHX-active Amiga-toggle hint in the Sound pane (B5).
   When an AHX file is playing only the Amiga emulation toggle is
   disabled (the stereo separation slider is live per the D9
   matrix), so the all-disabled "Sound settings only affect classic
   MOD files" banner doesn't fire. Without an explanation the
   greyed-out toggle was silent — users had no hint why it
   wouldn't respond. New one-line note next to the Amiga heading
   when activeEngine === "ahx": "Amiga emulation has no effect for
   AHX tracks — they render through ahx2play's built-in Paula
   model."

3. `npm test` added to `make verify` (E1).
   The previous Makefile target ran lint + typecheck + audit +
   build and explicitly omitted `npm test`. The 24 new unit tests
   for looksLikeAhx (and the 9 pre-existing resolveSafe tests)
   would not block a CI regression. Now `make verify` runs
   `npm test` between typecheck and audit. Pre-existing gap, but
   added in this PR because the same PR adds 145 lines of test
   scaffold that depend on something CI never invoked.

The remaining review findings (sticky activeEngine after stop,
m-key useEffect deps closure-staleness, banner string hardcoded
"TFMX", test label "AHX v2" ambiguity, HVL silent-reject) are
recorded in _bmad-output/implementation-artifacts/deferred-work.md
for joint follow-up.

make verify green (now including npm test — 33/33 tests pass).

Assisted-by: ClaudeCode:claude-opus-4-7
@indigo423 indigo423 deleted the feat/add-ahx-playback-chunk-b branch May 17, 2026 22:41
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