From 6e24b94a91228074ef0bbc36f335e8fccf45c8b1 Mon Sep 17 00:00:00 2001 From: Jens Mohrmann Date: Mon, 14 Sep 2026 00:21:43 +0200 Subject: [PATCH] Review phrase mode requirements and specify implementation contracts --- PHRASE_MODE_PLAN.md | 389 +++++++------------- docs/PHRASE_MODE_IMPLEMENTATION.md | 550 +++++++++++++++++++++++++++++ 2 files changed, 668 insertions(+), 271 deletions(-) create mode 100644 docs/PHRASE_MODE_IMPLEMENTATION.md diff --git a/PHRASE_MODE_PLAN.md b/PHRASE_MODE_PLAN.md index 120534a..762ebe6 100644 --- a/PHRASE_MODE_PLAN.md +++ b/PHRASE_MODE_PLAN.md @@ -1,300 +1,147 @@ -# Phrase Mode — Requirements & Implementation Plan +# Phrase Mode — Reviewed Requirements & Plan -**Status: DRAFT — awaiting approval ("Go") before implementation.** +**Status: revised draft; implementation still requires the user's “Go”.** -This document describes a proposed third game mode ("Phrase Mode") for the Sheet -Music Trainer. It is written as stakeholder requirements: what the mode does, -what it deliberately does **not** do, how it will be built, and which decisions -need the owner's sign-off. Section 8 lists the open decisions together with the -defaults I will assume if no feedback arrives in time — so an autonomous -overnight run can proceed without blocking on any of them. +This replaces the initial GLM draft. The product direction is good; the review +corrects several music, audio, scope, and deployment assumptions. Implementation +must follow this document **and** [the implementation contract](docs/PHRASE_MODE_IMPLEMENTATION.md). +The contract makes behavior and acceptance tests explicit for the next model. +If they appear inconsistent, resolve the inconsistency before implementing; +do not treat the original draft in Git history as an alternative specification. ---- +## 1. Recommendation -## 1. Vision +Add **Phrases**, alongside **Sight Reading** and **Ear Training**. Keep the +existing modes and their defaults. In Phrases, display a few bars of one melodic +line and follow the player through them. No chords or simultaneous voices yet. -Today the app trains **single notes**: one note appears, you play it, the app -confirms, repeat. This is great for building note-lookup reflexes, but real -sight-reading is about reading **sequences** — a bar or a few bars of connected -music, played in order, ideally in time. +Three material choices, not three additional top-level modes: -Phrase Mode adds exactly that, without disturbing the minimal feel of the -existing modes: +- **Melodies:** reproducible, rule-based generation in a selected key/mode and range. +- **Scales:** ascending, descending, or up-and-down scales in any key, optionally + constrained to a fretboard region. This is a core requirement, not spare-time polish. +- **Exercises:** a small, verified file-backed collection and basic local ABC import. -- The staff shows **1–8 bars of single-voice music** (notes with real durations, - bar lines, key/time signature). -- You **play the notes one by one**; the app tracks your position and gives the - same kind of quiet, unobtrusive feedback as today. -- Two paces: **Step mode** (beginner — play each note correctly, no clock) and - **Tempo mode** (metronome — the phrase moves on the beat, hit or miss). -- Material comes from two sources: **rule-based generated melodies** (in a key, - scale or fretboard position of your choosing) and a **bundled library of fixed - exercises** (e.g. Giuliani studies, folk tunes) parsed at runtime from a - simple text file format. +Two practice paces: -Zen mode, mic handling, virtual instruments, themes and all existing modes stay -as they are. Phrase Mode is *additive*: a third toggle next to "Sight Reading" -and "Ear Training". +- **At your pace** (default): wait for each correct pitch; rhythm is displayed, + not assessed. Wrong notes don't advance; an explicit Skip is available. +- **With tempo:** one-bar count-in, then advance according to written durations; + mark pitches matched during their slots and continue past misses. This is + deliberately forgiving **pitch-following with a metronome**, not a precise + assessment of note attacks, release times, or rhythmic accuracy. -## 2. User stories +All generation, imports, and practice remain browser-local. No backend, runtime +LLM calls, accounts, analytics, or new network dependency for practicing. -| # | Story | -|---|--------| -| U1 | As a guitar student, I want the app to generate short melodies within a key and a fretboard position, so I practice reading connected lines instead of isolated notes. | -| U2 | As a beginner, I want a mode where the phrase waits for me: the next note is only "armed" after I played the current one, so I can go at my own pace. | -| U3 | As a more advanced student, I want the phrase to advance on the metronome, marking notes I hit in time vs. missed, so I practice real sight-reading under tempo. | -| U4 | As a classical player, I want to practice fixed material (e.g. Giuliani right-hand studies) from a library, so I rehearse real repertoire. | -| U5 | As a future contributor, I want exercises stored in a simple text format in the repo, so more pieces can be added later — found somewhere else, converted, or generated. | -| U6 | As a user of the existing modes, I expect Zen mode, hints, virtual instruments and themes to keep working unchanged in the new mode. | +## 2. Important corrections to the first draft -## 3. Requirements +| Initial assumption | Reviewed requirement | +|---|---| +| Reusing `MatchTracker` unchanged solves sequence matching | It can repeatedly accept a held pitch. Add per-note result latching and repeated-note re-arming. A tied continuation is not a new attack. | +| `pitchData` is a fresh detection stream | Its pitch can remain displayed for 200 ms after detection stops. Add a separate timestamped raw frame stream, including null frames, for phrase matching. Keep existing display smoothing. | +| The metronome is an already-correct phrase clock | `useMetronome` calls `onTick` when scheduling audio, up to about 100 ms before it sounds. Schedule sounds ahead, but advance the cursor only when the clock reaches the event. | +| Speaker gating can simply cover every sound | Gating 400 ms after every metronome click would disable much of practice. Preview/virtual-note gating and metronome behavior need separate policies. | +| Fretboard positions come “for free” | Pitch ranges help generation, but hints must respect string/fret constraints. A microphone cannot identify which string/fret produced an equivalent pitch. Do not claim fingering verification. | +| Existing key-signature helpers suffice for phrases | They neither restrict current random-note generation nor track accidentals through a measure. Preserve pitch spelling and implement measure-aware accidentals. | +| Giuliani arpeggio studies are automatically single-voice and easy to transcribe | Many contain simultaneous bass/upper notes or sustained voices. Verify a source and explicitly label any single-line adaptation. Never invent an exercise and attribute it to Giuliani. | +| ABC is a roughly 300-line, notation-perfect shortcut | A documented subset is reasonable; full ABC is not. Validate supported syntax and reject unsupported music instead of silently changing it. MIDI is also viable, especially quantized MIDI; it is deferred for scope, not because it is inherently unsuitable. | +| Hiding the single-note UI prevents regressions | `useGameLogic` currently runs unconditionally in `App`. Its matching/metronome effects must actually be inactive in Phrases. | +| Pushing `origin/main` deploys GitHub Pages | `origin` is git.0jm.de; `github` is the GitHub remote. The checked-in workflow deploys on GitHub `main`. Do not conflate backup and production deployment. | -### 3.1 Core (MUST) +## 3. Stakeholder requirements — required for v1 -- **R1 — Third game mode.** `gameMode: 'phrase'` alongside the existing two. - Header shows three toggles. Switching modes must not regress existing modes. -- **R2 — Multi-bar rendering.** The staff renders a phrase of 1–8 bars with - correct notation: key signature (reusing existing key machinery), time - signature, note durations (whole/half/quarter/eighth, dots if the library - needs them), bar lines. Reuses the per-instrument clef + guitar transpose - logic, incl. grand staff for piano. Notes fit the width; narrow screens show - fewer bars per system (wrapping into multiple systems). -- **R3 — Cursor & feedback.** The note you currently have to play is visually - marked (e.g. colored). Played notes get subtle pass/fail coloring. No big - popups per note — the "minimal feeling" of the existing modes must be kept. -- **R4 — Step mode (beginner).** The next note is armed only after the current - one was matched (same flicker-tolerant matching as today: `MatchTracker`, - 50 ms hold). Wrong notes simply don't advance (plus the existing "skip" - affordance to bail out of a note). -- **R5 — Tempo mode.** Metronome at the existing BPM setting; 1-bar count-in; - the cursor advances at each note's notated duration regardless of hit/miss. - A note counts as *hit* if the correct pitch is detected and held for the - match threshold at any point within its window. Missed notes are marked but - never stop the run. At the end: a compact summary (X/Y notes) with - *Retry* / *Next phrase*. -- **R6 — Generated melodies.** Rule-based generator producing short - single-voice melodies: all pitches within the chosen key/scale **and** the - instrument's playable note set (existing difficulty ranges — incl. custom - fret windows, i.e. "position playing" comes for free via the existing - custom_fret difficulty), durations summing exactly to full bars, sane - musical rules (see 4.1), deterministic under a seed → fully unit-testable. -- **R7 — Exercise library.** A bundled set of fixed exercises parsed at - runtime from the chosen file format (see 5), plus a picker in the mode's - UI (title list, grouped by source/difficulty). -- **R8 — Play-phrase preview.** A "Play" button (and keyboard shortcut) plays - the phrase through the existing `AudioEngine` so the user can hear it before - reading it — the phrase analog of the existing "Play Note". -- **R9 — Zen mode compatibility.** Phrase Mode honors Zen mode (hide chrome, - keep staff + minimal controls) and works in landscape/mobile layouts. -- **R10 — No regressions.** Existing tests pass; new logic is unit-tested; - build and lint stay green; README and TECHNICAL_REALIZATION.md updated. +| ID | Requirement / observable outcome | +|---|---| +| R1 | **Preserve simplicity.** Third mode only; a compact material selector and transport. Advanced generation/import settings live in a collapsible panel. No per-note popups or dashboard. | +| R2 | **Readable notation.** A moving highlight in a stable, responsive view of 1–4 bars at a time; pieces may be longer. Show real durations, rests, ties, bar numbers, clef, and key/time signatures. Do not shrink eight bars onto a phone. | +| R3 | **Correct sequential matching.** Wrong notes wait in at-your-pace mode. Repeated equal pitches need separate playing; holding one note cannot finish a phrase. Rest and tie behavior is explicitly defined in the contract. | +| R4 | **Forgiving tempo practice.** Count-in, Start/Pause/Resume, misses that never stop the run, and a compact matched/skipped summary. No claim of millisecond-level rhythmic assessment. | +| R5 | **Musical generation.** 1–8 bars, default two bars in 4/4; 3/4 also supported. Seeded generation respects the selected scale and playable note set. Favor steps, small contours, and a stable ending rather than unconstrained randomness. | +| R6 | **Any key and scale drills.** All 12 tonic pitch classes; major, natural minor, major/minor pentatonic, and the diatonic modes. One-/two-octave scales or a scale-through-position drill; up/down/both. Only offer a full octave scale when the range can contain it. | +| R7 | **Position-aware guidance.** Reuse note-set/tuning settings; additionally offer a phrase-local fret window for fretted instruments, including bass. Restrict hints to allowed positions. Explain that mic matching checks pitch, not fingering. | +| R8 | **Fixed material.** File-backed library with provenance, work/exercise identification, and adaptation notes. Aim for 4–8 good items, not a quota of unverified transcriptions. Include verified Giuliani material if a suitable lawful source is available. | +| R9 | **Add material without rebuilding.** Minimal local `.abc` file import, validation feedback, and a documented example. In-session use is sufficient; no library manager or import persistence required. | +| R10 | **Focused practice.** Select a bar range in a longer exercise. Retry preserves exactly the same notes; Next moves to the next range (or generates a new melody). Optional Repeat loops the selected range with a count-in in tempo mode. | +| R11 | **Listen before playing.** Cancellable phrase preview at the selected BPM, isolated from scoring. It must stop on mode/material changes and not leave queued notes sounding later. | +| R12 | **Zen, mobile, and accessibility.** Zen retains discreet touch-accessible transport/mic controls and Retry/Next at completion. Keyboard use, visible focus, non-color-only current-note indication, both themes, and reduced motion work. | +| R13 | **Safe lifecycle.** No background scoring/playback after leaving the mode. Pause on a hidden tab, capture interruption, or audio suspension. Existing microphone permission/error handling and virtual instruments remain usable. | +| R14 | **Tested delivery.** Unit/integration tests plus real-browser checks of notation, virtual playing, and synthetic microphone input. Existing tests, lint, and build pass. Documentation states actual limitations. | -### 3.2 Nice to have in v1 (SHOULD — done only if time permits after the core) +### Default experience -- S1 — End-of-phrase auto-advance ("practice loop"): after the summary, the - same phrase repeats (Retry) or the next one loads automatically. -- S2 — Scale drill material: runs/arpeggios in a selected key and position, - as either generated material or tiny bundled exercises (this is the - "scales in different positions" ask expressed in the simplest useful form). -- S3 — Repeat marks / looping a sub-range of a longer exercise. -- S4 — Ear-training variant of phrases ("listen to the phrase, then play it - back") — deliberately deferred, see 3.3. +Open Phrases → a two-bar C-major melody in the current instrument range → press +Start → play the highlighted notes at your pace → Retry the identical melody +or choose New melody. Nothing sounds until requested; microphone capture still +requires permission/user intent. If the microphone is unavailable, choose the +virtual instrument rather than presenting an unplayable screen. -### 3.3 Explicitly out of scope for v1 (WON'T) +Settings are progressive: the compact selector can read +“Melody · C major · At your pace”; editing it opens the setup panel. The old +single-note metronome and key-signature controls must not also appear as +competing phrase controls. -- **Chords / multiple simultaneous voices.** The matching pipeline is - monophonic by design (single pitch per frame). The data model will keep a - `voice`-shaped structure so a polyphonic future is not blocked, but v1 - renders and matches single voice only. -- **Polyphonic pitch detection** (fundamental research territory). -- **Scoring history / statistics persistence.** -- **Importing arbitrary user files via file picker** — the library lives in - the repo; a user-facing importer (file picker → localStorage) is a small - follow-up once the format is proven. -- **MIDI import.** See 5 for the reasoning; can be added later as a - build-time/offline converter into the same internal model. +## 4. Content and format decisions -## 4. Functional specification +**ABC is the primary v1 interchange format**, with a deliberately limited, +documented single-voice subset. The normalized score is independent of ABC, +so a future MIDI or MusicXML adapter can use the same trainer and renderer. -### 4.1 Melody generator (rule set for v1) +Support common simple notation, including rests, dotted durations, and ties. +Simple repeats and pickups have explicit bounded behavior in the contract. +Unsupported tuplets, polyphony, meter changes, etc. produce actionable errors. +Do not advertise support for arbitrary ABC files. -Configurable via settings: key/mode, number of bars, rhythm complexity level, -and the existing difficulty range (which constrains the playable pitch set, -including fretboard position via the custom fret window). +A bundled exercise must identify its source and the transcription's rights, +not just cite the composer's death date. Generic arpeggio patterns may be +original exercises; they must not be presented as numbered historical studies. +If a Giuliani transcription cannot be verified, ship other verified/original +material and record that limitation. Do not let content research block the app. -Rules (v1 — deliberately simple, testable invariants in parentheses): +## 5. Scope boundaries and optional improvements -1. Pitch pool = scale of the chosen key/mode ∩ playable notes of the - instrument/difficulty. *(invariant: every generated pitch ∈ pool)* -2. Start on a stable degree (1, 3 or 5 of the scale); end on the tonic - (or dominant), placed on a strong beat. -3. Mostly stepwise motion (≈70% steps of a 2nd); leaps mostly 3rd/4th, - rare 5th/6th/octave; a leap is followed by a step in the opposite - direction (classic counterpoint heuristic — produces melodic, not - chaotic, lines). *(invariant: no two consecutive leaps > 4th without - an intervening step in opposite direction)* -4. No immediate repetition of the same pitch more than twice in a row. -5. Rhythm: per-bar patterns drawn from a complexity-tiered pool - (level 1: quarters; level 2: + paired eighths / half notes; level 3: - + dotted quarters & mixed). No rests in v1. Final note ≥ half note. - *(invariant: durations per bar sum to the bar exactly)* -6. Motivic shape: 4-bar phrases built as 2 bars of motif + 2 bars answering - (transposed/varied motif) — cheap to implement, dramatically improves - "sounds like a melody" impression. Optional at difficulty level 3 only. -7. v1 keys: all majors and minors from the existing key-signature list plus - major/minor **pentatonic**; additional modes (Dorian, Mixolydian, …) are - trivial follow-ups of the same scale machinery. +**Not v1:** chords, polyphonic recognition, a full notation editor, complete +ABC/MIDI/MusicXML compatibility, phrase ear-training, persistent score history, +strict onset/duration scoring, exhaustive fingering optimization, or automatic +production deployment. -### 4.2 Playback state machine +**Only after all required behavior is working:** more repertoire, scale-in-thirds +and arpeggio drill patterns, motif-based call/response generation, saving the +last phrase setup locally, and a MIDI adapter. These must not displace audio +correctness, scales, Zen/mobile behavior, import validation, or tests. -``` -IDLE → (start) → COUNT_IN (tempo mode only) → PLAYING → DONE → (retry|next) → … -In PLAYING: cursor at note i, - step mode: wait for match(i) → color green → cursor i+1 - tempo mode: window(i) = [t_i, t_i + dur_i); at window end evaluate - MatchTracker state → green/red → cursor i+1 -``` +## 6. Implementation sequence and checkpoints -Matching reuses `MatchTracker` unchanged (hold 50 ms, 150 ms flicker grace). -The mic is gated during speaker output exactly as today (`audioEngine. -isAudible()`), so the preview button and virtual instruments can't self-match. +| Phase | Deliverable | Gate before continuing | +|---|---|---| +| P0 | Baseline checks; normalized score, range/scale helpers, matcher/transport contracts; feature branch | Existing tests still green; regression fixtures added | +| P1 | Generated melodies + scale drills + phrase renderer + at-your-pace mode | Playable through mic and virtual input; repeated notes/rests/ties covered; responsive/Zen path usable | +| P2 | Tempo transport, count-in, pause/resume, preview, cancellation | Fake-clock and synthetic-audio tests prove no early cursor, self-scoring, or stale events | +| P3 | ABC adapter, verified library, local import, range selection/repeat | Every bundled file validated; invalid/unsupported imports handled; source notes recorded | +| P4 | Accessibility/lifecycle regression pass, browser QA, docs, delivery | All R1–R14 checked; limitations and exact verification results written down | -### 4.3 UI concept +Quality work in P4 is **not optional**. Optional features are cut first if the +implementation budget is tight. Finish required phases or explicitly report +what is incomplete; never declare the task done just because it compiles. -- Header: third toggle **"Phrases"** (naming: see 8.1). -- Main stage (non-zen): phrase staff; below it a compact status line - (step mode: "Play the highlighted note" / tempo mode: bar count-in - indicator + running hit/miss count); controls: Play (preview), - Restart, New phrase / Next exercise, plus the existing hint/virtual - instrument buttons which keep working (hint marks the current note's - fretboard position). -- End of phrase: one-line summary + Retry / Next. No modal, no confetti. -- Zen mode: staff + exit button only, as today. +## 7. Handoff and publishing -## 5. Fixed-material file format — recommendation +- Implementation begins only after approval. Read + [the implementation contract](docs/PHRASE_MODE_IMPLEMENTATION.md) before coding. +- Work on `feature/phrase-mode`, branched from the current local state. Preserve + the existing local commits. Commit coherent, tested checkpoints. +- A normal backup push of the **feature branch to `origin`** is the proposed + delivery default after “Go”. Do not update either remote's `main`, merge, + force-push, or trigger production deployment without explicit approval. +- Verify actual model/tool availability before delegation; do not assume the + suggested GLM model ID or a subagent tool exists. Use cheaper workers only + for bounded independent work with fixed interfaces and tests. The main + implementer reviews all output. No unbounded retry/research loops. +- Maintain a short implementation checklist with R1–R14 status, test commands, + decisions/deviations, known limitations, commit, and push status. This is the + completion/handoff artifact, not a claim that physical-instrument testing was + performed when only synthetic input was available. -The internal representation is a plain TypeScript model (notes with midi + -duration in beats, metadata) that both the generator and the library feed -into. The open question is the ** interchange format** for the library. - -| Option | Pros | Cons | -|--------|------|------| -| **ABC notation** ✅ recommended | Text: easy to bundle in git, to author by hand, to LLM-generate, to review in diffs; huge public-domain corpus (folk tunes in ABC everywhere); encodes bars, durations, key, **and note spelling** directly — maps 1:1 onto VexFlow; a subset parser is ~300 lines, fully unit-testable | Little pre-existing classical-pedagogy material in ABC (Giuliani studies must be transcribed — but arpeggio studies are extremely regular and transcribe quickly) | -| MIDI | Abundant files; import of anything | Binary; durations need quantization (grid inference); no spelling — must be re-derived; repeats/structure opaque; parser + quantizer ≈ same size as ABC parser with worse output | -| MusicXML | Notation-perfect | Heavy, verbose, no light-weight parser; overkill for monophonic v1 | - -**Recommendation:** bundle exercises as small `.abc` files in the repo -(`src/exercises/library/`), parsed at runtime by a small subset parser -(inline notes, durations, octaves, accidentals, bar lines, key/meter/time -headers, simple `|:` `:|` repeats expanded at parse time; no ties/grace -notes/tuplets in v1). Seed library (public domain — Giuliani died 1829): - -- Giuliani Op. 1 right-hand arpeggio studies (selection, transcribed) -- 1–2 Sor/Carcassi-style étude excerpts if the arpeggios go fast -- A handful of well-known public-domain folk melodies -- Scale/run patterns as tiny generated "built-ins" if useful - -MIDI stays possible later: an offline converter (Node script) MIDI → same -model can be added without touching the app. - -## 6. Technical plan - -New code, mirroring the existing layout: - -``` -src/ -├── music/ -│ ├── scales.ts # scale/mode definitions, key→pitch-class sets -│ ├── melodyGenerator.ts # rules → Phrase model (seeded RNG, pure) -│ └── NoteUtils.ts # extended: duration→VexFlow glyph mapping (shared) -├── exercises/ -│ ├── model.ts # Phrase / PhraseNote / ExerciseInfo types -│ ├── abcParser.ts # ABC subset → Phrase (pure, tested) -│ ├── library.ts # bundled .abc index + metadata -│ └── library/*.abc # the exercise files themselves -├── hooks/ -│ ├── usePhraseTrainer.ts # state machine from 4.2 (uses MatchTracker) -│ └── usePhraseClock.ts # audio-clock lookahead scheduler (tempo mode), -│ # patterned after the existing useMetronome -├── components/ -│ ├── PhraseSheetMusic.tsx # multi-bar VexFlow renderer w/ cursor+colors -│ ├── PhraseControls.tsx # mode-local buttons/status line -│ └── ExercisePicker.tsx # library picker (R7) -└── types/SettingsTypes.ts # new phrase-related settings -``` - -Integration approach in `App.tsx`: pitch detection stays exactly as-is and -produces `pitchData` once. If `gameMode === 'phrase'`, App renders the -`PhraseTrainer` subtree (staff + controls) fed by the same `pitchData`, mic -gate and audio engine; the existing single-note subtree is untouched. This -keeps `useGameLogic` clean and guarantees the existing modes are unregressed -by construction. - -Settings additions (all optional, with defaults — existing installs keep -working): - -```ts -phrase?: { - material: 'generated' | 'library'; - libraryId?: string; // selected exercise - key: string; // generator key/mode (e.g. 'G', 'Am', 'C-penta-maj') - bars: number; // 1–8 - rhythmLevel: 1 | 2 | 3; - pace: 'step' | 'tempo'; - bpm: number; // tempo mode; defaults to existing rhythm.bpm -} -``` - -Tempo-mode scheduling follows the proven metronome pattern (lookahead timer -scheduling against `AudioContext.currentTime`) so cursor movement doesn't -drift; note windows are derived from the same schedule, not from `Date.now()`. - -### Testing - -- `melodyGenerator.test.ts` — invariants from 4.1 (pool membership, bar sums, - leap rules, determinism under seed). -- `abcParser.test.ts` — golden strings for each syntax feature; error cases. -- `usePhraseTrainer` logic — extract pure reducer/clock math and test the - state machine transitions with a fake clock (step & tempo mode, hit/miss). -- Renderer gets a thin smoke test; visual QA happens in the browser. -- Full `npm run lint && npm run build && npm test` must pass before finishing. - -### Execution / cost control - -- Core architecture, integration and the clock/trainer logic: implemented - directly (these are the parts where mistakes are expensive). -- Delegate to cheaper subagents (pi supports spawning agents with a cheaper - model, e.g. GLM-5.3-Flash): ABC transcriptions of the Giuliani studies, - the parser/generator test corpora, the exercise picker UI and other - template-like UI work — each with tight, verifiable specs, and their output - re-checked by the main agent before merging. -- Each phase ends in a green build + commit, so the night produces usable - checkpoints, not one big risky blob. - -## 7. Implementation phases - -| Phase | Contents | Exit criterion | -|-------|----------|-----------------| -| **P1** | Model + scales + generator (rules 1–5) + multi-bar renderer + Step mode end-to-end (R1–R4, R6 core) | Playable generated melodies in step mode, no clock | -| **P2** | Tempo mode: phrase clock, count-in, per-window hit/miss, summary (R5) | Full generated-melody experience | -| **P3** | ABC parser + bundled library + picker (R7), Play-preview (R8) | Fixed exercises playable in both paces | -| **P4** | Motifs/pentatonic (rule 6–7), S1/S2 if time, Zen-mode polish, keyboard shortcuts, help text, README/TECH docs, final lint/build/tests, push | Night done | - -If time runs short, P1–P3 are the must-have spine; everything in P4 is -incremental and can be dropped mid-phase without leaving the app broken. - -## 8. Open decisions — defaults if no feedback - -| # | Decision | Default I'll assume | -|---|----------|---------------------| -| 8.1 | Mode name / labels | Third toggle labeled **"Phrases"**; existing two keep their current names ("Sight Reading", "Ear Training"). If you'd rather rename the pair (e.g. "Notes" / "Ear" / "Phrases"), say so — cosmetic change. | -| 8.2 | Library format | **ABC** (rationale in 5). MIDI deferred to a later offline converter. *If you feel strongly about MIDI as the primary format, this is the decision to veto.* | -| 8.3 | Tempo-mode strictness | Lenient: misses are marked, never abort. No per-note gating beyond the existing 50 ms hold. | -| 8.4 | Generator keys in v1 | Majors + relative minors from the existing key-signature list + major/minor pentatonic. Church modes: later. | -| 8.5 | Phrase length default | 4 bars, 4/4 only in v1 (3/4 comes free with the model; generator stays 4/4 unless trivial to add). | -| 8.6 | Hints in phrase mode | Reuse existing hint toggle: fretboard/piano hint marks the **current** note only. | -| 8.7 | Publishing | Repo `main` is currently 4 commits ahead of `origin`. Pushing `main` triggers the GitHub Pages deploy. Default: at the end of the night, push **everything** (your 4 commits + mine) after a green build, so the work isn't lost. Say "don't push" if you'd rather review first. | -| 8.8 | Seed library size | ~10–15 items (5–8 Giuliani arpeggio studies, 3–5 folk tunes, 1–2 technical patterns). Quality-checked, not bulk-dumped. | - -**Approval:** reply "Go" (optionally with edits/vetoes on the table above) and -I'll implement autonomously overnight following P1→P4. +**Approval defaults:** Phrases label; ABC subset with local import; scales and +all-key support required; at-your-pace/two-bar defaults; tempo mode is forgiving; +feature-branch backup only. Reply “Go”, optionally changing these defaults. diff --git a/docs/PHRASE_MODE_IMPLEMENTATION.md b/docs/PHRASE_MODE_IMPLEMENTATION.md new file mode 100644 index 0000000..644f53c --- /dev/null +++ b/docs/PHRASE_MODE_IMPLEMENTATION.md @@ -0,0 +1,550 @@ +# Phrase Mode — Implementation Contract + +Read [the reviewed requirements](../PHRASE_MODE_PLAN.md) first. This document is +normative for the implementation handoff. It specifies behavior, not a mandate +to use exactly these filenames. Equivalent architecture is fine if the same +invariants and tests hold. Product scope changes must be recorded, not hidden. + +## A. Verified baseline and hazards + +Review baseline: `0d11570` (initial plan, before this review commit). +`npm run lint`, `npm run build`, and `npm test -- --run` all passed during review: +39 tests in three files. Vite already warns about a large bundle. Use the +non-watch test command for unattended execution. + +Relevant current behavior: + +- `App.tsx` calls `useGameLogic(pitchData)` unconditionally, owns single-note + shortcuts, and always renders the old `Controls` outside Zen. +- `useGameLogic.ts` owns random-note selection, matching, autoplay, and the old + metronome. Merely hiding its staff does not disable these effects. +- `MatchTracker.ts` returns true repeatedly under sustained matching, resetting + itself after each success. Its threshold is an elapsed matching span tolerant + of short dropouts, **not** 50 ms of strictly accumulated matching samples. +- `usePitchDetector.ts` preserves the last display pitch up to 200 ms during + null detection. `audioLevel` is smoothed. Neither is a reliable release stream. +- `useMetronome.ts` calls `onTick` while scheduling future sounds; it also creates + a separate AudioContext and emits an 880 Hz, 50 ms tone. Do not copy that callback + behavior into the phrase clock or assume those clicks cannot be recognized as A5. +- `AudioEngine.ts` has immediate `playNote` and an audible-until gate. It currently + has no cancellable group, phrase scheduling API, or public transport clock. +- `getNoteInKey` is stateless and uses a limited signature table. It cannot alone + handle measure accidental carry, correct E#/B# spelling, or arbitrary modes. +- `SettingsProvider.tsx` stores settings in memory only. There is no existing + localStorage migration/persistence mechanism to rely on. +- Guitar/bass notation normally adds 12 semitones to sounding MIDI. Voice range + overrides and whistle's -12 display offset must continue to work. + +## B. Score contract: separate music, presentation, and performance + +Use a versioned normalized score. Both generators and import adapters must +produce it; VexFlow and the matcher must not consume raw ABC directly. + +A suitable minimal shape (names may change): + +```ts +const PPQ = 480; // ticks per quarter note; never floating beat accumulation + +type SpelledPitch = { + midi: number; // SOUNDING pitch, integer 0..127 + step: 'C'|'D'|'E'|'F'|'G'|'A'|'B'; + alter: -2|-1|0|1|2; + octave: number; // spelling octave, not floor(midi/12) +}; +type ScoreEvent = { + id: string; // stable through rendering/retries + startTick: number; + durationTicks: number; + pitch: SpelledPitch | null; // null = rest +}; +type Measure = { number: number; startTick: number; durationTicks: number }; +type Score = { + version: 1; + id: string; + title: string; + meter: { numerator: 3|4; denominator: 4 }; + key: { tonic: string; mode: string; signature: string }; // signature = parent major name, e.g. 'Bb' + measures: Measure[]; + voices: [{ id: 'melody'; events: ScoreEvent[] }]; // exactly one in v1 + source?: { + composer?: string; + work?: string; + locator?: string; // exercise/page/measure identification + url?: string; + rights: string; // source AND transcription status + adaptation?: string; + writtenToSoundingSemitones: number; // explicit import normalization + }; +}; +``` + +Invariants: + +1. Ticks are finite nonnegative integers; durations are positive. A full measure + is `numerator * PPQ` for supported meters. Explicit pickup/closing measures + may be short; other measures must be full. +2. Events are ordered, nonoverlapping, and contiguous (silence is an explicit + rest). The final event ends at the score's final tick. Reject empty/all-rest + scores for practice. No chords or second voice are silently discarded. +3. A **logical note** is one required attack. Explicit ties between equal + sounding pitches merge into one event. The renderer splits that event into + tied glyphs at measure boundaries; matcher/summary count it once. Adjacent + equal pitches without a tie stay distinct events. +4. Keep runtime state separate: selected bar range, source-event mapping, + seed, cursor, status, matched/missed/skipped results, and scheduled audio are + not mutable fields on the score. +5. A selected range clips events to the range and rebases ticks. A tie entering + the selection becomes a new attack at its beginning; internal ties remain + one logical note. This makes starting at any bar well-defined. +6. Match and synthesize SOUNDING MIDI. Preserve enharmonic spelling. Apply the + active instrument/range display transpose **once**, only when rendering. + Example: guitar-source ABC `E,` means written E3 (MIDI 52); an explicitly + configured -12 source conversion normalizes it to sounding E2 (40). Guitar + rendering then shows E3 (52), while the mic must match 40, not 52 or 64. +7. Imported ABC defaults to concert pitch. An import option explicitly selects + “written guitar/bass (sounds octave lower)” if needed. Do not infer this + merely because the user's current instrument is guitar. Bundled metadata + supplies the appropriate convention. An optional practice octave shift is + separate and visible; never silently shift individual notes to fit a range. + +## C. Theory, playable range, and generation + +### C1. Shared range resolution + +Extract the existing playable-note calculation into a pure helper with tests. +Preserve its old single-note behavior. Phrase mode uses that set by default. +When an explicit phrase-local fret window is enabled, derive pitches directly +from the tuning and that window instead of intersecting with the old note-set +bounds (which would incorrectly exclude high positions, especially on bass). +Make this override visible and disable the competing note-set control; retain +a selected single-string restriction if applicable. Generated material then +intersects the resolved playable set with the scale; fixed scores do not. + +- Deduplicate and numerically sort pitches. Validate instrument, tuning, range, + and `0 <= minFret <= maxFret <= 24`; prevent NaN/empty values from reaching + generation. A stored guitar tuning must not constrain piano/voice/whistle. + The microphone's instrument-floor calculation must likewise ignore irrelevant + tunings; test whistle/voice/piano after switching from guitar. +- Fret constraints apply to guitar **and bass**. Do not depend on bass having a + `custom_fret` range in `InstrumentConfigs` (currently it does not). +- Each allowed guitar/bass pitch must have at least one actual allowed + `{stringIndex, fret}`. Restrict hints to those positions, including a selected + single string. Ensure the board can display the selected frets (it currently + hardcodes 15 in `App`). Do not show off-position hints. +- This is pitch availability, not an optimized fingering path or a guarantee + about what string the microphone heard. State that limitation in setup/help. +- Empty pools produce an actionable error (“Widen the range or change key”), + never a fallback chromatic/out-of-range note. Generated melodies require at + least two distinct eligible pitches; generation loops must be bounded. +- Fixed scores keep their pitches, including chromatic notes outside the key's + scale. Key signature is a notation context, not a pitch filter for repertoire. + Show range conflicts and offer an explicit octave shift or range change; do + not remove notes. Require a playable selected + range before starting. Virtual keys must also cover that selected range. + +### C2. Scales and spelling + +Support 12 tonic pitch classes, not just the limited existing dropdown. Modes: + +| Mode | Semitone offsets from tonic | +|---|---| +| Major / Ionian | 0, 2, 4, 5, 7, 9, 11 | +| Dorian | 0, 2, 3, 5, 7, 9, 10 | +| Phrygian | 0, 1, 3, 5, 7, 8, 10 | +| Lydian | 0, 2, 4, 6, 7, 9, 11 | +| Mixolydian | 0, 2, 4, 5, 7, 9, 10 | +| Natural minor / Aeolian | 0, 2, 3, 5, 7, 8, 10 | +| Locrian | 0, 1, 3, 5, 6, 8, 10 | +| Major pentatonic | 0, 2, 4, 7, 9 | +| Minor pentatonic | 0, 3, 5, 7, 10 | + +Use a conventional enharmonic key spelling requiring at most seven signature +accidentals. The chosen **mode** affects the tonic spelling and parent signature; +do not force one spelling of every root across all modes. Present the resolved +name in the UI (e.g. C# minor, not a theoretical Db-minor signature). + +Derive modal signatures from their parent major scale. Pentatonics use their +corresponding major/natural-minor signature and omit scale degrees. Preserve +E#, B#, Cb, and Fb where required; do not approximate using a sharp/flat chromatic +name array. Allow imported double accidentals in the spelling model. + +### C3. Melody generator + +API concept: `generateMelody(config, eligiblePitches, seed) -> Result`. +All randomness flows through the supplied seeded PRNG. Save the seed with the +session; Retry never calls generation. New melody explicitly changes the seed. + +- Generated length: integer 1–8 bars, default 2; meter 4/4 default, 3/4 supported. +- Two rhythm levels for v1: **Simple** (quarter/half/dotted-half/whole notes as + meter permits) and **Mixed** (also paired eighths and dotted quarters). + Use validated per-bar rhythm templates, never an unbounded fill-and-retry loop. + Simple may use quarters internally and a longer final note; do not promise + both “quarters only” and “last note must be a half note”. Rests need not be + generated in v1, but the score/importer/trainer must support them. +- Prefer movement to an adjacent degree of the full chosen scale, then thirds; + larger moves are rarer. An “adjacent eligible pitch” is not necessarily an + adjacent scale degree in a sparse fret/string pool. Weight by actual interval. +- Prefer start/end on tonic, otherwise another available stable degree. Land + the final note on a beat and normally hold it for at least two quarter beats. +- Avoid more than two equal notes in a row where alternatives exist. Favor a + step back after a leap of at least five semitones when such a step is playable. +- Hard invariants: scale/range membership, exact bar totals, valid spelling, + determinism, and termination. Musical preferences are **soft** when sparse + ranges make them impossible. Do not write unconditional “70% steps” or leap + assertions that fail valid restricted ranges. Do test preferences over a + large seeded sample with a normal diatonic range. +- Large disconnected pools should yield the best available in-range path or a + clear “range too sparse for this exercise” result, not silent constraint escape. +- Motif/answer transformations are optional and must revalidate all constraints. + +### C4. Scale drills (required, separate from random melodies) + +- Options: direction up/down/up-and-down; coverage one octave, two octaves, or + **within position**. Same tonic/mode/range controls as generated melodies. +- For octave drills, choose the lowest eligible tonic whose complete requested + scale path fits the playable set. If none fits, explain and offer “within + position” or a wider range; never output an incomplete “one-octave scale”. +- Within-position drills traverse the eligible scale notes in order and are + labeled as such; they may start/end away from tonic. They are not a promise + of a conventional named fingering pattern. +- Default quarter notes, no duplicate turning-point note in up-and-down runs. + Derive bar count from the run, not the melody's bars setting; extend the final + note to complete a bar, splitting with ties if needed. Cap score sizes. +- Hints apply the same position filter as generation. No finger numbers/tab + are needed for v1. Step and tempo practice use the exact same score. + +## D. Input, matching, and scoring + +### D1. Fresh input contract + +Add a separate raw-frame callback/subscription to the pitch-detection path: +`{ sequence, observedAtMs: performance.now(), midi: number|null, rms }`. +Emit it on **every** analysis tick, including null pitch. Preserve the existing +smoothed display outputs and instrument-specific detector algorithms. + +Deliver frames directly to the phrase controller (or a small subscribed store), +not by pretending every React render is another audio sample. Ignore duplicate +sequence IDs; clear subscribers on unmount. Read raw RMS from the analyzer, not +the smoothed UI meter, if using it. Do not reconstruct releases from `pitchData`. + +For timed practice, stamp a frame with the **phrase transport AudioContext's** +current time when its callback is delivered. This is detection-arrival time, +not a measured acoustic attack time. Do not compare timestamps from unrelated +AudioContexts or compare `Date.now()` to AudioContext seconds. Keep performance +time for freshness/gap checks. No latency calibration is claimed in v1. + +Offer microphone or virtual input explicitly. Start in microphone mode waits +for capture to actually succeed; permission denial shows the existing error and +a virtual-input alternative. Virtual mode works without microphone permission. + +### D2. Matching rules + +Implement a phrase matcher/controller around or beside `MatchTracker`: + +1. Match exact MIDI including octave. Require the existing 50 ms tolerant + matching span on fresh mic frames; reset the candidate and accumulated + release evidence on a sample gap over 100 ms or playback gating. A single old frame cannot become a held note. +2. Each logical event has a **latched**, one-way result. A second success for + that event is ignored. Never inspect the reset tracker at a window's end + and mistakenly conclude that an earlier success did not happen. +3. For consecutive untied equal pitches, a sustained signal must not satisfy + the next event. Re-arm after at least **80 ms** of fresh null/different-pitch + frames, then require a fresh match. Brief detector dropouts must not re-arm. + Track this even across rests and tempo boundaries; one sounding episode + cannot earn credit again when a later slot returns to the same pitch. + Matching a genuinely different pitch establishes a new episode. +4. Baseline v1 cannot reliably detect a same-pitch repluck with no detected gap. + Tell the user “For repeated notes, briefly release/mute between notes.” + Fast repeated notes may require a slower BPM, particularly with long bass + analysis windows. Do not invent a robust onset detector from smoothed volume. + A tested raw-RMS reattack detector is optional later, not a release condition. +5. A virtual click/tap is an explicit attack and can immediately match once; + repeated taps can match repeated equal pitches without a mic hold. Its + synthesized sound must not contribute a second microphone result. +6. At-your-pace mode: match advances immediately to the next logical note; + ties are one note; rests display but are automatically traversed, with no + silence requirement or timing score. Wrong notes wait. Skip marks a separate + skipped result and advances. Never label eventual completion “100% accuracy”. +7. With-tempo mode: event window is `[start, end)`. A confirmed pitch anywhere + within the window latches matched; otherwise mark missed at its end. + Confirmation must complete before `end`. There is no early/late allowance + crossing into neighbors. Finish expired windows **before** applying a frame + at a boundary; never credit one frame/event to multiple targets. +8. Timed rests consume their written duration, ignore playing, and are excluded + from the score. A tied note has the combined-duration window and counts + once. V1 does not score articulation, sustained duration, or silence at rests. +9. No scoring in ready, count-in, paused, preview, done, or error states. + Keep a run/session generation token so stale callbacks cannot affect a new run. +10. Carry the consumed-pitch/re-arm protection across automatic repeats; looping + a one-note exercise must not repeatedly score one held note. + +Summary: tempo mode “Matched X of Y notes”; at-your-pace “Completed X notes, +skipped Y”. Count logical notes, not tied glyphs or rests. No live scoreboard in +Zen; small end-of-run feedback only. + +## E. Transport, audio, and cancellation + +Use a pure transition layer with an injected clock for tests, and a thin hook +for lifecycle/audio. States: `ready`, `countIn`, `playing`, `paused`, `preview`, +`done`, `error`. A score is generated/loaded once per explicit material change, +not whenever pitch, theme, BPM, or window size changes. + +### E1. Timing and transitions + +- Phrase BPM is independent of the single-note rhythm/timer controls: quarter + note = 30–180 BPM, default 60. Validate finite numbers in the controller too. +- Invoke audio-context creation/resume directly in the Start gesture, before + awaiting microphone permission (important for Safari autoplay policy). Await + both successful capture, when needed, and context resume before running. + Begin neither count-in nor timers on a suspended clock; show a recoverable + Start action. +- Use one transport/output AudioContext for phrase clicks, preview, and cursor + timing. A 25 ms scheduler with roughly 100 ms lookahead is appropriate for + audio scheduling only. Update cursor/state when `currentTime` reaches the + boundary, not when the future oscillator is queued. +- Derive each boundary from `runStart + tick / PPQ * 60 / bpm`, not repeated + floating additions or `setInterval` increments. Visual animation may use rAF. +- Count-in is one complete measure (3 or 4 quarter beats), also for a pickup. + At 60 BPM, a 4/4 count-in clicking at times 0, 1, 2, 3 starts the score at 4. +- At-your-pace Pause/Resume preserves the current logical target/results. + With-tempo Resume re-counts and restarts the current bar, clearing results + from that bar onward; a note tied into that bar becomes a fresh attack. + This is simpler and less confusing than resuming midway through a note. +- Retry resets results and restarts the identical selected music (count-in if + timed). New melody changes the seed. Library/import initially selects the + first two measures (or all if shorter); let the user select start and count, + capped at eight measures per practice range. Next advances to the next + selected-length bar range; at the end offer “Back to beginning”, + not an unrelated piece. Scale drills offer Repeat rather than random Next. +- A Repeat checkbox (off by default) retries the selected range automatically; + timed repeats get a fresh count-in. The user can always pause/stop it. +- Applying structural setup changes (key, range, source, meter, instrument, + tuning, pace) cancels the run and rebuilds to ready. Changing BPM while running + pauses; the next Resume recomputes timing. Do not retime a partly played note. +- Pause on document hidden, mic disconnection in mic mode, audio suspension, + opening a settings/help/import dialog, or an observed processing gap >250 ms. + Do not fast-forward a backgrounded phrase and mark everything missed. + +### E2. Output ownership and mic isolation + +Extend `AudioEngine` or add a shared-context phrase transport with: +`resume`, `now`, scheduled-note playback, and cancellable playback groups. +Keep existing single-note behavior intact. Track oscillator/gain nodes, not +just timers; clearing a timer cannot stop audio already queued by lookahead. + +- **Preview:** pause practice, play the selected range at its written durations, + with no scoring. The button toggles Preview/Stop preview. On stop/end return + to ready/paused, never automatically to playing. Gate mic input through the + actual output plus the existing 400 ms tail. Cancel future sounds and remove + their future gate reservations; a canceled long preview must not leave the + mic gated until its former end time. +- **Virtual-note audio:** explicit taps count; their speaker sound does not. + Preserve the existing mic gate for pitched reference/virtual audio. +- **Metronome:** do not feed every click through that 400 ms pitched-audio gate. + Default click sound off (visual count-in/beat cue on). Audible clicks are an + explicit option with a headphones recommendation. Use a short non-tonal + click (e.g. <=10 ms noise burst), not the current 880 Hz tone. Exclude mic + samples for 30 ms after a scheduled audible click; do not count that blanked + interval as release evidence. Validate that normal notes still match with + clicks enabled. This is a modest bleed safeguard, not acoustic echo cancellation; + speakers/room reverb may still require headphones or silent clicks. +- Silent/zero-volume output must not create audible gates or tiny residual tones. +- Cancel all scheduled groups, rAFs, timers, and subscriptions on Stop, mode + switch, score replacement, or unmount. A canceled callback also checks its + run token. Do not create a new AudioContext for every render or note. + +## F. Notation and view contract + +Create a phrase renderer rather than forcing multi-note behavior into the +single-whole-note `SheetMusic` component. Share styling utilities if useful. + +- Preserve source spelling; maintain accidental state by letter AND octave + within each measure, initialized from the key signature and reset at barlines. + Naturals cancel a signature accidental; reintroduced sharps/flats need a sign. + Example, `K:G`, `L:1/4`, `=F F ^F F | F4 |`: first two Fs natural, next two + sharp, next-bar F sharp by signature. The third note needs an explicit sharp. +- Use real note spacing, stems, dots, and beat-appropriate eighth-note beams. + Split long logical notes with ties. Support 3/4 and 4/4 without abusing + VexFlow soft voices to conceal malformed bar totals. Pickups are explicit. +- Use the active instrument/range clef/transpose. For piano grand staff, align + both staves to the same timeline, using invisible rhythmic placeholders where + needed; the score is still monophonic, not two simultaneously scored voices. +- Show a bounded window of measures: typically two, one on narrow screens, + up to four when space permits. Advance at a bar/page boundary, not on every + note; keep the current note and following material readable. Repeat clef/key + at each new displayed system. Show source bar numbers/range for orientation. +- Re-layout on score/range/width/theme changes. Do not rebuild the entire VexFlow + SVG at microphone frame rate. Memoize static notation; update current-note + indicator and result styles at event transitions. No live detected-note + “second measure” should push the phrase around. +- Current note uses a caret/underline or outline as well as color. Passed and + missed coloring remains subtle and legible in both themes. Honor reduced + motion; preserve the existing ledger-line and system-theme-change fixes. +- Small portraits may scroll a dense measure horizontally as a last resort, + but no clipping/illegibly tiny global scaling. Validate actual browser output, + not just a JSX smoke test. Give the score an accessible text description. + +## G. ABC adapter, library, and local import + +Keep the parser behind `parseAbc(text, options) -> Result`. +A documented subset parser is acceptable; a maintained parser dependency is +also acceptable if its output is normalized/validated and the bundle impact +is justified. Do not implement full ABC from an optimistic line-count estimate. + +Required subset: + +- One tune/voice; headers `X`, `T`, `C`, `S`, `Z`, `M`, `L`, `Q`, `K`; blank + lines and `%` comments. Keys: tonic letter plus optional `#`/`b`, followed + by no suffix/`maj` (major), `m`/`min` (natural minor), or the modal suffixes + `ion`, `dor`, `phr`, `lyd`, `mix`, `aeo`, `loc`; e.g. `K:F#`, `K:Am`, + `K:Ddor`. Reject unrecognized key specifications, never fall back to C. +- `A`–`G`, `a`–`g`, comma/apostrophe octaves, `^`, `_`, `=`, and doubled + accidentals; durations via integer/fraction suffixes; rests `z`. +- `L:1/4` and `L:1/8`; supported resulting durations: eighth, quarter, + dotted quarter, half, dotted half, whole. Other values return a clear error. + Dots are encoded using ABC length multipliers, not invented note-dot syntax. +- `|`, `||`, `|]`; simple nonnested `|: ... :|` repeats exactly twice, expanded + before normalization. No alternate endings or implicit/nested repeats. +- Explicit `-` ties between equal pitches, including across barlines. Preserve + spelling/accidental carry while parsing, merge ties into logical events after. +- 3/4 and 4/4; optional first short pickup. A short final measure is allowed + if it complements that pickup; other short/overfull measures are errors. + Source barlines are validated, not guessed from an arbitrary stream of notes. +- `Q` is metadata/default suggestion; the user's phrase BPM controls practice. + No tempo/meter/key changes partway through a tune in v1. + +Reject with line/column and a useful message: chords `[CEG]`, multiple tunes or +voices, tuplets, grace notes, unsupported decorations/directives, alternate +endings, malformed ties, unsupported lengths/meters, unknown syntax. An explicit +whitelist may ignore recognized nonmusical metadata; never silently strip +unknown notation. Single-voice extraction from a polyphonic file is not automatic. + +Bounds: at most 256 KiB input, 256 expanded measures, and 4096 events. Apply +bounds during tokenization/repeat expansion, not only after allocating output. +No eval/HTML injection, fetched URLs, or external resource interpretation. +Render titles/errors as text. Invalid imports keep the previous score intact. +File loading is local/in-session and available via a setup-panel action. + +Use this concert-pitch example as a parser/authoring fixture: + +```abc +X:1 +T:First steps +C:Original exercise +M:4/4 +L:1/4 +Q:1/4=60 +K:C +C D E F | G2 E C |] +``` + +Expected MIDI: `60, 62, 64, 65, 67, 64, 60`; durations in ticks: +`480, 480, 480, 480, 960, 480, 480`; two complete bars. Also test +`C4- | C4 |` as one eight-quarter-note logical event, and a pickup such as +`G | C D E F | G3 |]` (with the same meter/unit/key). + +Bundle files with a manifest giving title, composer, source URL/locator, +transcription rights, adaptation, and pitch convention. Use Vite `?raw` imports +or base-path-safe assets, not a dev-only filesystem fetch. Parse/validate every +bundled item in tests. Document the exact subset plus a copyable working file. + +Content policy: + +- Target 4–8 checked items, including original scale/interval/arpeggio exercises + and verified public-domain melody excerpts. Strive for a verified Giuliani + single-line adaptation, but do not fabricate provenance to satisfy a quota. +- Giuliani Op. 1 may require omitting simultaneous or sustained parts. Identify + which exercise/measures and precisely what was changed. A composer's old + work does not automatically clear a modern edition/arrangement/transcription. +- Original generic technical exercises are labeled “Original exercise”, not + “Giuliani No. X” or a guessed Sor/Carcassi excerpt. Keep a small SOURCES/readme + alongside the manifest. Quality and verifiability outweigh library size. + +## H. App integration, settings, and shortcuts + +Suggested responsibilities: + +- Pure `music/` helpers: range resolution, scale/key spelling, seeded generators. +- `exercises/`: score model/validation, ABC adapter, library and source metadata. +- `game/`: phrase matcher and pure transport transitions. +- `audio/` plus a hook: cancellable scheduled audio and raw-frame/clock adapter. +- Components: phrase trainer, renderer, compact setup/transport. + +Render **either** `SingleNoteTrainer` (owns `useGameLogic`) **or** `PhraseTrainer`. +Extracting the old UI/hook into a child is preferable to calling hooks +conditionally. Alternatively gate all old effects explicitly and test it. +App may continue to own shared microphone capture, theme, and global chrome. +Clean up pre-existing timers when extracting the old subtree. + +Phrase configuration includes material (`melody|scale|library|import`), tonic +pitch class/mode, melody bars/meter/rhythm level, scale direction/coverage, +optional fret window, source/range/octave shift, pace, input, BPM, click sound, +and Repeat. Runtime status/seed/results are separate. Defaults: melody, C major, +2 bars, 4/4, Simple, at-your-pace, mic input, 60 BPM, click off, Repeat off. +Scale defaults: one octave/up-and-down, with an actionable error if it cannot fit. + +Keep phrase controls independent from legacy `rhythm.active/autoAdvance/seconds`. +Hide/replace the legacy key/rhythm panels in phrase mode while keeping shared +instrument, tuning, range, tuner, and theme controls. Do not accidentally force +single-note autoplay for phrases. Settings changes must not regenerate scores +when merely toggling hints/theme or receiving a microphone frame. + +Zen shows notation plus a discreet transport (Start/Pause, mic status/control, +Exit); at completion expose Retry/Next. Never require leaving Zen or using a +keyboard to recover from pause/completion/error. Setup/library UI stays hidden. + +Keyboard policy: retain existing single-note shortcuts in those modes. In +Phrases: Space Start/Pause/Resume, P Preview/Stop preview, R Retry, N Next/New, +S Skip (at-your-pace only). M cycles all three modes; Z/H/V/L retain their roles. +Help reflects the active mode. Do not handle global shortcuts inside input, +textarea, select, button, contenteditable, or open dialog; ignore modifiers and +key auto-repeat for destructive/transport actions. Enter/Space on focused buttons +must trigger only their native action, not an additional global skip/start. + +Optional persistence stores only validated versioned setup/selection; never +mic permission, autoplay/resume state, audio objects, raw frames, or live results. +Failure/unavailable storage must not prevent practice. + +## I. Required verification and completion record + +Run baseline and final commands: + +```sh +npm run lint +npm run build +npm test -- --run +``` + +Minimum regression cases (automate pure logic/integration cases): + +| Area | Required evidence | +|---|---| +| Theory | Every tonic × supported mode; valid spelling/signature, correct pitch classes, F# major E# and C# major B#, instrument octave conversions | +| Generation | At least 100 seeds per normal configuration; exact bar sums, allowed pitches, determinism; empty/single/sparse pools; alternate tunings and invalid fret ranges; zero unbounded retries | +| Scales | C major one octave gives C D E F G A B C in eligible register; down/both order; turning tonic once; full-octave refusal when a degree is unavailable; position-only hints | +| At your pace | C–D–E advances only in order; wrong pitches wait; C–C needs release/re-tap; 20 ms dropout does not re-arm; 80 ms release does; ties count once; rests don't stall; Skip is not a hit | +| Freshness | Duplicate/stale frames do not score; held display data on null raw frames cannot match; >100 ms sample gap resets candidate; mic-off/permission failure doesn't run a broken session | +| Tempo | Fake-clock count-in starts exactly at the notated boundary; queuing 100 ms ahead does not advance early; last-note hit latches; boundary samples belong only to new window; misses/rests advance on time; held C cannot score C–rest–C twice | +| Audio | Preview never scores; cancel removes future sounds/gates; repeated virtual taps work; zero-volume has no gate; synthetic metronome-only input does not score A5/other notes; clicks plus played notes remain usable | +| Lifecycle | Rapid Start/Stop/Retry and React StrictMode do not duplicate timers/notes; mode switch leaves exactly one trainer active; pause/resume/bar reset; tab hidden and audio/mic interruption pause rather than catch up | +| Import | Every bundled ABC validates; octave/accidental carry, naturals, dotted lengths, ties, pickups, bounded repeats; unsupported polyphony/tuplets, malicious title strings, malformed/oversized inputs fail safely | +| Range/repeat | Retry uses same seed/events; Next selects correct bars; clipped tie becomes attack; automatic Repeat cannot farm one sustained pitch; out-of-range material never silently changes pitch | +| Rendering/UX | Real browser: desktop, ~390px portrait, short landscape; light/dark/auto, treble/bass/grand, fret hints, mixed rhythm/ties; readable notation; no full SVG reconstruction at raw frame rate; keyboard and touch Zen completion | +| Legacy | Sight Reading and Ear Training, autoplay/quiet wait, tuner, microphone toggle, virtual instruments, theme, shortcuts, and old metronome continue to work | + +Use synthetic microphone input in a real browser where feasible (a scripted +oscillator into `MediaStreamDestination`, substituting `getUserMedia`), not +only virtual clicks. Distinguish that from testing a physical guitar in a room. +Save reproducible tests/scripts rather than relying on one-off visual claims. +If a browser/device is unavailable, identify the missing coverage explicitly. + +Delivery checklist must map R1–R14 to code/tests and state what was actually +verified, any content/parser/detection limitations, and remaining optional work. +Do not suppress lint/type errors, disable tests, silently reduce required scope, +or claim “no regressions by construction”. + +Git: the reviewed baseline has local commits not on `origin/main`. Preserve +those; use `feature/phrase-mode` and ordinary checkpoint commits. Approved backup +means that branch to `origin` (git.0jm.de). GitHub Pages would be triggered by +updating GitHub `main`; deployment is not part of approval by default. Record +push failures; do not force-push or change remote configuration to bypass them.