From 7a7b0a4c3c4b3a71475fc29d03896622f794bec9 Mon Sep 17 00:00:00 2001 From: Robert Patterson Date: Fri, 14 Aug 2026 17:45:20 -0500 Subject: [PATCH] feat: model lyric extension endpoints in mx::api Preserve legacy bare lyric extensions while exposing start, continue, and stop endpoints for typed word extensions, including textless terminating lyrics. --- src/include/mx/api/LyricData.h | 16 +++++++++-- src/private/mx/impl/NoteReader.cpp | 36 +++++++++++++++++++++++- src/private/mx/impl/NoteWriter.cpp | 34 ++++++++++++++++++++-- src/private/mxtest/api/LyricDataTest.cpp | 6 ++++ 4 files changed, 87 insertions(+), 5 deletions(-) diff --git a/src/include/mx/api/LyricData.h b/src/include/mx/api/LyricData.h index 0598ca9b2..f88c6f216 100644 --- a/src/include/mx/api/LyricData.h +++ b/src/include/mx/api/LyricData.h @@ -23,12 +23,22 @@ enum class LyricSyllabic middle }; +// unspecified writes the legacy bare form. The other values identify the +// beginning, continuation, or end of a word extension across notes. +enum class LyricExtendType +{ + unspecified, + start, + continue_, + stop +}; + class LyricData { public: LyricData() - : text{}, verseNumber{}, verseName{}, syllabic{LyricSyllabic::unspecified}, hasExtend{false}, positionData{}, - printData{} + : text{}, verseNumber{}, verseName{}, syllabic{LyricSyllabic::unspecified}, hasExtend{false}, + extendType{LyricExtendType::unspecified}, positionData{}, printData{} { } @@ -37,6 +47,7 @@ class LyricData std::string verseName; LyricSyllabic syllabic; bool hasExtend; + LyricExtendType extendType; PositionData positionData; PrintData printData; }; @@ -47,6 +58,7 @@ MXAPI_EQUALS_MEMBER(verseNumber) MXAPI_EQUALS_MEMBER(verseName) MXAPI_EQUALS_MEMBER(syllabic) MXAPI_EQUALS_MEMBER(hasExtend) +MXAPI_EQUALS_MEMBER(extendType) MXAPI_EQUALS_MEMBER(positionData) MXAPI_EQUALS_MEMBER(printData) MXAPI_EQUALS_END; diff --git a/src/private/mx/impl/NoteReader.cpp b/src/private/mx/impl/NoteReader.cpp index e47d577a8..d95270a18 100644 --- a/src/private/mx/impl/NoteReader.cpp +++ b/src/private/mx/impl/NoteReader.cpp @@ -10,6 +10,7 @@ #include "mx/core/generated/LyricTextGroup.h" #include "mx/core/generated/Notations.h" #include "mx/core/generated/NotationsChoice.h" +#include "mx/core/generated/StartStopContinue.h" #include "mx/core/generated/Syllabic.h" #include "mx/core/generated/TextElementData.h" #include "mx/core/generated/Tied.h" @@ -547,7 +548,25 @@ void NoteReader::setLyric() lyricData.syllabic = textGroup.syllabic().has_value() ? convertLyricSyllabic(*textGroup.syllabic()) : api::LyricSyllabic::unspecified; - lyricData.hasExtend = textGroup.extend().has_value(); + if (textGroup.extend().has_value()) + { + lyricData.hasExtend = true; + if (textGroup.extend()->type().has_value()) + { + switch (textGroup.extend()->type()->tag()) + { + case core::StartStopContinue::Tag::start: + lyricData.extendType = api::LyricExtendType::start; + break; + case core::StartStopContinue::Tag::continue_: + lyricData.extendType = api::LyricExtendType::continue_; + break; + case core::StartStopContinue::Tag::stop: + lyricData.extendType = api::LyricExtendType::stop; + break; + } + } + } myLyrics.emplace_back(lyricData); myHasLyric = true; break; @@ -555,6 +574,21 @@ void NoteReader::setLyric() case core::LyricChoice::Kind::extend: lyricData.hasExtend = true; + if (textChoice.asExtend().type().has_value()) + { + switch (textChoice.asExtend().type()->tag()) + { + case core::StartStopContinue::Tag::start: + lyricData.extendType = api::LyricExtendType::start; + break; + case core::StartStopContinue::Tag::continue_: + lyricData.extendType = api::LyricExtendType::continue_; + break; + case core::StartStopContinue::Tag::stop: + lyricData.extendType = api::LyricExtendType::stop; + break; + } + } lyricData.printData = getLyricPrintData(lyric, nullptr); myLyrics.emplace_back(lyricData); myHasLyric = true; diff --git a/src/private/mx/impl/NoteWriter.cpp b/src/private/mx/impl/NoteWriter.cpp index 8e3381b5e..90f6b18b3 100644 --- a/src/private/mx/impl/NoteWriter.cpp +++ b/src/private/mx/impl/NoteWriter.cpp @@ -575,7 +575,22 @@ void NoteWriter::setLyrics() const if (lyricData.text.empty() && lyricData.hasExtend) { - lyric.setChoice(core::LyricChoice::extend(core::Extend{})); + core::Extend extend; + switch (lyricData.extendType) + { + case api::LyricExtendType::unspecified: + break; + case api::LyricExtendType::start: + extend.setType(core::StartStopContinue::start()); + break; + case api::LyricExtendType::continue_: + extend.setType(core::StartStopContinue::continue_()); + break; + case api::LyricExtendType::stop: + extend.setType(core::StartStopContinue::stop()); + break; + } + lyric.setChoice(core::LyricChoice::extend(std::move(extend))); } else { @@ -591,7 +606,22 @@ void NoteWriter::setLyrics() const textGroup.setText(std::move(text)); if (lyricData.hasExtend) { - textGroup.setExtend(core::Extend{}); + core::Extend extend; + switch (lyricData.extendType) + { + case api::LyricExtendType::unspecified: + break; + case api::LyricExtendType::start: + extend.setType(core::StartStopContinue::start()); + break; + case api::LyricExtendType::continue_: + extend.setType(core::StartStopContinue::continue_()); + break; + case api::LyricExtendType::stop: + extend.setType(core::StartStopContinue::stop()); + break; + } + textGroup.setExtend(std::move(extend)); } lyric.setChoice(core::LyricChoice::lyricTextGroup(std::move(textGroup))); diff --git a/src/private/mxtest/api/LyricDataTest.cpp b/src/private/mxtest/api/LyricDataTest.cpp index 41204b8d5..a617a387f 100644 --- a/src/private/mxtest/api/LyricDataTest.cpp +++ b/src/private/mxtest/api/LyricDataTest.cpp @@ -43,6 +43,7 @@ mx::api::ScoreData makeScoreWithLyrics() first.verseName = "verse"; first.syllabic = LyricSyllabic::begin; first.hasExtend = true; + first.extendType = LyricExtendType::start; first.positionData.placement = Placement::below; first.positionData.horizontalAlignment = HorizontalAlignment::center; first.positionData.isDefaultYSpecified = true; @@ -58,6 +59,7 @@ mx::api::ScoreData makeScoreWithLyrics() LyricData second; second.verseNumber = "2"; second.hasExtend = true; + second.extendType = LyricExtendType::stop; second.printData.printObject = Bool::no; note.lyrics.emplace_back(second); @@ -92,11 +94,13 @@ TEST(lyricsWriteToMusicXml, LyricData) CHECK_EQUAL(std::string{"Hel"}, std::string{lyric.child("text").text().get()}); CHECK_EQUAL(std::string{"Bravura Text"}, std::string{lyric.child("text").attribute("font-family").value()}); CHECK(!lyric.child("extend").empty()); + CHECK_EQUAL(std::string{"start"}, std::string{lyric.child("extend").attribute("type").value()}); auto extendOnly = lyric.next_sibling("lyric"); CHECK_EQUAL(std::string{"2"}, std::string{extendOnly.attribute("number").value()}); CHECK_EQUAL(std::string{"no"}, std::string{extendOnly.attribute("print-object").value()}); CHECK(!extendOnly.child("extend").empty()); + CHECK_EQUAL(std::string{"stop"}, std::string{extendOnly.child("extend").attribute("type").value()}); CHECK(extendOnly.child("text").empty()); } @@ -113,6 +117,7 @@ TEST(lyricsRoundTripThroughApi, LyricData) CHECK_EQUAL(std::string{"verse"}, note.lyrics.at(0).verseName); CHECK(note.lyrics.at(0).syllabic == LyricSyllabic::begin); CHECK(note.lyrics.at(0).hasExtend); + CHECK(note.lyrics.at(0).extendType == LyricExtendType::start); CHECK(note.lyrics.at(0).positionData.placement == Placement::below); CHECK(note.lyrics.at(0).positionData.horizontalAlignment == HorizontalAlignment::center); CHECK(note.lyrics.at(0).printData.printObject == Bool::yes); @@ -125,6 +130,7 @@ TEST(lyricsRoundTripThroughApi, LyricData) CHECK_EQUAL(std::string{"2"}, note.lyrics.at(1).verseNumber); CHECK(note.lyrics.at(1).text.empty()); CHECK(note.lyrics.at(1).hasExtend); + CHECK(note.lyrics.at(1).extendType == LyricExtendType::stop); CHECK(note.lyrics.at(1).printData.printObject == Bool::no); }