Skip to content

Release 2.1.0 - #228

Merged
sbeitzel merged 13 commits into
mainfrom
develop
Oct 4, 2026
Merged

sbeitzel merged 13 commits into
mainfrom
develop

Conversation

@sbeitzel

@sbeitzel sbeitzel commented Oct 4, 2026

Copy link
Copy Markdown
Owner

Clefs, octaves and stems, as abcm2ps draws them.

A minor release. Notes are placed for the clef in force, clef changes part way through a
voice are drawn, octave= takes effect, and stems and beams follow abcm2ps's rules. The
model gains Event.clefChange. See Upgrading.

Clefs and octaves

  • Notes follow the clef (Place notes on the staff for the clef in force, not always treble #222). Every notehead, stem, ledger line, accidental, tie and
    slur used to be placed at its treble-clef position whatever clef the staff carried. Staff
    position is now counted from the clef's bottom line (ClefSpec.staffPosition(of:)). An
    octave clef (treble-8) moves nothing, as in abcm2ps.
  • octave= on K: and V: (Apply octave= on K: and V: — it is parsed and never used #224) shifts Note.pitch in the semantic pass, scoped as
    abcm2ps scopes it: a header K: covers every voice, a body K: or a V: its own voice,
    and each lasts until another octave= replaces it. Transposition.statesOctave lets
    octave=0 reset it.
  • A K: with a clef and no key (Accept a K: that names a clef but no key (K:bass, [K:clef=bass]) #223). K:bass, K:clef=bass and [K:clef=bass] are
    read as options only (§3.1.14, §4.6) instead of being rejected. KeySignature records
    what was stated (statesKey, statesClef); what was left unsaid carries on from what is
    in force, so K:G after K:bass stays in the bass clef. An unreadable K: still warns,
    and now changes nothing.
  • Clef changes are drawn (Accept a K: that names a clef but no key (K:bass, [K:clef=bass]) #223). A clef change part way through a voice is a new
    Event.clefChange where the field was written. A change against a bar line is a small
    clef before that bar (a 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; a clef
    change alone draws no key signature. Measure.key is set only by a K: that names a key.

Stems and beams

Fixes #217, Fixes #218, Fixes #221, Fixes #222, Fixes #223, Fixes #224

🤖 Generated with Claude Code

sbeitzel and others added 13 commits October 3, 2026 18:37
A note is beamable when it is shorter than a quarter note, whatever the
meter (ABC v2.2 §4.7: "if L:1/8 then ABC2DE is equivalent to AB C2 DE").
BeamResolver used to compare against the meter's beat instead, so in
compound meter (and 2/2, 3/2) quarter notes were beamed to their
neighbours: `B2A G2d` in 6/8 came out as two beamed pairs.

- BeamResolver takes only the unit note length; the meter no longer
  reaches it, so SemanticPass stops rebuilding it on [M:] and the
  now-unused BodyContext.openingMeter goes.
- The #85 [M:] tests asserted the old rule; they now check the meter
  lands on the right measure and leaves the beams alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each beamed note used to be placed — stem x, and the stem's base on the
notehead — for the direction it would have taken alone, and then drawn
the way the group's first note went.  In `Gd` the d was placed stem-down
at its notehead's left edge and drawn up to the beam.

- SVGEmitter decides each group's direction before drawing any of it, as
  abcm2ps does: down where the highest note is farther above the middle
  line than the lowest is below, up in the opposite case, and a group
  as far above as below follows the part's previous stem (up if none).
- That previous stem is threaded across bars, systems and pages
  alongside the pending ties and slurs.
- A voice with a direction of its own (stem=, pipeformat, a shared
  staff's opposition) is left as it was.
- The jig and Canzonetta snapshots are re-recorded: both had a group
  with stems on the wrong side of their noteheads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fix #217 — don't beam notes too long to carry a flag
Fix #218 — one stem direction per beam group, every stem placed for it
octave= was parsed on V: and ignored, and not parsed on K: at all. It now
shifts Note.pitch in the semantic pass, as abcm2ps's get_note does, scoped as
abcm2ps scopes it: a header K: covers every voice, a body K: or a V: its own
voice, and each lasts until another octave= replaces it. Transposition gains
an additive statesOctave flag so octave=0 can reset one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every notehead, stem, ledger line, accidental, tie and slur was placed at its
treble-clef position whatever clef the staff carried. Staff position is now
counted from the clef's bottom-line pitch (ClefSpec.staffPosition(of:)), the
same table the floating-voice split already used, now shared. An octave clef
(treble-8) moves nothing, as in abcm2ps. The vertical layout's ledger-line
reservation uses the same mapping.

The §7 and §14.4 page snapshots are re-recorded: their bass voices are on
clef=bass octave=-2, which the treble placement had nearly cancelled out
(#224). Both now match abcm2ps -g note for note.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	Tests/CeolKitSVGRendererTests/__Snapshots__/CanzonettaConformanceTests/pageMatchesSnapshot.1.txt
Fix #222, #224 — place notes for the clef in force; apply octave=
An unbeamed note on the middle line in an `.auto` voice now follows the
previous stem in its part (the `lastStemUp` state #218 threads through
pages, systems and measures), and stems up where there is none — the
same tie-break as an even beam group, and what abcm2ps 8.14 does. It
carries across bar lines and rests. `stem=`, pipe format and a shared
staff's opposition still decide outright.

The Canzonetta snapshot changes in three stems, each a B following a
down stem; abcm2ps stems all three down.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fix #221 — stem a middle-line note the way the stem before it went
…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>
Fix #223 — accept a K: that names a clef but no key, and draw clef changes
@sbeitzel
sbeitzel merged commit a9bae96 into main Oct 4, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment