A browser-based vocal-gesture rhythm game. Make mouth sounds (hiss, airy, pop, hum — or record your own) into your microphone; a real-time audio engine classifies each sound and a rhythm rail scores your timing, precision, and consistency. No sound ever leaves your device: the microphone signal is analyzed entirely client-side, and calibration/training data is persisted locally in IndexedDB.
- Real-time gesture detection. A small DSP pipeline (FFT-based
spectral features + a lightweight autocorrelation pitch tracker) turns
raw mic audio into a per-frame feature vector, running either as a
compiled WebAssembly engine (
agility-rt.wasm) inside anAudioWorkletProcessor, or a JS fallback when WASM isn't available. - Trainable classifier. An in-browser RBF-SVM (one-vs-rest) learns to tell your sounds apart from silence and from each other. Training data is captured through guided calibration panels (press-and-hold for sustained sounds like hums/hisses, a rhythmic "Pop Panel" for plosives) and stored per-label in IndexedDB.
- Quick Calibrate. A short guided flow (mic gain → pop calibration → sustained-sound calibration) gets a new player playable in under a minute.
- Custom sound library. Beyond the four built-in presets (hiss, airy, pop, hum), you can define your own labeled sounds (icon, label, percussive vs. sustained, pitch-invariant vs. pitch-discriminating), and export/import your whole library as JSON.
- Configurable rhythm game. Choose which sounds are active, run length, speed, hit/perfect windows, scoring, and how often/how long targets require sustained holds vs. quick taps.
- Diagnostics / Settings panel. Live feature visualizer (radar + scrolling history + pitch readout), per-sound practice/calibration, raw training-data inspection/export (TSV), and fine-grained DSP tuning (gate thresholds, gain, frequency bands, pitch tracker settings) with an auto-calibration assistant.
- PWA-ready. Installable, with a service worker precaching the app shell and caching the WASM engine and catalog assets.
npm install
npm run dev # start the dev server
npm run build # production buildOpen the app, grant microphone access, and either run Quick Calibrate for guided setup or go straight to Settings to train sounds and tune DSP parameters, then Play.
src/
audio/ mic acquisition, AudioContext graph wiring, worklet host
engine/ DSP feature extraction, clustering/classification, ABI
types shared with the AudioWorkletProcessor, WASM loader
calibration/ guided calibration helpers (impulse localization, etc.)
game/ scoring, rhythm rail, courses, the main game loop,
diagnostics session driving the Settings panel
render/ canvas2d renderer, sound feedback (Sonifier), haptics
app/ router, shell (top-level screens), reactive Signal store
ui/ custom elements (ac-*) for HUD, settings, panels
storage/ IndexedDB-backed persistence (model, training set,
DSP config, phoneme library, baseline profile)
theme/ naming/iconography for different presentation themes
a11y/ accessibility preferences (haptics, reduced motion, captions)
pwa/ service worker registration + Workbox SW source
- The microphone feeds a
MediaStreamAudioSourceNodeinto anAudioWorkletNoderunning theagility-engineprocessor. - Each audio frame is windowed and FFT'd to produce a
FeatureVector(spectral centroid, noise/voicing/tilt ratios, energy, transient onset, flatness, rolloff, plus an NSDF-based pitch estimate). - Feature vectors are either matched against hand-authored
ClusterDefcenters (seed-clusters.ts) or, once trained, classified by theNeuralClassifier(an RBF-SVM ensemble) — seeengine/gate.tsfor the energy/transient gating and rejection-confidence logic that keeps silence and noise from being misclassified. - Classified events stream to the
GameLoop(during play) orDiagnostics(during calibration/settings), which drive theRhythmRail/Scoreror the live Settings UI, respectively.
Because feature vectors are only meaningful relative to the DSP
parameters that produced them, the app fingerprints the active DSP
config (dspFingerprint() in engine/dsp.ts) and uses it to detect
drift between a persisted training corpus/model and the live
configuration — automatically discarding stale data rather than silently
degrading detection quality.
- Cluster / label — an identifiable sound (e.g.
hiss,pop, or a custom id). Presets ship with baked-in naming/icon themes (theme/naming.ts,theme/icons.ts); custom sounds carry their own label/icon pluspercussiveandpitchInvariantflags. - Percussive vs. sustained — percussive sounds (like pops) are
scored on onset timing; sustained sounds (hums, hisses) require holding
the sound for a duration and are scored via
RhythmRail.updateSustain. - Calibration — capturing labeled feature vectors for a sound, either via a timed/held capture window or a guided rhythmic panel, which are used to (re)train the classifier.
- DSP config — the tunable parameters controlling feature
extraction (gates, gains, frequency bands, pitch tracker settings).
Persisted via
storage/dsp-config.tsand adjustable live from Settings.
- The WASM engine (
agility-rt.wasm+ its Emscripten glue) is optional; if it fails to compile/load, the app transparently falls back to a pure-JS DSP implementation with equivalent behavior. - Extensive
console.info/console.debug/console.warnlogging is used throughout the engine, classifier, and game loop to make audio/ML issues (saturation, feature-space drift, empty training data, etc.) diagnosable directly from the browser console. - State is managed with a minimal reactive
Signal<T>(seeapp/state/signal.ts) rather than a framework; UI is built from plain Custom Elements (ui/ac-*.ts).
index.htmlships full metadata: canonical URL, description/keywords, Open Graph + Twitter Card tags, Apple web-app meta, and JSON-LD (VideoGame) structured data.public/robots.txtallows crawling of the app shell while excluding build internals (/src/,/lib/) and points to the sitemap.public/sitemap.xmllists the single indexable SPA entry point (the app is hash-routed, so all in-app screens share one crawlable URL) plus the social preview image.- Image generation specs live alongside the assets they produce:
public/images/social-preview.op.md→social-preview.png(1200×630 Open Graph card key art).public/images/icon.op.md→icon.png(512×512 maskable app icon / favicon source).Note: update the
https://vocalparkour.app/origin inindex.html,robots.txt, andsitemap.xmlto match the production deployment domain.