Skip to content

feat: artificial and natural harmonic detail in mx::api - #384

Merged
webern merged 1 commit into
webern:mainfrom
rpatters1:artificial-harmonics
Aug 5, 2026
Merged

feat: artificial and natural harmonic detail in mx::api#384
webern merged 1 commit into
webern:mainfrom
rpatters1:artificial-harmonics

Conversation

@rpatters1

Copy link
Copy Markdown
Contributor

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::Harmonic models 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.

Following the TremoloMarkData / ArpeggiateMarkData precedent, this adds a fourth MarkDataChoice alternative:

  • HarmonicMarkData::kind is a HarmonicKind of unspecified, natural, or artificial.
  • HarmonicMarkData::pitch is a HarmonicPitch of unspecified, basePitch, touchingPitch, or 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 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 uses setAttributesFromMarkData and sets print-object explicitly, because 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 the impl::setPrintObject template in PrintFunctions.h that 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.xml already existed but had never passed; it is now pinned (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.

Testing

  • 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
  • Harmonic tests pass (*Harmonic*: 43 assertions in 3 test cases)
  • Full api suite passes (5512 assertions in 499 test cases)
  • make api-roundtrip passes (297 of 297 pinned)
  • make core-roundtrip-test passes (837 test cases)

References

@webern

webern commented Aug 4, 2026

Copy link
Copy Markdown
Owner

I'm getting caught up at work post vacation and will get on these PRs soon!

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
webern force-pushed the artificial-harmonics branch from b41d36e to 2671b8e Compare August 5, 2026 07:26
@webern
webern merged commit afbf0ed into webern:main Aug 5, 2026
7 checks passed
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.

2 participants