Skip to content

feat(api): support the full time-signature model#321

Merged
webern merged 15 commits into
mainfrom
claude/eloquent-turing-us0rtv
Jul 11, 2026
Merged

feat(api): support the full time-signature model#321
webern merged 15 commits into
mainfrom
claude/eloquent-turing-us0rtv

Conversation

@webern

@webern webern commented Jul 6, 2026

Copy link
Copy Markdown
Owner

Human Summary

When @rpatters1 opened #268, my immediate thought was, oh no, we have to support all of the time signature model. My reasoning is that just adding the one case that is needed now will only last so long before a different semantic situation is encountered.

I wanted to preserve some sort of "simple" path, so I created a "choice" class where TimeSignatureData used to be. It can be either simple or complex where simple basically means numerator/denominator with or without the C or Cut symbol.

Anything beyond that is shuttled into ComplexTimeSignature where, I believe, we have modeled all of what MusicXML supports.

!!Breaking!!

We use "breaking" somewhat loosely around here, since just about any mx::api change introduces some amount of change to the client code, but this one is bigger than the usual breakage. It requires edits anywhere time signatures are involved in client code. As such, I spent some extra time on it and I hope I have a stable design here.

Summary

Redesigns the time-signature model in mx::api to cover the full MusicXML <time> element instead of a single beats/beat-type pair with a narrow symbol set.

  • TimeSignatureData is now built around a shared TimeFraction leaf (beats/beatType); its TimeSignatureSymbol is narrowed to unspecified/common/cut.
  • New ComplexTimeSignature (ComplexTimeSignature.h) quarantines everything unusual: composite/additive multi-fraction meters, the full symbol vocabulary (singleNumber, note, dottedNote), separators, interchangeable (dual) meters, and senza-misura.
  • New TimeChoice (TimeChoice.h) is a {simple, complex} switch carrying the whole-<time> attributes (isImplicit, display). Building a TimeChoice from a ComplexTimeSignature that turns out to be simple-equivalent auto-collapses it back to simple, so there's always one canonical representation.
  • MeasureData::timeSignature is now a TimeChoice; a new MeasureData::staffTimeSignatures (std::map<int, TimeChoice>) holds per-staff <time number="N"> overrides.
  • impl: TimeReader now translates every <time> in a measure (not just the first) into a TimeChoice + staff index; PropertiesWriter::writeTime regenerates all shapes and writes per-staff number=; MeasureReader/MeasureWriter/Cursor carry per-staff overrides forward measure-to-measure; Converter gains symbol/separator/relation enum tables.
  • Four previously-failing corpus files now round-trip and are pinned in roundtrip-baseline.txt (ly11d/ly11e composite meters, ly11f symbols, ly11h senza-misura).

This is a breaking change to mx::api: beats/beatType move into fraction, isImplicit/display move from TimeSignatureData onto TimeChoice, and TimeSignatureSymbol::singleNumber moves to the complex-only ComplexTimeSymbol.

Testing

  • TimeSignatureApiTest.cpp: new cases for default/auto-collapse/stays-complex/equality behavior, plus round-trip coverage for composite meters, senza-misura (with and without glyph), interchangeable pairs, widened symbol+separator, and per-staff overrides with carry-forward
  • roundtrip-baseline.txt: ly11d, ly11e, ly11f, ly11h unpinned (now pass the api round-trip)
  • Existing api/impl fixtures updated to the new TimeChoice construction (ApiK007a/c, ApiK009bSlur, ApiK014aFermatas, ApiK015aLayout, ApiLy43e, ApiMuAccidentals1, ChordApiTest)
  • timesigs_composite-ref.musicxml which was submitted by @rpatters1 to cover his use case

References

@webern webern added feature new feature request breaking fixes or implementation that require breaking changes api Affects the mx::api layer impl Affects the mx::impl layer ai Issues opened by, or through, a coding agent. labels Jul 6, 2026 — with Claude
@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

Coverage report

Core-dev coverage src/private/mx/core/

Metric Coverage Covered / Total
Lines 77.8% 28513 / 36640
Functions 74.3% 6352 / 8552
Branches 50.7% 22678 / 44751

API coverage src/private/mx/{api,impl,utility}/

Metric Coverage Covered / Total
Lines 80.9% 6448 / 7974
Functions 67.7% 2224 / 3284
Branches 49.8% 5542 / 11132

Core HTML report | API HTML report

Commit 302722164546a53f08e0194c3f6204a040bb3c5b.

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

gen-quality gen/

gen-quality: 84.5 / 100   (floor 84.5, +0.0)

  structure     86.5  x0.50   [fn 90.5 / file 82.6]
  cyclomatic    88.4  x0.25
  cognitive     76.6  x0.25

  409 functions across 31 files, 7702 lines (largest file 1044)
  max cc 56  max cognitive 44  max fn loc 152

Worst offenders (top 5 per axis; full lists in score.json):
  cyclomatic gen/xsd/analyze.py:311     report                             56
  cyclomatic gen/plates/build.py:956    _validate_config_against_ir        35
  cyclomatic gen/press/context.py:145   plate_context                      34
  cyclomatic gen/__main__.py:46         _ir                                23
  cyclomatic gen/tests/test_ir.py:102   _check_references                  20
  cognitive  gen/xsd/analyze.py:311     report                             44
  cognitive  gen/ir/resolve.py:119      flat_elements                      40
  cognitive  gen/tests/test_ir.py:102   _check_references                  38
  cognitive  gen/press/context.py:145   plate_context                      37
  cognitive  gen/xsd/analyze.py:207     _sccs                              37
  size       gen/xsd/analyze.py:311     report                             152
  size       gen/press/context.py:145   plate_context                      96
  size       gen/plates/build.py:533    _value_plate                       89
  size       gen/plates/build.py:956    _validate_config_against_ir        89
  size       gen/ir/resolve.py:119      flat_elements                      78

Commit 302722164546a53f08e0194c3f6204a040bb3c5b.

@webern webern force-pushed the claude/eloquent-turing-us0rtv branch from 96c10f3 to 16274e6 Compare July 7, 2026 09:24
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

Coverage report

Core-dev coverage src/private/mx/core/

Metric Coverage Covered / Total
Lines 77.8% 28513 / 36640
Functions 74.3% 6352 / 8552
Branches 50.7% 22678 / 44751

API coverage src/private/mx/{api,impl,utility}/

Metric Coverage Covered / Total
Lines 81.0% 6481 / 8004
Functions 67.8% 2228 / 3285
Branches 49.8% 5570 / 11180

Core HTML report | API HTML report

Commit 156117869514f0a8463e5a473ccf4c0c7291398b.

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

gen-quality gen/

gen-quality: 84.5 / 100   (floor 84.5, +0.0)

  structure     86.5  x0.50   [fn 90.5 / file 82.6]
  cyclomatic    88.4  x0.25
  cognitive     76.6  x0.25

  409 functions across 31 files, 7702 lines (largest file 1044)
  max cc 56  max cognitive 44  max fn loc 152

Worst offenders (top 5 per axis; full lists in score.json):
  cyclomatic gen/xsd/analyze.py:311     report                             56
  cyclomatic gen/plates/build.py:956    _validate_config_against_ir        35
  cyclomatic gen/press/context.py:145   plate_context                      34
  cyclomatic gen/__main__.py:46         _ir                                23
  cyclomatic gen/tests/test_ir.py:102   _check_references                  20
  cognitive  gen/xsd/analyze.py:311     report                             44
  cognitive  gen/ir/resolve.py:119      flat_elements                      40
  cognitive  gen/tests/test_ir.py:102   _check_references                  38
  cognitive  gen/press/context.py:145   plate_context                      37
  cognitive  gen/xsd/analyze.py:207     _sccs                              37
  size       gen/xsd/analyze.py:311     report                             152
  size       gen/press/context.py:145   plate_context                      96
  size       gen/plates/build.py:533    _value_plate                       89
  size       gen/plates/build.py:956    _validate_config_against_ir        89
  size       gen/ir/resolve.py:119      flat_elements                      78

Commit 156117869514f0a8463e5a473ccf4c0c7291398b.

@webern webern force-pushed the claude/eloquent-turing-us0rtv branch from 16274e6 to 1cd06bf Compare July 8, 2026 21:16

@webern webern left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's try again.

Comment thread src/include/mx/api/MeasureData.h Outdated
Comment thread src/include/mx/api/TimeSignatureData.h Outdated
Comment thread src/include/mx/api/TimeSignatureData.h Outdated
Comment thread src/include/mx/api/TimeSignatureData.h Outdated
Comment thread src/include/mx/api/TimeSignatureData.h Outdated
Comment thread src/include/mx/api/TimeSignatureData.h Outdated
@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown

Coverage report

Core-dev coverage src/private/mx/core/

Metric Coverage Covered / Total
Lines 77.8% 28513 / 36640
Functions 74.3% 6352 / 8552
Branches 50.7% 22678 / 44751

API coverage src/private/mx/{api,impl,utility}/

Metric Coverage Covered / Total
Lines 80.9% 6481 / 8009
Functions 67.8% 2228 / 3285
Branches 49.8% 5568 / 11186

Core HTML report | API HTML report

Commit d5a425752ce17d92fa004a46cd7bdbccc17179b4.

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown

gen-quality gen/

gen-quality: 84.5 / 100   (floor 84.5, +0.0)

  structure     86.5  x0.50   [fn 90.5 / file 82.6]
  cyclomatic    88.4  x0.25
  cognitive     76.6  x0.25

  409 functions across 31 files, 7702 lines (largest file 1044)
  max cc 56  max cognitive 44  max fn loc 152

Worst offenders (top 5 per axis; full lists in score.json):
  cyclomatic gen/xsd/analyze.py:311     report                             56
  cyclomatic gen/plates/build.py:956    _validate_config_against_ir        35
  cyclomatic gen/press/context.py:145   plate_context                      34
  cyclomatic gen/__main__.py:46         _ir                                23
  cyclomatic gen/tests/test_ir.py:102   _check_references                  20
  cognitive  gen/xsd/analyze.py:311     report                             44
  cognitive  gen/ir/resolve.py:119      flat_elements                      40
  cognitive  gen/tests/test_ir.py:102   _check_references                  38
  cognitive  gen/press/context.py:145   plate_context                      37
  cognitive  gen/xsd/analyze.py:207     _sccs                              37
  size       gen/xsd/analyze.py:311     report                             152
  size       gen/press/context.py:145   plate_context                      96
  size       gen/plates/build.py:533    _value_plate                       89
  size       gen/plates/build.py:956    _validate_config_against_ir        89
  size       gen/ir/resolve.py:119      flat_elements                      78

Commit d5a425752ce17d92fa004a46cd7bdbccc17179b4.

Redesign the time-signature api around a TimeChoice that switches
between the simple case (the ordinary N/D you want 99% of the time) and
a complex case that quarantines everything unusual. The simple type and
the complex machinery live in separate headers, so ordinary 4/4 work
never has to look at the complex surface.

Public shape (src/include/mx/api/):
- TimeSignatureData.h: TimeFraction (shared beats/beat-type leaf) and
  TimeSignatureData { TimeSignatureSymbol symbol; TimeFraction fraction; }
  where the symbol is the narrow {unspecified, common, cut}.
- ComplexTimeSignature.h: the full ComplexTimeSymbol vocabulary,
  TimeSeparator, TimeRelation, InterchangeableTimeSignature,
  MeteredTimeSignature, and ComplexTimeSignature -- itself a
  { metered, senzaMisura } choice mirroring the spec's one real fork.
- TimeChoice.h: the { simple, complex } switch, carrying the
  whole-<time> attributes (isImplicit, display).

Guarantees:
- TimeChoice::complex auto-collapses a simple-equivalent meter (one
  fraction, no interchangeable, no separator, a symbol the simple case
  can express) back to simple, so a plain N/D can never hide in the
  complex arena and there is one canonical representation.
- The two symbol enums do not overlap on the unusual values:
  single-number/note/dotted-note exist only in the complex vocabulary,
  so they cannot appear on the simple type.

Per-staff signatures move to std::map<int, TimeChoice>
staffTimeSignatures on MeasureData (keyed by staff index; no staff
index baked into the signature type).

Choice-type logic (factories, accessors, equality, auto-collapse) lives
in TUs under src/private/mx/api/, keeping the headers declaration-only.

impl: TimeReader translates each core::Time to a TimeChoice;
PropertiesWriter::writeTime regenerates all shapes and takes the staff
index separately; MeasureReader/MeasureWriter route by staff scope and
carry per-staff overrides forward; Converter gains narrow/full symbol,
separator, and relation tables.

Four corpus files newly pass the api round-trip and are pinned
(ly11d/e composite, ly11f symbols, ly11h senza-misura).
@webern webern force-pushed the claude/eloquent-turing-us0rtv branch from 1cd06bf to 0a3855c Compare July 8, 2026 22:19
@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown

Coverage report

Core-dev coverage src/private/mx/core/

Metric Coverage Covered / Total
Lines 77.8% 28513 / 36640
Functions 74.3% 6352 / 8552
Branches 50.7% 22678 / 44751

API coverage src/private/mx/{api,impl,utility}/

Metric Coverage Covered / Total
Lines 81.1% 6565 / 8093
Functions 68.1% 2250 / 3306
Branches 49.8% 5637 / 11317

Core HTML report | API HTML report

Commit 52649b0a7f0c21fdcba3401fb3ba170b6c92396b.

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown

gen-quality gen/

gen-quality: 84.5 / 100   (floor 84.5, +0.0)

  structure     86.5  x0.50   [fn 90.5 / file 82.6]
  cyclomatic    88.4  x0.25
  cognitive     76.6  x0.25

  409 functions across 31 files, 7702 lines (largest file 1044)
  max cc 56  max cognitive 44  max fn loc 152

Worst offenders (top 5 per axis; full lists in score.json):
  cyclomatic gen/xsd/analyze.py:311     report                             56
  cyclomatic gen/plates/build.py:956    _validate_config_against_ir        35
  cyclomatic gen/press/context.py:145   plate_context                      34
  cyclomatic gen/__main__.py:46         _ir                                23
  cyclomatic gen/tests/test_ir.py:102   _check_references                  20
  cognitive  gen/xsd/analyze.py:311     report                             44
  cognitive  gen/ir/resolve.py:119      flat_elements                      40
  cognitive  gen/tests/test_ir.py:102   _check_references                  38
  cognitive  gen/press/context.py:145   plate_context                      37
  cognitive  gen/xsd/analyze.py:207     _sccs                              37
  size       gen/xsd/analyze.py:311     report                             152
  size       gen/press/context.py:145   plate_context                      96
  size       gen/plates/build.py:533    _value_plate                       89
  size       gen/plates/build.py:956    _validate_config_against_ir        89
  size       gen/ir/resolve.py:119      flat_elements                      78

Commit 52649b0a7f0c21fdcba3401fb3ba170b6c92396b.

@webern webern requested a review from Copilot July 9, 2026 20:51

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@webern webern changed the title feat: redesign TimeSignatureData for the full time model feat(api): support the full time signature model Jul 9, 2026
@webern webern requested a review from Copilot July 9, 2026 20:54

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@webern webern changed the title feat(api): support the full time signature model feat(api): support the full time-signature model Jul 10, 2026
@github-actions

Copy link
Copy Markdown

Coverage report

Core-dev coverage src/private/mx/core/

Metric Coverage Covered / Total
Lines 77.8% 28513 / 36640
Functions 74.3% 6352 / 8552
Branches 50.7% 22678 / 44751

API coverage src/private/mx/{api,impl,utility}/

Metric Coverage Covered / Total
Lines 81.0% 6557 / 8091
Functions 68.0% 2249 / 3306
Branches 49.7% 5626 / 11321

Core HTML report | API HTML report

Commit 70f0b898698bbc65d694318ef2c71f3f4ffe9001.

@github-actions

Copy link
Copy Markdown

gen-quality gen/

gen-quality: 84.5 / 100   (floor 84.5, +0.0)

  structure     86.5  x0.50   [fn 90.5 / file 82.6]
  cyclomatic    88.4  x0.25
  cognitive     76.6  x0.25

  409 functions across 31 files, 7702 lines (largest file 1044)
  max cc 56  max cognitive 44  max fn loc 152

Worst offenders (top 5 per axis; full lists in score.json):
  cyclomatic gen/xsd/analyze.py:311     report                             56
  cyclomatic gen/plates/build.py:956    _validate_config_against_ir        35
  cyclomatic gen/press/context.py:145   plate_context                      34
  cyclomatic gen/__main__.py:46         _ir                                23
  cyclomatic gen/tests/test_ir.py:102   _check_references                  20
  cognitive  gen/xsd/analyze.py:311     report                             44
  cognitive  gen/ir/resolve.py:119      flat_elements                      40
  cognitive  gen/tests/test_ir.py:102   _check_references                  38
  cognitive  gen/press/context.py:145   plate_context                      37
  cognitive  gen/xsd/analyze.py:207     _sccs                              37
  size       gen/xsd/analyze.py:311     report                             152
  size       gen/press/context.py:145   plate_context                      96
  size       gen/plates/build.py:533    _value_plate                       89
  size       gen/plates/build.py:956    _validate_config_against_ir        89
  size       gen/ir/resolve.py:119      flat_elements                      78

Commit 70f0b898698bbc65d694318ef2c71f3f4ffe9001.

webern added 5 commits July 11, 2026 11:25
The simple()/complex()/metered()/senzaMisura() accessors return a
default-constructed value on the wrong kind rather than throwing, so
'Precondition' was the wrong word (it implies undefined behavior when
violated). Reword to point the reader at the isX() check instead.
…nd-trip allow list

timesigs_composite-ref.musicxml is the repro rpatters1 attached to #268
before this PR's TimeSignatureData redesign began; it now round-trips
losslessly alongside the other fixtures this PR unblocks.
The api always normalizes a font-family list's comma separator to ", "
on write (core::FontFamily), so the fixture's single-space-free
"Maestro,engraved" byte-diverged from the round-tripped output despite
matching in substance. Match the writer's normalized spacing.
<score-part>/<group> (Finale's ensemble-grouping metadata) has no
mx::api surface -- PartData carries no field for it and PartReader
silently drops it, which is an unrelated pre-existing gap this fixture
isn't meant to exercise. Trimmed so the fixture stays scoped to the
composite time-signature round-trip it was added to pin.
@rpatters1

Copy link
Copy Markdown
Contributor

I quite like the approach you have taken.

webern added 2 commits July 11, 2026 12:00
… instead

Restores timesigs_composite-ref.musicxml byte-exact to the original
rpatters1 attached to #268 -- it stays out of the round-trip allow list
as-is, since it also exercises unrelated pre-existing mx::api gaps
(score-part/group, music-font comma spacing, measure-numbering
multiple-rest-* attributes).

timesigs_composite-ref-modified.musicxml is a new sibling with just
those three unrelated spots trimmed; it's the one pinned in
roundtrip-baseline.txt, keeping the fixture scoped to what it's meant
to exercise: the composite/additive time-signature round-trip.

Bumped the corert pinned file count (833 -> 834) for the added file.
<sound>/<swing> is documented (SoundData.h) as intentionally out of
mx::api's scope alongside midi-instrument/midi-device/play/offset.
Another unrelated pre-existing gap this fixture wasn't meant to
exercise; trimmed from the modified sibling like the other three.
@github-actions

Copy link
Copy Markdown

Coverage report

Core-dev coverage src/private/mx/core/

Metric Coverage Covered / Total
Lines 77.8% 28514 / 36640
Functions 74.3% 6352 / 8552
Branches 50.7% 22680 / 44751

API coverage src/private/mx/{api,impl,utility}/

Metric Coverage Covered / Total
Lines 81.0% 6557 / 8091
Functions 68.0% 2249 / 3306
Branches 49.7% 5626 / 11321

Core HTML report | API HTML report

Commit f98af336227c89db9cb098652a2162777ee5e78f.

@github-actions

Copy link
Copy Markdown

gen-quality gen/

gen-quality: 84.5 / 100   (floor 84.5, +0.0)

  structure     86.5  x0.50   [fn 90.5 / file 82.6]
  cyclomatic    88.4  x0.25
  cognitive     76.6  x0.25

  409 functions across 31 files, 7702 lines (largest file 1044)
  max cc 56  max cognitive 44  max fn loc 152

Worst offenders (top 5 per axis; full lists in score.json):
  cyclomatic gen/xsd/analyze.py:311     report                             56
  cyclomatic gen/plates/build.py:956    _validate_config_against_ir        35
  cyclomatic gen/press/context.py:145   plate_context                      34
  cyclomatic gen/__main__.py:46         _ir                                23
  cyclomatic gen/tests/test_ir.py:102   _check_references                  20
  cognitive  gen/xsd/analyze.py:311     report                             44
  cognitive  gen/ir/resolve.py:119      flat_elements                      40
  cognitive  gen/tests/test_ir.py:102   _check_references                  38
  cognitive  gen/press/context.py:145   plate_context                      37
  cognitive  gen/xsd/analyze.py:207     _sccs                              37
  size       gen/xsd/analyze.py:311     report                             152
  size       gen/press/context.py:145   plate_context                      96
  size       gen/plates/build.py:533    _value_plate                       89
  size       gen/plates/build.py:956    _validate_config_against_ir        89
  size       gen/ir/resolve.py:119      flat_elements                      78

Commit f98af336227c89db9cb098652a2162777ee5e78f.

@webern webern merged commit 9dc31b1 into main Jul 11, 2026
10 checks passed
@webern webern deleted the claude/eloquent-turing-us0rtv branch July 11, 2026 12:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai Issues opened by, or through, a coding agent. api Affects the mx::api layer breaking fixes or implementation that require breaking changes feature new feature request impl Affects the mx::impl layer

Projects

None yet

Development

Successfully merging this pull request may close these issues.

api: time signature improvements

3 participants