diff --git a/CMakeLists.txt b/CMakeLists.txt index eb0d55da2..cbd5f3e99 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -110,14 +110,15 @@ if(MX_CORE_DEV) # corert: round-trips each data/ file through mx::core (pugixml-backed) # and compares against a normalized, fixup-adjusted form of the input. # Fixer applies .fixup.xml sidecars; cpul is the Catch test runner - # (main); the import/ helpers normalize whitespace, decimals, and - # attribute order. + # (main); the import/ helpers normalize whitespace, decimals, + # comma-separated-text spacing, and attribute order. file(GLOB_RECURSE SRC_CORERT ${PRIVATE_DIR}/mxtest/corert/*.cpp ${PRIVATE_DIR}/mxtest/corert/*.h) file(GLOB_RECURSE SRC_CPUL ${PRIVATE_DIR}/cpul/*.cpp ${PRIVATE_DIR}/cpul/*.h) set(SRC_CORERT_HELPERS ${PRIVATE_DIR}/mxtest/import/Normalize.cpp ${PRIVATE_DIR}/mxtest/import/Normalize.h - ${PRIVATE_DIR}/mxtest/import/DecimalFields.h) + ${PRIVATE_DIR}/mxtest/import/DecimalFields.h + ${PRIVATE_DIR}/mxtest/import/CommaSeparatedFields.h) # PathRoot.h hands the corert file walker the repo root at configure time. file(WRITE ${PRIVATE_DIR}/mxtest/file/PathRoot.h @@ -219,7 +220,8 @@ if(MX_API) ${PRIVATE_DIR}/mxtest/corert/Fixer.h ${PRIVATE_DIR}/mxtest/import/Normalize.cpp ${PRIVATE_DIR}/mxtest/import/Normalize.h - ${PRIVATE_DIR}/mxtest/import/DecimalFields.h) + ${PRIVATE_DIR}/mxtest/import/DecimalFields.h + ${PRIVATE_DIR}/mxtest/import/CommaSeparatedFields.h) target_include_directories(mxtest-api-roundtrip PRIVATE ${PRIVATE_DIR}) target_link_libraries(mxtest-api-roundtrip mx ${CMAKE_THREAD_LIBS_INIT}) diff --git a/data/README.md b/data/README.md index af18fbfc0..b432cdfbb 100644 --- a/data/README.md +++ b/data/README.md @@ -93,7 +93,11 @@ both so the comparison is canonical-against-canonical. The pipeline lives in 3. Strip trailing zeros from decimal fields: `mx` serializes the shortest round-trip form, so a trailing-zero decimal like `40.00000` in a source file would otherwise mismatch. The field list lives in `DecimalFields.h`. -4. Sort each element's attributes alphabetically by qualified name (`xlink:href`, not `href`); it +4. Re-space comma-separated-text fields (e.g. `font-family`) to `"item, item"`: the type allows + `"x,y"` or `"x, y"` on the wire and a parsed-but-unmutated value keeps its exact spelling on + write, but `mx`'s api round trip always re-serializes as `"x, y"`. The field list lives in + `CommaSeparatedFields.h`. +5. Sort each element's attributes alphabetically by qualified name (`xlink:href`, not `href`); it runs last. Comparison rules that took debugging to get right: 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/DefaultsData.h b/src/include/mx/api/DefaultsData.h index 6742977b3..5ce05c039 100644 --- a/src/include/mx/api/DefaultsData.h +++ b/src/include/mx/api/DefaultsData.h @@ -133,15 +133,9 @@ class DefaultsData /// The `` defaults (one per lyric line/verse). std::vector lyricFonts; - // TODO - this appears not to be used anywhere, please do not use - /// Measure numbering setting, at the global level, will be stated - /// in first measure's tag. This can can be overridden by a - /// value in the Measure. - MeasureNumbering measureNumbering; - DefaultsData() : scalingMillimeters{DOUBLE_UNSPECIFIED}, scalingTenths{DOUBLE_UNSPECIFIED}, pageLayout{}, systemLayout{}, - appearance{}, musicFont{}, wordFont{}, lyricFonts{}, measureNumbering{MeasureNumbering::unspecified} + appearance{}, musicFont{}, wordFont{}, lyricFonts{} { } }; @@ -157,7 +151,6 @@ if (!areVectorsEqual(lhs.lyricFonts, rhs.lyricFonts)) MX_SHOW_UNEQUAL("DefaultsData", "lyricFonts"); return false; } -MXAPI_EQUALS_MEMBER(measureNumbering) MXAPI_EQUALS_END; MXAPI_NOT_EQUALS_AND_VECTORS(DefaultsData); } // namespace api 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..e0d69975f 100644 --- a/src/include/mx/api/PartData.h +++ b/src/include/mx/api/PartData.h @@ -148,6 +148,18 @@ class PartData PrintData displayAbbreviationPrintData; PositionData displayAbbreviationPositionData; + // Names of this 's repeatable elements. MusicXML documentation: + // "The group element allows the use of different versions of the part for different + // purposes. Typical values include score, parts, sound, and data." In practice this is + // an implementation detail of some notation software (e.g. Finale), which uses it to + // tag which exported/extracted version of a part a entry represents. + // + // This is NOT the "real"/visual part grouping mechanism -- braces and brackets tying + // staves together on the page are PartGroupData (see ScoreData::partGroups), a + // completely different element (, declared in around a run of + // entries, not inside one). + std::vector groups; + InstrumentData instrumentData; /// The initial transposition for the part. If the music entered into the part is not in @@ -254,6 +266,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..010080e37 100644 --- a/src/include/mx/api/SoundData.h +++ b/src/include/mx/api/SoundData.h @@ -6,17 +6,50 @@ #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 . // // 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 +85,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 +98,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 +122,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..5dd14b0ec 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