griff-preview (the preview/ crate) is a headless-testable, ratatui
terminal piano-roll for a single .mid file: it draws the score as a
pitch × time grid, classifies sections (Riff / Solo / Breakdown / Clean),
measures structure and complexity, and — with a chunk record attached —
lets you re-curate that record interactively. There is no audio;
griff is a symbolic model (MIDI in → MIDI out).
Press ? inside the app for the keybinding cheatsheet at any time.
griff-preview <file.mid> # interactive TUI
griff-preview <file.mid> --snapshot=120x40 # one headless frame to stdout, then exit
griff-preview <file.mid> --record=<chunk.json> # re-curation: a/x, t/T, r, s persist into the record
griff-preview <file.mid> --record=<chunk.json> --merge=<next.json> # also unlocks m (merge with <next>)
griff-preview -h | --help # usage line
From a checkout:
cargo run -p griff-preview -- path/to/riff.midThe curation keys edit the --record file (a corpus ChunkMeta JSON),
not the MIDI; the MIDI is shown only for context. Without --record,
tagging / rename / split / merge are inert (see Gotchas).
| Key(s) | Action |
|---|---|
? |
Toggle the help overlay (any key closes it) |
q / Esc |
Quit (writes pending curation to disk) |
Space |
Play / pause (from the end, replays from the start) |
← / h · → / l |
Scroll left / right in time |
↑ / k · ↓ / j |
Move the visible pitch band up / down |
+ / = · - / _ |
Zoom in / out (fewer / more ticks per column) |
[ · ] / Tab |
Previous / next named section (jumps to it) |
0 / Home |
Jump to the start of the score |
i |
Show / hide the inspector dock |
PageUp / PageDown |
Scroll the inspector up / down |
a |
Approve the chunk (press again to clear) |
x |
Reject the chunk (press again to clear) |
t |
Move the tag cursor to the next palette entry (wraps) |
T (Shift+t) |
Toggle the tag under the cursor |
r |
Rename the record (enter rename mode) |
s |
Mark / unmark the split point at the playhead |
m |
Toggle the merge with the --merge partner |
Rename mode (after r): type to edit, Enter commits (trimmed,
non-empty), Esc cancels, Backspace deletes. Inside this mode q is a
literal character, not quit.
- Sections (
[/]/Tab) jump between auto-classified spans. The coloured band under the header is the section map; the selected one is underlined. Colours: Riff = blue, Breakdown = red, Solo = orange, Clean = green, Unknown = grey. - Playback (
Space) advances a yellow playhead; the view autoscrolls to follow it while playing. Scroll manually when paused. - Zoom / pan are independent. On launch the zoom is chosen so the whole piece fits the plot width.
- The header reads: file ·
♩=tempo · bar count ·bar:beatposition.
A 32-column dock on the right (auto-hidden when the window is too narrow;
bring it back with i). Top to bottom:
track— the focus track's name.- The current section's class badge and
bars X–Y · N bar(s). curation— the decision pending this session (a/x).transport— tempo and▶ playing/⏸ paused, pluspos bar:beat.- Record digest (only with
--record):record <title>(orname▸ <buffer>█while renaming),review <prior decision>,tags <active>, the tag cursortag▸ <name> [x]/[ ], and any pendingsplit▸ at bar N/merge▸ + <partner>. structure (S14)—loopability,repeatability,variation,complexity, and the detectedpatternperiod.complexity (S14)— the six-axis profile (below).
Note the two decision lines: curation is what you are about to write this
session; review is what the loaded record already held. On a short
terminal the metric tail clips — reach it with PageDown.
On a single-bar score the bar-ratio metrics show — instead of a
percentage: repeatability has no second bar to compare against, so
variation (1 − 0) and the distinct-bar-signature ratios (complexity
and the str axis, 1/1) are artifacts of bar_count = 1, not
measurements. loopability (a seam measurement on any span) and the
per-note axes stay numeric.
You do not type complexity in. It is computed from the notes at import
time (core::structure::measure_complexity) and is read-only in the TUI.
The inspector shows it in two places:
- The
complexityline inside thestructure (S14)block — a single scalar: the distinct-bar-signature ratio (distinct / total). It is the same value as thestraxis below. - The
complexity (S14)block — a six-axis vector (a fact for reranking, not a verdict; ADR-0015). Each axis is in[0, 1], shown as a percentage:
| Axis | Meaning | How it is computed |
|---|---|---|
rhy |
rhythmic variety | variety of inter-onset intervals over distinct onsets: (distinct − 1)/(count − 1) |
pit |
melodic variety | the same variety over absolute melodic intervals of the highest-pitch-per-onset line |
tec |
technicality | share of notes carrying a mark or inside a technique span of the same voice |
har |
chromaticism | 1 − scale_fit of the estimated key (Krumhansl–Schmuckler, duration-weighted) |
ply |
playability | max_fret_jump / 12 on the optimal fingering path; 1.0 if a note is unreachable |
str |
structural | distinct-bar-signature ratio (same as the structure block's complexity) |
Whenever an extent changes (after a split or merge), the record's
structure / gesture / complexity fields are reset: each result is
a new record to review and re-measure.
There are two entry points:
griff curate <file.mid>(the CLI,cli/) creates a newChunkMetarecord from a MIDI file. It prompts for id, title, tuning (defaultstandard_e), cohort, tags, quality flags, and reviewer decision, and auto-measures structure / gesture / complexity. It writes<file>.chunk.json(or--output;--ensemblewrites one chunk per note-bearing track plus a group record with measured pair relations).griff-preview … --record=<chunk.json>(this TUI) re-curates an existing record. Edits accumulate during the session and are written on quit (q/Esc) — there is no autosave mid-session.
Marks the record accepted / rejected. The same key again clears it; the
other key overwrites. Persisted into reviewer on quit.
t cycles the cursor through the 27-tag palette; T toggles the tag under
the cursor. On quit a changed set rewrites the record's tags. The palette
(SwancoreTag, in order):
style:
clean_riff·syncopated_riff·tapping_passage·legato_passageharmony:maj7·min7·sus2·add9·slash_chord·power_chordtechnique:hammer_on·pull_off·slide·bend·vibrato·palm_mute·natural_harmonic·artificial_harmonicrhythm:syncopated·triplet_feel·polyrhythmstructure:intro·verse·chorus·bridge·outro·interlude
Enters rename mode seeded with the current title. Enter commits (trimmed,
non-empty), Esc cancels. Persisted into title on quit.
Place the playhead and press s to mark the split point (the same spot
again clears it). The mark floors to the containing bar. Gates
(viewport.rs): the first bar cannot be split (the point must be on the
second barline or later), and a point in a note's ringing tail past the
final barline is refused. Mutually exclusive with merge.
On quit (split_record_at_tick): the first half [start, at_bar − 1]
replaces the record file (id suffix .1, title … (1/2)); the second
half [at_bar, end] lands in the first free sibling slot chunk.2.json,
chunk.3.json, … (id .N, title … (2/2)). Both halves reset their
decision, measurements, and ensemble link.
Requires both --record and --merge=<partner.json>. m arms /
disarms it; mutually exclusive with split. Conditions (merge_records):
both records share a source (filename, format, ticks-per-quarter, time
signature, tuning — else MergeMismatch), their bar ranges are
consecutive (a_end + 1 == b_start — else NotAdjacent), and both
carry a source.bar_range (else MissingBarRange).
The first record's identity wins; tags / techniques / quality flags union; cohort and ensemble survive only on agreement; decision and measurements reset. On success the partner file is absorbed (deleted).
- Without
--record,a/xonly update the inspector indicator and persist nothing;t/T,r,sare no-ops;malso needs--merge. - No autosave — everything is applied on quit. If a rewrite cannot apply
(e.g. split / merge on a record with no
bar_range), it fails loudly to stderr and the files are rolled back, not corrupted. - Split and merge are mutually exclusive — one record rewrite per pass.
- On a narrow terminal the inspector auto-hides (
irestores it) and the complexity tail scrolls into view withPageDown.
- TUI render + key map + help overlay:
preview/src/tui.rs - Interaction core (intents, viewport state, gates):
preview/src/viewport.rs - Curation persistence (tag / rename / split / merge):
preview/src/curation.rs - Launch, flags, on-quit persistence:
preview/src/main.rs - Complexity / structure measurement:
core/src/structure.rs griff curateCLI:cli/src/main.rs- Design: ADR-0016 (shared UI core), ADR-0015 (structure controls & metrics),
stage
docs/stages/S8-preview-app.md.