Skip to content
avitusPublic

About

Call and response ear training for jazz musicians

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

1,634 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Mankunku

Jazz ear-training 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 keeps your data on-device (page loads need the network).

Features

Practice

  • Call-and-response ear training in the day's key and scale, with automatic scoring (/ear-training)
  • Lick practice: your own licks through all 12 keys over a generated rhythm section, with per-lick gradual key unlocking and tempo adaptation — sessions pinned to one progression, Daily Practice across every progression you've tagged, and single-lick Deep Practice on a worst-first key rotation; the report's weak-key recommendation starts a focus ramp on that key alone
  • Sheet music only where it helps: the newest-unlocked key, while it scores under 75%, gets an engraved lead-sheet row, heralded by a two-bar reading pause and (in continuous mode) held for three passes
  • Tricks: parameterized melodic devices — enclosures (major, minor and dominant ladders) and eight triad-pair families — drilled for fluency, scored on conformance to the device rather than note-for-note reproduction, with a mastery ladder of variants
  • Tune practice: runtime progression detection inside a song form, mastery-aware lick suggestions at each insertion point, and freestyle solo recognition
  • Adaptive difficulty: per-scale and per-key proficiency levels 1-100 over 10 content tiers
  • Guided tours of the app, ear training, lick practice, your licks, your tunes and tune practice, replayable from Settings

Your book

  • Licks: a step-entry notation editor (with Weimar Jazz Database attribution suggestions while you name a line), record-a-lick — play it over a two-bar count-in and get it transcribed, swung eighths notated straight — and major/minor mode per lick
  • Tunes: a songbook with Real Book–style engraving — jazz chord stacking, section markers, repeats, stacked voltas, pickup bars, multi-system reflow — shown in your instrument's written key
  • Tune importers: iReal Pro links, Band-in-a-Box .SGU/.MGU (with a MusicXML fallback), MuseScore .mscz/.mscx, and PDF lead sheets — AI-assisted extraction with a review pass, optionally fused with an OMR transcription from the omr/ subsystem
  • Community sharing for both licks and tunes
  • Progress: practice calendar, trend and per-scale charts, period comparisons, per-lick key detail

Listening

  • Real-time pitch detection (McLeod method via Pitchy, 60fps)
  • Note onset detection via custom AudioWorklet (HFC algorithm)
  • Five-tier re-articulation detection down to legato tonguing that leaves no energy evidence — only a waveform-shape break
  • Band-limited (250–5000 Hz) evidence so metronome clicks can neither mask nor fake an articulation
  • DTW alignment-based scoring (pitch 60% + rhythm 40%, with latency correction)

The band

  • A generated rhythm section in four styles — swing, bossa nova, ballad, straight — with planned walking bass in swing, rootless voice-led comping and a composed drum vocabulary, seeded so a replay is identical
  • Tenor, alto and soprano sax play bundled multi-layer samples; trumpet and a concert-pitch setting play through a General MIDI SoundFont

Content

  • 33 scales and 452 curated jazz licks (ii-V-I, blues, bebop, modal, and more), plus 471 generated by combining scale patterns with rhythm templates
  • In-app documentation (/docs) with an AI docs assistant, and a scales reference (/scales)

Platform

  • 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
  • Installable web app (manifest-based; there is no service worker, so page loads need the network)
  • Cross-device progress sync via Supabase auth (optional)
  • Dark and light themes

Tech Stack

Technology Role
SvelteKit 2 + Svelte 5 (runes), Vite 8 Framework, SSR (Node adapter), file-based routing
TypeScript 6 (strict mode) Type safety throughout
Tailwind CSS 4 Utility-first styling, dark/light theming
Tone.js 15 + smplr Audio scheduling, sampled instruments and SoundFont fallback
Pitchy McLeod pitch detection
Custom AudioWorklet Real-time onset detection
abcjs Sheet music notation rendering
pdf.js (pdfjs-dist) PDF text layer and page geometry for tune import
Anthropic SDK Docs assistant and PDF tune extraction (server-side, optional)
Supabase Auth + PostgreSQL + Storage cloud sync (optional)
Sentry Error monitoring and tracing, client and server
driver.js Guided tours
Vitest 4 + Playwright Unit/integration and cross-browser E2E testing
Python 3.12 (uv) The standalone OMR subsystem under omr/

Node: package.json requires >=22.12.0 (enforced by engine-strict); .nvmrc pins 26.5.1, the version CI and production run. Production runs under PM2 behind nginx.

Quick Start

Prerequisites: Node.js 22.12+ (nvm use picks up .nvmrc), a modern browser with Web Audio API support

git clone https://github.com/avitus/mankunku.git
cd mankunku
npm install
cp .env.example .env
npm run dev

The app opens at http://localhost:5173. The .env step is required even without a backend: the Supabase client reads PUBLIC_SUPABASE_URL / PUBLIC_SUPABASE_ANON_KEY from $env/static/public, so without them the build fails and every dev-server page answers 500 — the placeholder values in .env.example are enough for local-first practice. Signed out, / is the landing page; the onboarding flow (instrument, microphone) opens the first time you visit a practice route. A microphone is needed for full functionality but not required to explore the codebase.

.env.example also lists two server-only keys: ANTHROPIC_API_KEY (docs assistant and PDF tune import) and SUPABASE_SERVICE_ROLE_KEY (account deletion and the owner-only admin page). See Development Setup for what each one unlocks.

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:54321

Point your local .env PUBLIC_SUPABASE_URL / PUBLIC_SUPABASE_ANON_KEY (and SUPABASE_SERVICE_ROLE_KEY) at the values from npx supabase status. npx 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. The CLI is linked to production, so always pass --local when you mean the local stack. Production is unaffected — its credentials are injected by CI at build time, not read from .env.

Project Structure

src/
  lib/
    audio/          Web Audio: playback, capture + capture window, pitch/onset detection, segmentation, quantizer, metronome, backing-track engine
    scoring/        DTW alignment, pitch/rhythm scoring, fluency, grades
    music/          Music theory + engraving policy: scales, keys, transposition, chords, notation, pickups, enharmonic spelling
    notation/       DOM-free chart plumbing: hit geometry, abcjs adapter, the one abcjs loader, follow-scroll
    phrases/        Lick library loading, combinatorial generator, validation, duplicate detection
    tunes/          Tune domain: flatten, book loading, importers, progression detection, lick matching
    tricks/         Melodic devices (enclosures, triad pairs): conformance, example generation, mastery ladder
    matching/       N-gram melodic matcher (WJazzD attribution + freestyle recognition)
    difficulty/     Per-scale/per-key proficiency, 10-tier profiles, lick phases
    tonality/       Daily key/scale selection, progressive unlocking, scale compatibility
    step-entry/     Manual note-entry helpers (durations, accidentals)
    tour/           driver.js guided-tour definitions and config
    docs/           In-app docs tree, markdown rendering, assistant context
    content/        Shared UI copy (tooltips)
    state/          Svelte 5 runes state modules (.svelte.ts) + their plain logic modules
    persistence/    localStorage/IndexedDB storage + Supabase sync
    components/     Svelte UI components, by domain
    ui/             Pure layout and colour helpers used by components
    server/         Server-only code: health snapshot, error handler, Anthropic client, account deletion, admin
    supabase/       Client factories and hand-maintained DB types
    types/          TypeScript interfaces grouped by domain
    data/           Curated lick + tune catalogs, pattern tables, progression templates and shapes
    util/           Small shared utilities
  routes/           SvelteKit pages: ear-training, lick-practice, tricks, licks, tunes, progress, scales,
                    settings, docs, auth, diagnostics, admin, api/ (chat, tune-parse, lick-match, health, ...)
tests/
  unit/             Vitest unit tests, one directory per domain
  integration/      Vitest integration tests (pipelines, cloud sync, importers against recorded fixtures)
  e2e/              Playwright browser tests (Chromium, Firefox, WebKit)
  fixtures/         Recorded takes, lead-sheet corpora, backing-track golden files
supabase/
  migrations/       SQL migrations (legacy 00001–00023, then UTC-timestamped)
omr/                Standalone Python OMR subsystem (uv-managed) — see docs/omr/
deploy/             Server-side scripts: app release (deploy/app/release.sh) and nginx deploy
nginx/              Production nginx config
documentation/      Player guides, architecture docs, API reference, contributing guides
docs/               OMR docs, WJazzD attribution

Architecture Highlights

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. If the worklet can't load, onsets are derived from gaps and pitch changes in the pitch readings instead.

Shared AudioContext — Tone.js and smplr share one AudioContext (audio-context.ts), so Transport scheduling, sample playback, the metronome and the backing band all run on the same timeline.

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 signs in. Practice needs no backend at all.

Tunes bridge into the Phrase pipeline — Tunes were added without forking playback, scoring, or notation. flattenTune collapses a tune's sections into one continuous timeline (in notation order or repeat-expanded playback order, with index maps between them), and tuneToPhrase wraps that in a Phrase so the existing engines consume a song form with no new orchestration. See Tune System.

See Architecture Overview for detailed system design documentation, and CLAUDE.md for the densest current reference.

Contributing

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 catalog (guide)
  • Add scales to the catalog (guide)
  • Improve test coverage in any domain under tests/unit/
  • Report bugs or suggest features via issues

See the full Contributing Guide for setup, code style, commit conventions, the dev → main workflow and CI. In short: Svelte 5 runes only, TypeScript strict mode, Conventional Commits.

Testing

npm test              # Unit + integration tests (Vitest)
npm run test:watch    # Watch mode
npm run test:e2e      # Playwright E2E tests (Chromium, Firefox, WebKit)
npm run test:deploy   # The server-side release script (bash, stubbed npm/pm2/curl)
npm run check         # TypeScript + svelte-check
cd omr && uv run pytest   # The Python OMR subsystem (hermetic)

Playwright builds and previews the app on port 4173 and locally reuses whatever already holds that port — beside another checkout, set PLAYWRIGHT_PORT to a free port or you will test the other checkout's build.

See the Testing Guide for patterns, audio mocking strategies, and conventions.

Deployment

Production is a single Node process (SvelteKit's Node adapter) under PM2 on a Digital Ocean droplet, behind nginx. CircleCI deploys every merge to main — test → build → db-migrate → deploy, with the cross-browser E2E job also gating the deploy.

App releases

The deploy job rsyncs build/ and the package files into releases/<id>/ on $DEPLOY_HOST, ships ecosystem.config.cjs (checksum-verified) and deploy/app/release.sh, and runs the script on the server. It:

  1. Installs production dependencies once into shared/deps, keyed on the lockfile and the Node version, and symlinks them into the release — so most deploys install nothing.
  2. Merges the build's content-hashed _app/immutable chunks into a shared pool and links the pool into the release, so a tab opened against an older deploy can still load its chunks.
  3. Swaps the current symlink to the new release and restarts the app with pm2 delete + pm2 start ecosystem.config.cjs --env production (runtime secrets load from shared/runtime.env on the server).
  4. Polls http://127.0.0.1:3000/api/health until it reports the new release id — pm2 start returning 0 only means the process spawned.
  5. Prunes old releases (keeps 5). A failed deploy removes its own staged release, unless current already points at it.

Deploys are serialized with flock. The job then fetches the public https://mankunkujazz.com/api/health and requires it to report the commit SHA. /api/health is unauthenticated and returns the commit, the live release id, the Node version and uptime. npm run test:deploy covers release.sh's invariants. To roll back, from the app root on the server: ln -sfn releases/<previous-id> current && pm2 restart mankunku (the rolled-back release runs on the newer shared dependencies).

Nginx

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:

  1. Backs up the existing /etc/nginx/sites-available/mankunku to /etc/nginx/backups/mankunku.<timestamp>.conf.
  2. Installs the new config (mode 0644, owner root:root).
  3. Ensures the sites-enabled/mankunku symlink exists.
  4. Runs sudo nginx -t.
  5. On success, warns (without blocking) about any server_name its certificate doesn't cover, then reloads nginx with sudo systemctl reload nginx.
  6. On failure, restores the backup and exits non-zero (no reload).

CircleCI environment

Set in the CircleCI project settings (none of these live in the repo):

Variable Purpose
DEPLOY_HOST Server hostname or IP, read by both the app and nginx deploy jobs. A server move is finished only once this is updated — the 2026-09-08 move to a new droplet missed it, and PR #247's deploy landed on the retired box until the public verify step caught it
DEPLOY_USER SSH user (must have passwordless sudo for nginx, systemctl, install, mkdir, cp, ln, rm on the nginx paths)
DEPLOY_PORT (optional, nginx job only) SSH port; defaults to 22
SUPABASE_ACCESS_TOKEN Lets db-migrate run supabase link + supabase db push --linked — no database password needed
PRODUCTION_URL (optional) Base URL for the public health check; defaults to https://mankunkujazz.com

The build job — and the e2e job, whose Playwright webServer builds the app itself — also need PUBLIC_SUPABASE_URL / PUBLIC_SUPABASE_ANON_KEY in the environment, since they are inlined at build time. The SSH key is attached via add_ssh_keys using the fingerprint registered for the deploy jobs.

Manual nginx deploy. Copy nginx/mankunku.conf to the server (e.g. /tmp/mankunku.nginx.staged) and run:

bash deploy/nginx/deploy.sh /tmp/mankunku.nginx.staged

The script is idempotent and safe to re-run.

License

License pending. This project does not yet have a license file — one will be added before the first public release.

Third-party data and assets

  • Weimar Jazz Database — 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.
  • Fonts (self-hosted in static/fonts/, SIL Open Font License 1.1, license texts alongside) — Fraunces, the display and chord-letter face; Edwin, MuseScore's engraved text face, which supplies the Δ ♭ ♯ glyphs Fraunces lacks; MuseJazzText, MuseScore's handwritten jazz face by Torsten Hämmerle and Abraham Lee.
  • Samples (static/samples/, each set with an ATTRIBUTION.md) — saxophones from MTG Solo Sax (Music Technology Group, Universitat Pompeu Fabra; CC-BY 4.0); drums from Virtuosity Drums (Karoryfer Samples / Versilian Studios; CC0); the room impulse response is synthesized for this project (CC0).

Acknowledgments

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.

About

Call and response ear training for jazz musicians

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages