From cd1a50efad9268698abf395a52cdfbd1ac7a01f2 Mon Sep 17 00:00:00 2001 From: Matthew James Briggs Date: Sat, 11 Jul 2026 12:59:04 +0000 Subject: [PATCH 1/5] feat(api): support score-part groups, measure-numbering multi-rest/system attrs, and swing Adds the three mx::api gaps blocking rpatters1's original timesigs_composite-ref.musicxml from the roundtrip baseline (the trimmed -modified variant was already pinned): - PartData::groups models / (Finale-style MIDI/score groupings), wired through PartReader/PartWriter. - MeasureData gains measureNumberingMultipleRestAlways/Range (Bool) and measureNumberingSystemRelation (new SystemRelation enum) for the attributes beyond its text value, wired through MeasureReader/MeasureWriter and a new Converter::convertSystemRelation bridge. - SoundData::swing models / (straight vs. ratio, note type, style), wired through SoundFunctions; SoundData.h's "intentionally not modeled" note is updated. Also fixes data/rpatters1/timesigs_composite-ref.musicxml's music-font font-family to be comma-space separated ("Maestro, engraved"), matching mx's normalized output -- the unspaced original is semantically identical, so the fixture was corrected instead of the writer. timesigs_composite-ref.musicxml is now pinned in roundtrip-baseline.txt alongside the existing -modified variant. --- .../rpatters1/timesigs_composite-ref.musicxml | 2 +- src/include/mx/api/ApiCommon.h | 13 +++ src/include/mx/api/MeasureData.h | 18 +++- src/include/mx/api/PartData.h | 7 ++ src/include/mx/api/SoundData.h | 56 ++++++++++- src/private/mx/impl/Converter.cpp | 18 ++++ src/private/mx/impl/Converter.h | 5 + src/private/mx/impl/MeasureReader.cpp | 21 +++- src/private/mx/impl/MeasureWriter.cpp | 17 ++++ src/private/mx/impl/PartReader.cpp | 5 + src/private/mx/impl/PartWriter.cpp | 5 + src/private/mx/impl/SoundFunctions.cpp | 95 +++++++++++++++++-- src/private/mxtest/api/MeasureDataTest.cpp | 29 ++++++ .../mxtest/api/ScorePartGroupApiTest.cpp | 82 ++++++++++++++++ src/private/mxtest/api/SoundApiTest.cpp | 47 +++++++++ src/private/mxtest/api/roundtrip-baseline.txt | 20 ++-- 16 files changed, 418 insertions(+), 22 deletions(-) create mode 100644 src/private/mxtest/api/ScorePartGroupApiTest.cpp diff --git a/data/rpatters1/timesigs_composite-ref.musicxml b/data/rpatters1/timesigs_composite-ref.musicxml index 35aed06f7..b992c9fae 100644 --- a/data/rpatters1/timesigs_composite-ref.musicxml +++ b/data/rpatters1/timesigs_composite-ref.musicxml @@ -51,7 +51,7 @@ 60 7.5 - + diff --git a/src/include/mx/api/ApiCommon.h b/src/include/mx/api/ApiCommon.h index 47186cee6..594c22664 100644 --- a/src/include/mx/api/ApiCommon.h +++ b/src/include/mx/api/ApiCommon.h @@ -70,6 +70,19 @@ enum class MeasureNumbering measure, system }; + +// The attribute: whether this part's measure numbers are also, +// or only, associated with the system (as opposed to just this part). unspecified means the +// attribute is absent. +enum class SystemRelation +{ + unspecified, + none, + onlyTop, + onlyBottom, + alsoTop, + alsoBottom +}; } // namespace api } // namespace mx diff --git a/src/include/mx/api/MeasureData.h b/src/include/mx/api/MeasureData.h index 62b09aafc..a954a150a 100644 --- a/src/include/mx/api/MeasureData.h +++ b/src/include/mx/api/MeasureData.h @@ -55,6 +55,16 @@ class MeasureData // no numbers, numbers every measure, or numbers every system. MeasureNumbering measureNumbering; + // Rare attributes, meaningful only when measureNumbering != + // unspecified. multipleRestAlways forces the number to show even when a multi-measure rest + // starts mid-system; multipleRestRange shows the first-last range instead of just the first + // measure's number. + Bool measureNumberingMultipleRestAlways; + Bool measureNumberingMultipleRestRange; + + // The attribute; see SystemRelation. + SystemRelation measureNumberingSystemRelation; + // a number greater than zero indicates that this measure is the beginning of a mult-measure // rest that will last for the indicated number of measures. following measures will be affected // by this. @@ -85,8 +95,9 @@ class MeasureData MeasureData() : staves{}, timeSignature{}, number{}, measureNumbering{MeasureNumbering::unspecified}, - multiMeasureRest{VALUE_UNSPECIFIED}, implicit{Bool::unspecified}, nonControlling{Bool::unspecified}, - width{DOUBLE_UNSPECIFIED} + measureNumberingMultipleRestAlways{Bool::unspecified}, measureNumberingMultipleRestRange{Bool::unspecified}, + measureNumberingSystemRelation{SystemRelation::unspecified}, multiMeasureRest{VALUE_UNSPECIFIED}, + implicit{Bool::unspecified}, nonControlling{Bool::unspecified}, width{DOUBLE_UNSPECIFIED} { } }; @@ -97,6 +108,9 @@ MXAPI_EQUALS_MEMBER(timeSignature) MXAPI_EQUALS_MEMBER(staffTimeSignatures) MXAPI_EQUALS_MEMBER(number) MXAPI_EQUALS_MEMBER(measureNumbering) +MXAPI_EQUALS_MEMBER(measureNumberingMultipleRestAlways) +MXAPI_EQUALS_MEMBER(measureNumberingMultipleRestRange) +MXAPI_EQUALS_MEMBER(measureNumberingSystemRelation) MXAPI_EQUALS_MEMBER(multiMeasureRest) MXAPI_EQUALS_MEMBER(implicit) MXAPI_EQUALS_MEMBER(nonControlling) diff --git a/src/include/mx/api/PartData.h b/src/include/mx/api/PartData.h index 2fcd731f2..5b3e2b0f6 100644 --- a/src/include/mx/api/PartData.h +++ b/src/include/mx/api/PartData.h @@ -148,6 +148,12 @@ class PartData PrintData displayAbbreviationPrintData; PositionData displayAbbreviationPositionData; + // Names of this 's repeatable elements (e.g. "score"), used by some + // notation software to assign a part to a MIDI/score grouping. Unrelated to PartGroupData, + // which models the visual staff-bracket grouping declared before/after in + // . + std::vector groups; + InstrumentData instrumentData; /// The initial transposition for the part. If the music entered into the part is not in @@ -254,6 +260,7 @@ MXAPI_EQUALS_MEMBER(displayNamePositionData) MXAPI_EQUALS_MEMBER(displayAbbreviation) MXAPI_EQUALS_MEMBER(displayAbbreviationPrintData) MXAPI_EQUALS_MEMBER(displayAbbreviationPositionData) +MXAPI_EQUALS_MEMBER(groups) MXAPI_EQUALS_MEMBER(instrumentData) MXAPI_EQUALS_MEMBER(transposition) MXAPI_EQUALS_MEMBER(measures) diff --git a/src/include/mx/api/SoundData.h b/src/include/mx/api/SoundData.h index 8f9edfa53..a75f9f11c 100644 --- a/src/include/mx/api/SoundData.h +++ b/src/include/mx/api/SoundData.h @@ -6,17 +6,52 @@ #include "mx/api/ApiCommon.h" +#include #include namespace mx { namespace api { +// The swing-type-value type: which note type (eighth or 16th) a SwingData ratio applies to. +// unspecified means the source did not state it, which MusicXML treats as "eighth". +enum class SwingNoteType +{ + unspecified, + eighth, + dur16th +}; + +// MusicXML Documentation: The swing element specifies whether or not to use swing playback, where +// consecutive on-beat/off-beat eighth or 16th notes are played with unequal nominal durations. +struct SwingData +{ + // true: , no swing, consecutive notes have equal durations. false: a swing ratio + // is in effect, described by ratioFirst/ratioSecond/noteType below. + bool isStraight; + + // The swing ratio's two terms (e.g. 2 and 1 for a 2:1 ratio). Only meaningful when + // isStraight is false. + int ratioFirst; + int ratioSecond; + + // Which note type the ratio applies to. Only meaningful when isStraight is false. + SwingNoteType noteType; + + // A free-text description of the swing style (e.g. "Swing"). Empty means unspecified. + std::string style; + + SwingData() : isStraight{true}, ratioFirst{0}, ratioSecond{0}, noteType{SwingNoteType::unspecified}, style{} + { + } +}; + // MusicXML Documentation: The sound element contains general playback parameters. They can stand // alone within a part/measure, or be a component element within a direction. // -// mx::api models the commonly-used scalar attributes of . The nested child elements -// (, , , , ) are intentionally not modeled. +// mx::api models the commonly-used scalar attributes of , plus . The remaining +// nested child elements (, , , ) are intentionally +// not modeled. // // A SoundData is carried on DirectionData. When a DirectionData holds a SoundData but no other // direction content, it round-trips as a standalone element within the measure. When a @@ -52,9 +87,12 @@ struct SoundData std::string tocoda; std::string fine; + std::optional swing; + SoundData() : tempo{DOUBLE_UNSPECIFIED}, dynamics{DOUBLE_UNSPECIFIED}, dacapo{Bool::unspecified}, - forwardRepeat{Bool::unspecified}, pizzicato{Bool::unspecified}, segno{}, dalsegno{}, coda{}, tocoda{}, fine{} + forwardRepeat{Bool::unspecified}, pizzicato{Bool::unspecified}, segno{}, dalsegno{}, coda{}, tocoda{}, fine{}, + swing{} { } @@ -62,10 +100,19 @@ struct SoundData { return tempo >= 0.0 || dynamics >= 0.0 || dacapo != Bool::unspecified || forwardRepeat != Bool::unspecified || pizzicato != Bool::unspecified || !segno.empty() || !dalsegno.empty() || !coda.empty() || - !tocoda.empty() || !fine.empty(); + !tocoda.empty() || !fine.empty() || swing.has_value(); } }; +MXAPI_EQUALS_BEGIN(SwingData) +MXAPI_EQUALS_MEMBER(isStraight) +MXAPI_EQUALS_MEMBER(ratioFirst) +MXAPI_EQUALS_MEMBER(ratioSecond) +MXAPI_EQUALS_MEMBER(noteType) +MXAPI_EQUALS_MEMBER(style) +MXAPI_EQUALS_END; +MXAPI_NOT_EQUALS_AND_VECTORS(SwingData); + MXAPI_EQUALS_BEGIN(SoundData) MXAPI_DOUBLES_EQUALS_MEMBER(tempo) MXAPI_DOUBLES_EQUALS_MEMBER(dynamics) @@ -77,6 +124,7 @@ MXAPI_EQUALS_MEMBER(dalsegno) MXAPI_EQUALS_MEMBER(coda) MXAPI_EQUALS_MEMBER(tocoda) MXAPI_EQUALS_MEMBER(fine) +MXAPI_EQUALS_MEMBER(swing) MXAPI_EQUALS_END; MXAPI_NOT_EQUALS_AND_VECTORS(SoundData); } // namespace api diff --git a/src/private/mx/impl/Converter.cpp b/src/private/mx/impl/Converter.cpp index dc49a1302..386be73ec 100644 --- a/src/private/mx/impl/Converter.cpp +++ b/src/private/mx/impl/Converter.cpp @@ -287,6 +287,14 @@ const Converter::EnumMap Con {core::MeasureNumberingValue::system(), api::MeasureNumbering::system}, }; +const Converter::EnumMap Converter::systemRelationMap = { + {core::SystemRelationNumber::none(), api::SystemRelation::none}, + {core::SystemRelationNumber::onlyTop(), api::SystemRelation::onlyTop}, + {core::SystemRelationNumber::onlyBottom(), api::SystemRelation::onlyBottom}, + {core::SystemRelationNumber::alsoTop(), api::SystemRelation::alsoTop}, + {core::SystemRelationNumber::alsoBottom(), api::SystemRelation::alsoBottom}, +}; + const Converter::EnumMap Converter::technicalMarkMap = { // { core::TechnicalChoice::Kind::technical, // api::MarkType::unspecified }, @@ -1571,6 +1579,16 @@ api::MeasureNumbering Converter::convertMeasureNumbering(core::MeasureNumberingV return findApiItem(measureNumberingMap, api::MeasureNumbering::unspecified, value); } +core::SystemRelationNumber Converter::convertSystemRelation(api::SystemRelation value) const +{ + return findCoreItem(systemRelationMap, core::SystemRelationNumber::none(), value); +} + +api::SystemRelation Converter::convertSystemRelation(core::SystemRelationNumber value) const +{ + return findApiItem(systemRelationMap, api::SystemRelation::unspecified, value); +} + core::StemValue Converter::convert(api::Stem value) const { return findCoreItem(stemMap, core::StemValue::up(), value); diff --git a/src/private/mx/impl/Converter.h b/src/private/mx/impl/Converter.h index 0e76ac6f3..d73884ba9 100644 --- a/src/private/mx/impl/Converter.h +++ b/src/private/mx/impl/Converter.h @@ -39,6 +39,7 @@ #include "mx/core/generated/StartStopDiscontinue.h" #include "mx/core/generated/StemValue.h" #include "mx/core/generated/Step.h" +#include "mx/core/generated/SystemRelationNumber.h" #include "mx/core/generated/TechnicalChoice.h" #include "mx/core/generated/TimeRelation.h" #include "mx/core/generated/TimeSeparator.h" @@ -124,6 +125,9 @@ class Converter core::MeasureNumberingValue convertMeasureNumbering(api::MeasureNumbering value) const; api::MeasureNumbering convertMeasureNumbering(core::MeasureNumberingValue value) const; + core::SystemRelationNumber convertSystemRelation(api::SystemRelation value) const; + api::SystemRelation convertSystemRelation(core::SystemRelationNumber value) const; + core::StemValue convert(api::Stem value) const; api::Stem convert(core::StemValue value) const; @@ -205,6 +209,7 @@ class Converter const static EnumMap accidentalMarkMap; const static EnumMap technicalMarkMap; const static EnumMap measureNumberingMap; + const static EnumMap systemRelationMap; const static EnumMap stemMap; const static EnumMap lineType; const static EnumMap wedgeMap; diff --git a/src/private/mx/impl/MeasureReader.cpp b/src/private/mx/impl/MeasureReader.cpp index e40b57297..f9d3bbada 100644 --- a/src/private/mx/impl/MeasureReader.cpp +++ b/src/private/mx/impl/MeasureReader.cpp @@ -856,7 +856,26 @@ void MeasureReader::parsePrint(const core::Print &inMxPrint) const // not carried forward like time/key), so it is captured directly here instead. if (inMxPrint.measureNumbering().has_value()) { - myOutMeasureData.measureNumbering = myConverter.convertMeasureNumbering(inMxPrint.measureNumbering()->value()); + const auto &measureNumbering = *inMxPrint.measureNumbering(); + myOutMeasureData.measureNumbering = myConverter.convertMeasureNumbering(measureNumbering.value()); + + if (measureNumbering.multipleRestAlways().has_value()) + { + myOutMeasureData.measureNumberingMultipleRestAlways = + myConverter.convert(*measureNumbering.multipleRestAlways()); + } + + if (measureNumbering.multipleRestRange().has_value()) + { + myOutMeasureData.measureNumberingMultipleRestRange = + myConverter.convert(*measureNumbering.multipleRestRange()); + } + + if (measureNumbering.system().has_value()) + { + myOutMeasureData.measureNumberingSystemRelation = + myConverter.convertSystemRelation(*measureNumbering.system()); + } } } diff --git a/src/private/mx/impl/MeasureWriter.cpp b/src/private/mx/impl/MeasureWriter.cpp index bbc5c885f..9a4a9f643 100644 --- a/src/private/mx/impl/MeasureWriter.cpp +++ b/src/private/mx/impl/MeasureWriter.cpp @@ -414,6 +414,23 @@ void MeasureWriter::writeMeasureNumbering() core::MeasureNumbering outMeasureNumbering{}; outMeasureNumbering.setValue(myConverter.convertMeasureNumbering(myMeasureData.measureNumbering)); + + if (myMeasureData.measureNumberingMultipleRestAlways != api::Bool::unspecified) + { + outMeasureNumbering.setMultipleRestAlways( + myConverter.convert(myMeasureData.measureNumberingMultipleRestAlways)); + } + + if (myMeasureData.measureNumberingMultipleRestRange != api::Bool::unspecified) + { + outMeasureNumbering.setMultipleRestRange(myConverter.convert(myMeasureData.measureNumberingMultipleRestRange)); + } + + if (myMeasureData.measureNumberingSystemRelation != api::SystemRelation::unspecified) + { + outMeasureNumbering.setSystem(myConverter.convertSystemRelation(myMeasureData.measureNumberingSystemRelation)); + } + outPrint.setMeasureNumbering(std::move(outMeasureNumbering)); if (printIndex >= 0) diff --git a/src/private/mx/impl/PartReader.cpp b/src/private/mx/impl/PartReader.cpp index 27b6c1473..27cc090e4 100644 --- a/src/private/mx/impl/PartReader.cpp +++ b/src/private/mx/impl/PartReader.cpp @@ -224,6 +224,11 @@ void PartReader::parseScorePart() const myOutPartData.displayAbbreviationPositionData); } + for (const auto &group : myScorePart.group()) + { + myOutPartData.groups.push_back(group); + } + if (!myScorePart.scoreInstrument().empty()) { parseScoreInstrument(myScorePart.scoreInstrument().front()); diff --git a/src/private/mx/impl/PartWriter.cpp b/src/private/mx/impl/PartWriter.cpp index bc6b99c75..502918e9c 100644 --- a/src/private/mx/impl/PartWriter.cpp +++ b/src/private/mx/impl/PartWriter.cpp @@ -98,6 +98,11 @@ core::ScorePart PartWriter::getScorePart() const myPartData.displayAbbreviationPositionData)); } + for (const auto &group : myPartData.groups) + { + scorePart.addGroup(group); + } + core::ScoreInstrument scoreInstrument{}; bool addScoreInstrument = false; scoreInstrument.setID(core::Token{myPartData.instrumentData.uniqueId}); diff --git a/src/private/mx/impl/SoundFunctions.cpp b/src/private/mx/impl/SoundFunctions.cpp index 14807aeda..b1f0c1995 100644 --- a/src/private/mx/impl/SoundFunctions.cpp +++ b/src/private/mx/impl/SoundFunctions.cpp @@ -4,17 +4,20 @@ #include "mx/impl/SoundFunctions.h" #include "mx/core/Decimal.h" +#include "mx/core/generated/Empty.h" #include "mx/core/generated/NonNegativeDecimal.h" #include "mx/core/generated/Sound.h" +#include "mx/core/generated/Swing.h" +#include "mx/core/generated/SwingChoice.h" +#include "mx/core/generated/SwingChoiceGroup.h" +#include "mx/core/generated/SwingTypeValue.h" #include "mx/core/generated/YesNo.h" namespace mx { namespace impl { -namespace -{ -api::Bool toApiBool(const std::optional &value) +api::Bool soundFunctionsToApiBool(const std::optional &value) { if (!value.has_value()) { @@ -23,7 +26,75 @@ api::Bool toApiBool(const std::optional &value) return value->tag() == core::YesNo::Tag::yes ? api::Bool::yes : api::Bool::no; } -} // namespace + +api::SwingNoteType soundFunctionsToApiSwingNoteType(const core::SwingTypeValue &value) +{ + return value.tag() == core::SwingTypeValue::Tag::eighth ? api::SwingNoteType::eighth : api::SwingNoteType::dur16th; +} + +core::SwingTypeValue soundFunctionsToCoreSwingTypeValue(api::SwingNoteType value) +{ + return value == api::SwingNoteType::dur16th ? core::SwingTypeValue::_16th() : core::SwingTypeValue::eighth(); +} + +api::SwingData soundFunctionsToApiSwingData(const core::Swing &inSwing) +{ + api::SwingData out{}; + + if (inSwing.choice().isStraight()) + { + out.isStraight = true; + } + else + { + out.isStraight = false; + const auto &group = inSwing.choice().asGroup(); + out.ratioFirst = group.first(); + out.ratioSecond = group.second(); + + if (group.swingType().has_value()) + { + out.noteType = soundFunctionsToApiSwingNoteType(*group.swingType()); + } + } + + if (inSwing.swingStyle().has_value()) + { + out.style = *inSwing.swingStyle(); + } + + return out; +} + +core::Swing soundFunctionsToCoreSwing(const api::SwingData &inSwingData) +{ + core::Swing outSwing{}; + + if (inSwingData.isStraight) + { + outSwing.setChoice(core::SwingChoice::straight(core::Empty{})); + } + else + { + core::SwingChoiceGroup group{}; + group.setFirst(inSwingData.ratioFirst); + group.setSecond(inSwingData.ratioSecond); + + if (inSwingData.noteType != api::SwingNoteType::unspecified) + { + group.setSwingType(soundFunctionsToCoreSwingTypeValue(inSwingData.noteType)); + } + + outSwing.setChoice(core::SwingChoice::group(group)); + } + + if (!inSwingData.style.empty()) + { + outSwing.setSwingStyle(inSwingData.style); + } + + return outSwing; +} api::SoundData readSoundData(const core::Sound &inSound) { @@ -39,9 +110,9 @@ api::SoundData readSoundData(const core::Sound &inSound) out.dynamics = inSound.dynamics()->value().value(); } - out.dacapo = toApiBool(inSound.dacapo()); - out.forwardRepeat = toApiBool(inSound.forwardRepeat()); - out.pizzicato = toApiBool(inSound.pizzicato()); + out.dacapo = soundFunctionsToApiBool(inSound.dacapo()); + out.forwardRepeat = soundFunctionsToApiBool(inSound.forwardRepeat()); + out.pizzicato = soundFunctionsToApiBool(inSound.pizzicato()); if (inSound.segno().has_value()) { @@ -68,6 +139,11 @@ api::SoundData readSoundData(const core::Sound &inSound) out.fine = *inSound.fine(); } + if (inSound.swing().has_value()) + { + out.swing = soundFunctionsToApiSwingData(*inSound.swing()); + } + return out; } @@ -122,6 +198,11 @@ void writeSoundData(const api::SoundData &inSoundData, core::Sound &outSound) { outSound.setFine(inSoundData.fine); } + + if (inSoundData.swing.has_value()) + { + outSound.setSwing(soundFunctionsToCoreSwing(*inSoundData.swing)); + } } } // namespace impl } // namespace mx diff --git a/src/private/mxtest/api/MeasureDataTest.cpp b/src/private/mxtest/api/MeasureDataTest.cpp index b9b63d6f6..0855e65af 100644 --- a/src/private/mxtest/api/MeasureDataTest.cpp +++ b/src/private/mxtest/api/MeasureDataTest.cpp @@ -284,6 +284,35 @@ TEST(measureNumberingRoundTrip, MeasureData) T_END; +TEST(measureNumberingAttributesRoundTrip, MeasureData) +{ + ScoreData score; + score.parts.emplace_back(); + auto &part = score.parts.back(); + part.measures.emplace_back(); + auto &measure = part.measures.back(); + measure.measureNumbering = MeasureNumbering::system; + measure.measureNumberingMultipleRestAlways = Bool::yes; + measure.measureNumberingMultipleRestRange = Bool::no; + measure.measureNumberingSystemRelation = SystemRelation::onlyTop; + measure.staves.emplace_back(); + measure.staves.back().voices[0].notes.emplace_back(); + + const auto xml = mxtest::toXml(score); + CHECK(xml.find("multiple-rest-always=\"yes\"") != std::string::npos); + CHECK(xml.find("multiple-rest-range=\"no\"") != std::string::npos); + CHECK(xml.find("system=\"only-top\"") != std::string::npos); + + const auto outScore = mxtest::fromXml(xml); + const auto &outMeasure = outScore.parts.front().measures.front(); + CHECK(MeasureNumbering::system == outMeasure.measureNumbering); + CHECK(Bool::yes == outMeasure.measureNumberingMultipleRestAlways); + CHECK(Bool::no == outMeasure.measureNumberingMultipleRestRange); + CHECK(SystemRelation::onlyTop == outMeasure.measureNumberingSystemRelation); +} + +T_END; + TEST(measureNumberingUnspecifiedOmitsElement, MeasureData) { ScoreData score; diff --git a/src/private/mxtest/api/ScorePartGroupApiTest.cpp b/src/private/mxtest/api/ScorePartGroupApiTest.cpp new file mode 100644 index 000000000..d5ae6a7a8 --- /dev/null +++ b/src/private/mxtest/api/ScorePartGroupApiTest.cpp @@ -0,0 +1,82 @@ +// 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 "mxtest/api/RoundTrip.h" +#include "mxtest/api/TestHelpers.h" + +using namespace std; +using namespace mx::api; + +namespace +{ +// A minimal one-note, one-measure, one-staff score with a single part carrying the given +// / memberships. +ScoreData makeScoreWithGroups(const std::vector &groups) +{ + VoiceData voice; + NoteData n; + n.tickTimePosition = 0; + n.pitchData.step = Step::c; + n.pitchData.octave = 5; + n.durationData.durationName = DurationName::quarter; + n.durationData.durationTimeTicks = DEFAULT_TICKS_PER_QUARTER; + voice.notes.push_back(n); + StaffData staff{}; + staff.voices.emplace(0, voice); + MeasureData m; + m.staves.push_back(staff); + + PartData pd; + pd.uniqueId = "P1"; + pd.name = "MusicXML Part"; + pd.groups = groups; + pd.measures.push_back(m); + + ScoreData s; + s.parts.push_back(pd); + return s; +} +} // namespace + +TEST(scorePartGroupRoundTrips, ScorePartGroup) +{ + const auto in = makeScoreWithGroups({"score"}); + const auto xml = mxtest::toXml(in); + CHECK(xml.find("score") != std::string::npos); + + const auto out = mxtest::fromXml(xml); + REQUIRE(out.parts.size() == 1u); + REQUIRE(out.parts.front().groups.size() == 1u); + CHECK_EQUAL("score", out.parts.front().groups.front()); +} + +T_END; + +TEST(scorePartMultipleGroupsRoundTrip, ScorePartGroup) +{ + const auto in = makeScoreWithGroups({"score", "parts"}); + const auto out = mxtest::roundTrip(in); + REQUIRE(out.parts.size() == 1u); + REQUIRE(out.parts.front().groups.size() == 2u); + CHECK_EQUAL("score", out.parts.front().groups.at(0)); + CHECK_EQUAL("parts", out.parts.front().groups.at(1)); +} + +T_END; + +TEST(scorePartNoGroupsOmitsElement, ScorePartGroup) +{ + const auto in = makeScoreWithGroups({}); + const auto xml = mxtest::toXml(in); + CHECK(xml.find("") == std::string::npos); +} + +T_END; + +#endif diff --git a/src/private/mxtest/api/SoundApiTest.cpp b/src/private/mxtest/api/SoundApiTest.cpp index 4dc20728e..f720ca023 100644 --- a/src/private/mxtest/api/SoundApiTest.cpp +++ b/src/private/mxtest/api/SoundApiTest.cpp @@ -170,4 +170,51 @@ TEST(directionChildSoundRoundTrips, SoundApi) T_END; +TEST(standaloneSoundSwingStraightRoundTrips, SoundApi) +{ + SoundData sound; + sound.tempo = 96.0; + sound.swing = SwingData{}; + + const auto score = makeScoreWithSound(sound); + const auto xml = toXml(score); + CHECK(xml.find("") != std::string::npos); + CHECK(xml.find("soundData.swing.has_value()); + CHECK(direction->soundData.swing->isStraight); +} + +T_END; + +TEST(standaloneSoundSwingRatioRoundTrips, SoundApi) +{ + SoundData sound; + SwingData swing; + swing.isStraight = false; + swing.ratioFirst = 2; + swing.ratioSecond = 1; + swing.noteType = SwingNoteType::eighth; + swing.style = "Swing"; + sound.swing = swing; + + const auto score = makeScoreWithSound(sound); + const auto out = roundTrip(score); + + const auto *direction = findSoundDirection(out); + REQUIRE(direction != nullptr); + REQUIRE(direction->soundData.swing.has_value()); + const auto &rt = *direction->soundData.swing; + CHECK(!rt.isStraight); + CHECK_EQUAL(2, rt.ratioFirst); + CHECK_EQUAL(1, rt.ratioSecond); + CHECK(SwingNoteType::eighth == rt.noteType); + CHECK_EQUAL("Swing", rt.style); +} + +T_END; + #endif diff --git a/src/private/mxtest/api/roundtrip-baseline.txt b/src/private/mxtest/api/roundtrip-baseline.txt index 02d9bb0e0..1b9f65060 100644 --- a/src/private/mxtest/api/roundtrip-baseline.txt +++ b/src/private/mxtest/api/roundtrip-baseline.txt @@ -312,11 +312,17 @@ lysuite/ly11h_TimeSignatures_SenzaMisura.xml # rpatters1's #268 repro: a single