Simple, low-latency Taiko rhythm game compatible with OpenTaiko song libraries
  • Rust 97.4%
  • Shell 2.5%
  • Dockerfile 0.1%
Find a file
joe 42bd3ebd98
Correct note sizes to include the outline, and cite YataiDON
The first measurement took only the coloured face of each note and came
out 10% small, because our notes draw their black outline inside the
radius. With the outline a small note is 0.44 of the lane's height, and
YataiDON's Nijiiro skin agrees: 109 px and 163 px at 1080p, so a big
note is 1.5 times a small one.

reference-constants.md now lists the official demo and YataiDON as
sources for the playfield's proportions, GPL-3.0 and so used for
numbers only, and records the measurements in a new section.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 22:20:56 +01:00
crates Correct note sizes to include the outline, and cite YataiDON 2026-10-04 22:20:56 +01:00
docs Correct note sizes to include the outline, and cite YataiDON 2026-10-04 22:20:56 +01:00
scripts Say which machine the docs mean instead of "this machine" 2026-10-04 19:07:19 +01:00
test-charts Add a balloon-only test chart 2026-08-22 21:32:25 +01:00
.gitignore Ignore Claude Code's local settings 2026-08-16 22:25:25 +01:00
Cargo.lock Release 0.9.4 2026-09-30 20:22:26 +01:00
Cargo.toml Release 0.9.4 2026-09-30 20:22:26 +01:00
CHANGELOG.md Correct note sizes to include the outline, and cite YataiDON 2026-10-04 22:20:56 +01:00
implementation-plan.md Add a candidate to step 27: menu music from the library, with jacket toasts 2026-10-04 19:12:09 +01:00
LICENSE Step 0: groundwork — format research, toolchain proof, timing spike 2026-08-15 05:51:44 +01:00
plan.md Bring the docs up to date with what 0.9.4 is 2026-09-30 20:22:26 +01:00
README.md Correct note sizes to include the outline, and cite YataiDON 2026-10-04 22:20:56 +01:00

Simple Taiko

A simple, stable, low-latency Taiko rhythm game that plays songs from an existing OpenTaiko library, in place and unmodified.

Status: v0.9.4 — playable, and it looks the part. Browse a library, pick a chart, play it, see your score. Scoring matches OpenTaiko exactly, scores persist, and it plays on keyboard or pad with latency calibration. Every screen sits on a drifting pattern, the notes and gauge wear colours measured out of OpenTaiko's own skin, and the difficulty picker is a row of cards. Controls are rebindable from the Bindings screen, on whatever you play on. Song select opens on what you last played, screens change with a wipe, and anything that goes wrong is said on screen rather than in a terminal. Not yet done: the modes listed in docs/v1-scope.md.

See CHANGELOG.md for what each release contains, and implementation-plan.md for where it is going.

Docs

plan.md The idea and the feature list
implementation-plan.md Phases, gates, decisions
docs/format-notes.md TJA / box.def / library layout, measured from a real 506-chart corpus; skin and hit-sound investigation
docs/shapeshifter-jist.md One chart's gimmicks explained by the person who wrote them — the source for how branching really behaves
docs/timing-notes.md Toolchain, audio clock measurements, latency budget
docs/reference-constants.md Judge windows, score formula, OFFSET semantics, read from OpenTaiko source; playfield proportions from the official game and YataiDON
docs/v1-scope.md What v1 does and, more importantly, does not do
CHANGELOG.md What changed in each release

The format itself is documented outside this repository, and better than we could: IepIweidieng's tja.md gives each command's scope and effect time and records where the simulators disagree. It is the first thing to read before changing the parser.

Layout

crates/tja/       TJA parsing            — pure, no raylib
crates/library/   song tree scanning     — pure, no raylib
crates/timing/    the master clock       — pure, no raylib
crates/play/      judgement and scoring  — pure, no raylib
crates/save/      scores on disk         — pure, no raylib
crates/taiko/     the game               — the only crate that may touch graphics/audio/input

The split is deliberate: it makes the compiler enforce that the parser stays headless and testable against the whole chart corpus without opening a window.

Getting songs

Simple Taiko needs two things: charts, and a pair of drum samples. Neither is bundled — they belong to other people — but both are a download away, and you do not need to install OpenTaiko: the samples are two files in its repository.

Songs

The OpenTaiko community soundtrack is a plain git repository of chart folders, which is exactly the layout the scanner expects:

git clone --depth 1 https://github.com/OpenTaiko/OpenTaiko-Soundtrack Songs

That is around 500 songs. --depth 1 matters: the full history is roughly 3.4 GB on top of a 1.2 GB working tree, and none of it is any use for playing.

Hit sounds

The drum samples are in the OpenTaiko repository itself, under OpenTaiko/Global/HitSounds/<set>/, and in the release archive under publish/Global/HitSounds/. Two files out of either is the whole job:

mkdir -p Global/HitSounds/Taiko
base=https://raw.githubusercontent.com/0auBSQ/OpenTaiko/main/OpenTaiko/Global/HitSounds/Taiko
curl -L -o Global/HitSounds/Taiko/dong.wav "$base/dong.wav"
curl -L -o Global/HitSounds/Taiko/ka.wav   "$base/ka.wav"

Taiko is one of nine sets — Old Taiko, Konga, Electro, Break, Clap, Gorgeous Taiko, Bol and '08 Dual are the rest, of which Bol and Old Taiko are .ogg rather than .wav. Or supply your own: any directory holding dong.wav and ka.wav works — .ogg is read too — and the game runs, silently, without them.

The layout

Put both next to the binary and it runs with no arguments:

simple-taiko/
├── taiko
├── Songs/
│   ├── 01 OpenTaiko Chapter I/
│   │   ├── box.def                 category name, colours, description
│   │   ├── 001 - CITYSCAPE/
│   │   │   ├── CITYSCAPE.tja       the chart
│   │   │   ├── CITYSCAPE.ogg       the audio, named by the chart's WAVE: header
│   │   │   ├── cityscape.png       jacket, named by its PREIMAGE: header
│   │   │   └── uniqueID.json       stable song id — what your scores are keyed on
│   │   └── 002 - .../
│   └── 02 OpenTaiko Chapter II/
└── Global/
    └── HitSounds/
        └── Taiko/
            ├── dong.wav
            └── ka.wav

Categories may nest as deeply as you like, and a flat folder of song folders works too.

If OpenTaiko is already installed you need not lay any of this out: its publish/ directory is this layout, with Songs/ and Global/HitSounds/ both in place. Point at it with --base /path/to/OpenTaiko/publish, as below, or drop the taiko binary into publish/ and run it with no arguments — a binary's own directory is one of the places it looks for content. Either way you get the hit sounds too, which --songs on its own does not. Dropping the binary in is the only thing that puts a file in OpenTaiko's folder; the game itself still writes nothing there.

Or point at what you already have

If OpenTaiko is already installed, use it in place — nothing is copied and nothing is written to it:

taiko --base ~/Applications/OpenTaiko/publish

Or mix sources, which is the point of --songs being repeatable:

taiko --songs ~/OpenTaiko-Soundtrack --songs ~/my-charts \
      --hitsounds ~/Applications/OpenTaiko/publish/Global/HitSounds/Taiko

Skins are not used. OpenTaiko's skin repository is another 1.4 GB and Simple Taiko draws its own playfield, so there is no reason to clone it — see docs/format-notes.md for why that format was turned down.

Play

cargo run --bin taiko -- --base ~/Applications/OpenTaiko/publish
cargo run --bin taiko -- --songs "<songs folder>"        # one or more libraries
cargo run --bin taiko -- "<song folder>" oni             # jump straight into one chart

The main menu offers Play, Settings and Quit. Settings holds calibration, audio, video, song select, bindings and an input test.

In song select: Up/Down move, Enter open, Backspace return, C calibrate, Esc return. Playing: F/J hit the drum centre (don), D/K the rim (ka), Esc returns to the list.

The drum works the menus too, as it does on the cabinet: the rim moves — D up, K down — and either don key, F or J, chooses. That matters most to anyone playing on a real drum, where those four inputs are the whole interface. It is why D no longer moves the cursor right: one key cannot both move and adjust on a screen that reads them together.

And the drum answers: the rim clicks as the cursor moves, and the centre thumps to choose something or to come back out. Since those are the same keys, a player on a real drum hears the drum they are actually hitting rather than a menu beep imitating one — which also means the hit-sound volume in Settings auditions itself as you change it.

On a controller the arcade layout applies: every face button is the drum — d-pad for the left hand, A/B/X/Y for the right — and the shoulders are the rim, L1/L2 left and R1/R2 right. The pad drives the menus too.

A library can be nested in categories or a flat folder of song folders; both work. Above them, once you have finished a song, is Recently Played: the last 20 songs you finished, newest first. It is built from your scores rather than from a folder, and song select opens on it. Settings → Song select changes how many it holds, or turns it off.

When something goes wrong, the game says so on screen, at the top right — a song that will not start and why, a score or setting that would not save. The same lines go to stderr. With no audio device the game still starts, silently, and says "No audio device found"; songs cannot be played until it is restarted with one.

Settings

~/.config/taiko/config.toml is written on first run, with comments. It holds key and gamepad bindings, the latency offset, colours, volumes, video settings and how many songs Recently Played holds. --list-keys prints every name it accepts.

On a variable-refresh display, a stutter that appears fullscreen and not in a window is worth suspecting the display for rather than the game. Hyprland's misc:vrr = 2 applies VRR to fullscreen windows only, which produces exactly that shape. Turning VRR off is the known cure; V-Sync is not one, and was measured not to be. See docs/timing-notes.md §6, including why the game's own frame timings cannot settle this.

Calibrate before playing seriously: press C in song select and tap along with the metronome. Every audio setup is late by some constant amount, and that is what the offset removes.

Settings → Bindings changes any of them without a keyboard or a text editor: Select on a row waits for an input and adds it to that action, Left drops the last one, and a Reset to defaults row puts every action back. Adding rather than replacing is deliberate — the defaults put five inputs on each don, and binding a sixth should not cost you the other five. The five actions that move around the menus keep their last input whatever you do, so there is no way to strand yourself on a handheld with no config file in reach.

Every control is an action with a list of inputs bound to it, under [input.bindings]. Actions are don_left, don_right, ka_left, ka_right, menu_up, menu_down, menu_page_up, menu_page_down, confirm, back and debug_hud. Inputs are written F, pad:RIGHT_FACE_DOWN, pad2:MIDDLE for one specific device, or axis:LEFT_Y+. Anything you omit keeps its default. --save-config rewrites the file with every binding spelled out, and --list-keys prints everything accepted.

input.gamepad defaults to "all", which listens to every device the system offers. That is deliberate: a controller shows up as several devices — an 8BitDo Ultimate 2C presents its keyboard interface first and the actual joystick second — and the indices shift as things are plugged in and out. Set it to "none", an index, or part of a device name if you need to pin it.

If input is not doing what you expect, run --test-input: it lists every device being listened to and names each key and button as it arrives.

Fonts

Japanese titles need a font that has Japanese in it, and no such font is shipped: they are several megabytes with their own licences, and every desktop already has one. On startup the game asks fontconfig what is installed and picks two — the sans the rest of your desktop uses for the alphabet, and whichever installed font covers the most of your song titles for everything else. Each is checked after loading, so a font that turns out not to have the characters it advertises is dropped rather than drawn as boxes. The startup line names what was chosen:

font       NotoSans-Regular.ttf + HanaMinA.ttf (461 glyphs)

To choose yourself, set path under [font] in the config. It must be a single font file — .ttf or .otf, not a .ttc collection, which cannot be read. Anything it lacks still falls back to the automatic choice.

Wayland

Linux builds include GLFW's Wayland backend alongside the X11 one. One binary serves both: GLFW picks Wayland when WAYLAND_DISPLAY is set and X11 otherwise, and the startup log says which it chose (PLATFORM: DESKTOP (GLFW - Wayland) or (GLFW - X11)). Wayland's libraries are loaded by name at runtime, like X11's, so a machine without them still runs the binary over X11.

cargo build --no-default-features gives an X11-only build, which still runs under XWayland.

One known cost: GLFW's Wayland backend cannot report window position, so raylib cannot tell which monitor the window is on and assumes the first — for fullscreen, and for the refresh rate the frame cap follows. Set monitor under [video] in the config to the name the startup monitors line prints, and fullscreen goes there. Or run over XWayland instead by starting the game with WAYLAND_DISPLAY unset: env -u WAYLAND_DISPLAY ./taiko. See implementation-plan.md step 16.

Steam Deck

scripts/build-deck.sh produces a binary that runs there. Built on a current distribution it will not: Arch's glibc gives the binary a requirement of 2.43 and the Deck has 2.41, so it refuses to start with a message about a version not found. The script builds inside Valve's Steam Runtime "sniper" SDK instead, which targets glibc 2.31, and refuses to finish if the result ever asks for more than the Deck provides.

Confirmed running on a Deck as of 0.5.0. It needs Docker, and downloads about 1.4 GB of SDK the first time. docs/steam-deck-build.md covers how the container is put together and how to check a binary yourself.

scripts/build-deck.sh              # build, and report the glibc it needs
scripts/build-deck.sh --package    # tarball in dist/, ready to copy across

Steam is not required to run the result. Old glibc is a link-time floor rather than a runtime dependency, and X11, Wayland and GL are loaded by name rather than linked, so the binary needs nothing on the Deck but libc, libm and libgcc_s. Copy it across with a song library and run it.

Running natively on Wayland in Gaming Mode. Gamescope hides its Wayland socket from the games it runs unless it was started with --expose-wayland, which SteamOS does not do. So by default the game runs through gamescope's XWayland. To run it as a native Wayland client, add the game to Steam as a non-Steam game and set its launch options to:

WAYLAND_DISPLAY=gamescope-0 %command%

Gamescope always serves xdg-shell on that socket, so the game connects and the startup log says (GLFW - Wayland). This is not a setup Valve documents or supports. If the game shows no window or fails to start, remove the launch option. Desktop Mode is plain X11 unless the Plasma Wayland session is chosen, and there the game picks Wayland on its own.

Windows

cargo build --release --target x86_64-pc-windows-gnu produces taiko.exe, and scripts/release.sh builds and attaches it unless told --no-windows. Nothing extra ships beside it: every DLL it imports is a Windows system one. scripts/make-pack.sh --windows puts one in a folder with songs and hit sounds, for carrying to a Windows machine.

Two things differ from Linux and both are handled. Config and scores go to %APPDATA% and %LOCALAPPDATA% rather than ~/.config and ~/.local/share — XDG_CONFIG_HOME and XDG_DATA_HOME still win if set, which is what makes running it under Wine behave. And fonts are found in C:\Windows\Fonts rather than through fontconfig, including the .ttc collections that Windows keeps its Japanese faces in; those are unpacked a face at a time by sfnt.rs, because a rasteriser handed a collection whole quietly renders nothing.

Confirmed booting and reaching charts on Windows as of 0.9.2, on someone else's machine. Everything up to starting a song is therefore known to work; what nobody has checked yet is how it plays — audio latency in particular, which is the thing a rhythm game is judged on and the one thing testing under Wine says nothing about. Calibrate before believing the timing.

The startup lines are worth pasting back if you run it: they name the fonts that were found and where scores are being written, which are the two things that differ from Linux.

Where it looks for things

Content — songs, hit sounds — comes from a content root, the first of these that exists and contains Songs/ or Global/HitSounds/:

  1. --base <dir>
  2. $TAIKO_BASE
  3. the directory holding the executable — so a copied folder is a self-contained install
  4. ~/.local/share/taiko

--songs <dir> (repeatable) and --hitsounds <dir> override individual pieces, and pointing --base at an existing OpenTaiko installation works without copying anything.

Scores and config are always written to ~/.local/share/taiko and ~/.config/taiko (%LOCALAPPDATA%\taiko and %APPDATA%\taiko on Windows), never into the content root — which may be read-only, or somebody else's game folder. The game prints every resolved path at startup.

During development, set TAIKO_BASE once rather than passing --base each time; cargo clean deletes target/, so assets next to the debug binary would not survive.

F10 shows the debug readout: frame times as median / 99th / worst over the last few seconds, then how many frames have missed their budget for the whole run and the worst one. A stutter is a tail rather than an average, which a frames-per-second number cannot show. It is a binding like any other, so debug_hud in config.toml moves it or unbinds it.

Development flags: --windowed opens in a window even if the config says fullscreen — worth knowing before you run a screenshot on the machine you are working on. --auto plays it back automatically (a correct parse → clock → judge chain scores every note GOOD), --screenshot <file> captures a frame, --seconds <n> exits after a while, --pick opens the difficulty picker at startup. --help lists everything.

Two tests need a real library, and skip without one: every song reachable by navigation, and every listed difficulty loading as the difficulty that was listed:

TAIKO_BASE=~/Applications/OpenTaiko/publish cargo test --bin taiko

Releasing

scripts/release.sh <version> tests, builds, packages, tags, and publishes to Forgejo with fj. Three binaries go out: one built here, one built against glibc 2.31 in the Steam Runtime SDK for the Steam Deck and other older systems, and one cross-compiled for Windows. Each travels with the README, the changelog and the licence, and with nothing else — no songs and no hit sounds, which are not ours to redistribute.

scripts/release.sh 0.9.4              # do it
scripts/release.sh 0.9.4 --dry        # build and package only; no tag, no publish
scripts/release.sh 0.9.4 --no-deck    # skip the Deck build (it needs Docker)
scripts/release.sh 0.9.4 --no-windows # skip the Windows build (needs the mingw-w64 target)

Three things have to be true first, and each is checked rather than trusted:

  • Cargo.toml already says the new version. The argument only confirms it, and the built binary is asked its --version afterwards, so a tag can never ship a binary reporting a different number.
  • The working tree is clean. Bumping the version rewrites Cargo.lock, so commit that with the bump — the script says as much when the lock is the only difference.
  • CHANGELOG.md has a section for the version. The release notes are read from it, so the two cannot drift, and a missing section stops the release rather than publishing nothing.

The branch is pushed before the tag, so a release never points at a commit no branch contains.

When SteamOS has had a major update, check the Deck's glibc with pacman -Q glibc on the device and update DECK_GLIBC in scripts/build-deck.sh. That number is the most the Deck build is allowed to require. A stale one breaks nothing today — it just holds the build to an older glibc than the Deck now has, and one day refuses a build that would have been fine. See docs/steam-deck-build.md.

Build

cargo build
cargo test

raylib-sys builds raylib from vendored source, so the first build is slow and needs cmake and a C toolchain.

Tools

# Measure the raw audio clock's resolution and drift.
cargo run --bin clockprobe -- <file.ogg> [seconds] [poll_hz] [buffer_frames]

# Check the smoothed master clock against real hardware (Step 2's gate).
cargo run --bin clocktest -- <file.ogg> [seconds] [poll_hz]

# Parse every chart in a library and report failures (Step 1's gate).
cargo run -p tja --example corpus -- <songs_dir>

# Scan a library into categories and songs.
cargo run -p library --example scan -- <songs_dir> [--tree]

Licence

MIT. No OpenTaiko assets are redistributed here — hit sounds and any skin art are read from the user's own installation at runtime.