No description
  • Rust 49%
  • TypeScript 22.3%
  • Vue 22%
  • CSS 5.7%
  • Makefile 0.5%
  • Other 0.5%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Antoine Letouzey dfb32e3a4f Add Makefile for building and running the app in Docker
start/stop/restart/logs/status/clean targets over docker compose.
Host ports are overridable via BACKEND_PORT and FRONTEND_PORT.
2026-09-03 16:14:31 +01:00
backend Derive instrument timbre and lanes from MIDI program per track 2026-09-03 16:07:42 +01:00
frontend Derive instrument timbre and lanes from MIDI program per track 2026-09-03 16:07:42 +01:00
songs Derive instrument timbre and lanes from MIDI program per track 2026-09-03 16:07:42 +01:00
tools/midi-import Derive instrument timbre and lanes from MIDI program per track 2026-09-03 16:07:42 +01:00
.dockerignore Handle dropped connections without mid-song kicks, and document the real lobby and Docker setup. 2026-09-03 12:15:47 +01:00
.gitignore Bootstrap the Vue frontend and Axum health server so both apps build. 2026-09-02 13:57:49 +01:00
Cargo.lock Add a WebSocket lobby so players can join, pick roles, and start together. 2026-09-02 16:22:31 +01:00
Cargo.toml Add a MIDI importer so charts come from Standard MIDI files instead of hand-authored events. 2026-09-02 14:12:18 +01:00
docker-compose.yml Add Makefile for building and running the app in Docker 2026-09-03 16:14:31 +01:00
Makefile Add Makefile for building and running the app in Docker 2026-09-03 16:14:31 +01:00
README.md Add Makefile for building and running the app in Docker 2026-09-03 16:14:31 +01:00
SPEC.md Let the host kick players and end the session on leave so closed tabs do not linger. 2026-09-03 07:43:16 +01:00
TASKS.md Handle dropped connections without mid-song kicks, and document the real lobby and Docker setup. 2026-09-03 12:15:47 +01:00

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

  1. Create a game and copy the link.
  2. Join from another browser or private window. Set a name in the header.
  3. Each player picks a unique part. Click a selected part again to free it. The host can reassign anyone.
  4. Start is on when there are at least two players and everyone has a part.
  5. 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.