Beat Saber automapper: analyses a song and generates playable maps (web UI)
2.3K
Source, issues and docs: jncchds/abeat
Automatic Beat Saber map generator: audio file in, playable map (all difficulties, walls, bombs, lights) out. Songs come from an uploaded file or a YouTube / YouTube Music link (only use audio you have the rights to), or from an existing Beat Saver map (zip or BeatSaver link), which is kept as a human version to compare with. A web UI lets you listen, inspect and tune the generator, keeps every generation as a version and compares any two (or a generation against an imported human map) lane by lane; the same server runs locally or in Docker.
audio ─▶ analysis worker (Python) ─▶ analysis.json + song.egg + cover.jpg
beats/downbeats, sections, onsets per layer, energy
│
▼
Abeat.Core (C#): rhythm selection ─▶ flow planner (beam search) ─▶ walls + lights
│ │
▼ ▼
Abeat.Cli / Abeat.Web Info.dat (v2.1) + *.dat (v3.3) + zip
Manual: everything ABeat does, every setting, the CLI and the Docker setup.
Download the app for your system from Releases: a portable
.exe for Windows (x64), an .AppImage for Linux (x86_64) and a .dmg for macOS (Apple Silicon). It is a
normal window with nothing to install; it keeps its folders next to itself (data/, models/,
runtime/) and installs the analysis runtime on first start, as the Docker image does. Details in the
manual.
docker compose up -d # then open http://localhost:8080
One small image (~420 MB) for every machine. On first start it installs the analysis runtime with the
PyTorch build for your hardware (CPU, NVIDIA CUDA, AMD ROCm or Intel XPU; detected, or ABEAT_ACCEL)
into bind-mounted folders: ./docker-data/data (songs, maps: back this up), ./docker-data/models
(downloaded models) and ./docker-data/runtime (Python environment, ArcViewer). GPUs:
docker compose -f docker-compose.yml -f docker-compose.nvidia.yml up -d (or .rocm.yml, .intel.yml).
Optional features: ABEAT_EXTRAS=lyrics,roformer. Details in the manual.
Prebuilt images (amd64, arm64) are on Docker Hub as jncchds/abeat: set
image: jncchds/abeat:latest in docker-compose.yml and run docker compose up -d --no-build to skip building.
Requirements: .NET 10 SDK, Node.js 22 (or 20.19+) and uv (it installs its own Python).
cd analysis && uv sync --extra ml --extra cpu && cd .. # torch flavor: cpu, cuda, cuda12, rocm or xpu; add --extra lyrics / roformer
(cd src/abeat-ui && npm install && npm run build) # builds the UI into src/Abeat.Web/wwwroot
dotnet run --project src/Abeat.Web # web UI on http://localhost:5080
# or the CLI:
dotnet run --project src/Abeat.Cli -- generate "Artist - Song.mp3" -d all
dotnet run --project src/Abeat.Cli -- generate "https://music.youtube.com/watch?v=..." -d all
dotnet run --project src/Abeat.Cli -- check path/to/any/map.zip # flow report for any map
dotnet run --project src/Abeat.Cli -- movement work/beatsaver # hand movement per difficulty
Web data defaults to ~/.local/share/abeat (set ABEAT_DATA to change). Songs are only analysed on upload;
set ABEAT_AUTO_GENERATE=true to also generate a map right after each analysis.
ABEAT_MARK_AI=true adds " (AI)" to the level author of generated maps.
scripts/fetch-arcviewer.sh downloads ArcViewer (GPL-3.0, ~80 MB)
so the web app can open maps in it from the same server (the container fetches it on first start). Without it the
ArcViewer button uses the public site.
Folders: the Songs page shows folders first, then songs that are in no folder; folders nest two levels. Drag a song onto a folder or a breadcrumb, or pick its folder on the song page. Deleting a folder keeps its songs. The filter searches all folders and lists matching folders and songs.
Playlists: add versions from a song page ("+ playlist"), then download the playlist zip and import
it in BSManager (Maps → Import; the .bplist inside goes to Playlists → Import). BSManager's one-click
links only work for maps published on BeatSaver.
Analysis (analysis/, Python): beat tracking with beat_this
(or librosa), a constant-BPM grid fit (or, for drifting live recordings, a tempo map written as v3
BPM events), phase refinement against a sharp attack envelope (spectral-flux
envelopes lag ~50 ms, which players feel), downbeats, energy curve, novelty-based sections with
labels for repeated parts, and onsets per layer (Demucs
stems, the default, or frequency bands with --no-stems). The audio is padded so grid beat 0 is at t = 0 and written as song.egg.
Vocal onsets (per song, re-analysis reuses the stems): spectral flux; sung notes (default),
flux onsets kept only where a pitched voice follows (CREPE);
or lyric syllables, the lyrics forced-aligned to the vocal stem (MMS aligner) and split into
syllables at their vowels. Paste the lyrics on the song page (repeats written out), or let Whisper
transcribe them (uv sync --extra ml --extra lyrics); the transcription can be loaded into the
lyrics box to correct it.
Drums, notes and stems: drum-stem hits are labelled kick / snare / hat, so hats count less when
choosing notes and the drummer plays the lights. --pitched notes transcribes the other and bass
stems with basic-pitch (note lengths
drive arcs). --roformer takes the vocal stem from BS-RoFormer
(python-audio-separator, extra roformer):
cleaner vocals, but slow without a GPU. With lyrics, sections whose words repeat (a returning chorus)
share a label even when the arrangement changes.
Lead melody (with stems): one melody line through the song, whether a singer, a violin or a synth
carries it. Stems can't tell (a violin or synth lead often lands in the "vocal" stem), so
CREPE pitch on the vocal and other stems decides which one
leads, moment by moment, or that nothing does (drum breaks, pads); basic-pitch gives that stem's notes.
The line becomes the melody layer: its notes are favoured when choosing notes, and they drive arcs,
angle leans and the melody lights. The timeline shows it tinted by the stem carrying it.
Rhythm selection (RhythmSelector): onsets are grouped into 1/12-beat grid slots (sixteenths, and
triplets only for songs with a triplet feel), scored by layer weight × strength × metric position ×
energy, and picked bar by bar to hit a notes-per-second target that follows section energy. Notes are
not snapped: each lands at the detected time of its sound (corrected for the stem's detection lag). Strong,
isolated hits become doubles, spread over the sections (calm parts get nearly as many as loud ones, as in curated maps). Before a drop the map pauses for a beat or two and lands on a double.
Taps: tap along with the song (Tap beats, two runs) to mark where you want notes; each run's latency is
removed, taps snap onto the song's onsets, and tapped moments get notes first (tap priority), with a
double where both runs tapped. Taps appended later get their own latency. A song with a human map can
turn one of its difficulties into taps (Use as taps, exact timing) to remake that map or build easier
levels on its rhythm. Fit to taps (Speed and density settings, only when pressed) loosens
Expert+'s limits so every tap gets its note and moves the other difficulties by the same ratios. Vocal, bass and other stems use spectral-flux onsets (one per sung
syllable or note) rather than energy rises, which fired on consonants and breaths.
Flow planning (FlowPlanner + SwingCostModel): beam search over both sabers' states (position,
last swing direction, parity). Each candidate cut is scored for:
Expression (Expression): the melody notes held longest over the gap to the same hand's next
note (0.3-2 s later) get an arc to it, both hands at once on a double, on about as many gaps as curated
mappers use (6-9 % by difficulty). A long melody or vocal note the hand has nothing after (it sounds
on and the hand cuts again only later) gets a held arc instead: it follows the sound to its end and stops
there in a dot without a note, next to the head or one cell towards the hand's next note (a few per map,
about 8 % of the arcs, as in curated maps); notes in a rising or falling melody line get a small cut-angle offset that leans with it;
notes followed by a roll, flam or stutter too fast for single notes become chains (Hard and up, at
most one per 30-120 s, as rare as in curated maps).
All of them leave hands, cells and directions alone, so flow is unaffected. Each can be switched off in the
settings.
Walls (WallGenerator): dodge walls in note-free gaps, side walls in calm sections (only while notes are played) and short
rhythm walls in the outer lanes (full height where free, long enough to make the controller buzz) on the strongest kicks and snares of loud sections, as
most curated maps have; full-width crouch walls before drops are optional (off by default). No wall
ever holds a note or bomb.
Pattern memory: when a part of the song comes back, the map is planned a second time with a bonus
for playing it the way its first occurrence was played, so a returning chorus brings its patterns back
(repetition weight).
Environments and lights: every environment of moddable Beat Saber, from The First to The Prodigy.
--environment auto (the default) picks one that suits the song: a four-on-the-floor dance track lands
in EDM-style stages, a band with a backbeat in the rock ones, dark songs in darker places. Classic
environments get classic lights (section palettes, note flashes, drum-driven lasers and rings, plus their
extra lasers and moving set pieces); Weave and later get a v3 group lightshow laid out from how curated
maps use that environment's light groups, with lights that turn and move: they drift in quiet parts,
swing on the bars when it gets loud, fold together over a build-up and burst open in white on the drop,
and one group follows the melody; runways and floors that mappers keep lit glow between flashes. Colours come from the cover art (sabers, walls, and lights in their own hues; the boost lights that flash
up on drops wear the note colours); after the last note the lights dim with the outro instead of cutting out;
Detect copies them into the settings, where any of them can be changed by hand. Click the cover on a
song page to replace it with your own image. --modes onesaber,90,360 adds One
Saber (planned for one saber over the whole grid) and 90°/360° (the Standard notes with lane rotations).
All weights are in FlowWeights and editable in the UI or a settings file (abeat settings).
Evaluation (FlowAnalyzer): the same physical cost model scores any map, including human-made
ones (abeat check), so the weights can be calibrated against maps people like.
Hand movement (MovementAnalyzer, abeat movement): for every pair of consecutive swings of one
saber it measures how far the swing turns away from a clean back-and-forth and how far the saber tip
travels between them, and turns that into strain, the effective swings per second the move asks for
(turns past 45° and travel past one cell count extra). A map's 90th-percentile strain tells which
difficulty its movement plays like, calibrated on curated maps; the report cards show it as
"plays like …" next to the flow score. The generator matches the human mix of turns, travel and strain
for each difficulty, so Expert+ maps no longer move like a human Hard.
abeat fetch-maps downloads top-rated curated BeatSaver maps (no mods); abeat bench re-maps each
map's own song and compares. Current results on 18 songs / 62 difficulties with Demucs stems (2026-10-04):
| ABeat | human | |
|---|---|---|
| BPM | exact on 17/18 (1 octave) | |
| note timing vs human (F1 at ±50 ms) | 0.69 (Expert 0.71, Expert+ 0.76) | |
| timing offset | 1–4 ms | |
| notes per second | 3.1 | 3.5 |
| flow score | 94 | 85 |
| direction / position distribution distance | 0.18 / 0.22 | 0 |
| hand strain p90 (swings/s per hand; Easy … Expert+) | 2.7 · 3.1 · 3.5 · 4.0 · 5.8 | 2.8 · 3.2 · 3.9 · 5.1 · 6.5 |
| mean turn / tip travel between swings | 17° / 1.44 cells | 21° / 1.55 cells |
The environment layouts (scripts/environment_prior.py: which light groups, event types, axes,
ranges, poses, mirror partners, how often each group is switched on and how the basic light types are used next to the groups that curated maps use in each environment, read from BeatSaver without downloading the audio) are
embedded as environment-prior.json; rerun the script when Beat Saber adds environments.
The style prior (scripts/style_prior.py: cell and direction shares, figure and double-shape
vocabularies per difficulty) is learned from the curators' playlist
Curators' Favorites of 2025 plus map 37114:
dotnet run --project src/Abeat.Cli -- fetch-maps --playlist 1116456 -o work/beatsaver-curated
dotnet run --project src/Abeat.Cli -- fetch-maps --map 37114 -o work/beatsaver-curated
python3 scripts/style_prior.py work/beatsaver-curated
| Path | What |
|---|---|
analysis/ | Python worker (`abeat-analyze analyze |
src/Abeat.Core | Map model, v2/v3/v4 reader, v3 writer (notes, arcs, chains, BPM/rotation events, group lights), generator, analyzer, packager |
src/Abeat.Cli | `abeat generate |
src/Abeat.Web | ASP.NET Core API, serves the built UI from wwwroot/ |
src/abeat-ui | React 19 + TypeScript + Vite UI (ABook layout, Beat Saber palette) |
desktop/ | Electron app around the server (npm run dist builds it for the current OS) |
tests/ | xUnit tests for the core |
FlowWeights and the flow score against curated BeatSaver maps (abeat check).ABeat is made by CHDS, designed and built together with Claude (Anthropic's AI assistant, via Claude Code), which co-wrote the analysis worker, generator, web UI and Docker setup.
Content type
Image
Digest
sha256:d7262a3b9…
Size
110.1 MB
Last updated
1 day ago
docker pull jncchds/abeat