- Rust 49%
- TypeScript 22.3%
- Vue 22%
- CSS 5.7%
- Makefile 0.5%
- Other 0.5%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
start/stop/restart/logs/status/clean targets over docker compose. Host ports are overridable via BACKEND_PORT and FRONTEND_PORT. |
||
| backend | ||
| frontend | ||
| songs | ||
| tools/midi-import | ||
| .dockerignore | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| docker-compose.yml | ||
| Makefile | ||
| README.md | ||
| SPEC.md | ||
| TASKS.md | ||
Beat Assembly — MVP
Beat Assembly is a cooperative multiplayer rhythm game.
Each player controls one part of a band:
- 🥁 Drums
- 🎸 Bass
- 🎹 Chords
- 🎵 Melody
The important difference from a conventional rhythm game is that the music is actually generated by the players' actions.
If a player hits a note in time, that note is generated at the written pitch.
If they hit late or early (outside Good, inside the match window), the note still sounds, but the pitch drifts.
If they never press, the note is absent.
The result is a shared musical performance that emerges from the players' collective accuracy.
MVP
The MVP supports:
- 2–4 players
- Host-created games
- Shareable lobby URLs
- Player-chosen roles (host can override)
- Four musical roles
- Desktop-only keyboard rhythm gameplay
- MIDI-derived song charts
- Browser-generated audio
- Synchronized multiplayer timing
- Shared musical events
- Shared Groove indicator
- Results screen
The MVP contains one approximately 2-minute song.
Documentation
The main specification is:
SPEC.md
The initial development plan is:
TASKS.md
Read SPEC.md before implementing features.
Architecture
Rust / Axum Game Server
|
WebSocket
|
+---------------+---------------+
| | |
Vue App Vue App Vue App
| | |
Web Audio Web Audio Web Audio
| | |
Speaker Speaker Speaker
The server manages:
- Lobbies
- Players
- Roles (picking a part is ready)
- Game state
- Authoritative timing
- Event validation
- Musical event broadcasts
The browser manages:
- Rendering
- Keyboard input
- Local timing
- Audio generation
The server does not generate or stream audio.
Technology Stack
Frontend
- Vue 3
- TypeScript
- Vite
- Web Audio API
- Browser WebSocket API
Backend
- Rust
- Tokio
- Axum
- WebSockets
- Serde
- serde_json
Song Authoring
- MIDI
- MIDI importer/converter
- Beat Assembly JSON runtime format
Project Structure
beat-assembly/
├── frontend/
├── backend/
├── tools/
│ └── midi-import/
├── songs/
│ ├── demo.mid
│ └── demo.json
├── docker-compose.yml
├── README.md
├── SPEC.md
└── TASKS.md
Getting Started
The preferred development environment is Docker Compose, driven by the Makefile.
make start # build images and start in the background
make logs # follow output
make stop # stop and remove containers
make help lists every target. Host ports default to 3000/5173; override with make start BACKEND_PORT=3001.
Then open:
http://localhost:5173
If Docker is not used:
cargo run -p beat-assembly-server
cd frontend && npm install && npm run dev
The backend listens on port 3000 by default. If that port is taken:
PORT=3001 cargo run -p beat-assembly-server
cd frontend && VITE_API_PROXY=http://localhost:3001 npm run dev
Open http://localhost:5173. Create Game hosts a lobby. Practice is solo, same chart and keys.
Play a lobby
- Create a game and copy the link.
- Join from another browser or private window. Set a name in the header.
- Each player picks a unique part. Click a selected part again to free it. The host can reassign anyone.
- Start is on when there are at least two players and everyone has a part.
- Play D F J K. Hits generate notes. Off-timing hits drift. Notes you never press stay silent.
This window can be a Bot with an accuracy slider so one person can fill extra parts.
Kick is lobby-only. If a guest closes the tab during a song, the rest of the band keeps playing. If the host leaves, the session ends.
Tests
cargo test
cd frontend && npm test
Development Strategy
The first goal is not to build the entire multiplayer game.
The first goal is to prove the central gameplay mechanic.
Build this vertical slice first:
MIDI chart
↓
one instrument
↓
rhythm events
↓
keyboard input
↓
correct input
↓
generated note
and:
MIDI chart
↓
rhythm event
↓
never pressed
↓
no note
Only after this works should multiplayer infrastructure be added.
Then expand:
1 instrument
↓
4 instruments
↓
multiplayer clock
↓
networked musical events
↓
lobby
↓
complete game
Important Musical Constraint
There must not be a complete prerecorded backing track playing underneath the game.
The game should instead work like this:
MIDI
|
v
Expected musical events
|
v
Player performance
/ | \
HIT (on time) HIT (off) NEVER PRESSED
| | |
v v v
Written pitch Drifted pitch Silence
For example, if the MIDI chart contains:
C2 C2 G2 C2
and the player performs:
HIT (never) HIT OFF-late
the actual audio should be:
C2 -- G2 (sharp C)
The missing C2 must genuinely be absent from the audio. The off-late C2 must still sound, out of tune.
Multiplayer Audio
The server does not transmit audio.
Instead:
Player A hits
|
| local audio immediately
v
Player A's speakers
|
| hit (notes, timing_error_ms)
v
Server
|
| note_played (same event_id)
+----------+----------+
| | |
v v v
Client A Client B Client C
(skip dup) Web Audio Web Audio
The hitter hears the note without waiting for the round trip. Other clients generate the same event locally from the broadcast. Deduplicate by event_id.
Timing
The server owns the authoritative musical timeline.
Clients synchronize against the server.
Never use WebSocket packet arrival time as musical time.
For a song at 120 BPM:
1 beat = 0.5 seconds
If an event occurs at beat 64:
event_time = start_time + 64 * 0.5
The Web Audio API should schedule audio against the local audio clock using the synchronized musical time.
Testing the Core Mechanic
One of the most important MVP tests is:
Good performance
Play the same song with approximately 95% successful inputs.
Poor performance
Play the same song with approximately 50% successful inputs.
The two performances should sound substantially different.
The good performance should sound close to the intended composition.
The poor performance should contain obvious gaps (notes never pressed) and sour notes (off-timing hits).
If this test does not produce an obvious difference, the core game mechanic has not been implemented correctly.
In a lobby, set Bot accuracy to ~95% in one window and ~50% in another.
MIDI import
Charts come from Standard MIDI files. The runtime format is Beat Assembly JSON in songs/.
cargo run -p midi-import -- write-demo songs/demo.mid
cargo run -p midi-import -- import songs/demo.mid songs/demo.json --id demo_song --title "Demo Song"
Pass --map path/to/mapping.json to override which MIDI tracks become drums, bass, chords, and melody.
The server embeds songs/demo.json at compile time. Re-import, then rebuild the backend for the live game to pick up chart changes.
Protocol
The WebSocket protocol is defined in SPEC.md (section 12). Frontend and backend types live in frontend/src/protocol.ts and backend/src/protocol.rs.
Hits and misses are validated on the server and broadcast as note_played / note_missed. Groove is broadcast as groove. The song ends with game_end, then the same lobby reopens.
Future Features
Potential future features include:
- Multiple songs
- Multiple difficulties
- Better MIDI preprocessing
- More instruments
- Better synthesizers/samples
- Groove-driven arrangement changes
- Wrong-lane / extra notes beyond timing drift
- More sophisticated musical consequences
- Additional visual feedback
These are not MVP requirements.
Do not implement them early unless the architecture requires them.
Guiding Principle
Beat Assembly is a rhythm game where the players collectively create the song.
The game should feel like playing in a band, not like following a prerecorded soundtrack.