Welcome to Mankunku

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

Data Model

All core types live in src/lib/types/. This document describes every interface and type alias.

Music Types (src/lib/types/music.ts)

PitchClass

type PitchClass = 'C' | 'Db' | 'D' | 'Eb' | 'E' | 'F' | 'Gb' | 'G' | 'Ab' | 'A' | 'Bb' | 'B';

The 12 chromatic pitch classes using flat notation. The constant PITCH_CLASSES provides them in order.

ChordQuality

type ChordQuality =
  | 'maj7' | 'min7' | '7' | 'min7b5' | 'dim7'
  | 'maj6' | 'min6' | 'aug7' | 'sus4' | 'sus2'
  | '7alt' | '7#11' | '7b9' | '7#9' | '7b13'
  | 'minMaj7' | 'aug' | 'dim';

18 chord qualities covering standard jazz harmony.

PhraseCategory

type PhraseCategory =
  | 'ii-V-I-major' | 'ii-V-I-minor' | 'blues' | 'bebop-lines'
  | 'pentatonic' | 'enclosures' | 'digital-patterns' | 'approach-notes'
  | 'turnarounds' | 'rhythm-changes' | 'ballad' | 'modal'
  | 'user';

Categories for organizing phrases. Nine categories have curated/combinatorial licks totaling ~250 licks. The 'user' category is for user-recorded licks.

Fraction

type Fraction = [number, number];  // [numerator, denominator]

Represents durations and offsets as fractions of a whole note. Examples: [1, 4] = quarter note, [1, 8] = eighth note, [1, 12] = triplet eighth.

Note

interface Note {
  pitch: number | null;        // MIDI note (concert pitch), null = rest
  duration: Fraction;          // Length as fraction of whole note
  offset: Fraction;            // Position from phrase start
  velocity?: number;           // 0-127 (default ~100)
  articulation?: Articulation; // 'normal' | 'accent' | 'ghost' | 'bend-up' | 'staccato' | 'legato'
  scaleDegree?: string;        // e.g. '1', 'b3', '#4'
}

HarmonicSegment

interface HarmonicSegment {
  chord: {
    root: PitchClass;
    quality: ChordQuality;
    bass?: PitchClass;
  };
  scaleId: string;             // References ScaleDefinition.id (e.g. 'major.dorian')
  startOffset: Fraction;
  duration: Fraction;
}

Defines the harmonic context for a portion of a phrase — the chord and the associated scale.

Phrase

interface Phrase {
  id: string;                          // Unique ID (e.g. 'ii-V-I-maj-001' or 'gen-1710000000-0')
  name: string;
  timeSignature: [number, number];     // e.g. [4, 4]
  key: PitchClass;                     // Concert pitch key
  notes: Note[];
  harmony: HarmonicSegment[];
  difficulty: DifficultyMetadata;
  category: PhraseCategory;
  tags: string[];
  source: 'curated' | 'generated' | string;  // or 'mutated:<parentId>'
}

The central data structure. Curated licks are stored in concert C and transposed at runtime.

ScaleDefinition

interface ScaleDefinition {
  id: string;                          // e.g. 'major.dorian'
  name: string;                        // e.g. 'Dorian'
  family: ScaleFamily;                 // 'major' | 'melodic-minor' | etc.
  mode: number | null;                 // 1-based mode number (null for non-modal)
  intervals: number[];                 // Semitone steps, must sum to 12
  degrees: string[];                   // Scale degree labels
  chordApplications: ChordQuality[];   // Applicable chord types
  avoidNotes?: string[];               // Degrees to avoid sustaining
  targetNotes: string[];               // Chord tones for generator to land on
}

Audio Types (src/lib/types/audio.ts)

DetectedNote

interface DetectedNote {
  midi: number;       // MIDI note number (concert pitch)
  cents: number;      // Cents deviation from nearest note (-50 to +50)
  onsetTime: number;  // Onset relative to recording start (seconds)
  duration: number;   // Duration in seconds
  clarity: number;    // Pitch detection clarity (0-1)
}

Output of the note segmentation pipeline. Each represents one detected note from the microphone.

PlaybackOptions

interface PlaybackOptions {
  tempo: number;              // BPM
  swing: number;              // 0.5 = straight, 0.67 = triplet swing
  countInBeats: number;       // Count-in beats before recording
  metronomeEnabled: boolean;
  metronomeVolume: number;    // 0-1
}

AudioEngineState

type AudioEngineState = 'uninitialized' | 'loading' | 'ready' | 'playing' | 'recording' | 'error';

MicPermissionState

type MicPermissionState = 'prompt' | 'granted' | 'denied' | 'unavailable';

Scoring Types (src/lib/types/scoring.ts)

Score

interface Score {
  pitchAccuracy: number;       // 0-1
  rhythmAccuracy: number;      // 0-1
  overall: number;             // pitch * 0.6 + rhythm * 0.4
  grade: Grade;                // 'perfect' | 'great' | 'good' | 'fair' | 'try-again'
  noteResults: NoteResult[];
  notesHit: number;            // Correctly identified notes
  notesTotal: number;          // Total expected notes
  timing: TimingDiagnostics;   // Bias, spread, and per-note offsets
}

TimingDiagnostics

interface TimingDiagnostics {
  meanOffsetMs: number;                // + = late, - = early
  medianOffsetMs: number;
  stdDevMs: number;                    // timing jitter proxy
  latencyCorrectionMs: number;         // constant offset subtracted by scorer
  perNoteOffsetMs: (number | null)[];  // parallel to noteResults
}

BleedFilterLog

interface BleedFilterLog {
  totalNotes: number;
  keptNotes: number;
  filteredNotes: DetectedNote[];
  unfilteredScore: Score | null;
  filteredScore: Score | null;
}

Produced by runScorePipeline() when a bleed-filter result is available. Drives the A/B comparison in the /diagnostics panel.

NoteResult

interface NoteResult {
  expected: Note;
  detected: DetectedNote | null;  // null if missed
  pitchScore: number;             // 0-1
  rhythmScore: number;            // 0-1
  missed: boolean;
  extra: boolean;                 // Extra note not in phrase
}

AlignmentPair

interface AlignmentPair {
  expectedIndex: number | null;   // null = extra detected note
  detectedIndex: number | null;   // null = missed expected note
  cost: number;
}

Progress Types (src/lib/types/progress.ts)

UserProgress

interface UserProgress {
  adaptive: AdaptiveState;
  sessions: SessionResult[];                    // Last 200 sessions
  categoryProgress: Record<string, CategoryProgress>;
  keyProgress: Partial<Record<PitchClass, { attempts: number; averageScore: number }>>;
  totalPracticeTime: number;
  streakDays: number;
  lastPracticeDate: string;                     // ISO date string (YYYY-MM-DD)
}

AdaptiveState

interface AdaptiveState {
  currentLevel: number;                // Rounded avg of pitch + rhythm (1-100)
  pitchComplexity: number;             // 1-100, adjusted independently
  rhythmComplexity: number;            // 1-100, adjusted independently
  recentScores: number[];              // Circular buffer of last 25 overall scores
  recentPitchScores: number[];         // Circular buffer of last 25 pitch accuracy scores
  recentRhythmScores: number[];        // Circular buffer of last 25 rhythm accuracy scores
  attemptsAtLevel: number;
  attemptsSinceChange: number;         // Min of pitch/rhythm cooldowns
  pitchAttemptsSinceChange: number;    // Per-dimension cooldown for pitch
  rhythmAttemptsSinceChange: number;   // Per-dimension cooldown for rhythm
}

SessionResult

interface SessionResult {
  id: string;
  timestamp: number;
  phraseId: string;
  category: PhraseCategory;
  key: PitchClass;
  tempo: number;
  difficultyLevel: number;
  pitchAccuracy: number;
  rhythmAccuracy: number;
  overall: number;
  grade: Grade;
}

Instrument Types (src/lib/types/instruments.ts)

InstrumentConfig

interface InstrumentConfig {
  name: string;
  key: TransposingKey;                 // 'Bb' | 'Eb' | 'C' | 'F'
  transpositionSemitones: number;      // Concert + this = written pitch
  concertRangeLow: number;            // Lowest MIDI note (concert)
  concertRangeHigh: number;           // Highest MIDI note (concert)
  clef: 'treble' | 'bass';
  gmProgram: number;                   // General MIDI program number
}

Built-in Instruments

IDNameKeyTranspositionRange (MIDI)GM Program
tenor-saxTenor SaxophoneBb+1444–7666
alto-saxAlto SaxophoneEb+949–8065
trumpetTrumpetBb+252–8256