Skip to content

Fix #223 — accept a K: that names a clef but no key, and draw clef changes - #227

Merged
sbeitzel merged 1 commit into
developfrom
feature/fix_223
Oct 4, 2026
Merged

sbeitzel merged 1 commit into
developfrom
feature/fix_223

Conversation

@sbeitzel

@sbeitzel sbeitzel commented Oct 4, 2026

Copy link
Copy Markdown
Owner

Fix #223

Problem

K:bass, K:clef=bass and [K:clef=bass] were rejected ("Key must start with A–G…") and the clef was dropped. Even K:C clef=bass part way through a tune never reached the page, because the renderer drew one clef per voice for the whole tune.

Parsing and semantics

  • KeyFieldParser: a payload with no tonic is read as options only (§3.1.14, §4.6). An empty K: is accepted too. A payload that is neither a key nor options still warns, and now changes nothing.
  • KeySignature.statesKey / statesClef record what the field stated. The semantic pass resolves both, so the domain model only carries decided keys:
    • in the header, an empty key is K:none;
    • part way through a voice, the key in force carries on, and a K: with no clef keeps the clef in force (K:G after K:bass stays bass, as in abcm2ps).
  • New Event.clefChange(ClefSpec), at the point the field was written. Measure.key is set only by a K: that names a key.

Rendering (checked against abcm2ps -g 8.14)

  • A change written against a bar line is drawn as a small clef before that bar. At a line break this is the courtesy clef at the end of the line, and the next system opens with the new clef full size and the key signature already in force.
  • A change in the middle of a bar is drawn as a small clef between the notes. The notes before it stay on the old clef.
  • A clef change alone draws no key signature. A later key change is drawn for the new clef.
  • Noteheads, ledger lines and ledger space follow the clef in force at each event (ResolvedEvent.clef). The emitter's per-system clef is gone.

Tests

  • KeyWithoutTonicTests (parser/semantic, 15) and MidTuneClefChangeTests (renderer, 6).
  • Full suite passes. No existing snapshot changed.

🤖 Generated with Claude Code

…anges

K:bass, K:clef=bass and [K:clef=bass] were rejected as malformed and the clef
was dropped. A K: with no tonic is now read as options only (§3.1.14, §4.6),
and KeySignature records whether a key and a clef were stated (statesKey,
statesClef). The semantic pass resolves what was left unsaid against what is
in force: in the header an empty key is K:none; part way through a voice the
key carries on, and a K: with no clef keeps the clef (K:G after K:bass stays
bass, as in abcm2ps). An unreadable K: still warns and now changes nothing.

A clef change part way through a voice is a new Event.clefChange where the
field was written; Measure.key is set only by a K: that names a key. The
renderer tracks the clef in force per column and per event, as abcm2ps -g
8.14 draws it: a change against a bar line is a small clef before that bar
(the courtesy clef at the end of a line, with the new clef full size at the
head of the next), a change mid-bar is a small clef between the notes, and a
clef change alone draws no key signature. Notes, ledger lines and ledger space
follow the clef in force.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sbeitzel
sbeitzel merged commit 31d2abf into develop Oct 4, 2026
2 checks passed
@sbeitzel
sbeitzel deleted the feature/fix_223 branch October 4, 2026 22:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant