Skip to content

chore: release packages (player 0.5.0, player-tui 0.3.0, utils 0.3.0)#146

Merged
nulltask merged 49 commits into
mainfrom
devel
Jun 14, 2026
Merged

chore: release packages (player 0.5.0, player-tui 0.3.0, utils 0.3.0)#146
nulltask merged 49 commits into
mainfrom
devel

Conversation

@nulltask

Copy link
Copy Markdown
Owner

Release PR (develmain). Merging this triggers the Release workflow, which publishes a GitHub Release per bumped package and attaches the player / audio-renderer SEA executables.

Released packages

Minor

  • @be-music/player 0.4.3 → 0.5.0 — LR2 judge windows / HARD・EASY・DEATH gauges / mine detonation model
  • @be-music/player-tui 0.2.6 → 0.3.0 — Node SEA (single executable) support, embedded node-web-audio-api / libav.js
  • @be-music/utils 0.2.1 → 0.3.0 — new @be-music/utils/optional-node-module subpath (loadOptionalNodeModule)

Patch

  • @be-music/parser 0.2.2 → 0.2.3 — UTF-16 BOM detection / encoding scoring, control-flow spelling variants, object-data whitespace truncation, bmson ln_type
  • @be-music/audio-renderer 0.2.2 → 0.2.3 — missing keysounds default to silence
  • @be-music/player-web 0.6.1 → 0.6.2 — LR2 groove gauge bar rendering, dynamic volume scoping
  • @be-music/json 0.2.1 → 0.2.2, @be-music/stringifier 0.3.0 → 0.3.1, @be-music/chart 0.3.1 → 0.3.2 — bmson ln_type round-trip / internal dependency bumps
  • @be-music/lr2-skin 0.1.3 → 0.1.4, @be-music/beatoraja-skin 0.1.1 → 0.1.2, @be-music/editor 0.2.2 → 0.2.3 — internal dependency bumps

@be-music/player-web-demo (private) is bumped 0.3.0 → 0.3.1 by the internal-dependency cascade but is not published.

Per-package details are in each packages/*/CHANGELOG.md.

🤖 Generated with Claude Code

dependabot Bot and others added 30 commits May 24, 2026 02:33
Bumps [node-web-audio-api](https://github.com/ircam-ismm/node-web-audio-api) from 1.0.9 to 2.0.0.
- [Changelog](https://github.com/ircam-ismm/node-web-audio-api/blob/main/CHANGELOG.md)
- [Commits](ircam-ismm/node-web-audio-api@v1.0.9...v2.0.0)

---
updated-dependencies:
- dependency-name: node-web-audio-api
  dependency-version: 2.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [oxlint](https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint) from 1.67.0 to 1.69.0.
- [Release notes](https://github.com/oxc-project/oxc/releases)
- [Changelog](https://github.com/oxc-project/oxc/blob/main/npm/oxlint/CHANGELOG.md)
- [Commits](https://github.com/oxc-project/oxc/commits/oxlint_v1.69.0/npm/oxlint)

---
updated-dependencies:
- dependency-name: oxlint
  dependency-version: 1.69.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [@typescript/native-preview](https://github.com/microsoft/typescript-go) from 7.0.0-dev.20260607.1 to 7.0.0-dev.20260611.2.
- [Changelog](https://github.com/microsoft/typescript-go/blob/main/CHANGES.md)
- [Commits](https://github.com/microsoft/typescript-go/commits)

---
updated-dependencies:
- dependency-name: "@typescript/native-preview"
  dependency-version: 7.0.0-dev.20260611.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [@cloudflare/workers-types](https://github.com/cloudflare/workerd) from 4.20260608.1 to 4.20260611.1.
- [Release notes](https://github.com/cloudflare/workerd/releases)
- [Changelog](https://github.com/cloudflare/workerd/blob/main/RELEASE.md)
- [Commits](https://github.com/cloudflare/workerd/commits)

---
updated-dependencies:
- dependency-name: "@cloudflare/workers-types"
  dependency-version: 4.20260611.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Extract decodeBmsText / decodeUtf8Text from parser.ts into
core/bms-text-decode.ts as the single bytes-to-text pipeline for every
runtime, and switch the web player's collection loader to it instead of
its local duplicate. Remove the unreachable iconv-lite based
bms-text-decoder.ts (never imported) together with the iconv-lite
dependency. Export decodeUtf8Text with a bench case and test.
…pture

Move the object-line interpretation rule (channel 02 measure length +
non-zero token stream) into event-utils buildBmsObjectLineEntry so the
strict parse path and the #RANDOM / #IF / #SWITCH capture path can no
longer drift apart. Output shapes are unchanged.
Replace the per-backend sourceFormat === 'bms' checks that encode the
LR2-compatible one-voice-per-slot retrigger policy with a single
usesMonophonicWavPlayback predicate in @be-music/chart. The Node mixer,
debug active-audio estimator, WebAudio session, offline renderer, and
web preview now consult the same definition.
Add bmsExRankValueToJudgeRankPercent (the RANK 2 = 100 unit conversion
shared by #DEFEXRANK and #EXRANKxx) and route the dynamic channel-A0
path through a dedicated resolveBmsJudgeWindowsMsForExRankValue entry
point in judge-window.ts. Behavior is unchanged: the dynamic path still
skips the conversion (spec audit A-2); the deviation is now documented
and pinned by tests so the future fix is a single-line change in one
module.
Export DEFAULT_POOR_BGA_DISPLAY_SECONDS from core/bga-timeline.ts and
consume it from the TUI compositor and the web LR2 scene instead of
their local 2 s / 2000 ms constants. Replace the web scene's hardcoded
'gauge >= 80' clear check with core isGrooveGaugeCleared so the clear
threshold has a single owner; GROOVE behavior is identical (80 %),
HARD / EASY skin variants now follow the core per-variant thresholds.
The Vite build emits worker and lazy chunks with a .js extension
(WASMAudioDecoderWorker-*.js, opus-ml-*.js) that the inliner skipped,
leaving top-level relative requires in the embedded entry. The SEA
injected require only resolves builtins, so every launch (including
--help) crashed with ERR_UNKNOWN_BUILTIN_MODULE.
vite build() runs the client environment, whose default conditions and
main fields prefer browser entries. The SEA bundle therefore shipped
pino/browser (flush is a noop, so the logger close() hung forever,
swallowing playback errors into a silent exit 0, writing zero bytes to
the NDJSON log file, and dumping log objects to the console) and
isoworker's Web Worker implementation. Resolve with the server
conditions and main fields instead.
… SPEED head

Resolve the first wave of BMS spec-audit findings:

- HARD / DEATH gauges now FAIL when they bottom out at 0 %;
  isGrooveGaugeCleared previously read 0 >= 0 as cleared, so survival
  gauges could never fail. Propagates to the web scene via the shared
  helper.
- Dynamic #EXRANKxx (channel A0) values now pass through the same
  RANK 2 = 100 unit conversion as #DEFEXRANK, so #EXRANK 100 restores
  exactly the NORMAL windows instead of widening them by 4/3.
- bmson key_channels per-mine damage is applied as the gauge damage,
  taking precedence over the BMS value/2 rule; damage 0 is honored as
  a no-damage decoration mine.
- #SPEEDxx now holds the first keyframe's value before its beat
  (Bemuse reference semantics) instead of ramping linearly from 1.0.
  #SCROLLxx keeps its implicit 1.0 head.

Update player-spec / bmson-spec docs (both languages) to state the
EXRANK unit, the pre-first-keyframe SPEED rule, and the spec-correct
negative-total handling.
A SEA binary cannot load the gameplay/UI/BGA-video worker scripts: the
worker .js files are not shipped next to the executable, the injected
require only resolves builtins, and the CJS bundle compiles
import.meta.url to undefined, so resolving the worker URL threw before
the Worker was even constructed. Playback therefore never started.

Embed the bundle itself as a SEA asset and spawn eval workers that
re-run it; the new sea-main entry dispatches on a workerData role
marker to the matching worker module before the CLI entry runs. The
CLI entry additionally refuses to start from a worker thread, since
inside SEA workers process.argv[1] still equals process.execPath.
An undefined / missing / undecodable #WAVxx reference must be silent
(LR2 / beatoraja both skip it); the synthesized sine fallback used to
play unconditionally on the Node / TUI path while the web runtime was
already silent. The tone is now opt-in: fallbackToneSeconds in the
audio-renderer APIs, missingSampleToneSeconds in PlayerOptions.
…eBmsText

Implement the documented detection pipeline in the shared decoder:
BOM (UTF-8 / UTF-16LE / UTF-16BE) -> #CHARSET -> ASCII fast path ->
strict UTF-8 validation -> scored candidates (shift_jis / utf-8 /
euc-jp / iso-8859-1), all on the WHATWG TextDecoder so CLI / TUI / web
behave identically. Previously UTF-16 charts decoded as garbled
shift_jis (losing every event) and BOM-less UTF-8 titles were
mojibake.
The channel stream of #mmmcc:data is one contiguous token run in
de-facto BMS implementations. The tokenizer used to skip whitespace and
keep pairing, so trailing text fabricated note events (junk words ->
JU/NK/WO/RD) and inflated the denominator of legitimate tokens. The
shared tokenizer change covers both the strict parse path and
control-flow captured lines.
Inside a SEA, import() of a bare specifier always fails with
ERR_UNKNOWN_BUILTIN_MODULE regardless of any node_modules on disk, so
node-web-audio-api (audio playback) and @uwx/libav.js-fat (video BGA)
could never load and the player stayed permanently silent. Add a
loadOptionalNodeModule helper that falls back to createRequire
resolution anchored next to the executable and the working directory,
letting a SEA binary load these modules from a node_modules directory
shipped alongside it.
node --build-sea invalidates the executable's signature, and macOS
kills unsigned binaries with SIGKILL at launch. The signing step was
best-effort and silent, which turned a codesign failure into a
"build succeeded but the binary dies with Killed: 9" mystery.
…ed voices

docs/bms-spec.md specifies that a #xxx97 / #xxx98 volume change applies
as the initial gain of voices triggered from that point on and must not
touch voices already playing. The WebAudio session wrote the level onto
the shared bus mixers via setValueAtTime, retroactively rescaling
in-flight voices and diverging from the Node engine. The session now
tracks the current BGM / key dynamic gain and splices it into each new
source's per-voice gain alongside the #WAVCMD multiplier.
These misspellings appear in released charts. Without the aliases the
enclosing #IF block never closed, so on a non-matching #RANDOM roll
everything after the directive vanished to EOF (and on a matching roll
a junk END extras header leaked in). The variants normalize onto the
canonical #ENDIF / #ELSEIF commands at parse time; #END with any other
value stays an unknown header.
Allow BE_MUSIC_OPTIONAL_NODE_MODULE_DIR to name an extra base directory
whose node_modules is consulted between the executable directory and
the working directory. SEA binaries will point it at the extraction
location of their embedded native modules; worker threads pick it up
through the inherited environment.
Embed the package and its production dependency closure (filtered to
the build host's native addon) as SEA assets. At startup the player
extracts them once into a content-addressed cache directory under
~/.be-music/sea-embedded-modules and points loadOptionalNodeModule at
it via BE_MUSIC_OPTIONAL_NODE_MODULE_DIR, so audio playback works out
of the box without shipping a node_modules directory next to the
executable. A node_modules beside the binary still takes precedence.
Embed @uwx/libav.js-fat into the player SEA binary with per-package
exclude patterns: Node always selects the .wasm build and the player
runs libav with noworker, so the asm.js fallback (~122MB), the
worker-threaded build (~35MB), and the LGPL source tarballs (~30MB)
are dropped, keeping the addition to ~31MB.
The inliner re-indented every line of an inlined chunk and rewrote its
require calls with a regex. Both transformations can mutate the inside
of multi-line template literals, and the 4-space indent corrupted the
yEnc-encoded decoder wasm of @wasm-audio-decoders/* — in the player
bundle every decoder copy lives inside a chunk, so ogg/opus/mp3
keysounds failed crc32 validation and played silently (the select
preview fell back to dummy tones). Redirect chunk requires by shadowing
the require parameter instead and wrap the entry in an IIFE, leaving
both sources byte-for-byte intact.
import() wraps CJS exports in a namespace whose default holds them, but
the createRequire fallback used inside a SEA returns the exports object
directly. createLibAvInstance assumed the namespace shape, so under SEA
it threw 'Cannot read properties of undefined (reading LibAV)' — the
error was swallowed by the video loader and video BGA never played.
Accept both module shapes and validate the factory before use.
Preserve the beatoraja bmson extensions info.ln_type and per-note t
(1: LN, 2: CN, 3: HCN) in the IR, round-trip them through JSON and
bmson stringifier output, and resolve the player's long-note mode from
them (per-note t > info.ln_type > default). Unspecified charts now
default to LN -- no tail release judgment, matching the LR2-aligned BMS
default -- instead of always being judged as CN.
Replace the IIDX-baseline windows with linear judgerank scaling by the
LR2 measurements (hitkey diary 2015-01-19, lr2oraja JudgeProperty):
RANK 0-3 -> PG 8/15/18/21ms, GR 24/30/40/60ms, GD 40/60/100/120ms,
RANK 4 treated as NORMAL, BAD fixed at 200ms for every rank, scratch
sharing the key windows. #DEFEXRANK / #EXRANKxx / bmson judge_rank
interpolate piecewise-linearly between the rank anchors (the
JudgeWindowRule.LR2 model), extrapolate past EASY, and clamp to the
fixed BAD gate.
nulltask and others added 19 commits June 12, 2026 00:48
Adopt beatoraja's LR2-compat gauge constants: HARD recovery is the
fixed +0.1/+0.1/+0.05 with damage scaled by the LR2 #TOTAL multiplier
table and softened x0.6 under 30%; EASY uses -3.2/-4.8/-1.6 damage and
clears at 80% like GROOVE; DEATH follows HAZARD_LR2 (+0.15/+0.06/0,
instant death on BAD/POOR, -10 on empty POOR). Survival gauges collapse
to 0% once below 2% and never recover, and raw deltas (mine damage,
HCN drain) route through applyGrooveGaugeRawDelta which bypasses
guts / TOTAL scaling while keeping the life rules.
A phantom press now charges an empty POOR only when the lane has a
note within the next second (lr2oraja JudgeProperty LR2 miss window
{0, 1000000}us, early side only, rank-independent). Presses after a
note or in note-free stretches play the fallback keysound and nothing
else -- previously every phantom press drained the gauge and flashed
the POOR BGA, which made HARD / DEATH runs lose gauge for harmless
taps between phrases.
A mine now explodes while the lane's key is ON and the mine sits within
the GOOD window of the judge line (losak's LR2 writeup; beatoraja's
JudgeManager) -- both pressing with a mine in range and holding through
a passing mine detonate; an un-pressed mine passes harmlessly. The old
press-time matching (which detonated mines up to the BAD window early
and let mines swallow presses aimed at nearby notes) is gone, regular
note judgment runs independently, and explosions no longer emit a BAD
or break combo. Damage is the raw base36 value as a percentage
(LR2 / beatoraja; the nanasi value/2 rule is not what LR2 does),
bypasses HARD guts / #TOTAL scaling via the raw-delta gauge path, and
ZZ instantly fails survival gauges. Held state uses the kitty
press/release set with a short press-grace approximation for
release-less fallback input.
…1.69.0

chore(deps-dev): bump oxlint from 1.67.0 to 1.69.0
…are/workers-types-4.20260611.1

chore(deps-dev): bump @cloudflare/workers-types from 4.20260608.1 to 4.20260611.1
…ipt/native-preview-7.0.0-dev.20260611.2

chore(deps-dev): bump @typescript/native-preview from 7.0.0-dev.20260607.1 to 7.0.0-dev.20260611.2
The demo's Vite config enumerates @be-music/utils subpath exports as
explicit aliases, and the new optional-node-module subpath (used by
player/src/audio-sink.ts since the SEA embedded-modules work) was
missing -- the bare '@be-music/utils' file alias swallowed it and
rolldown tried to load 'index.ts/optional-node-module' (Not a
directory, os error 20), breaking 'vite build'. The module is
browser-safe by design (Node built-ins load lazily in the fallback
path only), so a direct alias to the source file fixes the bundle.
Two skin-side divergences found by reverse-engineering the actual LR2
default 7K skin (LR2files/.../7keys/7_LL0.csv) and its #SRC_GROOVEGAUGE
definition:

- Remove the peak-hold / afterimage bead. LR2's groove gauge has no
  peak indicator -- the single #SRC/#DST_GROOVEGAUGE draws the bar purely
  from the live value. Drops the whole gaugePeak subsystem (fields,
  decay constants, updateGaugePeak tick).
- Suppress the green clear-zone split for survival gauges. LR2 reuses
  one 4-cell SRC (表赤/表緑/裏赤/裏緑) and only paints the >=80% clear
  zone green for gauges that have a clear border (GROOVE / EASY).
  HARD / DEATH have no border, so LR2 renders the whole bar red; we now
  pass the gauge type through and suppress the green zone for them.

Extract the per-bead cell selection into the pure, unit-tested
resolveGrooveGaugeBeads helper.
The mine tests placed the mine on measure 0 (chart 0 s) and pressed the
key immediately at speed 240. After A-3 switched mines to LR2's
passage-based detonation (key held while the mine crosses the GOOD
window), a single sped-up poll tick could advance chart time past the
mine's window before the input was processed, so detonation became
timing-dependent -- passing locally but flaking on CI (the bmson
per-mine damage test failed there).

Move the mine to measure 1 (chart 2.0 s) and hold the lane key through
its passage via a shared kitty-state input schedule at speed 1, the
same proven timing as the hold-through test. Detonation now lands on a
frame tick inside the GOOD window every run.
Bumps [wrangler](https://github.com/cloudflare/workers-sdk/tree/HEAD/packages/wrangler) from 4.98.0 to 4.100.0.
- [Release notes](https://github.com/cloudflare/workers-sdk/releases)
- [Commits](https://github.com/cloudflare/workers-sdk/commits/wrangler@4.100.0/packages/wrangler)

---
updated-dependencies:
- dependency-name: wrangler
  dependency-version: 4.99.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [oxfmt](https://github.com/oxc-project/oxc/tree/HEAD/npm/oxfmt) from 0.52.0 to 0.54.0.
- [Release notes](https://github.com/oxc-project/oxc/releases)
- [Changelog](https://github.com/oxc-project/oxc/blob/main/npm/oxfmt/CHANGELOG.md)
- [Commits](https://github.com/oxc-project/oxc/commits/oxfmt_v0.54.0/npm/oxfmt)

---
updated-dependencies:
- dependency-name: oxfmt
  dependency-version: 0.54.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
…r-4.99.0

chore(deps-dev): bump wrangler from 4.98.0 to 4.100.0
….54.0

chore(deps-dev): bump oxfmt from 0.52.0 to 0.54.0
…b-audio-api-2.0.0

chore(deps): bump node-web-audio-api from 1.0.9 to 2.0.0
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
be-music-player-demo d9df3f6 Jun 14 2026, 04:57 AM

@github-actions

Copy link
Copy Markdown

Exports Benchmark

  • Base SHA: c996e7f8815f
  • Head SHA: d9df3f6bbac0
  • Comparable cases: 231
  • Regression threshold: 8.00%
  • Base runs: median of 3
  • Head runs: median of 3

Summary

Metric Value
Improved (>= threshold) 3
Regressed (<= -threshold) 3
Unchanged 225
Median change +0.41%
Mean change +0.60%
Head benchmarked cases 233
Head skipped cases 21

Top Regressions

API Base ops/s Head ops/s Change
stringifier.stringifyBms 21548.41 18707.50 -13.18%
player.resolveJudgeWindowsMs 6050172.43 5253227.82 -13.17%
player-tui.loadTerminalAnsiImage 994.70 895.00 -10.02%

Top Improvements

API Base ops/s Head ops/s Change
editor.saveJsonFile 516.16 767.90 +48.77%
editor.exportChart 545.13 809.10 +48.42%
audio-renderer.writeAudioFile 581.90 787.49 +35.33%

@nulltask nulltask merged commit 1d606bc into main Jun 14, 2026
25 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