feat: artificial and natural harmonic detail in mx::api - #384
Merged
Conversation
Owner
|
I'm getting caught up at work post vacation and will get on these PRs soon! |
webern
approved these changes
Aug 4, 2026
MusicXML's `<technical><harmonic>` states how a harmonic is produced
(`<natural/>` or `<artificial/>`) and which of its three pitches the written
notehead represents (`<base-pitch/>`, `<touching-pitch/>`, `<sounding-pitch/>`).
`mx::core::Harmonic` modeled both as `choice` and `choice2`, but `mx::api`
dropped them in both directions: `NotationsWriter::addTechnical` built a bare
`core::Harmonic` and never called `setChoice`/`setChoice2`, and
`TechnicalFunctions::parseTechicalMark` read only the attributes. Only an
undecorated `<harmonic/>` could be written, so an artificial harmonic was
indistinguishable from a natural one.
Following the `TremoloMarkData` / `ArpeggiateMarkData` precedent, this adds a
fourth `MarkDataChoice` alternative:
- `HarmonicMarkData::kind` (`HarmonicKind::unspecified/natural/artificial`)
- `HarmonicMarkData::pitch` (`HarmonicPitch::unspecified/basePitch/
touchingPitch/soundingPitch`)
Both default to `unspecified`, so authoring without the payload writes the plain
`<harmonic/>` exactly as before, and the reader leaves `choice` as `none` for a
bare element rather than inventing a natural/artificial value. The two notes of
an artificial harmonic each carry their own mark -- `artificial` + `basePitch`
on the stopped note, `artificial` + `touchingPitch` on the diamond-notehead note
-- so neither note depends on its neighbor for meaning.
Also fixes a write-side gap found while adding it: the harmonic writer used
`setAttributesFromPositionData`, so the element's font, color, and
`print-object` were all lost on write. It now uses `setAttributesFromMarkData`
and sets `print-object` explicitly, since `setAttributesFromPrintData` writes
color and font but never touches `printObject`. That omission still affects
every other mark going through `setAttributesFromMarkData` (fermata, dynamics,
arpeggiate, non-arpeggiate) and is left for a separate change.
`data/synthetic/harmonic.3.0.xml` already existed but had never passed; it is
now pinned in `roundtrip-baseline.txt` (296 -> 297). `lysuite/ly32a_Notations.xml`
and `ksuite/k004a_Technical.xml` get past their harmonic content but still fail
on unrelated `<lyric>` and `<alter>` ordering, so neither is pinned.
- [x] Three new cases in MarkRoundTripTest: the artificial pair (base pitch and
touching pitch), all eleven kind/pitch combinations, and a bare
`<harmonic/>` that must read back with no payload
- [x] Harmonic tests pass (`*Harmonic*`: 43 assertions in 3 test cases)
- [x] Full api suite passes (5512 assertions in 499 test cases)
- [x] `make api-roundtrip` passes (297 of 297 pinned)
- [x] `make core-roundtrip-test` passes (837 test cases)
webern
force-pushed
the
artificial-harmonics
branch
from
August 5, 2026 07:26
b41d36e to
2671b8e
Compare
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
mx::api could only ever write an undecorated
<harmonic/>. The natural/artificial distinction and the base/touching/sounding-pitch detail were dropped in both directions, so an artificial harmonic was indistinguishable from a natural one. This adds them as a MarkDataChoice payload.Also fixes the harmonic writer dropping print-object, font, and color. The wider print-object gap it uncovered is called out in the Summary and left for a separate change.
Summary
MusicXML's
<technical><harmonic>states how a harmonic is produced (<natural/>or<artificial/>) and which of its three pitches the written notehead represents (<base-pitch/>,<touching-pitch/>,<sounding-pitch/>).mx::core::Harmonicmodels both, aschoiceandchoice2, butmx::apidropped them in both directions:NotationsWriter::addTechnicalbuilt a barecore::Harmonicand never calledsetChoice/setChoice2, andTechnicalFunctions::parseTechicalMarkread only the attributes.Following the
TremoloMarkData/ArpeggiateMarkDataprecedent, this adds a fourthMarkDataChoicealternative:HarmonicMarkData::kindis aHarmonicKindof unspecified, natural, or artificial.HarmonicMarkData::pitchis aHarmonicPitchof unspecified, basePitch, touchingPitch, or soundingPitch.Both default to unspecified, so authoring without the payload writes the plain
<harmonic/>exactly as before, and the reader leaveschoiceas none for a bare element rather than inventing a natural/artificial value. The two notes of an artificial harmonic each carry their own mark, artificial with basePitch on the stopped note and artificial with touchingPitch on the diamond-notehead note, so neither note depends on its neighbor for meaning.Also fixes a write-side gap found while adding it. The harmonic writer used
setAttributesFromPositionData, so the element's font, color, and print-object were all lost on write. It now usessetAttributesFromMarkDataand sets print-object explicitly, becausesetAttributesFromPrintDatawrites color and font but never touches printObject. That omission still affects every other mark going throughsetAttributesFromMarkData(fermata, dynamics, arpeggiate, non-arpeggiate), and theimpl::setPrintObjecttemplate inPrintFunctions.hthat looks like it was meant to cover it is unused and has its condition inverted. Left for a separate change.data/synthetic/harmonic.3.0.xmlalready existed but had never passed; it is now pinned (296 -> 297).lysuite/ly32a_Notations.xmlandksuite/k004a_Technical.xmlget past their harmonic content but still fail on unrelated<lyric>and<alter>ordering, so neither is pinned.Testing
<harmonic/>that must read back with no payload*Harmonic*: 43 assertions in 3 test cases)make api-roundtrippasses (297 of 297 pinned)make core-roundtrip-testpasses (837 test cases)References
MarkDataChoicealternatives. Whichever of the three lands first, the others need a union resolution inMarkDataChoice.handMarkDataChoice.cpp; the three payloads are independent.