diff --git a/src/include/mx/api/NoteData.h b/src/include/mx/api/NoteData.h index 99963d24..3a95a6ad 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" @@ -11,6 +12,7 @@ #include "mx/api/PositionData.h" #include "mx/api/PrintData.h" +#include #include namespace mx @@ -75,6 +77,30 @@ enum class Stem both }; +// 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. + PositionData positionData; // default-x/y, relative-x/y, placement + CurveOrientation curveOrientation; + bool isColorSpecified; + ColorData colorData; + + TieLetRing() : positionData{}, curveOrientation{CurveOrientation::unspecified}, isColorSpecified{false}, colorData{} + { + } +}; + +MXAPI_EQUALS_BEGIN(TieLetRing) +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 +124,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. + std::optional tieLetRing; + NoteType noteType; // normal, cue, grace Notehead notehead; PitchData pitchData; // step, alter, octave, accidental, etc @@ -140,6 +170,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 fe53eb72..70cdeb2b 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 b7c56d6e..3ec60c54 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,71 @@ void writeAttributesFromCurveStop(const api::CurveStop inCurve, ATTRIBUTES_TYPE } } +// Emits a lone from an api::TieLetRing, carrying its +// shared visual attributes (position, orientation, color). +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.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 ac4cc504..9add7b86 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.has_value()) + { + 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 3574997e..c8dd0220 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,275 @@ TEST(writeAttributesFromCurveStop_relativeY, CurveFunctions) T_END +///////////////////////////////////////////////////////////////////////////////// +// laissez-vibrer / let-ring ties (a lone ) + +namespace +{ +api::TieLetRing seedLetRing() +{ + api::TieLetRing c; + 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_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.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())); +} + +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.has_value()); + 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