Jazz ear training progressive web app with call-and-response practice. The app plays a jazz phrase, you play it back on your instrument via microphone, and it scores your pitch and rhythm accuracy in real time.
Named after Winston "Mankunku" Ngozi's 1968 album Yakhal' Inkomo — one of the greatest South African jazz recordings. Under the hood: real-time pitch detection at 60fps, Dynamic Time Warping for score alignment, a custom AudioWorklet for onset detection, adaptive difficulty that grows with you, and a local-first architecture that works offline.
- Call-and-response practice with automatic scoring
- Real-time pitch detection (McLeod method via Pitchy, 60fps)
- Note onset detection via custom AudioWorklet (HFC algorithm)
- DTW alignment-based scoring (pitch 60% + rhythm 40%, with latency correction)
- 33 scales and 452 curated jazz licks (ii-V-I, blues, bebop, modal, and more)
- Combinatorial phrase generation from scale patterns and rhythm templates
- Adaptive difficulty: proficiency levels 1-100, 10 content tiers
- Concert pitch canonical — transposition to written pitch at display time only
- Fraction-based rhythm representation (no floating-point drift with triplets or dotted notes)
- Local-first: writes to localStorage/IndexedDB, optional Supabase cloud sync
- Offline-capable installable PWA with cached SoundFont samples
- Cross-device progress sync via Supabase auth (optional)
- Dark and light themes
| Technology | Role |
|---|---|
| SvelteKit 2 + Svelte 5 (runes) | Framework, SSR, file-based routing |
| TypeScript (strict mode) | Type safety throughout |
| Tailwind CSS 4 | Utility-first styling, dark/light theming |
| Tone.js + smplr | Audio scheduling, SoundFont sample playback |
| Pitchy | McLeod pitch detection |
| Custom AudioWorklet | Real-time onset detection |
| abcjs | Sheet music notation rendering |
| Supabase | Auth + PostgreSQL cloud sync (optional) |
| Vitest + Playwright | Unit and E2E testing |
| @vite-pwa/sveltekit | PWA with Workbox service worker |
Prerequisites: Node.js >= 22, a modern browser with Web Audio API support
git clone https://github.com/avitus/mankunku.git
cd mankunku
npm install
npm run devThe app opens at http://localhost:5173. The onboarding flow will prompt for instrument selection and microphone access. A microphone is needed for full functionality but not required to explore the codebase.
Optional — Supabase cloud sync: Copy .env.example to .env and add your Supabase project URL and anon key. See Development Setup for full setup including database migrations.
Local Supabase stack (for auth/sync development): Development runs against a local Supabase instance so it never touches production data. Requires Docker.
npm run db:start # boots local Postgres + Auth + Storage via Docker and applies all migrations
npm run dev # dev server now talks to the local stack at http://127.0.0.1:54321npx supabase migration up --local applies any migrations added since the stack was started (e.g. after pulling main); npm run db:reset re-applies them from a clean slate; npm run db:stop shuts the stack down. npm run db:types:check verifies the hand-maintained src/lib/supabase/types.ts still matches the schema. Point your local .env PUBLIC_SUPABASE_URL / PUBLIC_SUPABASE_ANON_KEY at the values from npx supabase status. Production is unaffected — its credentials are injected by CI at build time, not read from .env.
src/
lib/
audio/ Audio pipeline: playback, capture, pitch detection, onset detection
scoring/ DTW alignment and scoring engine
music/ Music theory: scales, keys, intervals, transposition, chords
phrases/ Phrase generation, mutation, validation, library loading
difficulty/ Adaptive difficulty algorithm and 10-tier profiles
tonality/ Daily key/scale selection, progressive unlocking
state/ Svelte 5 runes state modules (.svelte.ts)
persistence/ localStorage/IndexedDB storage + Supabase sync
components/ UI components (audio, practice, library, notation, onboarding)
supabase/ Client setup and generated DB types
types/ TypeScript interfaces grouped by domain
data/ Curated lick library and static data
routes/ SvelteKit pages: practice, library, progress, settings, auth
tests/
unit/ Unit tests across 8 domains (audio, scoring, music, phrases, ...)
integration/ Integration tests (auth route chain, etc.)
e2e/ Playwright browser tests
supabase/
migrations/ 23 SQL migrations (profiles, progress, settings, licks, RLS, +evolutions)
documentation/ Architecture docs, API reference, contributing guides
Concert pitch canonical — All MIDI note numbers, scale data, and lick data use concert pitch. Transposition to written pitch (Bb/Eb instruments) happens only at display time in two functions: phraseToAbc() and concertToWritten(). This eliminates an entire class of transposition bugs.
Fraction-based rhythm — Note durations use [numerator, denominator] tuples (e.g., [1, 8] = eighth note, [1, 12] = triplet eighth). No floating-point drift with triplets or dotted rhythms. Conversion to seconds happens only when computing audio timing.
DTW scoring with latency correction — Dynamic Time Warping aligns expected and detected note sequences, handling extra notes, missed notes, and tempo drift. The scorer computes the median timing offset across matched pairs and subtracts it, absorbing constant human and detection latency without affecting relative timing accuracy.
AudioWorklet onset detection — A custom AudioWorklet processor runs on the audio thread for low-latency onset detection using High-Frequency Content weighting with an adaptive threshold. Falls back to pitch-gap detection in browsers without AudioWorklet support.
Local-first with optional cloud — All writes go to localStorage and IndexedDB first for instant feedback and offline resilience. Supabase sync runs in the background when the user opts in. The app is fully functional without any backend.
See Architecture Overview for detailed system design documentation.
Contributions are welcome — whether that is adding jazz licks, improving the scoring algorithm, fixing bugs, or improving documentation.
Good first contributions:
- Add curated licks to the library (guide)
- Add scales to the catalog (guide)
- Improve test coverage across any of the 8 test domains
- Report bugs or suggest features via issues
See the full Contributing Guide for code style, branch naming, commit conventions, and PR process. In short: Svelte 5 runes only, TypeScript strict mode, Conventional Commits.
npm test # Run unit + integration tests (Vitest)
npm run test:watch # Watch mode
npm run test:e2e # Playwright E2E tests
npm run check # TypeScript + svelte-checkSee the Testing Guide for patterns, audio mocking strategies, and conventions.
The production nginx config at nginx/mankunku.conf is the single source of truth. It is deployed to /etc/nginx/sites-available/mankunku on the server by a dedicated CircleCI workflow.
When does it deploy? The nginx-deploy workflow runs only when a push to main changes any of:
nginx/**deploy/nginx/**.circleci/**
App-only changes (under src/, tests/, static/, etc.) do not trigger nginx-deploy. Path filtering is wired via the circleci/path-filtering orb from .circleci/config.yml; the actual jobs live in .circleci/continue-config.yml. Running this workflow requires "Dynamic config using setup workflows" enabled in CircleCI (Project Settings → Advanced).
What happens on the server? The job scp's nginx/mankunku.conf and deploy/nginx/deploy.sh to /tmp, then invokes the script over SSH. The script:
- Backs up the existing
/etc/nginx/sites-available/mankunkuto/etc/nginx/backups/mankunku.<timestamp>.conf. - Installs the new config (mode
0644, ownerroot:root). - Ensures the
sites-enabled/mankunkusymlink exists. - Runs
sudo nginx -t. - On success, reloads nginx with
sudo systemctl reload nginx. - On failure, restores the backup and exits non-zero (no reload).
Required CircleCI environment variables (set in a context or the project):
| Variable | Purpose |
|---|---|
DEPLOY_HOST |
Server hostname or IP |
DEPLOY_USER |
SSH user (must have passwordless sudo for nginx, systemctl, install, mkdir, cp, ln, rm on the relevant paths) |
DEPLOY_PORT |
(optional) SSH port; defaults to 22 |
The SSH key is attached via add_ssh_keys using the fingerprint already registered for the app deploy job.
Manual deploy. Copy nginx/mankunku.conf to the server (e.g. /tmp/mankunku.nginx.staged) and run:
bash deploy/nginx/deploy.sh /tmp/mankunku.nginx.stagedThe script is idempotent and safe to re-run.
License pending. This project does not yet have a license file — one will be added before the first public release.
The lick-naming feature uses a derivative index of the Weimar Jazz Database (CC-BY-NC-SA 4.0). See docs/wjazzd-attribution.md for license details and rebuild instructions.
Named after Winston "Mankunku" Ngozi (1943-2009), South African jazz tenor saxophonist whose 1968 album Yakhal' Inkomo ("Cry of the Bull") remains a landmark of South African jazz.
Built with SvelteKit, Tone.js, Pitchy, smplr, abcjs, and Supabase.