mirror of
https://github.com/9x/sheetmusictrainer.git
synced 2026-11-03 22:03:54 +01:00
Review phrase mode requirements and specify implementation contracts
This commit is contained in:
@@ -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
|
This replaces the initial GLM draft. The product direction is good; the review
|
||||||
Music Trainer. It is written as stakeholder requirements: what the mode does,
|
corrects several music, audio, scope, and deployment assumptions. Implementation
|
||||||
what it deliberately does **not** do, how it will be built, and which decisions
|
must follow this document **and** [the implementation contract](docs/PHRASE_MODE_IMPLEMENTATION.md).
|
||||||
need the owner's sign-off. Section 8 lists the open decisions together with the
|
The contract makes behavior and acceptance tests explicit for the next model.
|
||||||
defaults I will assume if no feedback arrives in time — so an autonomous
|
If they appear inconsistent, resolve the inconsistency before implementing;
|
||||||
overnight run can proceed without blocking on any of them.
|
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
|
Three material choices, not three additional top-level modes:
|
||||||
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.
|
|
||||||
|
|
||||||
Phrase Mode adds exactly that, without disturbing the minimal feel of the
|
- **Melodies:** reproducible, rule-based generation in a selected key/mode and range.
|
||||||
existing modes:
|
- **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,
|
Two practice paces:
|
||||||
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.
|
|
||||||
|
|
||||||
Zen mode, mic handling, virtual instruments, themes and all existing modes stay
|
- **At your pace** (default): wait for each correct pitch; rhythm is displayed,
|
||||||
as they are. Phrase Mode is *additive*: a third toggle next to "Sight Reading"
|
not assessed. Wrong notes don't advance; an explicit Skip is available.
|
||||||
and "Ear Training".
|
- **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 |
|
## 2. Important corrections to the first draft
|
||||||
|---|--------|
|
|
||||||
| 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. |
|
|
||||||
|
|
||||||
## 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.
|
| ID | Requirement / observable outcome |
|
||||||
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
|
| 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. |
|
||||||
correct notation: key signature (reusing existing key machinery), time
|
| 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. |
|
||||||
signature, note durations (whole/half/quarter/eighth, dots if the library
|
| 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. |
|
||||||
needs them), bar lines. Reuses the per-instrument clef + guitar transpose
|
| 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. |
|
||||||
logic, incl. grand staff for piano. Notes fit the width; narrow screens show
|
| 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. |
|
||||||
fewer bars per system (wrapping into multiple systems).
|
| 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. |
|
||||||
- **R3 — Cursor & feedback.** The note you currently have to play is visually
|
| 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. |
|
||||||
marked (e.g. colored). Played notes get subtle pass/fail coloring. No big
|
| 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. |
|
||||||
popups per note — the "minimal feeling" of the existing modes must be kept.
|
| 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. |
|
||||||
- **R4 — Step mode (beginner).** The next note is armed only after the current
|
| 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. |
|
||||||
one was matched (same flicker-tolerant matching as today: `MatchTracker`,
|
| 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. |
|
||||||
50 ms hold). Wrong notes simply don't advance (plus the existing "skip"
|
| 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. |
|
||||||
affordance to bail out of a note).
|
| 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. |
|
||||||
- **R5 — Tempo mode.** Metronome at the existing BPM setting; 1-bar count-in;
|
| 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. |
|
||||||
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.
|
|
||||||
|
|
||||||
### 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
|
Open Phrases → a two-bar C-major melody in the current instrument range → press
|
||||||
same phrase repeats (Retry) or the next one loads automatically.
|
Start → play the highlighted notes at your pace → Retry the identical melody
|
||||||
- S2 — Scale drill material: runs/arpeggios in a selected key and position,
|
or choose New melody. Nothing sounds until requested; microphone capture still
|
||||||
as either generated material or tiny bundled exercises (this is the
|
requires permission/user intent. If the microphone is unavailable, choose the
|
||||||
"scales in different positions" ask expressed in the simplest useful form).
|
virtual instrument rather than presenting an unplayable screen.
|
||||||
- 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.
|
|
||||||
|
|
||||||
### 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
|
## 4. Content and format decisions
|
||||||
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. 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,
|
A bundled exercise must identify its source and the transcription's rights,
|
||||||
and the existing difficulty range (which constrains the playable pitch set,
|
not just cite the composer's death date. Generic arpeggio patterns may be
|
||||||
including fretboard position via the custom fret window).
|
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
|
**Not v1:** chords, polyphonic recognition, a full notation editor, complete
|
||||||
instrument/difficulty. *(invariant: every generated pitch ∈ pool)*
|
ABC/MIDI/MusicXML compatibility, phrase ear-training, persistent score history,
|
||||||
2. Start on a stable degree (1, 3 or 5 of the scale); end on the tonic
|
strict onset/duration scoring, exhaustive fingering optimization, or automatic
|
||||||
(or dominant), placed on a strong beat.
|
production deployment.
|
||||||
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.
|
|
||||||
|
|
||||||
### 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.
|
||||||
|
|
||||||
```
|
## 6. Implementation sequence and checkpoints
|
||||||
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
|
|
||||||
```
|
|
||||||
|
|
||||||
Matching reuses `MatchTracker` unchanged (hold 50 ms, 150 ms flicker grace).
|
| Phase | Deliverable | Gate before continuing |
|
||||||
The mic is gated during speaker output exactly as today (`audioEngine.
|
|---|---|---|
|
||||||
isAudible()`), so the preview button and virtual instruments can't self-match.
|
| 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).
|
## 7. Handoff and publishing
|
||||||
- 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.
|
|
||||||
|
|
||||||
## 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 +
|
**Approval defaults:** Phrases label; ABC subset with local import; scales and
|
||||||
duration in beats, metadata) that both the generator and the library feed
|
all-key support required; at-your-pace/two-bar defaults; tempo mode is forgiving;
|
||||||
into. The open question is the ** interchange format** for the library.
|
feature-branch backup only. Reply “Go”, optionally changing these defaults.
|
||||||
|
|
||||||
| 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.
|
|
||||||
|
|||||||
550
docs/PHRASE_MODE_IMPLEMENTATION.md
Normal file
550
docs/PHRASE_MODE_IMPLEMENTATION.md
Normal 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.
|
||||||
Reference in New Issue
Block a user