From 5525b29cff69dddf08068bf8ed5adcfd9304cec6 Mon Sep 17 00:00:00 2001 From: Matthew James Briggs Date: Wed, 1 Jul 2026 07:43:58 +0200 Subject: [PATCH 1/4] feat(api): support lv (let-ring) ties Add api::TieLetRing, a start-only structure on NoteData for laissez-vibrer ties, kept separate from the isTieStart/isTieStop pair. Parse a lone in parseCurve and emit it in NotationsWriter, with reader/writer/round-trip unit tests. --- src/include/mx/api/NoteData.h | 43 +++ src/private/mx/api/NoteData.cpp | 4 +- src/private/mx/impl/CurveFunctions.h | 62 +++- src/private/mx/impl/NotationsWriter.cpp | 10 + .../mxtest/impl/CurveFunctionsTest.cpp | 286 ++++++++++++++++++ 5 files changed, 400 insertions(+), 5 deletions(-) diff --git a/src/include/mx/api/NoteData.h b/src/include/mx/api/NoteData.h index 99963d24b..d90a58f98 100644 --- a/src/include/mx/api/NoteData.h +++ b/src/include/mx/api/NoteData.h @@ -4,6 +4,7 @@ #pragma once +#include "mx/api/CurveData.h" #include "mx/api/DurationData.h" #include "mx/api/LyricData.h" #include "mx/api/NoteAttachmentData.h" @@ -75,6 +76,43 @@ enum class Stem both }; +// A laissez-vibrer tie, also called a "let-ring" or "lv" tie. Unlike a normal +// tie, which is a start/stop pair joining two notes of the same pitch, an lv +// tie applies to a single note and has no matching stop: it instructs the +// renderer to let the note ring undamped. In MusicXML this is a lone +// notation (there is no sound-level counterpart, +// since the element's type is only start or stop). Because it is +// structurally unlike a start/stop tie, it is modeled with its own struct +// rather than by overloading NoteData's isTieStart / isTieStop. +struct TieLetRing +{ + // Whether this note carries an lv (let-ring) tie. When false the remaining + // fields are unused. A default-constructed, all-fields-default TieLetRing + // with isSpecified true corresponds to a bare . + bool isSpecified; + + // Optional visual attributes carried by the element. + PositionData positionData; // default-x/y, relative-x/y, placement + CurveOrientation curveOrientation; + bool isColorSpecified; + ColorData colorData; + + TieLetRing() + : isSpecified{false}, positionData{}, curveOrientation{CurveOrientation::unspecified}, isColorSpecified{false}, + colorData{} + { + } +}; + +MXAPI_EQUALS_BEGIN(TieLetRing) +MXAPI_EQUALS_MEMBER(isSpecified) +MXAPI_EQUALS_MEMBER(positionData) +MXAPI_EQUALS_MEMBER(curveOrientation) +MXAPI_EQUALS_MEMBER(isColorSpecified) +MXAPI_EQUALS_MEMBER(colorData) +MXAPI_EQUALS_END; +MXAPI_NOT_EQUALS_AND_VECTORS(TieLetRing); + class NoteData { public: @@ -98,6 +136,10 @@ class NoteData bool isTieStart; bool isTieStop; + // A laissez-vibrer / let-ring tie on this note (a lone + // with no matching stop). Independent of isTieStart / isTieStop. See TieLetRing. + TieLetRing tieLetRing; + NoteType noteType; // normal, cue, grace Notehead notehead; PitchData pitchData; // step, alter, octave, accidental, etc @@ -140,6 +182,7 @@ MXAPI_EQUALS_MEMBER(isDisplayStepOctaveSpecified) MXAPI_EQUALS_MEMBER(isChord) MXAPI_EQUALS_MEMBER(isTieStart) MXAPI_EQUALS_MEMBER(isTieStop) +MXAPI_EQUALS_MEMBER(tieLetRing) MXAPI_EQUALS_MEMBER(noteType) MXAPI_EQUALS_MEMBER(pitchData) MXAPI_EQUALS_MEMBER(userRequestedVoiceNumber) diff --git a/src/private/mx/api/NoteData.cpp b/src/private/mx/api/NoteData.cpp index fe53eb721..70cdeb2b2 100644 --- a/src/private/mx/api/NoteData.cpp +++ b/src/private/mx/api/NoteData.cpp @@ -10,8 +10,8 @@ namespace api { NoteData::NoteData() : isRest{false}, isMeasureRest{false}, isUnpitched{false}, isDisplayStepOctaveSpecified{false}, isChord{false}, - isTieStart{false}, isTieStop{false}, noteType{NoteType::normal}, notehead{Notehead::normal}, pitchData{}, - userRequestedVoiceNumber{-1}, stem{Stem::unspecified}, tickTimePosition{0}, durationData{}, beams{}, + isTieStart{false}, isTieStop{false}, tieLetRing{}, noteType{NoteType::normal}, notehead{Notehead::normal}, + pitchData{}, userRequestedVoiceNumber{-1}, stem{Stem::unspecified}, tickTimePosition{0}, durationData{}, beams{}, positionData{}, printData{}, noteAttachmentData{}, lyrics{} { } diff --git a/src/private/mx/impl/CurveFunctions.h b/src/private/mx/impl/CurveFunctions.h index b7c56d6e2..bcd089559 100644 --- a/src/private/mx/impl/CurveFunctions.h +++ b/src/private/mx/impl/CurveFunctions.h @@ -5,6 +5,7 @@ #pragma once #include "mx/api/CurveData.h" +#include "mx/api/NoteData.h" #include "mx/core/generated/Slur.h" #include "mx/core/generated/Tied.h" #include "mx/impl/LineFunctions.h" @@ -282,18 +283,73 @@ void writeAttributesFromCurveStop(const api::CurveStop inCurve, ATTRIBUTES_TYPE } } +// Emits a lone from an api::TieLetRing, carrying its +// shared visual attributes (position, orientation, color). The caller decides +// whether to emit at all (i.e. checks isSpecified). +inline void writeAttributesFromTieLetRing(const api::TieLetRing &inTie, core::Tied &outTied) +{ + outTied.setType(core::TiedType::letRing()); + impl::setAttributesFromPositionData(inTie.positionData, outTied); + + if (inTie.isColorSpecified) + { + setAttributesFromColorData(inTie.colorData, outTied); + } + + if (inTie.curveOrientation != api::CurveOrientation::unspecified) + { + outTied.setOrientation(inTie.curveOrientation == api::CurveOrientation::overhand ? core::OverUnder::over() + : core::OverUnder::under()); + } +} + +// Parses a lone into an api::TieLetRing. A let-ring +// tie has no start/stop pairing, so it is captured on its own rather than in +// the curve vectors. The visual attributes it shares with any tied element +// (position, orientation, color) are carried across. +inline api::TieLetRing parseTieLetRing(const core::Tied &inTied) +{ + api::TieLetRing c; + c.isSpecified = true; + c.positionData = impl::getPositionData(inTied); + c.isColorSpecified = checkHasColor(&inTied); + + if (c.isColorSpecified) + { + c.colorData = impl::getColor(inTied); + } + + if (inTied.orientation().has_value()) + { + c.curveOrientation = *inTied.orientation() == core::OverUnder::over() ? api::CurveOrientation::overhand + : api::CurveOrientation::underhand; + } + return c; +} + // takes either an mx::core::Tied or an mx::core::Slur // populates the outNoteData.curveStart, cureContinuations // or curveStop vector with the result template void parseCurve(const SLUR_OR_TIE_ELEMENT_TYPE &slurOrTie, api::NoteData &outNoteData) { - // Slur's type is StartStopContinue; Tied's is TiedType (whose let-ring - // alternative the old core could not represent; it falls through all - // branches and is ignored here). + // Slur's type is StartStopContinue; Tied's is TiedType, which also has a + // let-ring alternative. A let-ring value only occurs on , so the + // if constexpr below keeps this branch out of the slur instantiation (whose + // type has no letRing) and routes the lone into + // NoteData::tieLetRing instead of the start/continue/stop curve vectors. const auto outputType = slurOrTie.type(); using CurveTypeAttribute = std::decay_t; + if constexpr (std::is_same_v, core::Tied>) + { + if (core::TiedType::letRing() == outputType) + { + outNoteData.tieLetRing = parseTieLetRing(slurOrTie); + return; + } + } + if (CurveTypeAttribute::start() == outputType) { outNoteData.noteAttachmentData.curveStarts.emplace_back(parseCurveStart(slurOrTie)); diff --git a/src/private/mx/impl/NotationsWriter.cpp b/src/private/mx/impl/NotationsWriter.cpp index ac4cc5048..b197e432d 100644 --- a/src/private/mx/impl/NotationsWriter.cpp +++ b/src/private/mx/impl/NotationsWriter.cpp @@ -147,6 +147,16 @@ core::Notations NotationsWriter::getNotations() const } } + // A laissez-vibrer / let-ring tie is a lone with no + // start/stop pairing, so it is emitted from its own field rather than the + // curve vectors above. + if (myNoteData.tieLetRing.isSpecified) + { + core::Tied tied; + writeAttributesFromTieLetRing(myNoteData.tieLetRing, tied); + outNotations.addChoice(core::NotationsChoice::tied(tied)); + } + for (const auto &tupletStop : myNoteData.noteAttachmentData.tupletStops) { core::Tuplet tuplet; diff --git a/src/private/mxtest/impl/CurveFunctionsTest.cpp b/src/private/mxtest/impl/CurveFunctionsTest.cpp index 3574997ef..3b0b1bbc5 100644 --- a/src/private/mxtest/impl/CurveFunctionsTest.cpp +++ b/src/private/mxtest/impl/CurveFunctionsTest.cpp @@ -6,8 +6,10 @@ #ifdef MX_COMPILE_IMPL_TESTS #include "cpul/cpulTestHarness.h" +#include "mx/api/NoteData.h" #include "mx/core/generated/Slur.h" #include "mx/core/generated/Tied.h" +#include "mx/core/generated/TiedType.h" #include "mx/impl/CurveFunctions.h" using namespace mx; @@ -1019,4 +1021,288 @@ TEST(writeAttributesFromCurveStop_relativeY, CurveFunctions) T_END +///////////////////////////////////////////////////////////////////////////////// +// laissez-vibrer / let-ring ties (a lone ) + +namespace +{ +api::TieLetRing seedLetRing() +{ + api::TieLetRing c; + c.isSpecified = true; + c.positionData.isDefaultXSpecified = true; + c.positionData.defaultX = 3.0; + c.positionData.isDefaultYSpecified = true; + c.positionData.defaultY = 4.0; + c.positionData.isRelativeXSpecified = true; + c.positionData.relativeX = 5.0; + c.positionData.isRelativeYSpecified = true; + c.positionData.relativeY = 6.0; + c.positionData.placement = api::Placement::below; + c.isColorSpecified = true; + c.colorData.red = 8; + c.colorData.green = 9; + c.colorData.blue = 10; + c.colorData.isAlphaSpecified = false; + c.curveOrientation = api::CurveOrientation::overhand; + return c; +} +} // namespace + +TEST(parseTieLetRing_isSpecified, CurveFunctions) +{ + using namespace mx::impl; + using namespace mx; + auto e = seed(); + e.setType(core::TiedType::letRing()); + auto c = impl::parseTieLetRing(e); + CHECK(c.isSpecified); +} + +T_END + +TEST(parseTieLetRing_positionData_x, CurveFunctions) +{ + using namespace mx::impl; + using namespace mx; + auto e = seed(); + e.setType(core::TiedType::letRing()); + auto c = impl::parseTieLetRing(e); + CHECK(c.positionData.isDefaultXSpecified); + CHECK_DOUBLES_EQUAL(3.0, c.positionData.defaultX, 0.01); +} + +T_END + +TEST(parseTieLetRing_positionData_y, CurveFunctions) +{ + using namespace mx::impl; + using namespace mx; + auto e = seed(); + e.setType(core::TiedType::letRing()); + auto c = impl::parseTieLetRing(e); + CHECK(c.positionData.isDefaultYSpecified); + CHECK_DOUBLES_EQUAL(4.0, c.positionData.defaultY, 0.01); +} + +T_END + +TEST(parseTieLetRing_positionData_rx, CurveFunctions) +{ + using namespace mx::impl; + using namespace mx; + auto e = seed(); + e.setType(core::TiedType::letRing()); + auto c = impl::parseTieLetRing(e); + CHECK(c.positionData.isRelativeXSpecified); + CHECK_DOUBLES_EQUAL(5.0, c.positionData.relativeX, 0.01); +} + +T_END + +TEST(parseTieLetRing_positionData_ry, CurveFunctions) +{ + using namespace mx::impl; + using namespace mx; + auto e = seed(); + e.setType(core::TiedType::letRing()); + auto c = impl::parseTieLetRing(e); + CHECK(c.positionData.isRelativeYSpecified); + CHECK_DOUBLES_EQUAL(6.0, c.positionData.relativeY, 0.01); +} + +T_END + +TEST(parseTieLetRing_placement, CurveFunctions) +{ + using namespace mx::impl; + using namespace mx; + auto e = seed(); + e.setType(core::TiedType::letRing()); + auto c = impl::parseTieLetRing(e); + CHECK(api::Placement::below == c.positionData.placement); +} + +T_END + +TEST(parseTieLetRing_color, CurveFunctions) +{ + using namespace mx::impl; + using namespace mx; + auto e = seed(); + e.setType(core::TiedType::letRing()); + auto c = impl::parseTieLetRing(e); + CHECK(c.isColorSpecified); + CHECK_EQUAL(8, static_cast(c.colorData.red)); + CHECK_EQUAL(9, static_cast(c.colorData.green)); + CHECK_EQUAL(10, static_cast(c.colorData.blue)); +} + +T_END + +TEST(parseTieLetRing_orientation, CurveFunctions) +{ + using namespace mx::impl; + using namespace mx; + auto e = seed(); + e.setType(core::TiedType::letRing()); + auto c = impl::parseTieLetRing(e); + CHECK(api::CurveOrientation::overhand == c.curveOrientation); +} + +T_END + +// parseCurve must route a let-ring into NoteData::tieLetRing and leave +// the start/continue/stop curve vectors untouched. +TEST(parseCurve_letRing_routesToTieLetRing, CurveFunctions) +{ + using namespace mx::impl; + using namespace mx; + auto e = seed(); + e.setType(core::TiedType::letRing()); + api::NoteData nd; + impl::parseCurve(e, nd); + CHECK(nd.tieLetRing.isSpecified); + CHECK_EQUAL(0, static_cast(nd.noteAttachmentData.curveStarts.size())); + CHECK_EQUAL(0, static_cast(nd.noteAttachmentData.curveStops.size())); + CHECK_EQUAL(0, static_cast(nd.noteAttachmentData.curveContinuations.size())); +} + +T_END + +// A regular tie start must still route into the curve vectors and must not be +// mistaken for a let-ring tie. +TEST(parseCurve_start_doesNotSetTieLetRing, CurveFunctions) +{ + using namespace mx::impl; + using namespace mx; + auto e = seed(); // TiedType defaults to start + api::NoteData nd; + impl::parseCurve(e, nd); + CHECK(!nd.tieLetRing.isSpecified); + CHECK_EQUAL(1, static_cast(nd.noteAttachmentData.curveStarts.size())); +} + +T_END + +TEST(writeAttributesFromTieLetRing_type, CurveFunctions) +{ + using namespace mx::impl; + using namespace mx; + core::Tied attr; + auto c = seedLetRing(); + impl::writeAttributesFromTieLetRing(c, attr); + CHECK(core::TiedType::letRing() == attr.type()); +} + +T_END + +TEST(writeAttributesFromTieLetRing_defaultX, CurveFunctions) +{ + using namespace mx::impl; + using namespace mx; + core::Tied attr; + auto c = seedLetRing(); + impl::writeAttributesFromTieLetRing(c, attr); + CHECK(attr.defaultX().has_value()); + CHECK_DOUBLES_EQUAL(3.0, attr.defaultX()->value().value(), 0.01); +} + +T_END + +TEST(writeAttributesFromTieLetRing_defaultY, CurveFunctions) +{ + using namespace mx::impl; + using namespace mx; + core::Tied attr; + auto c = seedLetRing(); + impl::writeAttributesFromTieLetRing(c, attr); + CHECK(attr.defaultY().has_value()); + CHECK_DOUBLES_EQUAL(4.0, attr.defaultY()->value().value(), 0.01); +} + +T_END + +TEST(writeAttributesFromTieLetRing_relativeX, CurveFunctions) +{ + using namespace mx::impl; + using namespace mx; + core::Tied attr; + auto c = seedLetRing(); + impl::writeAttributesFromTieLetRing(c, attr); + CHECK(attr.relativeX().has_value()); + CHECK_DOUBLES_EQUAL(5.0, attr.relativeX()->value().value(), 0.01); +} + +T_END + +TEST(writeAttributesFromTieLetRing_relativeY, CurveFunctions) +{ + using namespace mx::impl; + using namespace mx; + core::Tied attr; + auto c = seedLetRing(); + impl::writeAttributesFromTieLetRing(c, attr); + CHECK(attr.relativeY().has_value()); + CHECK_DOUBLES_EQUAL(6.0, attr.relativeY()->value().value(), 0.01); +} + +T_END + +TEST(writeAttributesFromTieLetRing_placement, CurveFunctions) +{ + using namespace mx::impl; + using namespace mx; + core::Tied attr; + auto c = seedLetRing(); + impl::writeAttributesFromTieLetRing(c, attr); + CHECK(attr.placement().has_value()); + CHECK(core::AboveBelow::below() == *attr.placement()); +} + +T_END + +TEST(writeAttributesFromTieLetRing_color, CurveFunctions) +{ + using namespace mx::impl; + using namespace mx; + core::Tied attr; + auto c = seedLetRing(); + impl::writeAttributesFromTieLetRing(c, attr); + CHECK(attr.color().has_value()); + CHECK_EQUAL(8, static_cast(attr.color()->red())); + CHECK_EQUAL(9, static_cast(attr.color()->green())); + CHECK_EQUAL(10, static_cast(attr.color()->blue())); +} + +T_END + +TEST(writeAttributesFromTieLetRing_orientation, CurveFunctions) +{ + using namespace mx::impl; + using namespace mx; + core::Tied attr; + auto c = seedLetRing(); + impl::writeAttributesFromTieLetRing(c, attr); + CHECK(attr.orientation().has_value()); + CHECK(core::OverUnder::over() == *attr.orientation()); +} + +T_END + +// Round-trip: write a seeded TieLetRing to a core::Tied, then read it back and +// confirm the api values survive. +TEST(tieLetRing_roundTrip, CurveFunctions) +{ + using namespace mx::impl; + using namespace mx; + const auto original = seedLetRing(); + core::Tied attr; + impl::writeAttributesFromTieLetRing(original, attr); + const auto roundTripped = impl::parseTieLetRing(attr); + CHECK(original == roundTripped); +} + +T_END + #endif From 8d19f84cda2a2f319ce3644dbc0624d48efca1f8 Mon Sep 17 00:00:00 2001 From: Matthew James Briggs Date: Thu, 2 Jul 2026 17:50:10 +0000 Subject: [PATCH 2/4] refactor(api): replace TieLetRing::isSpecified with std::optional Per review feedback on #286: the isSpecified bool inside TieLetRing is awkward because presence of the lv tie should be expressed at the field level, not inside the struct. Change NoteData::tieLetRing from TieLetRing to std::optional and remove the now-redundant isSpecified member. Update the writer (has_value()), reader (assign directly), and tests (has_value() / remove parseTieLetRing_isSpecified test). --- src/include/mx/api/NoteData.h | 14 ++++---------- src/private/mx/impl/CurveFunctions.h | 4 +--- src/private/mx/impl/NotationsWriter.cpp | 4 ++-- src/private/mxtest/impl/CurveFunctionsTest.cpp | 17 ++--------------- 4 files changed, 9 insertions(+), 30 deletions(-) diff --git a/src/include/mx/api/NoteData.h b/src/include/mx/api/NoteData.h index d90a58f98..adc1b8692 100644 --- a/src/include/mx/api/NoteData.h +++ b/src/include/mx/api/NoteData.h @@ -12,6 +12,7 @@ #include "mx/api/PositionData.h" #include "mx/api/PrintData.h" +#include #include namespace mx @@ -86,26 +87,19 @@ enum class Stem // rather than by overloading NoteData's isTieStart / isTieStop. struct TieLetRing { - // Whether this note carries an lv (let-ring) tie. When false the remaining - // fields are unused. A default-constructed, all-fields-default TieLetRing - // with isSpecified true corresponds to a bare . - bool isSpecified; - - // Optional visual attributes carried by the element. + // Visual attributes carried by the element. PositionData positionData; // default-x/y, relative-x/y, placement CurveOrientation curveOrientation; bool isColorSpecified; ColorData colorData; TieLetRing() - : isSpecified{false}, positionData{}, curveOrientation{CurveOrientation::unspecified}, isColorSpecified{false}, - colorData{} + : positionData{}, curveOrientation{CurveOrientation::unspecified}, isColorSpecified{false}, colorData{} { } }; MXAPI_EQUALS_BEGIN(TieLetRing) -MXAPI_EQUALS_MEMBER(isSpecified) MXAPI_EQUALS_MEMBER(positionData) MXAPI_EQUALS_MEMBER(curveOrientation) MXAPI_EQUALS_MEMBER(isColorSpecified) @@ -138,7 +132,7 @@ class NoteData // A laissez-vibrer / let-ring tie on this note (a lone // with no matching stop). Independent of isTieStart / isTieStop. See TieLetRing. - TieLetRing tieLetRing; + std::optional tieLetRing; NoteType noteType; // normal, cue, grace Notehead notehead; diff --git a/src/private/mx/impl/CurveFunctions.h b/src/private/mx/impl/CurveFunctions.h index bcd089559..3ec60c54d 100644 --- a/src/private/mx/impl/CurveFunctions.h +++ b/src/private/mx/impl/CurveFunctions.h @@ -284,8 +284,7 @@ void writeAttributesFromCurveStop(const api::CurveStop inCurve, ATTRIBUTES_TYPE } // Emits a lone from an api::TieLetRing, carrying its -// shared visual attributes (position, orientation, color). The caller decides -// whether to emit at all (i.e. checks isSpecified). +// shared visual attributes (position, orientation, color). inline void writeAttributesFromTieLetRing(const api::TieLetRing &inTie, core::Tied &outTied) { outTied.setType(core::TiedType::letRing()); @@ -310,7 +309,6 @@ inline void writeAttributesFromTieLetRing(const api::TieLetRing &inTie, core::Ti inline api::TieLetRing parseTieLetRing(const core::Tied &inTied) { api::TieLetRing c; - c.isSpecified = true; c.positionData = impl::getPositionData(inTied); c.isColorSpecified = checkHasColor(&inTied); diff --git a/src/private/mx/impl/NotationsWriter.cpp b/src/private/mx/impl/NotationsWriter.cpp index b197e432d..9add7b86b 100644 --- a/src/private/mx/impl/NotationsWriter.cpp +++ b/src/private/mx/impl/NotationsWriter.cpp @@ -150,10 +150,10 @@ core::Notations NotationsWriter::getNotations() const // A laissez-vibrer / let-ring tie is a lone with no // start/stop pairing, so it is emitted from its own field rather than the // curve vectors above. - if (myNoteData.tieLetRing.isSpecified) + if (myNoteData.tieLetRing.has_value()) { core::Tied tied; - writeAttributesFromTieLetRing(myNoteData.tieLetRing, tied); + writeAttributesFromTieLetRing(*myNoteData.tieLetRing, tied); outNotations.addChoice(core::NotationsChoice::tied(tied)); } diff --git a/src/private/mxtest/impl/CurveFunctionsTest.cpp b/src/private/mxtest/impl/CurveFunctionsTest.cpp index 3b0b1bbc5..c8dd0220d 100644 --- a/src/private/mxtest/impl/CurveFunctionsTest.cpp +++ b/src/private/mxtest/impl/CurveFunctionsTest.cpp @@ -1029,7 +1029,6 @@ namespace api::TieLetRing seedLetRing() { api::TieLetRing c; - c.isSpecified = true; c.positionData.isDefaultXSpecified = true; c.positionData.defaultX = 3.0; c.positionData.isDefaultYSpecified = true; @@ -1049,18 +1048,6 @@ api::TieLetRing seedLetRing() } } // namespace -TEST(parseTieLetRing_isSpecified, CurveFunctions) -{ - using namespace mx::impl; - using namespace mx; - auto e = seed(); - e.setType(core::TiedType::letRing()); - auto c = impl::parseTieLetRing(e); - CHECK(c.isSpecified); -} - -T_END - TEST(parseTieLetRing_positionData_x, CurveFunctions) { using namespace mx::impl; @@ -1162,7 +1149,7 @@ TEST(parseCurve_letRing_routesToTieLetRing, CurveFunctions) e.setType(core::TiedType::letRing()); api::NoteData nd; impl::parseCurve(e, nd); - CHECK(nd.tieLetRing.isSpecified); + CHECK(nd.tieLetRing.has_value()); CHECK_EQUAL(0, static_cast(nd.noteAttachmentData.curveStarts.size())); CHECK_EQUAL(0, static_cast(nd.noteAttachmentData.curveStops.size())); CHECK_EQUAL(0, static_cast(nd.noteAttachmentData.curveContinuations.size())); @@ -1179,7 +1166,7 @@ TEST(parseCurve_start_doesNotSetTieLetRing, CurveFunctions) auto e = seed(); // TiedType defaults to start api::NoteData nd; impl::parseCurve(e, nd); - CHECK(!nd.tieLetRing.isSpecified); + CHECK(!nd.tieLetRing.has_value()); CHECK_EQUAL(1, static_cast(nd.noteAttachmentData.curveStarts.size())); } From 7a17746768994e6dc9a924e73924edc144191ca2 Mon Sep 17 00:00:00 2001 From: Matthew James Briggs Date: Thu, 2 Jul 2026 18:11:51 +0000 Subject: [PATCH 3/4] style: run clang-format on NoteData.h CI failed fmt-check after the previous commit; this applies clang-format's line-collapse for the now-shorter TieLetRing() initializer list. --- src/include/mx/api/NoteData.h | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/include/mx/api/NoteData.h b/src/include/mx/api/NoteData.h index adc1b8692..67b8dd7d1 100644 --- a/src/include/mx/api/NoteData.h +++ b/src/include/mx/api/NoteData.h @@ -93,8 +93,7 @@ struct TieLetRing bool isColorSpecified; ColorData colorData; - TieLetRing() - : positionData{}, curveOrientation{CurveOrientation::unspecified}, isColorSpecified{false}, colorData{} + TieLetRing() : positionData{}, curveOrientation{CurveOrientation::unspecified}, isColorSpecified{false}, colorData{} { } }; From 7f2e62b6f08dfaf9b1ff0561683e1ad15b1b5603 Mon Sep 17 00:00:00 2001 From: Matthew James Briggs Date: Thu, 2 Jul 2026 18:39:19 +0000 Subject: [PATCH 4/4] docs: shorten TieLetRing comment Trim the musical explanation of what an lv/let-ring tie is (musicians already know) and keep only the non-obvious parts: the MusicXML mapping and why it's a separate struct instead of overloading isTieStart/isTieStop. --- src/include/mx/api/NoteData.h | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/include/mx/api/NoteData.h b/src/include/mx/api/NoteData.h index 67b8dd7d1..3a95a6ad5 100644 --- a/src/include/mx/api/NoteData.h +++ b/src/include/mx/api/NoteData.h @@ -77,14 +77,9 @@ enum class Stem both }; -// A laissez-vibrer tie, also called a "let-ring" or "lv" tie. Unlike a normal -// tie, which is a start/stop pair joining two notes of the same pitch, an lv -// tie applies to a single note and has no matching stop: it instructs the -// renderer to let the note ring undamped. In MusicXML this is a lone -// notation (there is no sound-level counterpart, -// since the element's type is only start or stop). Because it is -// structurally unlike a start/stop tie, it is modeled with its own struct -// rather than by overloading NoteData's isTieStart / isTieStop. +// A lone : no matching stop, and no sound-level +// counterpart (whose type is start/stop only). Modeled as its own struct +// rather than folding into NoteData's isTieStart / isTieStop. struct TieLetRing { // Visual attributes carried by the element.