Review phrase mode requirements and specify implementation contracts

This commit is contained in:
2026-09-14 00:21:43 +02:00
parent 0d11570b58
commit 6e24b94a91
2 changed files with 668 additions and 271 deletions

View File

@@ -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.

View File

@@ -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<Score>`.
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<Score, Diagnostic[]>`.
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.