diff --git a/src/include/mx/api/DampData.h b/src/include/mx/api/DampData.h new file mode 100644 index 000000000..eb17e7ffa --- /dev/null +++ b/src/include/mx/api/DampData.h @@ -0,0 +1,67 @@ +// MusicXML Class Library +// Copyright (c) by Matthew James Briggs +// Distributed under the MIT License + +#pragma once + +#include "mx/api/ApiCommon.h" +#include "mx/api/ColorData.h" +#include "mx/api/FontData.h" +#include "mx/api/PositionData.h" + +#include +#include + +namespace mx +{ +namespace api +{ +// A harp damping mark, MusicXML's element: the player silences one or more ringing +// strings with the hand. positionData captures default/relative x-y plus the horizontal and +// vertical alignment; its placement member is unused here because has no placement +// attribute (placement lives on the parent ). +class DampData +{ + public: + PositionData positionData; + FontData fontData; + std::optional color; + std::optional id; + + DampData() : positionData{}, fontData{}, color{}, id{} + { + } +}; + +MXAPI_EQUALS_BEGIN(DampData) +MXAPI_EQUALS_MEMBER(positionData) +MXAPI_EQUALS_MEMBER(fontData) +MXAPI_EQUALS_MEMBER(color) +MXAPI_EQUALS_MEMBER(id) +MXAPI_EQUALS_END; +MXAPI_NOT_EQUALS_AND_VECTORS(DampData); + +// A harp damping mark, MusicXML's element: the player silences all of the ringing +// strings at once, notated as an X inside a circle. Formatting works the same way as DampData. +class DampAllData +{ + public: + PositionData positionData; + FontData fontData; + std::optional color; + std::optional id; + + DampAllData() : positionData{}, fontData{}, color{}, id{} + { + } +}; + +MXAPI_EQUALS_BEGIN(DampAllData) +MXAPI_EQUALS_MEMBER(positionData) +MXAPI_EQUALS_MEMBER(fontData) +MXAPI_EQUALS_MEMBER(color) +MXAPI_EQUALS_MEMBER(id) +MXAPI_EQUALS_END; +MXAPI_NOT_EQUALS_AND_VECTORS(DampAllData); +} // namespace api +} // namespace mx diff --git a/src/include/mx/api/DirectionData.h b/src/include/mx/api/DirectionData.h index 5888f03c5..d37510676 100644 --- a/src/include/mx/api/DirectionData.h +++ b/src/include/mx/api/DirectionData.h @@ -7,12 +7,16 @@ #include "mx/api/ApiCommon.h" #include "mx/api/ChordData.h" #include "mx/api/CodaData.h" +#include "mx/api/DampData.h" +#include "mx/api/EyeglassesData.h" #include "mx/api/FiguredBassData.h" #include "mx/api/MarkData.h" #include "mx/api/OttavaData.h" #include "mx/api/RehearsalData.h" #include "mx/api/SegnoData.h" #include "mx/api/SoundData.h" +#include "mx/api/StaffDivideData.h" +#include "mx/api/StringMuteData.h" #include "mx/api/TempoData.h" #include "mx/api/WedgeData.h" #include "mx/api/WordsData.h" @@ -39,7 +43,12 @@ enum class DirectionComponentKind chord, segno, coda, - rehearsal + rehearsal, + damp, + dampAll, + eyeglasses, + stringMute, + staffDivide }; struct DirectionComponent @@ -120,6 +129,11 @@ struct DirectionData std::vector segnos; std::vector codas; std::vector rehearsals; + std::vector damps; + std::vector dampAlls; + std::vector eyeglasses; + std::vector stringMutes; + std::vector staffDivides; // Preserves the original order of direction-type children from parsed XML // for round-trip fidelity. Do NOT populate this when constructing // DirectionData programmatically. If empty, the writer uses a default @@ -146,6 +160,9 @@ inline bool isDirectionDataEmpty(const DirectionData &directionData) directionData.tempos.size() == 0 && directionData.ottavaStarts.size() == 0 && directionData.ottavaStops.size() == 0 && directionData.words.size() == 0 && directionData.segnos.size() == 0 && directionData.codas.size() == 0 && + directionData.rehearsals.size() == 0 && directionData.damps.size() == 0 && + directionData.dampAlls.size() == 0 && directionData.eyeglasses.size() == 0 && + directionData.stringMutes.size() == 0 && directionData.staffDivides.size() == 0 && directionData.figuredBasses.size() == 0 && !directionData.isSoundDataSpecified && directionData.orderedComponents.size() == 0; } @@ -174,6 +191,12 @@ MXAPI_EQUALS_MEMBER(chords) MXAPI_EQUALS_MEMBER(figuredBasses) MXAPI_EQUALS_MEMBER(segnos) MXAPI_EQUALS_MEMBER(codas) +MXAPI_EQUALS_MEMBER(rehearsals) +MXAPI_EQUALS_MEMBER(damps) +MXAPI_EQUALS_MEMBER(dampAlls) +MXAPI_EQUALS_MEMBER(eyeglasses) +MXAPI_EQUALS_MEMBER(stringMutes) +MXAPI_EQUALS_MEMBER(staffDivides) MXAPI_EQUALS_MEMBER(orderedComponents) MXAPI_EQUALS_END; MXAPI_NOT_EQUALS_AND_VECTORS(DirectionData); diff --git a/src/include/mx/api/EyeglassesData.h b/src/include/mx/api/EyeglassesData.h new file mode 100644 index 000000000..39a3e7135 --- /dev/null +++ b/src/include/mx/api/EyeglassesData.h @@ -0,0 +1,45 @@ +// MusicXML Class Library +// Copyright (c) by Matthew James Briggs +// Distributed under the MIT License + +#pragma once + +#include "mx/api/ApiCommon.h" +#include "mx/api/ColorData.h" +#include "mx/api/FontData.h" +#include "mx/api/PositionData.h" + +#include +#include + +namespace mx +{ +namespace api +{ +// The eyeglasses symbol, MusicXML's element: a cue common in commercial music +// telling the player to watch the conductor (or another player) closely at this spot. +// positionData captures default/relative x-y plus the horizontal and vertical alignment; its +// placement member is unused here because has no placement attribute (placement +// lives on the parent ). +class EyeglassesData +{ + public: + PositionData positionData; + FontData fontData; + std::optional color; + std::optional id; + + EyeglassesData() : positionData{}, fontData{}, color{}, id{} + { + } +}; + +MXAPI_EQUALS_BEGIN(EyeglassesData) +MXAPI_EQUALS_MEMBER(positionData) +MXAPI_EQUALS_MEMBER(fontData) +MXAPI_EQUALS_MEMBER(color) +MXAPI_EQUALS_MEMBER(id) +MXAPI_EQUALS_END; +MXAPI_NOT_EQUALS_AND_VECTORS(EyeglassesData); +} // namespace api +} // namespace mx diff --git a/src/include/mx/api/StaffDivideData.h b/src/include/mx/api/StaffDivideData.h new file mode 100644 index 000000000..88b7ff898 --- /dev/null +++ b/src/include/mx/api/StaffDivideData.h @@ -0,0 +1,56 @@ +// MusicXML Class Library +// Copyright (c) by Matthew James Briggs +// Distributed under the MIT License + +#pragma once + +#include "mx/api/ApiCommon.h" +#include "mx/api/ColorData.h" +#include "mx/api/FontData.h" +#include "mx/api/PositionData.h" + +#include +#include + +namespace mx +{ +namespace api +{ +// Which staff-divide arrow a StaffDivideData shows: an arrow pointing down, up, or both. +enum class StaffDivideType +{ + down, + up, + upDown +}; + +// A staff-divide arrow, MusicXML's element (MusicXML 3.1): the arrow symbol used +// in condensed conductor scores where one staff carries two players, showing whether the music +// continues on the staff below (down), above (up), or both (up-down). positionData captures +// default/relative x-y plus the horizontal and vertical alignment; its placement member is +// unused here because has no placement attribute (placement lives on the parent +// ). +class StaffDivideData +{ + public: + StaffDivideType type; + PositionData positionData; + FontData fontData; + std::optional color; + std::optional id; + + StaffDivideData() : type{StaffDivideType::down}, positionData{}, fontData{}, color{}, id{} + { + } +}; + +MXAPI_EQUALS_BEGIN(StaffDivideData) +MXAPI_EQUALS_MEMBER(type) +MXAPI_EQUALS_MEMBER(positionData) +MXAPI_EQUALS_MEMBER(fontData) +MXAPI_EQUALS_MEMBER(color) +MXAPI_EQUALS_MEMBER(id) +MXAPI_EQUALS_END; +MXAPI_NOT_EQUALS_AND_VECTORS(StaffDivideData); +} // namespace api +} // namespace mx diff --git a/src/include/mx/api/StringMuteData.h b/src/include/mx/api/StringMuteData.h new file mode 100644 index 000000000..b156caba0 --- /dev/null +++ b/src/include/mx/api/StringMuteData.h @@ -0,0 +1,53 @@ +// MusicXML Class Library +// Copyright (c) by Matthew James Briggs +// Distributed under the MIT License + +#pragma once + +#include "mx/api/ApiCommon.h" +#include "mx/api/ColorData.h" +#include "mx/api/FontData.h" +#include "mx/api/PositionData.h" + +#include +#include + +namespace mx +{ +namespace api +{ +// Whether a StringMuteData turns muting on or off. +enum class StringMuteType +{ + on, + off +}; + +// A string mute change, MusicXML's element: the "mute on" or "mute off" symbol +// telling string players to apply or remove their mutes. positionData captures default/relative +// x-y plus the horizontal and vertical alignment; its placement member is unused here because +// has no placement attribute (placement lives on the parent ). +class StringMuteData +{ + public: + StringMuteType type; + PositionData positionData; + FontData fontData; + std::optional color; + std::optional id; + + StringMuteData() : type{StringMuteType::on}, positionData{}, fontData{}, color{}, id{} + { + } +}; + +MXAPI_EQUALS_BEGIN(StringMuteData) +MXAPI_EQUALS_MEMBER(type) +MXAPI_EQUALS_MEMBER(positionData) +MXAPI_EQUALS_MEMBER(fontData) +MXAPI_EQUALS_MEMBER(color) +MXAPI_EQUALS_MEMBER(id) +MXAPI_EQUALS_END; +MXAPI_NOT_EQUALS_AND_VECTORS(StringMuteData); +} // namespace api +} // namespace mx diff --git a/src/private/mx/impl/DirectionReader.cpp b/src/private/mx/impl/DirectionReader.cpp index de59380f1..c27e5030f 100644 --- a/src/private/mx/impl/DirectionReader.cpp +++ b/src/private/mx/impl/DirectionReader.cpp @@ -20,6 +20,7 @@ #include "mx/core/generated/DirectionType.h" #include "mx/core/generated/DirectionTypeChoice.h" #include "mx/core/generated/Dynamics.h" +#include "mx/core/generated/EmptyPrintStyleAlignID.h" #include "mx/core/generated/Fingering.h" #include "mx/core/generated/FirstFret.h" #include "mx/core/generated/Frame.h" @@ -41,6 +42,7 @@ #include "mx/core/generated/NumeralValue.h" #include "mx/core/generated/OctaveShift.h" #include "mx/core/generated/Offset.h" +#include "mx/core/generated/OnOff.h" #include "mx/core/generated/OtherDirection.h" #include "mx/core/generated/Pedal.h" #include "mx/core/generated/PedalType.h" @@ -51,6 +53,8 @@ #include "mx/core/generated/Scordatura.h" #include "mx/core/generated/Segno.h" #include "mx/core/generated/Sound.h" +#include "mx/core/generated/StaffDivide.h" +#include "mx/core/generated/StaffDivideSymbol.h" #include "mx/core/generated/StartStop.h" #include "mx/core/generated/StartStopContinue.h" #include "mx/core/generated/Step.h" @@ -305,7 +309,7 @@ void DirectionReader::parseDirectionType(const core::DirectionType &directionTyp break; } case K::staffDivide: { - /* unhandled - new in MusicXML 4.0 */ + parseStaffDivide(directionType); break; } case K::otherDirection: { @@ -819,15 +823,15 @@ void DirectionReader::parseOctaveShift(const core::DirectionType &directionType) static_cast(myOutDirectionData.ottavaStarts.size()) - 1); } -// The eleven stubs below (harp-pedals, damp, damp-all, eyeglasses, string-mute, scordatura, -// image, principal-voice, accordion-registration, percussion, other-direction) are genuinely -// unmodeled direction-type choices, tracked at #324, not a bug in the surrounding dispatch. -// When a contains only one of these, the resulting DirectionData carries no -// content and is correctly left unwritten -- MusicXML requires at least one direction-type -// child, so there is no schema-valid way to keep the (and its /) -// without modeling what it actually says. That is why round-trip discovery reports those -// files as dropping /// together: it is one gap -// (the unmodeled content), not four. +// The stubs below (harp-pedals, scordatura, image, principal-voice, accordion-registration, +// percussion, other-direction) are genuinely unmodeled direction-type choices, tracked at +// #324, not a bug in the surrounding dispatch. When a contains only one of these, +// the resulting DirectionData carries no content and is correctly left unwritten -- MusicXML +// requires at least one direction-type child, so there is no schema-valid way to keep the +// (and its /) without modeling what it actually says. That is why +// round-trip discovery reports those files as dropping +// /// together: it is one gap (the unmodeled +// content), not four. void DirectionReader::parseHarpPedals(const core::DirectionType &directionType) { MX_UNUSED(directionType); @@ -835,22 +839,111 @@ void DirectionReader::parseHarpPedals(const core::DirectionType &directionType) void DirectionReader::parseDamp(const core::DirectionType &directionType) { - MX_UNUSED(directionType); + const auto &damp = directionType.choice().asDamp(); + api::DampData outDamp; + outDamp.positionData = getPositionData(damp); + outDamp.fontData = getFontData(damp); + if (damp.color().has_value()) + { + outDamp.color = getColor(damp); + } + if (damp.id().has_value()) + { + outDamp.id = damp.id()->value(); + } + myOutDirectionData.damps.emplace_back(std::move(outDamp)); + appendOrderedComponent(api::DirectionComponentKind::damp, static_cast(myOutDirectionData.damps.size()) - 1); } void DirectionReader::parseDampAll(const core::DirectionType &directionType) { - MX_UNUSED(directionType); + const auto &dampAll = directionType.choice().asDampAll(); + api::DampAllData outDampAll; + outDampAll.positionData = getPositionData(dampAll); + outDampAll.fontData = getFontData(dampAll); + if (dampAll.color().has_value()) + { + outDampAll.color = getColor(dampAll); + } + if (dampAll.id().has_value()) + { + outDampAll.id = dampAll.id()->value(); + } + myOutDirectionData.dampAlls.emplace_back(std::move(outDampAll)); + appendOrderedComponent(api::DirectionComponentKind::dampAll, + static_cast(myOutDirectionData.dampAlls.size()) - 1); } void DirectionReader::parseEyeglasses(const core::DirectionType &directionType) { - MX_UNUSED(directionType); + const auto &eyeglasses = directionType.choice().asEyeglasses(); + api::EyeglassesData outEyeglasses; + outEyeglasses.positionData = getPositionData(eyeglasses); + outEyeglasses.fontData = getFontData(eyeglasses); + if (eyeglasses.color().has_value()) + { + outEyeglasses.color = getColor(eyeglasses); + } + if (eyeglasses.id().has_value()) + { + outEyeglasses.id = eyeglasses.id()->value(); + } + myOutDirectionData.eyeglasses.emplace_back(std::move(outEyeglasses)); + appendOrderedComponent(api::DirectionComponentKind::eyeglasses, + static_cast(myOutDirectionData.eyeglasses.size()) - 1); } void DirectionReader::parseStringMute(const core::DirectionType &directionType) { - MX_UNUSED(directionType); + const auto &stringMute = directionType.choice().asStringMute(); + api::StringMuteData outStringMute; + outStringMute.type = + stringMute.type().tag() == core::OnOff::Tag::off ? api::StringMuteType::off : api::StringMuteType::on; + outStringMute.positionData = getPositionData(stringMute); + outStringMute.fontData = getFontData(stringMute); + if (stringMute.color().has_value()) + { + outStringMute.color = getColor(stringMute); + } + if (stringMute.id().has_value()) + { + outStringMute.id = stringMute.id()->value(); + } + myOutDirectionData.stringMutes.emplace_back(std::move(outStringMute)); + appendOrderedComponent(api::DirectionComponentKind::stringMute, + static_cast(myOutDirectionData.stringMutes.size()) - 1); +} + +void DirectionReader::parseStaffDivide(const core::DirectionType &directionType) +{ + const auto &staffDivide = directionType.choice().asStaffDivide(); + api::StaffDivideData outStaffDivide; + switch (staffDivide.type().tag()) + { + case core::StaffDivideSymbol::Tag::up: + outStaffDivide.type = api::StaffDivideType::up; + break; + case core::StaffDivideSymbol::Tag::upDown: + outStaffDivide.type = api::StaffDivideType::upDown; + break; + case core::StaffDivideSymbol::Tag::down: + default: + outStaffDivide.type = api::StaffDivideType::down; + break; + } + outStaffDivide.positionData = getPositionData(staffDivide); + outStaffDivide.fontData = getFontData(staffDivide); + if (staffDivide.color().has_value()) + { + outStaffDivide.color = getColor(staffDivide); + } + if (staffDivide.id().has_value()) + { + outStaffDivide.id = staffDivide.id()->value(); + } + myOutDirectionData.staffDivides.emplace_back(std::move(outStaffDivide)); + appendOrderedComponent(api::DirectionComponentKind::staffDivide, + static_cast(myOutDirectionData.staffDivides.size()) - 1); } void DirectionReader::parseScordatura(const core::DirectionType &directionType) diff --git a/src/private/mx/impl/DirectionReader.h b/src/private/mx/impl/DirectionReader.h index 0898bca55..a55c042bd 100644 --- a/src/private/mx/impl/DirectionReader.h +++ b/src/private/mx/impl/DirectionReader.h @@ -62,6 +62,7 @@ class DirectionReader void parseDampAll(const core::DirectionType &directionType); void parseEyeglasses(const core::DirectionType &directionType); void parseStringMute(const core::DirectionType &directionType); + void parseStaffDivide(const core::DirectionType &directionType); void parseScordatura(const core::DirectionType &directionType); void parseImage(const core::DirectionType &directionType); void parsePrincipalVoice(const core::DirectionType &directionType); diff --git a/src/private/mx/impl/DirectionWriter.cpp b/src/private/mx/impl/DirectionWriter.cpp index 79e608691..402823659 100644 --- a/src/private/mx/impl/DirectionWriter.cpp +++ b/src/private/mx/impl/DirectionWriter.cpp @@ -23,6 +23,7 @@ #include "mx/core/generated/Dynamics.h" #include "mx/core/generated/EditorialVoiceDirectionGroup.h" #include "mx/core/generated/Empty.h" +#include "mx/core/generated/EmptyPrintStyleAlignID.h" #include "mx/core/generated/Figure.h" #include "mx/core/generated/FiguredBass.h" #include "mx/core/generated/FirstFret.h" @@ -49,6 +50,7 @@ #include "mx/core/generated/NumeralValue.h" #include "mx/core/generated/OctaveShift.h" #include "mx/core/generated/Offset.h" +#include "mx/core/generated/OnOff.h" #include "mx/core/generated/Pedal.h" #include "mx/core/generated/PedalType.h" #include "mx/core/generated/PerMinute.h" @@ -58,9 +60,12 @@ #include "mx/core/generated/Segno.h" #include "mx/core/generated/Semitones.h" #include "mx/core/generated/Sound.h" +#include "mx/core/generated/StaffDivide.h" +#include "mx/core/generated/StaffDivideSymbol.h" #include "mx/core/generated/StartStop.h" #include "mx/core/generated/StartStopContinue.h" #include "mx/core/generated/String.h" +#include "mx/core/generated/StringMute.h" #include "mx/core/generated/StringNumber.h" #include "mx/core/generated/StyleText.h" #include "mx/core/generated/Wedge.h" @@ -611,6 +616,98 @@ void DirectionWriter::emitRehearsal(const api::RehearsalData &item, core::Direct addDirectionType(std::move(dt), direction); } +core::EmptyPrintStyleAlignID DirectionWriter::createEmptyPrintStyleAlign(const api::PositionData &positionData, + const api::FontData &fontData, + const std::optional &color, + const std::optional &id) +{ + core::EmptyPrintStyleAlignID element{}; + setAttributesFromPositionData(positionData, element); + setAttributesFromFontData(fontData, element); + if (color.has_value()) + { + setAttributesFromColorData(*color, element); + } + if (id.has_value()) + { + element.setID(core::Token{*id}); + } + return element; +} + +void DirectionWriter::emitDamp(const api::DampData &item, core::Direction &direction) +{ + core::DirectionType dt{}; + dt.setChoice(core::DirectionTypeChoice::damp( + createEmptyPrintStyleAlign(item.positionData, item.fontData, item.color, item.id))); + addDirectionType(std::move(dt), direction); +} + +void DirectionWriter::emitDampAll(const api::DampAllData &item, core::Direction &direction) +{ + core::DirectionType dt{}; + dt.setChoice(core::DirectionTypeChoice::dampAll( + createEmptyPrintStyleAlign(item.positionData, item.fontData, item.color, item.id))); + addDirectionType(std::move(dt), direction); +} + +void DirectionWriter::emitEyeglasses(const api::EyeglassesData &item, core::Direction &direction) +{ + core::DirectionType dt{}; + dt.setChoice(core::DirectionTypeChoice::eyeglasses( + createEmptyPrintStyleAlign(item.positionData, item.fontData, item.color, item.id))); + addDirectionType(std::move(dt), direction); +} + +void DirectionWriter::emitStringMute(const api::StringMuteData &item, core::Direction &direction) +{ + core::StringMute stringMute{}; + stringMute.setType(item.type == api::StringMuteType::off ? core::OnOff::off() : core::OnOff::on()); + setAttributesFromPositionData(item.positionData, stringMute); + setAttributesFromFontData(item.fontData, stringMute); + if (item.color.has_value()) + { + setAttributesFromColorData(*item.color, stringMute); + } + if (item.id.has_value()) + { + stringMute.setID(core::Token{*item.id}); + } + core::DirectionType dt{}; + dt.setChoice(core::DirectionTypeChoice::stringMute(std::move(stringMute))); + addDirectionType(std::move(dt), direction); +} + +void DirectionWriter::emitStaffDivide(const api::StaffDivideData &item, core::Direction &direction) +{ + core::StaffDivide staffDivide{}; + switch (item.type) + { + case api::StaffDivideType::up: + staffDivide.setType(core::StaffDivideSymbol::up()); + break; + case api::StaffDivideType::upDown: + staffDivide.setType(core::StaffDivideSymbol::upDown()); + break; + case api::StaffDivideType::down: + staffDivide.setType(core::StaffDivideSymbol::down()); + break; + } + setAttributesFromPositionData(item.positionData, staffDivide); + setAttributesFromFontData(item.fontData, staffDivide); + if (item.color.has_value()) + { + setAttributesFromColorData(*item.color, staffDivide); + } + if (item.id.has_value()) + { + staffDivide.setID(core::Token{*item.id}); + } + core::DirectionType dt{}; + dt.setChoice(core::DirectionTypeChoice::staffDivide(std::move(staffDivide))); + addDirectionType(std::move(dt), direction); +} + void DirectionWriter::emitFixedOrder(core::Direction &direction) { for (const auto &mark : myDirectionData.marks) @@ -689,6 +786,31 @@ void DirectionWriter::emitFixedOrder(core::Direction &direction) { emitRehearsal(item, direction); } + + for (const auto &item : myDirectionData.damps) + { + emitDamp(item, direction); + } + + for (const auto &item : myDirectionData.dampAlls) + { + emitDampAll(item, direction); + } + + for (const auto &item : myDirectionData.eyeglasses) + { + emitEyeglasses(item, direction); + } + + for (const auto &item : myDirectionData.stringMutes) + { + emitStringMute(item, direction); + } + + for (const auto &item : myDirectionData.staffDivides) + { + emitStaffDivide(item, direction); + } } void DirectionWriter::emitOrderedComponents(core::Direction &direction) @@ -817,6 +939,41 @@ void DirectionWriter::emitOrderedComponents(core::Direction &direction) } break; + case api::DirectionComponentKind::damp: + if (i >= 0 && static_cast(i) < myDirectionData.damps.size()) + { + emitDamp(myDirectionData.damps.at(i), direction); + } + break; + + case api::DirectionComponentKind::dampAll: + if (i >= 0 && static_cast(i) < myDirectionData.dampAlls.size()) + { + emitDampAll(myDirectionData.dampAlls.at(i), direction); + } + break; + + case api::DirectionComponentKind::eyeglasses: + if (i >= 0 && static_cast(i) < myDirectionData.eyeglasses.size()) + { + emitEyeglasses(myDirectionData.eyeglasses.at(i), direction); + } + break; + + case api::DirectionComponentKind::stringMute: + if (i >= 0 && static_cast(i) < myDirectionData.stringMutes.size()) + { + emitStringMute(myDirectionData.stringMutes.at(i), direction); + } + break; + + case api::DirectionComponentKind::staffDivide: + if (i >= 0 && static_cast(i) < myDirectionData.staffDivides.size()) + { + emitStaffDivide(myDirectionData.staffDivides.at(i), direction); + } + break; + case api::DirectionComponentKind::chord: // Chords are emitted via createHarmonyElements, not as direction-types. break; diff --git a/src/private/mx/impl/DirectionWriter.h b/src/private/mx/impl/DirectionWriter.h index b801aa390..b19b7aa09 100644 --- a/src/private/mx/impl/DirectionWriter.h +++ b/src/private/mx/impl/DirectionWriter.h @@ -6,6 +6,7 @@ #include "mx/api/ApiCommon.h" #include "mx/api/DirectionData.h" +#include "mx/core/generated/EmptyPrintStyleAlignID.h" #include "mx/core/generated/MusicDataChoice.h" #include "mx/impl/Converter.h" #include "mx/impl/Cursor.h" @@ -51,6 +52,15 @@ class DirectionWriter void emitSegno(const api::SegnoData &item, core::Direction &direction); void emitCoda(const api::CodaData &item, core::Direction &direction); void emitRehearsal(const api::RehearsalData &item, core::Direction &direction); + core::EmptyPrintStyleAlignID createEmptyPrintStyleAlign(const api::PositionData &positionData, + const api::FontData &fontData, + const std::optional &color, + const std::optional &id); + void emitDamp(const api::DampData &item, core::Direction &direction); + void emitDampAll(const api::DampAllData &item, core::Direction &direction); + void emitEyeglasses(const api::EyeglassesData &item, core::Direction &direction); + void emitStringMute(const api::StringMuteData &item, core::Direction &direction); + void emitStaffDivide(const api::StaffDivideData &item, core::Direction &direction); void emitFixedOrder(core::Direction &direction); void emitOrderedComponents(core::Direction &direction); diff --git a/src/private/mxtest/api/DirectionMarksRoundTripTest.cpp b/src/private/mxtest/api/DirectionMarksRoundTripTest.cpp new file mode 100644 index 000000000..619e99ad5 --- /dev/null +++ b/src/private/mxtest/api/DirectionMarksRoundTripTest.cpp @@ -0,0 +1,138 @@ +// MusicXML Class Library +// Copyright (c) by Matthew James Briggs +// Distributed under the MIT License + +#include "mxtest/control/CompileControl.h" +#ifdef MX_COMPILE_API_TESTS + +#include "cpul/cpulTestHarness.h" +#include "mx/api/DocumentManager.h" + +#include + +using namespace std; +using namespace mx::api; + +namespace +{ +// Round-trips a DirectionData through the full serialize -> deserialize path and +// returns the directions read back from the first staff. Used by the direction-type +// data-loss regression tests below (#324). +std::vector roundTripDirectionData(const DirectionData &inDirectionData) +{ + ScoreData score; + score.parts.emplace_back(); + auto &part = score.parts.back(); + part.measures.emplace_back(); + auto &measure = part.measures.back(); + measure.staves.emplace_back(); + auto &staff = measure.staves.back(); + staff.directions.push_back(inDirectionData); + + auto &mgr = DocumentManager::getInstance(); + const auto r1 = mgr.createFromScore(score); + if (!r1.ok()) + return {}; + auto docId = r1.value(); + std::stringstream ss; + mgr.writeToStream(docId, ss); + mgr.destroyDocument(docId); + const std::string xml = ss.str(); + + std::istringstream iss{xml}; + const auto r2 = mgr.createFromStream(iss); + if (!r2.ok()) + return {}; + docId = r2.value(); + const auto rd = mgr.getData(docId); + mgr.destroyDocument(docId); + if (!rd.ok()) + return {}; + const auto &oscore = rd.value(); + + return oscore.parts.back().measures.back().staves.back().directions; +} +} // namespace + +TEST(Damp, DirectionMarksRoundTrip) +{ + DirectionData direction; + direction.damps.emplace_back(); + const auto directions = roundTripDirectionData(direction); + REQUIRE(directions.size() == 1); + CHECK_EQUAL(1, static_cast(directions.front().damps.size())); +} + +T_END; + +TEST(DampAll, DirectionMarksRoundTrip) +{ + DirectionData direction; + direction.dampAlls.emplace_back(); + const auto directions = roundTripDirectionData(direction); + REQUIRE(directions.size() == 1); + CHECK_EQUAL(1, static_cast(directions.front().dampAlls.size())); +} + +T_END; + +TEST(Eyeglasses, DirectionMarksRoundTrip) +{ + DirectionData direction; + direction.eyeglasses.emplace_back(); + const auto directions = roundTripDirectionData(direction); + REQUIRE(directions.size() == 1); + CHECK_EQUAL(1, static_cast(directions.front().eyeglasses.size())); +} + +T_END; + +TEST(StringMute, DirectionMarksRoundTrip) +{ + DirectionData direction; + StringMuteData stringMute; + stringMute.type = StringMuteType::off; + direction.stringMutes.push_back(stringMute); + const auto directions = roundTripDirectionData(direction); + REQUIRE(directions.size() == 1); + REQUIRE(directions.front().stringMutes.size() == 1); + CHECK(directions.front().stringMutes.front().type == StringMuteType::off); +} + +T_END; + +TEST(StaffDivide, DirectionMarksRoundTrip) +{ + DirectionData direction; + StaffDivideData staffDivide; + staffDivide.type = StaffDivideType::upDown; + direction.staffDivides.push_back(staffDivide); + const auto directions = roundTripDirectionData(direction); + REQUIRE(directions.size() == 1); + REQUIRE(directions.front().staffDivides.size() == 1); + CHECK(directions.front().staffDivides.front().type == StaffDivideType::upDown); +} + +T_END; + +TEST(DampFormatting, DirectionMarksRoundTrip) +{ + DirectionData direction; + DampData damp; + damp.positionData.isDefaultXSpecified = true; + damp.positionData.defaultX = 5.0; + damp.id = "damp-1"; + direction.damps.push_back(damp); + const auto directions = roundTripDirectionData(direction); + REQUIRE(directions.size() == 1); + REQUIRE(directions.front().damps.size() == 1); + const auto &outDamp = directions.front().damps.front(); + CHECK(outDamp.positionData.isDefaultXSpecified); + CHECK_DOUBLES_EQUAL(5.0, outDamp.positionData.defaultX, 0.0001); + REQUIRE(outDamp.id.has_value()); + CHECK_EQUAL("damp-1", *outDamp.id); +} + +T_END; + +#endif diff --git a/src/private/mxtest/api/roundtrip-baseline.txt b/src/private/mxtest/api/roundtrip-baseline.txt index 662abba89..9ea5ac10a 100644 --- a/src/private/mxtest/api/roundtrip-baseline.txt +++ b/src/private/mxtest/api/roundtrip-baseline.txt @@ -432,3 +432,15 @@ musuite/testGrace2.xml custom/musescore-slur-start-stop.musicxml musuite/testChord.xml musuite/testPiano.xml + +# Unblocked by modeling the attribute-only direction types (#324): damp, +# damp-all, eyeglasses, string-mute, and staff-divide now round-trip. +synthetic/damp-all.3.0.xml +synthetic/damp-all.3.1.xml +synthetic/damp.3.0.xml +synthetic/damp.3.1.xml +synthetic/eyeglasses.3.0.xml +synthetic/eyeglasses.3.1.xml +synthetic/staff-divide.3.1.xml +synthetic/string-mute.3.0.xml +synthetic/string-mute.3.1.xml