Be-Music is an open-source project for playing and working with BMS and bmson charts. It brings together browser and terminal players, online leaderboards, and reusable TypeScript packages for chart processing, gameplay, skins, and UI.
- Plays in the browser (WebGPU / WebGL, installable as a PWA), with touch play and portrait layouts.
- A terminal player (TUI, also as a single executable) draws BGA images, videos, loading screens and banners with the kitty graphics protocol, and reads left and right Shift separately with the kitty keyboard protocol.
- An embeddable preview / play view fills a window of any aspect ratio.
- LR2, beatoraja and That-Which-Must-Not-Be-Named judgment, gauges and scoring, switchable on one engine.
- The play options of all three in one place; options that only transform the chart or the view work under every rule set.
- Seek to any point during AUTO, replay or manual play, and two-player modes (BATTLE / COUPLE / 2P PLAY).
- Loads LR2 skins and beatoraja skins (JSON / Lua) as they are.
- Built-in skins: Plain, Native (DOM + Canvas) and Synesthesia, a 3D scene that moves with each frequency band of the music.
- Plays can be recorded as replays and re-judged under any rule set; the judge is deterministic, so replays can verify scores.
- Replay slots and a viewer for everything the player keeps in the browser.
- Chart fingerprints identify charts by their content rather than their file bytes, including charts with
#RANDOM. - LR2 course files, beatoraja's SQL-filtered random courses and NONSTOP courses (one stitched play), with course replays.
- Drop folders or ZIPs, including mixed chart and LR2 / beatoraja theme drops; large audio and video load on demand.
- Guesses the text encoding of BMS files (Shift_JIS, UTF-8, EUC-JP and more), and writes BMS back keeping the source lines that did not change.
- BGA images and videos, with in-browser transcoding for formats the browser cannot play, and MP4 / WebM recording of a play with WebCodecs.
- Compressor and limiter controls for the key, BGM and master buses; heavy preparation runs on a worker pool.
- Tools for editing charts, converting between BMS, bmson and JSON, and rendering a chart to an audio file.
- A Leaderboard built with the player: sending is opt-in, and the Leaderboard decides what it shows. Chart scores and course runs get share cards.
- One account across the player, the Leaderboard and the dashboard, through Be-Music's own OIDC provider; the services run on Cloudflare Workers.
- Open source under the MIT License.
- Built from reusable packages (
@be-music/*) for chart parsing and writing, chart models, gameplay, skins and UI. Runtime-neutral chart and engine packages run in browsers and Node; browser UI and Node audio are separate adapters, so applications can choose the parts they need. - Lit Web Components for player screens, with typed React and Vue wrappers, and a framework-free SDK for writing skins.
Each package's README covers its usage, options and compatibility status.
@be-music/chart-source: pure IR of BMS/bmson intermediate representation (JSON compatible) dedicated to Be-Music internal processing@be-music/chart-model: chart semantics helper such as beat resolution, event order, long note resolution, etc.@be-music/shared-utils: Generic utilities reused across all packages, with narrow subpath exports for browser-safe core helpers and Node-facing helpers@be-music/chart-parser:.bms/.bme/.bml/.pms/.bmson/ JSON parser@be-music/chart-writer: Stringization from JSON to.bms/.bmson@be-music/chart-audio-renderer: Render the music score and output.wav/.aiff@be-music/player-engine: Shared rules and playback engine: one play loop (manual, auto, replay), judgment, scoring, gauges and clear lamps, best records, course runs and NONSTOP, the replay format, and the runtime contracts hosts plug into@be-music/player-node-audio: Host adapter for Node audio sessions, sample decoding, and PCM output@be-music/player-terminal: Terminal UI andbms-playerCLI frontend for autoplay, keyboard play, Music Select, BGA, and SEA builds, plus thebms-replayreplay re-simulator@be-music/player-scene-model: Runtime-neutral scene models — library entries, browsing, play options, play state and results, course views, system sounds — shared by every skin family@be-music/skin-lr2: Renderer-independent Lunatic Rave 2 skin parser, asset resolver, and theme loader@be-music/skin-beatoraja: Renderer-independent beatoraja JSON/Lua skin parser, normalizer, and theme loader@be-music/player-skin-sdk: Framework-free SDK for writing Be-Music Skins for the browser player@be-music/player-ui,@be-music/player-ui-react,@be-music/player-ui-vue: Reusable Lit Web Components, with typed React and Vue wrappers@be-music/player-browser: Browser PixiJS runtime: every select, decide, play and result screen as a scene session drawn by a built-in, LR2 or beatoraja family view, behind one host API, plus the audio service, the chart collection, skin loading and gameplay recording@be-music/app-play: Private Vite player that wires folder/ZIP drops, LR2/beatoraja themes, the Options dialog and browser playback together, as services behind one scene router@be-music/app-leaderboard: Private Leaderboard API on Cloudflare Workers: replay submission, verification by re-simulation, and rankings@be-music/app-dashboard: Private service dashboard and the Accounts service: the OIDC provider for sign-in, passkeys, and linked providers@be-music/worker-play: Player delivery Worker for static assets, OIDC sessions, settings, Leaderboard forwarding, and URL/media proxies@be-music/service-auth-client: Shared OIDC client, service sessions, and API token verification@be-music/service-web-theme: Shared service styles and Lit views, with separate client and server entries@be-music/chart-editor: CLI editor (import/edit/export)
The diagram shows the chart and player layers, including the Leaderboard's engine dependency. An arrow points from a package to a package that depends on it; a dependency already reached through another arrow is not drawn again (@be-music/shared-utils sits under almost every package). Architecture — Target architecture shows the same layers inside the packages.
flowchart TD
subgraph data["Data"]
utils["shared-utils"] --> json["chart-source"] --> chart["chart-model"] --> parser["chart-parser"]
parser --> chart-writer["chart-writer"]
parser --> renderer["chart-audio-renderer"]
end
subgraph rules["Rules and engine"]
player["player-engine"]
end
subgraph models["Models"]
scene["player-scene-model"]
sdk["player-skin-sdk"]
lr2["skin-lr2"]
beatoraja["skin-beatoraja"]
end
subgraph presentation["Reusable UI"]
ui["player-ui"]
wrappers["player-ui-react · player-ui-vue"]
end
subgraph hosts["Runtimes and hosts"]
editor["chart-editor"]
tui["player-terminal"]
nodeaudio["player-node-audio"]
lb["app-leaderboard"]
web["player-browser"]
app["app-play"]
end
chart-writer --> editor
parser --> player
renderer --> nodeaudio
player --> scene
player --> nodeaudio --> tui
player --> lb
scene --> sdk
scene --> lr2 --> beatoraja
sdk --> ui --> web
ui --> wrappers
sdk --> web
beatoraja --> web
web --> app
- Node.js
>= 26 - pnpm workspaces
pnpm installpnpm run clean
pnpm run build
pnpm run check
pnpm run testThe toolchain is Vite+ (vite-plus), configured from the root vite.config.ts. pnpm run build runs each workspace's build script through the Vite+ task runner (vp run -r --cache build) in dependency order, replaying unchanged packages from the task cache; library packages build with vp pack (tsdown) from the pack block in their vite.config.ts and emit the bundle and type definitions (.d.ts) together. pnpm run check runs formatting, linting, and type checking (Oxfmt, Oxlint, and tsgolint) across the whole workspace in one pass; pnpm run format / pnpm run lint / pnpm run typecheck run the individual steps. Each package's tsconfig.json is its type-check project, while tsconfig.build.json drives declaration output. Installing dependencies also registers a Vite+ pre-commit hook that runs vp check --fix on staged files. pnpm run test:e2e plays the course browser checklist in the web player end to end with Playwright (a few minutes, so pnpm run test leaves it out); see docs/player-web.md#browser-verification.
In this repository, Changesets manages the version and changelog for each package.
pnpm run changeset
pnpm run release:status
pnpm run release:version- Normal feature PR adds
.changeset/*.mdfor the changed package - When you want to release, run
pnpm run release:versionondeveland commit the generatedpackages/*/package.jsonandpackages/*/CHANGELOG.mdupdates together. - In that state, merging the release PR of
devel -> maincreates a separate GitHub Release for each public package whose version has increased (private packages such as the player and the Leaderboard get none). - The player SEA executable is built from
@be-music/player-terminaland attached to the@be-music/player-terminalrelease;@be-music/chart-audio-rendereralso ships a SEA zip - The private repository root
package.jsonstays at0.0.0; releasable versions are tracked inpackages/*/package.json. scripts/release/changelog.tswrites the changelog lines: each changeset as- <commit>: <summary>, and a package bumped only because its workspace dependencies moved gets one- Updated dependencies: ...line listing their new versions.
The tag is created in the format @be-music/package-name@x.y.z.
- Shared utilities
- Specification top
- Architecture and the architecture decisions
- BMS implementation specification
- bmson implementation specification
- Bemuse implementation specification
- Player implementation specification
- Terminal player implementation notes
- Browser player implementation notes
- Scene models (
@be-music/player-scene-model) - Replay (play history) specification
- Courses and dan certification
- Writing a Be-Music Skin
- LR2 skin implementation notes
- beatoraja skin implementation notes
- BMS/bmson intermediate representation (
@be-music/chart-source) implementation specification - Glossary
The root package.json runs each package's CLI and dev server. See the package READMEs for the options.
| Command | Package |
|---|---|
pnpm run parse |
@be-music/chart-parser |
pnpm run stringify |
@be-music/chart-writer |
pnpm run audio-render, audio-renderer:sea |
@be-music/chart-audio-renderer |
pnpm run editor |
@be-music/chart-editor |
pnpm run player, replay, player:sea |
@be-music/player-terminal |
pnpm run player:web |
@be-music/app-play |
Deploying the services is described in Services and environments and in each service package's README.
# All packages
pnpm run bench
# Single package (example: parser)
pnpm --filter @be-music/chart-parser run bench
# Aggregate multiple runs
pnpm run bench:aggregate -- --output tmp/bench/head.json tmp/bench/head-runs/*.json
# Compare two revisions and generate Markdown
pnpm run bench:compare -- --head tmp/bench/head.json --base tmp/bench/base.json --output tmp/bench/benchmark.md- snapshot output:
tmp/bench/exports*.json - compare output: arbitrary Markdown and summary JSON
- In GitHub Actions, post base/head comparison as PR comment in PR for
devel/main - GitHub Actions also performs the previous revision comparison when pushing to
devel/mainand posts a commit comment to the target commit. - Both PR and push benchmarks measure only the packages whose files changed, not the packages that depend on them. Documentation and
*.test.*/*.spec.*changes are ignored; changes to no benchmarked package skip measurement. Shared tooling, lockfiles and root configuration measure the full inventory. Each case retains 50 ms of measurement and 25 ms of warmup in CI. - CI measures base and head on the same runner, taking turns (head, base, head, base, …) for 3 runs each, so host-to-host noise and slow stretches hit both sides alike.
- Fast functions are called many times per timed sample (about 50 µs a sample), so cases that take nanoseconds are measured rather than lost in the timer's resolution. Times are reported per call.
- A case counts as a regression or an improvement only when its median changes past the threshold (8% in CI) and the runs of the two sides do not overlap; a change past the threshold with overlapping runs is listed as "within noise".
- Cases marked
ioBound(they touch the filesystem) are listed apart and never judged, since their time follows the OS cache and the disk. - The comment leads with the verdict, links the base and head commits, lists the changed packages' cases and new or removed exports, and folds noise, exclusions and run details into collapsible sections.