fix: write back tuplet position data, midi-unpitched and accidental marks - #462
Merged
Merged
Conversation
webern
force-pushed
the
m/mxdev-writedrop
branch
from
September 19, 2026 14:01
6a2d96e to
be9c28e
Compare
This was referenced Sep 19, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Human Summary
Fixes a few random writer bugs. Found and fixed by AI.
Summary
Three values that
mx::apireads were never written back. Each was a silent drop: the value came inon a read and nothing went out on a write.
TupletStart::positionDataandTupletStop::positionDataare read by the tuplet reader, but neitherwriter path set a position attribute on the emitted
<tuplet>, so a tuplet's placement(
default-x,default-y,relative-x,relative-y,placement) was lost. Both paths now callsetAttributesFromPositionData, the way the mark emitters in the same file already do.MidiData::unpitchedis read from<midi-unpitched>, andPartWriternever wrote it. It is nowwritten when the value is present;
VALUE_UNSPECIFIEDstill means absent, so an ordinary pitchedinstrument writes no element. As with the sibling midi fields, a value the core type clamps is
reported through the diagnostics.
Accidental marks were read from two places into the same mark values,
<notations>and, inside anornament,
<ornaments>, while the mark loop had no branch for them, so they vanished.isMarkAccidentalMarknow covers the accidental values and the writer emits them.This is worth being plain about, because it is where the deferred half of #443 begins: the api does
not record which element a mark came from, so the writer cannot put it back where it came from. The
mark is written at the notations level. That is not a preference but a schema constraint. MusicXML's
ornaments group is a sequence whose first member is a required ornament choice, followed by optional
accidental-marks, and both the schema andmx::coreenforce it: an<ornaments>group must carryan ornament, and the parser rejects a group without one. Writing an accidental mark into
<ornaments>would therefore mean inventing an ornament to hang it on.<notations><accidental-mark>needs no such invention, is legal in every case, and the readeralready reads it, so a mark survives a write and a read either way.
This is non-breaking: no api type, field or vocabulary changes.
isMarkAccidentalMarkis addedbeside the other mark predicates.
What is deferred
The mark-placement half of #443 stays open, so this does not close it. A mark read from
<notations>can only be written back to
<notations>, and one read from<ornaments>back to<ornaments>,once the api can tell the two apart, which today it cannot: both become the same
MarkTypein thesame flat list, with no field recording the source. That needs an api-visible distinction, likely a
MarkDataChoicealternative per the doctrine, plus a split in the reader and its own tests. It is adesign pass over the public model rather than a writer branch, so it is deliberately not attempted
here.
Testing
the 5 new cases fail, 25 of 33 assertions.
midi-unpitchedis absent from the written XML andreads back as -1, the written tuplet carries no position attribute, the accidental mark is
absent and reads back as 0 marks, and the mark attached to an ornament reads back as 1 mark
instead of 2
make fmtandmake fmt-checkpassmake api-test: all tests pass (5949 assertions in 670 test cases)make api-roundtrip: 415 passed, 0 failed (of 415 pinned)make api-roundtrip-discover: 415 PASS, 425 FAIL; no file unlocked by this change, so nothingwas added to the baseline
make test-all: core round trip passes (841 test cases), core unit passes (603 assertions in69 test cases), plus the api suites above
The tests added are: a tuplet carried through a write and a read with all four position attributes
and a placement; a midi-unpitched that survives a write and a read, plus a guard that an ordinary
pitched part writes no
<midi-unpitched>; an authored accidental mark that survives a write and aread; and a source fixture whose accidental mark is attached to a trill-mark, checked to keep both
marks once the ornament attachment is normalized away.
References
midi-unpitchedand accidental marks #443mx::api#403