Welcome to Mankunku

Jazz ear training — call and response. Pick your instrument to get started.

Tonality System

The tonality system manages daily key/scale selection and progressive unlocking of tonalities as the player advances.

Source files: src/lib/tonality/tonality.ts, src/lib/tonality/scale-compatibility.ts

Concepts

A tonality is a combination of a key (pitch class) and a scale type (e.g., "D Dorian", "Bb Blues"). Practice sessions focus on a single tonality per day — all licks are transposed to the day's key.

Progressive Unlocking

Tonalities unlock based on the player's proficiency levels, with keys and scale types unlocking independently.

Key Unlock Order (Circle of Fifths)

OrderKeyUnlock Condition
1C0 (free)
2GKey proficiency prerequisites met
3FKey proficiency prerequisites met
4DKey proficiency prerequisites met
5BbKey proficiency prerequisites met
6AKey proficiency prerequisites met
7EbKey proficiency prerequisites met
8EKey proficiency prerequisites met
9AbKey proficiency prerequisites met
10BKey proficiency prerequisites met
11DbKey proficiency prerequisites met
12GbKey proficiency prerequisites met

Scale Type Unlock Order

OrderScale TypeUnlock Condition
1Major Pentatonic0 (free)
2Major0 (free)
3Blues0 (free)
4DorianScale proficiency prerequisites met
5MixolydianScale proficiency prerequisites met
6Minor (Aeolian)Scale proficiency prerequisites met
7LydianScale proficiency prerequisites met
8Melodic MinorScale proficiency prerequisites met
9AlteredScale proficiency prerequisites met
10Lydian DominantScale proficiency prerequisites met
11Bebop DominantScale proficiency prerequisites met

Cross-Product

Available tonalities = unlocked keys × unlocked scale types. Initially, the player has 3 tonalities (C Major Pentatonic, C Major, C Blues). As they build proficiency, the combinatorial space grows quickly.

Daily Tonality Selection

The daily tonality is selected deterministically from the set of unlocked tonalities using an FNV-1a hash of the date string. This ensures:

  • Same tonality all day for a given player
  • Different tonality each day (assuming > 1 unlocked)
  • Even distribution across unlocked tonalities over time
  • Deterministic — no server state needed
hash = fnv1a(dateString)  // e.g., "2026-03-19"
index = hash % unlockedTonalities.length
dailyTonality = unlockedTonalities[index]

Settings Override

Players can override the daily tonality in Practice Settings:

  • Key selector: Circle-of-fifths layout. Locked keys shown disabled with lock icon and proficiency requirements tooltip.
  • Scale type selector: Locked scales shown similarly.
  • Reset to daily: Button to restore automatic selection.

The override persists to localStorage via the settings state module (tonalityOverride: Tonality | null).

Integration with Practice

The practice page derives the active tonality from either the override or the daily pick:

const activeTonality = settings.tonalityOverride ?? getDailyTonality(today, unlockContext)

All licks in a session are transposed to activeTonality.key using transposeLickForTonality(). When the tonality changes (e.g., override selected), the current phrase is re-transposed via a $derived.

The practice page also displays the note count for the active scale (e.g., "5 notes" for pentatonic, "7 notes" for major) to help beginners understand what scale they're working with.

Scale-Aware Lick Filtering

Source file: src/lib/tonality/scale-compatibility.ts

Not all licks are appropriate for every scale type. A 7-note major lick that gets snapped down to 5 notes sounds awkward in a pentatonic session. The scale compatibility system filters licks by their native scale before presenting them to the player.

Design Decision

Pentatonic and blues scales are treated as first-class scales, not subsets of major. A pentatonic lick CAN appear in a major session (since pentatonic pitch classes are a subset of major), but a 7-note major lick should NOT appear in a pentatonic session.

Compatibility Rules

The rules are based on pitch-class subset relationships:

Lick's Native ScaleCompatible ScaleTypes
pentatonic.major (C D E G A)major-pentatonic, major, lydian, mixolydian
pentatonic.minor (C Eb F G Bb)minor, dorian
blues.minor (C Eb F Gb G Bb)blues, dorian, minor
major.ionian (7-note major)major, lydian, mixolydian, bebop-dominant
major.doriandorian, minor
major.mixolydianmixolydian, major, bebop-dominant
major.lydianlydian, major
major.aeolianminor, dorian
bebop.dominantbebop-dominant, mixolydian, major
melodic-minor.*melodic-minor, altered, lydian-dominant

For multi-chord progression categories (ii-V-I-major, ii-V-I-minor, turnarounds, rhythm-changes), compatibility is broader because the lick uses parent-key transposition:

CategoryCompatible ScaleTypes
ii-V-I-majormajor, dorian, mixolydian, lydian
ii-V-I-minorminor, dorian, melodic-minor, altered
turnaroundsmajor, mixolydian
rhythm-changesmajor, mixolydian

Resolution Order

getCompatibleScaleTypes(lick) resolves compatibility in this order:

  1. If lick.source === 'user' → compatible with all ScaleTypes (user-recorded licks always pass)
  2. Check lick.category for progression categories → use category-level mapping
  3. Inspect lick.harmony[0]?.scaleId → use scale-level mapping
  4. Fallback → compatible with all ScaleTypes (safe for unknown licks)

Fallback Behavior

If scale filtering leaves fewer than 3 licks at the player's difficulty level, the practice page widens to all licks at that difficulty level. This prevents empty sessions for rare scale type / difficulty level combinations.

API

Types

interface Tonality {
  key: PitchClass;
  scaleType: string;  // e.g., 'major', 'blues', 'dorian'
}

Functions

FunctionSignatureDescription
getDailyTonality(date, ctx: UnlockContext) → TonalityDeterministic daily pick from unlocked set
getUnlockedKeys(ctx: UnlockContext) → PitchClass[]Keys unlocked at given proficiency
getUnlockedScaleTypes(ctx: UnlockContext) → ScaleType[]Scale types unlocked at given proficiency
getUnlockedTonalities(ctx: UnlockContext) → Tonality[]All unlocked key × scale combinations
formatTonality(tonality) → stringDisplay string, e.g., "D Dorian"

Scale Compatibility Functions (scale-compatibility.ts)

FunctionSignatureDescription
getCompatibleScaleTypes(lick: Phrase) → ScaleType[]Derive which ScaleTypes a lick works with
isLickCompatible(lick: Phrase, scaleType: ScaleType) → booleanCheck if a lick is compatible with a given ScaleType