diff --git a/src/include/mx/api/NoteRelationData.h b/src/include/mx/api/NoteRelationData.h new file mode 100644 index 000000000..bfb02d871 --- /dev/null +++ b/src/include/mx/api/NoteRelationData.h @@ -0,0 +1,166 @@ +// MusicXML Class Library +// Copyright (c) by Matthew James Briggs +// Distributed under the MIT License + +#pragma once + +#include "mx/api/ApiCommon.h" +#include "mx/api/DurationData.h" +#include "mx/api/NoteData.h" + +#include +#include +#include + +namespace mx +{ +namespace api +{ +// Whether a metronome-tied marks the start or the stop of a tie between two note figures in a +// metric-relationship metronome mark. As with ordinary ties, both ends should be present, each on +// its own note figure. +enum class MetronomeTieType +{ + start, + stop +}; + +// Whether a metronome-tuplet marks the start or the stop of the tuplet bracket. +enum class MetronomeTupletType +{ + start, + stop +}; + +// Which numbers a metronome-tuplet displays: the actual count, both the actual and normal counts, +// or neither. unspecified means the source omitted show-number and it is not written back. +enum class MetronomeShowNumber +{ + unspecified, + actual, + both, + none +}; + +// A beam drawn on a note figure inside a metric-relationship metronome mark. value is the beam +// action (begin, continue = extend, end, or a forward/backward hook). number is the beam level, +// counting from 1 for the eighth-note beam upward; an absent number means MusicXML's default of +// level 1 and is written without the attribute. +struct MetronomeBeam +{ + Beam value; + std::optional number; + + MetronomeBeam() : value{Beam::unspecified}, number{} + { + } +}; + +// A tuplet drawn on a note figure inside a metric-relationship metronome mark: the +// time-modification ratio -- actualNotes in the time of normalNotes, where a normal note is a +// normalType note carrying normalDots dots -- plus how the bracket and numbers are shown. +// normalType unspecified means the ratio uses the figure's own note type. +struct MetronomeTuplet +{ + int actualNotes; + int normalNotes; + DurationName normalType; + int normalDots; + MetronomeTupletType type; + Bool bracket; + MetronomeShowNumber showNumber; + + MetronomeTuplet() + : actualNotes{0}, normalNotes{0}, normalType{DurationName::unspecified}, normalDots{0}, + type{MetronomeTupletType::start}, bracket{Bool::unspecified}, showNumber{MetronomeShowNumber::unspecified} + { + } +}; + +// One note figure inside a metric-relationship metronome mark. metronomeType is the note value +// drawn (quarter, eighth, ...) and dots its augmentation dots; beams, tie, and tuplet decorate the +// figure the way beams, ties, and tuplets decorate a real note, but using the reduced +// metronome-specific vocabulary. +struct MetronomeNoteData +{ + DurationName metronomeType; + int dots; + std::vector beams; + std::optional tie; + std::optional tuplet; + + MetronomeNoteData() : metronomeType{DurationName::unspecified}, dots{0}, beams{}, tie{}, tuplet{} + { + } +}; + +// The right-hand side of a metric relationship: the relation symbol drawn between the two note +// groups (MusicXML currently allows only "equals"; an empty symbol also means "equals") followed +// by the note figures to its right (at least one). +struct MetronomeRelation +{ + std::string symbol; + std::vector notes; + + MetronomeRelation() : symbol{}, notes{} + { + } +}; + +// A metric-relationship metronome mark -- the general form of a metric modulation, drawn with note +// pictures rather than the bare beat-units of the simpler MetricModulation form. A typical mark +// equates two note groups, e.g. "two beamed eighths = a quarter-note triplet". notes is the +// left-hand group (at least one figure). relation, when present, holds the relation symbol and the +// right-hand group; when absent the mark shows a single group with no equation. arrows draws +// metric-modulation arrows on both sides of the mark. +struct NoteRelation +{ + bool arrows; + std::vector notes; + std::optional relation; + + NoteRelation() : arrows{false}, notes{}, relation{} + { + } +}; + +MXAPI_EQUALS_BEGIN(MetronomeBeam) +MXAPI_EQUALS_MEMBER(value) +MXAPI_EQUALS_MEMBER(number) +MXAPI_EQUALS_END; +MXAPI_NOT_EQUALS_AND_VECTORS(MetronomeBeam); + +MXAPI_EQUALS_BEGIN(MetronomeTuplet) +MXAPI_EQUALS_MEMBER(actualNotes) +MXAPI_EQUALS_MEMBER(normalNotes) +MXAPI_EQUALS_MEMBER(normalType) +MXAPI_EQUALS_MEMBER(normalDots) +MXAPI_EQUALS_MEMBER(type) +MXAPI_EQUALS_MEMBER(bracket) +MXAPI_EQUALS_MEMBER(showNumber) +MXAPI_EQUALS_END; +MXAPI_NOT_EQUALS_AND_VECTORS(MetronomeTuplet); + +MXAPI_EQUALS_BEGIN(MetronomeNoteData) +MXAPI_EQUALS_MEMBER(metronomeType) +MXAPI_EQUALS_MEMBER(dots) +MXAPI_EQUALS_MEMBER(beams) +MXAPI_EQUALS_MEMBER(tie) +MXAPI_EQUALS_MEMBER(tuplet) +MXAPI_EQUALS_END; +MXAPI_NOT_EQUALS_AND_VECTORS(MetronomeNoteData); + +MXAPI_EQUALS_BEGIN(MetronomeRelation) +MXAPI_EQUALS_MEMBER(symbol) +MXAPI_EQUALS_MEMBER(notes) +MXAPI_EQUALS_END; +MXAPI_NOT_EQUALS_AND_VECTORS(MetronomeRelation); + +MXAPI_EQUALS_BEGIN(NoteRelation) +MXAPI_EQUALS_MEMBER(arrows) +MXAPI_EQUALS_MEMBER(notes) +MXAPI_EQUALS_MEMBER(relation) +MXAPI_EQUALS_END; +MXAPI_NOT_EQUALS_AND_VECTORS(NoteRelation); +} // namespace api +} // namespace mx diff --git a/src/include/mx/api/TempoData.h b/src/include/mx/api/TempoData.h index b7e3c97e8..383817671 100644 --- a/src/include/mx/api/TempoData.h +++ b/src/include/mx/api/TempoData.h @@ -7,64 +7,116 @@ #include "mx/api/ColorData.h" #include "mx/api/DurationData.h" #include "mx/api/FontData.h" +#include "mx/api/NoteRelationData.h" #include "mx/api/PositionData.h" #include "mx/api/PrintData.h" #include #include +#include +#include namespace mx { namespace api { -// Which kind of metronome mark a TempoData carries. -enum class TempoType +// A single note picture in a beat-unit metronome mark: a note value (type) with augmentation +// dots. Used for the tied continuations of a beat-unit, e.g. the eighth in "quarter + eighth". +struct BeatUnit { - unspecified, - beatsPerMinute, - metricModulation + DurationName type; + int dots; + + BeatUnit() : type{DurationName::unspecified}, dots{0} + { + } }; -// A metronome mark of the form "note = number", e.g. a quarter note at 120. durationName and -// dots give the beat-unit -- the note picture to the left of the equals sign. beatsPerMinute is -// the value to the right of it. MusicXML allows free text there ("120", "ca. 76", a range like -// "126-138"), so it is a string; an empty string is a valid "unset" value. A player that needs a -// numeric playback tempo should read SoundData::tempo, which is always numeric and expressed in -// quarter notes per minute. +// A metronome mark of the form "note = number", e.g. a quarter note at 120. durationName and dots +// give the beat-unit -- the note picture to the left of the equals sign. tiedBeatUnits holds any +// further note pictures tied to it (a mark like "quarter + eighth = 120" ties an eighth to the +// quarter). beatsPerMinute is the value to the right of the equals sign; MusicXML allows free text +// there ("120", "ca. 76", a range like "126-138"), so it is a string, and an empty string is a +// valid "unset" value. A player that needs a numeric playback tempo should read SoundData::tempo, +// which is always numeric and expressed in quarter notes per minute. struct BeatsPerMinute { DurationName durationName; int dots; + std::vector tiedBeatUnits; std::string beatsPerMinute; - BeatsPerMinute() : durationName{DurationName::unspecified}, dots{VALUE_UNSPECIFIED}, beatsPerMinute{} + BeatsPerMinute() + : durationName{DurationName::unspecified}, dots{VALUE_UNSPECIFIED}, tiedBeatUnits{}, beatsPerMinute{} { } }; // A metronome mark of the form "note = note", e.g. a dotted quarter equal to a half note -- the // two-beat-unit spelling of a metric modulation. The left and right beat-units are each a note -// picture (durationName + dots). playbackBeatsPerMinute is unused for this form. +// picture (durationName + dots), and each may carry tied continuations. playbackBeatsPerMinute is +// unused for this form. struct MetricModulation { DurationName leftDurationName; int leftDots; + std::vector leftTiedBeatUnits; DurationName rightDurationName; int rightDots; + std::vector rightTiedBeatUnits; BeatsPerMinute playbackBeatsPerMinute; MetricModulation() - : leftDurationName{DurationName::unspecified}, leftDots{VALUE_UNSPECIFIED}, - rightDurationName{DurationName::unspecified}, rightDots{VALUE_UNSPECIFIED}, playbackBeatsPerMinute{} + : leftDurationName{DurationName::unspecified}, leftDots{VALUE_UNSPECIFIED}, leftTiedBeatUnits{}, + rightDurationName{DurationName::unspecified}, rightDots{VALUE_UNSPECIFIED}, rightTiedBeatUnits{}, + playbackBeatsPerMinute{} { } }; -// A metronome (tempo) mark carried by a . tempoType selects the form: a -// note-equals-number mark (beatsPerMinute) or a note-equals-note metric modulation -// (metricModulation). positionData, fontData, color, and id give the mark's placement and +// The body of a metronome mark: exactly one of the three forms a can take. Construct +// it from whichever form you mean; query kind() (or the is...() helpers) and read the matching +// accessor. Reading the wrong accessor returns a default-constructed value rather than throwing. +class TempoChoice +{ + public: + enum class Kind + { + beatsPerMinute, + metricModulation, + noteRelation + }; + + // Defaults to an (empty) beats-per-minute mark, the first alternative. + TempoChoice(); + + TempoChoice(BeatsPerMinute value); + TempoChoice(MetricModulation value); + TempoChoice(NoteRelation value); + + Kind kind() const; + bool isBeatsPerMinute() const; + bool isMetricModulation() const; + bool isNoteRelation() const; + + // Each accessor returns a copy of the held alternative. Check the matching is...() first; a + // wrong-kind access returns a default-constructed value. + BeatsPerMinute beatsPerMinute() const; + MetricModulation metricModulation() const; + NoteRelation noteRelation() const; + + bool operator==(const TempoChoice &other) const; + + private: + std::variant myValue; +}; + +// A metronome (tempo) mark carried by a . choice holds the mark's body -- a +// note-equals-number mark, a note-equals-note metric modulation, or a note-relationship mark +// drawn with note figures. positionData, fontData, color, and id give the mark's placement and // appearance; justify aligns the mark's content within its box; printObject set to 'no' keeps the -// mark in the file without drawing it; isParenthetical draws the mark in parentheses. +// mark in the file without drawing it (common for relationship marks); isParenthetical draws the +// mark in parentheses. class TempoData { public: @@ -77,25 +129,25 @@ class TempoData std::optional id; HorizontalAlignment justify; Bool printObject; - TempoType tempoType; - - // only used when tempoType is 'beatsPerMinute' - BeatsPerMinute beatsPerMinute; - - // only used when tempoType is 'metricModulation' - MetricModulation metricModulation; + TempoChoice choice; TempoData() : tickTime{0}, isParenthetical{Bool::unspecified}, printData{}, positionData{}, fontData{}, color{}, id{}, - justify{HorizontalAlignment::unspecified}, printObject{Bool::unspecified}, tempoType{TempoType::unspecified}, - beatsPerMinute{}, metricModulation{} + justify{HorizontalAlignment::unspecified}, printObject{Bool::unspecified}, choice{} { } }; +MXAPI_EQUALS_BEGIN(BeatUnit) +MXAPI_EQUALS_MEMBER(type) +MXAPI_EQUALS_MEMBER(dots) +MXAPI_EQUALS_END; +MXAPI_NOT_EQUALS_AND_VECTORS(BeatUnit); + MXAPI_EQUALS_BEGIN(BeatsPerMinute) MXAPI_EQUALS_MEMBER(durationName) MXAPI_EQUALS_MEMBER(dots) +MXAPI_EQUALS_MEMBER(tiedBeatUnits) MXAPI_EQUALS_MEMBER(beatsPerMinute) MXAPI_EQUALS_END; MXAPI_NOT_EQUALS_AND_VECTORS(BeatsPerMinute); @@ -103,8 +155,10 @@ MXAPI_NOT_EQUALS_AND_VECTORS(BeatsPerMinute); MXAPI_EQUALS_BEGIN(MetricModulation) MXAPI_EQUALS_MEMBER(leftDurationName) MXAPI_EQUALS_MEMBER(leftDots) +MXAPI_EQUALS_MEMBER(leftTiedBeatUnits) MXAPI_EQUALS_MEMBER(rightDurationName) MXAPI_EQUALS_MEMBER(rightDots) +MXAPI_EQUALS_MEMBER(rightTiedBeatUnits) MXAPI_EQUALS_MEMBER(playbackBeatsPerMinute) MXAPI_EQUALS_END; MXAPI_NOT_EQUALS_AND_VECTORS(MetricModulation); @@ -119,9 +173,7 @@ MXAPI_EQUALS_MEMBER(color) MXAPI_EQUALS_MEMBER(id) MXAPI_EQUALS_MEMBER(justify) MXAPI_EQUALS_MEMBER(printObject) -MXAPI_EQUALS_MEMBER(tempoType) -MXAPI_EQUALS_MEMBER(beatsPerMinute) -MXAPI_EQUALS_MEMBER(metricModulation) +MXAPI_EQUALS_MEMBER(choice) MXAPI_EQUALS_END; MXAPI_NOT_EQUALS_AND_VECTORS(TempoData); } // namespace api diff --git a/src/private/mx/api/TempoData.cpp b/src/private/mx/api/TempoData.cpp new file mode 100644 index 000000000..c00353b8b --- /dev/null +++ b/src/private/mx/api/TempoData.cpp @@ -0,0 +1,83 @@ +// MusicXML Class Library +// Copyright (c) by Matthew James Briggs +// Distributed under the MIT License + +#include "mx/api/TempoData.h" + +#include + +namespace mx +{ +namespace api +{ + +TempoChoice::TempoChoice() : myValue{BeatsPerMinute{}} +{ +} + +TempoChoice::TempoChoice(BeatsPerMinute value) : myValue{std::move(value)} +{ +} + +TempoChoice::TempoChoice(MetricModulation value) : myValue{std::move(value)} +{ +} + +TempoChoice::TempoChoice(NoteRelation value) : myValue{std::move(value)} +{ +} + +TempoChoice::Kind TempoChoice::kind() const +{ + return static_cast(myValue.index()); +} + +bool TempoChoice::isBeatsPerMinute() const +{ + return std::holds_alternative(myValue); +} + +bool TempoChoice::isMetricModulation() const +{ + return std::holds_alternative(myValue); +} + +bool TempoChoice::isNoteRelation() const +{ + return std::holds_alternative(myValue); +} + +BeatsPerMinute TempoChoice::beatsPerMinute() const +{ + if (const auto *value = std::get_if(&myValue)) + { + return *value; + } + return BeatsPerMinute{}; +} + +MetricModulation TempoChoice::metricModulation() const +{ + if (const auto *value = std::get_if(&myValue)) + { + return *value; + } + return MetricModulation{}; +} + +NoteRelation TempoChoice::noteRelation() const +{ + if (const auto *value = std::get_if(&myValue)) + { + return *value; + } + return NoteRelation{}; +} + +bool TempoChoice::operator==(const TempoChoice &other) const +{ + return myValue == other.myValue; +} + +} // namespace api +} // namespace mx diff --git a/src/private/mx/impl/DirectionWriter.cpp b/src/private/mx/impl/DirectionWriter.cpp index ed8ae56e0..cb8ce34b4 100644 --- a/src/private/mx/impl/DirectionWriter.cpp +++ b/src/private/mx/impl/DirectionWriter.cpp @@ -9,7 +9,9 @@ #include "mx/core/generated/AccordionRegistration.h" #include "mx/core/generated/Bass.h" #include "mx/core/generated/BassStep.h" +#include "mx/core/generated/BeamLevel.h" #include "mx/core/generated/BeatUnitGroup.h" +#include "mx/core/generated/BeatUnitTied.h" #include "mx/core/generated/Beater.h" #include "mx/core/generated/Bracket.h" #include "mx/core/generated/Coda.h" @@ -48,10 +50,16 @@ #include "mx/core/generated/Membrane.h" #include "mx/core/generated/Metal.h" #include "mx/core/generated/Metronome.h" +#include "mx/core/generated/MetronomeBeam.h" #include "mx/core/generated/MetronomeChoice.h" #include "mx/core/generated/MetronomeChoiceGroup.h" +#include "mx/core/generated/MetronomeChoiceGroup2.h" +#include "mx/core/generated/MetronomeChoiceGroup2Group.h" #include "mx/core/generated/MetronomeChoiceGroupChoice.h" #include "mx/core/generated/MetronomeChoiceGroupChoiceGroup.h" +#include "mx/core/generated/MetronomeNote.h" +#include "mx/core/generated/MetronomeTied.h" +#include "mx/core/generated/MetronomeTuplet.h" #include "mx/core/generated/MusicDataChoice.h" #include "mx/core/generated/Numeral.h" #include "mx/core/generated/NumeralKey.h" @@ -79,6 +87,7 @@ #include "mx/core/generated/Scordatura.h" #include "mx/core/generated/Segno.h" #include "mx/core/generated/Semitones.h" +#include "mx/core/generated/ShowTuplet.h" #include "mx/core/generated/SmuflGlyphName.h" #include "mx/core/generated/SmuflPictogramGlyphName.h" #include "mx/core/generated/Sound.h" @@ -91,6 +100,7 @@ #include "mx/core/generated/StringMute.h" #include "mx/core/generated/StringNumber.h" #include "mx/core/generated/StyleText.h" +#include "mx/core/generated/TimeModificationGroup.h" #include "mx/core/generated/Timpani.h" #include "mx/core/generated/TuningGroup.h" #include "mx/core/generated/ValignImage.h" @@ -450,64 +460,177 @@ void DirectionWriter::emitDashesStop(const api::SpannerStop &item, core::Directi addDirectionType(std::move(dt), direction); } -void DirectionWriter::emitTempo(const api::TempoData &tempo, core::Direction &direction) +namespace +{ +core::BeatUnitGroup makeBeatUnitGroup(const Converter &converter, api::DurationName durationName, int dots) { - // Content-guard: a tempo with no beat-unit -- an unfilled/default TempoData, or the - // metronome-note form this api does not yet model -- has nothing to write. Skip it rather - // than emit an empty . Nothing throws; the direction simply carries no tempo. - const bool hasBeatUnit = tempo.beatsPerMinute.durationName != api::DurationName::unspecified; - const bool hasModulation = tempo.metricModulation.leftDurationName != api::DurationName::unspecified; - if (tempo.tempoType == api::TempoType::beatsPerMinute && !hasBeatUnit) + core::BeatUnitGroup beatUnitGroup{}; + beatUnitGroup.setBeatUnit(converter.convert(durationName)); + for (int d = 0; d < dots; ++d) { - return; + beatUnitGroup.addBeatUnitDot(core::Empty{}); } - if (tempo.tempoType == api::TempoType::metricModulation && !hasModulation) + return beatUnitGroup; +} + +core::BeatUnitTied makeBeatUnitTied(const Converter &converter, const api::BeatUnit &beatUnit) +{ + core::BeatUnitTied tied{}; + tied.setBeatUnit(makeBeatUnitGroup(converter, beatUnit.type, beatUnit.dots)); + return tied; +} + +core::MetronomeNote makeMetronomeNote(const Converter &converter, const api::MetronomeNoteData ¬e) +{ + core::MetronomeNote out{}; + out.setMetronomeType(converter.convert(note.metronomeType)); + for (int d = 0; d < note.dots; ++d) { - return; + out.addMetronomeDot(core::Empty{}); } - if (tempo.tempoType != api::TempoType::beatsPerMinute && tempo.tempoType != api::TempoType::metricModulation) + for (const auto &beam : note.beams) { - return; + core::MetronomeBeam coreBeam{}; + coreBeam.setValue(converter.convert(beam.value)); + if (beam.number.has_value()) + { + coreBeam.setNumber(core::BeamLevel{*beam.number}); + } + out.addMetronomeBeam(coreBeam); } - - const auto makeBeatUnitGroup = [&](api::DurationName durationName, int dots) { - core::BeatUnitGroup beatUnitGroup{}; - beatUnitGroup.setBeatUnit(myConverter.convert(durationName)); - for (int d = 0; d < dots; ++d) + if (note.tie.has_value()) + { + core::MetronomeTied tied{}; + tied.setType(*note.tie == api::MetronomeTieType::start ? core::StartStop::start() : core::StartStop::stop()); + out.setMetronomeTied(tied); + } + if (note.tuplet.has_value()) + { + const auto &tuplet = *note.tuplet; + core::MetronomeTuplet coreTuplet{}; + coreTuplet.setActualNotes(tuplet.actualNotes); + coreTuplet.setNormalNotes(tuplet.normalNotes); + if (tuplet.normalType != api::DurationName::unspecified) + { + core::TimeModificationGroup group{}; + group.setNormalType(converter.convert(tuplet.normalType)); + for (int d = 0; d < tuplet.normalDots; ++d) + { + group.addNormalDot(core::Empty{}); + } + coreTuplet.setGroup(group); + } + coreTuplet.setType(tuplet.type == api::MetronomeTupletType::start ? core::StartStop::start() + : core::StartStop::stop()); + if (tuplet.bracket != api::Bool::unspecified) { - beatUnitGroup.addBeatUnitDot(core::Empty{}); + coreTuplet.setBracket(converter.convert(tuplet.bracket)); + } + switch (tuplet.showNumber) + { + case api::MetronomeShowNumber::actual: + coreTuplet.setShowNumber(core::ShowTuplet::actual()); + break; + case api::MetronomeShowNumber::both: + coreTuplet.setShowNumber(core::ShowTuplet::both()); + break; + case api::MetronomeShowNumber::none: + coreTuplet.setShowNumber(core::ShowTuplet::none()); + break; + case api::MetronomeShowNumber::unspecified: + break; } - return beatUnitGroup; - }; + out.setMetronomeTuplet(coreTuplet); + } + return out; +} + +core::OneOrMore makeMetronomeNotes(const Converter &converter, + const std::vector ¬es) +{ + core::OneOrMore out{makeMetronomeNote(converter, notes.front())}; + for (std::size_t i = 1; i < notes.size(); ++i) + { + out.add(makeMetronomeNote(converter, notes.at(i))); + } + return out; +} +} // namespace +void DirectionWriter::emitTempo(const api::TempoData &tempo, core::Direction &direction) +{ + const auto kind = tempo.choice.kind(); core::Metronome metronome{}; - if (tempo.tempoType == api::TempoType::beatsPerMinute) + if (kind == api::TempoChoice::Kind::beatsPerMinute) { - // beat-unit (+dots) followed by a per-minute string, kept verbatim from the source - // (per-minute is xs:string: "120", "ca. 76", a range, ...). + // beat-unit (+dots, +tied continuations) followed by a per-minute string, kept verbatim + // from the source (per-minute is xs:string: "120", "ca. 76", a range, ...). + const auto bpm = tempo.choice.beatsPerMinute(); + // Content-guard: a default/empty tempo has no beat-unit; skip rather than emit an empty + // . Nothing throws; the direction simply carries no tempo. + if (bpm.durationName == api::DurationName::unspecified) + { + return; + } core::PerMinute pm{}; - pm.setValue(tempo.beatsPerMinute.beatsPerMinute); + pm.setValue(bpm.beatsPerMinute); core::MetronomeChoiceGroup mcg{}; - mcg.setBeatUnit(makeBeatUnitGroup(tempo.beatsPerMinute.durationName, tempo.beatsPerMinute.dots)); + mcg.setBeatUnit(makeBeatUnitGroup(myConverter, bpm.durationName, bpm.dots)); + for (const auto &tied : bpm.tiedBeatUnits) + { + mcg.addBeatUnitTied(makeBeatUnitTied(myConverter, tied)); + } mcg.setChoice(core::MetronomeChoiceGroupChoice::perMinute(pm)); metronome.setChoice(core::MetronomeChoice::group(mcg)); } - else + else if (kind == api::TempoChoice::Kind::metricModulation) { // Metric modulation: two beat-units, e.g. quarter - // = half. The second beat-unit is the 'group' - // alternative of the choice. - const auto &mm = tempo.metricModulation; + // = half. The second beat-unit is the 'group' alternative. + const auto mm = tempo.choice.metricModulation(); + if (mm.leftDurationName == api::DurationName::unspecified) + { + return; + } core::MetronomeChoiceGroupChoiceGroup rightBeatUnitHolder{}; - rightBeatUnitHolder.setBeatUnit(makeBeatUnitGroup(mm.rightDurationName, mm.rightDots)); + rightBeatUnitHolder.setBeatUnit(makeBeatUnitGroup(myConverter, mm.rightDurationName, mm.rightDots)); + for (const auto &tied : mm.rightTiedBeatUnits) + { + rightBeatUnitHolder.addBeatUnitTied(makeBeatUnitTied(myConverter, tied)); + } core::MetronomeChoiceGroup mcg{}; - mcg.setBeatUnit(makeBeatUnitGroup(mm.leftDurationName, mm.leftDots)); + mcg.setBeatUnit(makeBeatUnitGroup(myConverter, mm.leftDurationName, mm.leftDots)); + for (const auto &tied : mm.leftTiedBeatUnits) + { + mcg.addBeatUnitTied(makeBeatUnitTied(myConverter, tied)); + } mcg.setChoice(core::MetronomeChoiceGroupChoice::group(rightBeatUnitHolder)); metronome.setChoice(core::MetronomeChoice::group(mcg)); } + else + { + // Note-relation form: one or more metronome-note figures, optionally followed by a + // relation symbol and a second group of figures. + const auto noteRelation = tempo.choice.noteRelation(); + if (noteRelation.notes.empty()) + { + return; + } + core::MetronomeChoiceGroup2 group2{}; + group2.setMetronomeArrows(noteRelation.arrows); + group2.setMetronomeNote(makeMetronomeNotes(myConverter, noteRelation.notes)); + if (noteRelation.relation.has_value() && !noteRelation.relation->notes.empty()) + { + core::MetronomeChoiceGroup2Group relationGroup{}; + relationGroup.setMetronomeRelation(noteRelation.relation->symbol); + relationGroup.setMetronomeNote(makeMetronomeNotes(myConverter, noteRelation.relation->notes)); + group2.setGroup(relationGroup); + } + metronome.setChoice(core::MetronomeChoice::group2(group2)); + } // print-style-align (default-x/y, relative-x/y, font, color, halign, valign) + justify + // print-object + parentheses + id. diff --git a/src/private/mx/impl/MetronomeReader.cpp b/src/private/mx/impl/MetronomeReader.cpp index 61f8f3f41..0efdcc41a 100644 --- a/src/private/mx/impl/MetronomeReader.cpp +++ b/src/private/mx/impl/MetronomeReader.cpp @@ -3,13 +3,24 @@ // Distributed under the MIT License #include "mx/impl/MetronomeReader.h" +#include "mx/core/generated/BeamLevel.h" #include "mx/core/generated/BeatUnitGroup.h" +#include "mx/core/generated/BeatUnitTied.h" #include "mx/core/generated/Metronome.h" +#include "mx/core/generated/MetronomeBeam.h" #include "mx/core/generated/MetronomeChoice.h" #include "mx/core/generated/MetronomeChoiceGroup.h" +#include "mx/core/generated/MetronomeChoiceGroup2.h" +#include "mx/core/generated/MetronomeChoiceGroup2Group.h" #include "mx/core/generated/MetronomeChoiceGroupChoice.h" #include "mx/core/generated/MetronomeChoiceGroupChoiceGroup.h" +#include "mx/core/generated/MetronomeNote.h" +#include "mx/core/generated/MetronomeTied.h" +#include "mx/core/generated/MetronomeTuplet.h" #include "mx/core/generated/PerMinute.h" +#include "mx/core/generated/ShowTuplet.h" +#include "mx/core/generated/StartStop.h" +#include "mx/core/generated/TimeModificationGroup.h" #include "mx/impl/Converter.h" #include "mx/impl/FontFunctions.h" #include "mx/impl/PositionFunctions.h" @@ -19,6 +30,93 @@ namespace mx { namespace impl { +namespace +{ +api::BeatUnit readBeatUnitGroup(const Converter &converter, const core::BeatUnitGroup &group) +{ + api::BeatUnit beatUnit; + beatUnit.type = converter.convert(group.beatUnit()); + beatUnit.dots = static_cast(group.beatUnitDot().size()); + return beatUnit; +} + +// Reads the beat-unit (type + dots) and any tied continuation beat-units from a holder that +// exposes beatUnit() and beatUnitTied() -- the left group and the right (metric-modulation) group +// share this shape. +template +void readBeatUnitWithTies(const Converter &converter, const BeatUnitHolder &holder, api::DurationName &outName, + int &outDots, std::vector &outTied) +{ + outName = converter.convert(holder.beatUnit().beatUnit()); + outDots = static_cast(holder.beatUnit().beatUnitDot().size()); + for (const auto &tied : holder.beatUnitTied()) + { + outTied.push_back(readBeatUnitGroup(converter, tied.beatUnit())); + } +} + +api::MetronomeNoteData readMetronomeNote(const Converter &converter, const core::MetronomeNote ¬e) +{ + api::MetronomeNoteData out; + out.metronomeType = converter.convert(note.metronomeType()); + out.dots = static_cast(note.metronomeDot().size()); + + for (const auto &beam : note.metronomeBeam()) + { + api::MetronomeBeam apiBeam; + apiBeam.value = converter.convert(beam.value()); + if (beam.number().has_value()) + { + apiBeam.number = beam.number()->value(); + } + out.beams.push_back(apiBeam); + } + + if (note.metronomeTied().has_value()) + { + out.tie = note.metronomeTied()->type().tag() == core::StartStop::Tag::start ? api::MetronomeTieType::start + : api::MetronomeTieType::stop; + } + + if (note.metronomeTuplet().has_value()) + { + const auto &tuplet = *note.metronomeTuplet(); + api::MetronomeTuplet apiTuplet; + apiTuplet.actualNotes = tuplet.actualNotes(); + apiTuplet.normalNotes = tuplet.normalNotes(); + if (tuplet.group().has_value()) + { + apiTuplet.normalType = converter.convert(tuplet.group()->normalType()); + apiTuplet.normalDots = static_cast(tuplet.group()->normalDot().size()); + } + apiTuplet.type = tuplet.type().tag() == core::StartStop::Tag::start ? api::MetronomeTupletType::start + : api::MetronomeTupletType::stop; + if (tuplet.bracket().has_value()) + { + apiTuplet.bracket = converter.convert(*tuplet.bracket()); + } + if (tuplet.showNumber().has_value()) + { + switch (tuplet.showNumber()->tag()) + { + case core::ShowTuplet::Tag::actual: + apiTuplet.showNumber = api::MetronomeShowNumber::actual; + break; + case core::ShowTuplet::Tag::both: + apiTuplet.showNumber = api::MetronomeShowNumber::both; + break; + case core::ShowTuplet::Tag::none: + apiTuplet.showNumber = api::MetronomeShowNumber::none; + break; + } + } + out.tuplet = apiTuplet; + } + + return out; +} +} // namespace + MetronomeReader::MetronomeReader(MetronomeReaderParameters &¶ms) : myMutex{}, myOutTempoData{}, myMetronome{params.metronome}, myPreviousTempoData{std::move(params.previousTempoData)}, myCursor{std::move(params.cursor)}, @@ -54,8 +152,8 @@ api::TempoData MetronomeReader::getTempoData() const myOutTempoData.isParenthetical = converter.convert(*myMetronome.parentheses()); } - // the old core's beatUnitPer is the new core's group; the old core's - // noteRelationNote (metronome-note based) is group2 + // The 'group' alternative is the beat-unit form (note = number, or note = note); 'group2' is + // the metronome-note form (a metric relationship drawn with note figures). using FirstChoice = core::MetronomeChoice::Kind; const auto firstChoice = myBeatUnitPerOrNoteRelationNoteChoice.kind(); @@ -100,43 +198,58 @@ void MetronomeReader::parseBeatUnitPer() const void MetronomeReader::parseNoteRelationNote() const { - // The metronome-note form -- ... ... - // -- has no representation in api::TempoData yet. Leave the - // tempo 'unspecified' rather than crashing the whole api pipeline; the writer - // skips unspecified tempos. Previously this threw and produced no output at - // all (GETDATAFAIL). See issue #218. + // The metronome-note form: one or more metronome-note figures, optionally followed by a + // metronome-relation symbol and a second group of figures. See NoteRelation. + const auto &group2 = myBeatUnitPerOrNoteRelationNoteChoice.asGroup2(); + Converter converter; + + api::NoteRelation noteRelation; + noteRelation.arrows = group2.metronomeArrows(); + for (const auto ¬e : group2.metronomeNote()) + { + noteRelation.notes.push_back(readMetronomeNote(converter, note)); + } + + if (group2.group().has_value()) + { + api::MetronomeRelation relation; + relation.symbol = group2.group()->metronomeRelation(); + for (const auto ¬e : group2.group()->metronomeNote()) + { + relation.notes.push_back(readMetronomeNote(converter, note)); + } + noteRelation.relation = std::move(relation); + } + + myOutTempoData.choice = api::TempoChoice{std::move(noteRelation)}; } void MetronomeReader::parseBeatsPerMinute() const { - myOutTempoData.tempoType = api::TempoType::beatsPerMinute; const auto &beatUnitPer = myBeatUnitPerOrNoteRelationNoteChoice.asGroup(); - const auto &grp = beatUnitPer.beatUnit(); Converter converter; - myOutTempoData.beatsPerMinute.durationName = converter.convert(grp.beatUnit()); - myOutTempoData.beatsPerMinute.dots = static_cast(grp.beatUnitDot().size()); + + api::BeatsPerMinute bpm; + readBeatUnitWithTies(converter, beatUnitPer, bpm.durationName, bpm.dots, bpm.tiedBeatUnits); // per-minute is xs:string in MusicXML ("120", "ca. 76", a range, ...); keep it verbatim. - myOutTempoData.beatsPerMinute.beatsPerMinute = beatUnitPer.choice().asPerMinute().value(); + bpm.beatsPerMinute = beatUnitPer.choice().asPerMinute().value(); + + myOutTempoData.choice = api::TempoChoice{std::move(bpm)}; } void MetronomeReader::parseMetronomeModulation() const { - // Metric modulation: .. = ... - // The left beat-unit lives directly on the group; the right beat-unit is the - // 'group' alternative of the metronome choice. Previously this was an empty - // stub that left the tempo 'unspecified', which then crashed the writer - // (CREATEFAIL). See issue #218. - myOutTempoData.tempoType = api::TempoType::metricModulation; + // Metric modulation: .. = ... The left beat-unit + // lives directly on the group; the right beat-unit is the 'group' alternative of the choice. const auto &beatUnitPer = myBeatUnitPerOrNoteRelationNoteChoice.asGroup(); Converter converter; - const auto &leftBeatUnit = beatUnitPer.beatUnit(); - myOutTempoData.metricModulation.leftDurationName = converter.convert(leftBeatUnit.beatUnit()); - myOutTempoData.metricModulation.leftDots = static_cast(leftBeatUnit.beatUnitDot().size()); + api::MetricModulation mm; + readBeatUnitWithTies(converter, beatUnitPer, mm.leftDurationName, mm.leftDots, mm.leftTiedBeatUnits); + const auto &rightGroup = beatUnitPer.choice().asGroup(); + readBeatUnitWithTies(converter, rightGroup, mm.rightDurationName, mm.rightDots, mm.rightTiedBeatUnits); - const auto &rightBeatUnit = beatUnitPer.choice().asGroup().beatUnit(); - myOutTempoData.metricModulation.rightDurationName = converter.convert(rightBeatUnit.beatUnit()); - myOutTempoData.metricModulation.rightDots = static_cast(rightBeatUnit.beatUnitDot().size()); + myOutTempoData.choice = api::TempoChoice{std::move(mm)}; } } // namespace impl } // namespace mx diff --git a/src/private/mxtest/api/MetronomeApiTest.cpp b/src/private/mxtest/api/MetronomeApiTest.cpp index 3b4d05e18..9bc30db29 100644 --- a/src/private/mxtest/api/MetronomeApiTest.cpp +++ b/src/private/mxtest/api/MetronomeApiTest.cpp @@ -16,54 +16,10 @@ using namespace std; using namespace mx::api; using namespace mxtest; -TEST(roundTripBpm, MetronomeApi) -{ - const auto expectedDurationName = DurationName::dur16th; - const int expectedDots = 1; - const std::string expectedBeatsPerMinute = "123"; - const int expectedTickTimePosition = 77; - - ScoreData expectedScoreData; - expectedScoreData.ticksPerQuarter = 100; - expectedScoreData.parts.emplace_back(); - auto &expectedPart = expectedScoreData.parts.back(); - expectedPart.measures.emplace_back(); - auto &expectedMeasure = expectedPart.measures.back(); - expectedMeasure.staves.emplace_back(); - auto &expectedStaff = expectedMeasure.staves.back(); - expectedStaff.directions.emplace_back(); - auto &expectedDirection = expectedStaff.directions.back(); - expectedDirection.tempos.emplace_back(); - expectedDirection.tickTimePosition = expectedTickTimePosition; - auto &expectedTempo = expectedDirection.tempos.back(); - expectedTempo.tempoType = TempoType::beatsPerMinute; - auto &expectedBpm = expectedTempo.beatsPerMinute; - expectedBpm.durationName = expectedDurationName; - expectedBpm.dots = expectedDots; - expectedBpm.beatsPerMinute = expectedBeatsPerMinute; - - auto actualScoreData = roundTrip(expectedScoreData); - - auto &actualPart = actualScoreData.parts.back(); - auto &actualMeasure = actualPart.measures.back(); - auto &actualStaff = actualMeasure.staves.back(); - auto &actualDirection = actualStaff.directions.back(); - auto &actualTempo = actualDirection.tempos.back(); - actualTempo.tempoType = TempoType::beatsPerMinute; - auto &actualBpm = actualTempo.beatsPerMinute; - - CHECK(expectedBeatsPerMinute == actualBpm.beatsPerMinute); - CHECK_EQUAL(expectedDots, actualBpm.dots); - CHECK(expectedDurationName == actualBpm.durationName); - CHECK_EQUAL(expectedTickTimePosition, actualDirection.tickTimePosition); -} - -// --- issue #218: metronome/tempo marks must not crash the api pipeline ------- - namespace { -// A minimal partwise document carrying exactly one in one -// . `metronomeBody` is the inner markup of the element. +// A minimal partwise document carrying exactly one in one . +// `metronomeBody` is the inner markup of the element. std::string makeMetronomeDoc(const std::string &metronomeBody) { return R"( @@ -86,37 +42,52 @@ std::string makeMetronomeDoc(const std::string &metronomeBody) )"; } -} // namespace -// The metronome-note form has no api::TempoData representation. It used to throw -// "wtf is this" in the reader, which DocumentManager turned into a getData() -// failure -- the whole file produced no output (GETDATAFAIL). After the fix the -// tempo is dropped but reading succeeds. -TEST(metronomeNoteFormReadDoesNotFail, MetronomeApi) +// Round-trips a score carrying one direction with the given tempo and returns the resulting tempo +// (a default-constructed TempoData if anything is missing). +TempoData roundTripTempo(const TempoData &in) { - const std::string xml = makeMetronomeDoc(R"( - - quarter - - equals - - eighth - - )"); + ScoreData score; + score.ticksPerQuarter = 100; + score.parts.emplace_back(); + score.parts.back().measures.emplace_back(); + score.parts.back().measures.back().staves.emplace_back(); + score.parts.back().measures.back().staves.back().directions.emplace_back(); + score.parts.back().measures.back().staves.back().directions.back().tempos.push_back(in); - auto &mgr = DocumentManager::getInstance(); - std::istringstream iss{xml}; - const auto idResult = mgr.createFromStream(iss); - CHECK(idResult.ok()); - if (!idResult.ok()) + const auto out = roundTrip(score); + if (out.parts.empty() || out.parts.back().measures.empty() || out.parts.back().measures.back().staves.empty()) { - return; + return TempoData{}; } - const auto dataResult = mgr.getData(idResult.value()); - mgr.destroyDocument(idResult.value()); - // Before the fix this was an error (GETDATAFAIL); now reading must succeed. - CHECK(dataResult.ok()); + const auto &directions = out.parts.back().measures.back().staves.back().directions; + if (directions.empty() || directions.back().tempos.empty()) + { + return TempoData{}; + } + return directions.back().tempos.back(); } +} // namespace + +TEST(roundTripBpm, MetronomeApi) +{ + BeatsPerMinute bpm; + bpm.durationName = DurationName::dur16th; + bpm.dots = 1; + bpm.beatsPerMinute = "123"; + TempoData in; + in.choice = TempoChoice{bpm}; + + const auto out = roundTripTempo(in); + + CHECK(TempoChoice::Kind::beatsPerMinute == out.choice.kind()); + const auto outBpm = out.choice.beatsPerMinute(); + CHECK(DurationName::dur16th == outBpm.durationName); + CHECK_EQUAL(1, outBpm.dots); + CHECK(std::string{"123"} == outBpm.beatsPerMinute); +} + +T_END; // A non-numeric is legal -- per-minute is an xs:string. It is kept verbatim on // BeatsPerMinute::beatsPerMinute, so a mark like "quarter = fast" round-trips faithfully instead @@ -145,7 +116,6 @@ TEST(nonNumericPerMinuteRoundTrips, MetronomeApi) } const auto &score = dataResult.value(); - CHECK_EQUAL(1, static_cast(score.parts.size())); if (score.parts.empty() || score.parts.back().measures.empty() || score.parts.back().measures.back().staves.empty()) { return; @@ -157,9 +127,10 @@ TEST(nonNumericPerMinuteRoundTrips, MetronomeApi) return; } const auto &tempo = directions.back().tempos.back(); - CHECK(TempoType::beatsPerMinute == tempo.tempoType); - CHECK(DurationName::quarter == tempo.beatsPerMinute.durationName); - CHECK(std::string{"fast"} == tempo.beatsPerMinute.beatsPerMinute); + CHECK(TempoChoice::Kind::beatsPerMinute == tempo.choice.kind()); + const auto bpm = tempo.choice.beatsPerMinute(); + CHECK(DurationName::quarter == bpm.durationName); + CHECK(std::string{"fast"} == bpm.beatsPerMinute); // The mark must also write back without error. const auto id2Result = mgr.createFromScore(dataResult.value()); @@ -170,126 +141,152 @@ TEST(nonNumericPerMinuteRoundTrips, MetronomeApi) } } -// Metric modulation (two beat-units) used to be an empty read stub plus a writer -// throw. It now round-trips through the api. +T_END; + +// Metric modulation (two beat-units) round-trips through the api. TEST(roundTripMetricModulation, MetronomeApi) { - ScoreData score; - score.ticksPerQuarter = 100; - score.parts.emplace_back(); - score.parts.back().measures.emplace_back(); - score.parts.back().measures.back().staves.emplace_back(); - score.parts.back().measures.back().staves.back().directions.emplace_back(); - auto &direction = score.parts.back().measures.back().staves.back().directions.back(); - direction.tempos.emplace_back(); - auto &tempo = direction.tempos.back(); - tempo.tempoType = TempoType::metricModulation; - tempo.metricModulation.leftDurationName = DurationName::quarter; - tempo.metricModulation.leftDots = 1; - tempo.metricModulation.rightDurationName = DurationName::half; - tempo.metricModulation.rightDots = 0; + MetricModulation mm; + mm.leftDurationName = DurationName::quarter; + mm.leftDots = 1; + mm.rightDurationName = DurationName::half; + mm.rightDots = 0; + TempoData in; + in.choice = TempoChoice{mm}; - const auto out = roundTrip(score); + const auto out = roundTripTempo(in); - CHECK_EQUAL(1, static_cast(out.parts.size())); - if (out.parts.empty()) - { - return; - } - const auto &measures = out.parts.back().measures; - CHECK_EQUAL(1, static_cast(measures.size())); - if (measures.empty() || measures.back().staves.empty()) - { - return; - } - const auto &directions = measures.back().staves.back().directions; - CHECK_EQUAL(1, static_cast(directions.size())); - if (directions.empty() || directions.back().tempos.empty()) + CHECK(TempoChoice::Kind::metricModulation == out.choice.kind()); + const auto outMm = out.choice.metricModulation(); + CHECK(DurationName::quarter == outMm.leftDurationName); + CHECK_EQUAL(1, outMm.leftDots); + CHECK(DurationName::half == outMm.rightDurationName); + CHECK_EQUAL(0, outMm.rightDots); +} + +T_END; + +// A beat-unit tied to a further beat-unit ("quarter + eighth = 120") round-trips via +// BeatsPerMinute::tiedBeatUnits. +TEST(roundTripBeatUnitTied, MetronomeApi) +{ + BeatsPerMinute bpm; + bpm.durationName = DurationName::quarter; + BeatUnit tied; + tied.type = DurationName::eighth; + bpm.tiedBeatUnits.push_back(tied); + bpm.beatsPerMinute = "120"; + TempoData in; + in.choice = TempoChoice{bpm}; + + const auto out = roundTripTempo(in); + + CHECK(TempoChoice::Kind::beatsPerMinute == out.choice.kind()); + const auto outBpm = out.choice.beatsPerMinute(); + CHECK_EQUAL(1, static_cast(outBpm.tiedBeatUnits.size())); + if (!outBpm.tiedBeatUnits.empty()) { - return; + CHECK(DurationName::eighth == outBpm.tiedBeatUnits.front().type); } - const auto &outTempo = directions.back().tempos.back(); - CHECK(TempoType::metricModulation == outTempo.tempoType); - CHECK(DurationName::quarter == outTempo.metricModulation.leftDurationName); - CHECK_EQUAL(1, outTempo.metricModulation.leftDots); - CHECK(DurationName::half == outTempo.metricModulation.rightDurationName); - CHECK_EQUAL(0, outTempo.metricModulation.rightDots); } -T_END +T_END; + +// The metronome-note form -- note figures joined by a relation symbol -- round-trips as a +// NoteRelation, including beams, ties, tuplets, arrows, and the two-sided relation. +TEST(roundTripNoteRelation, MetronomeApi) +{ + NoteRelation nr; + nr.arrows = true; + + MetronomeNoteData left; + left.metronomeType = DurationName::eighth; + MetronomeBeam beam; + beam.value = Beam::begin; + beam.number = 1; + left.beams.push_back(beam); + left.tie = MetronomeTieType::start; + nr.notes.push_back(left); + + MetronomeRelation rel; + rel.symbol = "equals"; + MetronomeNoteData right; + right.metronomeType = DurationName::quarter; + MetronomeTuplet tuplet; + tuplet.actualNotes = 3; + tuplet.normalNotes = 2; + tuplet.normalType = DurationName::quarter; + tuplet.type = MetronomeTupletType::start; + tuplet.bracket = Bool::yes; + tuplet.showNumber = MetronomeShowNumber::actual; + right.tuplet = tuplet; + rel.notes.push_back(right); + nr.relation = rel; + + TempoData in; + in.choice = TempoChoice{nr}; + + const auto out = roundTripTempo(in); + + CHECK(TempoChoice::Kind::noteRelation == out.choice.kind()); + const auto outNr = out.choice.noteRelation(); + CHECK(outNr.arrows); + CHECK_EQUAL(1, static_cast(outNr.notes.size())); + CHECK(outNr.relation.has_value()); + // Strong check: the whole note-relation body must survive unchanged. + CHECK(nr == outNr); +} + +T_END; TEST(roundTripParentheses, MetronomeApi) { // must round-trip via TempoData::isParenthetical. - ScoreData score; - score.ticksPerQuarter = 100; - 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.emplace_back(); - auto &direction = staff.directions.back(); - direction.tempos.emplace_back(); - auto &tempo = direction.tempos.back(); - tempo.tempoType = TempoType::beatsPerMinute; - tempo.isParenthetical = Bool::yes; - tempo.beatsPerMinute.durationName = DurationName::quarter; - tempo.beatsPerMinute.beatsPerMinute = "100"; - - auto actualScoreData = roundTrip(score); - - const auto &actualTempo = - actualScoreData.parts.back().measures.back().staves.back().directions.back().tempos.back(); - CHECK(actualTempo.isParenthetical == Bool::yes); + BeatsPerMinute bpm; + bpm.durationName = DurationName::quarter; + bpm.beatsPerMinute = "100"; + TempoData in; + in.choice = TempoChoice{bpm}; + in.isParenthetical = Bool::yes; + + const auto out = roundTripTempo(in); + + CHECK(out.isParenthetical == Bool::yes); } -T_END +T_END; TEST(roundTripMetronomeAttributes, MetronomeApi) { - // The print-style-align, justify, print-object, color, and id attributes must - // survive a round trip via TempoData's positionData/fontData/justify/printObject/color/id. - ScoreData score; - score.ticksPerQuarter = 100; - score.parts.emplace_back(); - score.parts.back().measures.emplace_back(); - score.parts.back().measures.back().staves.emplace_back(); - score.parts.back().measures.back().staves.back().directions.emplace_back(); - auto &tempo = score.parts.back().measures.back().staves.back().directions.back().tempos.emplace_back(); - tempo.tempoType = TempoType::beatsPerMinute; - tempo.beatsPerMinute.durationName = DurationName::quarter; - tempo.beatsPerMinute.beatsPerMinute = "120"; - tempo.positionData.isDefaultYSpecified = true; - tempo.positionData.defaultY = 12.0; - tempo.positionData.horizontalAlignment = HorizontalAlignment::left; - tempo.fontData.style = FontStyle::italic; - tempo.justify = HorizontalAlignment::center; - tempo.printObject = Bool::no; - tempo.id = std::string{"tempo1"}; + // The print-style-align, justify, print-object, and id attributes must survive a + // round trip via TempoData's positionData/fontData/justify/printObject/id. + BeatsPerMinute bpm; + bpm.durationName = DurationName::quarter; + bpm.beatsPerMinute = "120"; + TempoData in; + in.choice = TempoChoice{bpm}; + in.positionData.isDefaultYSpecified = true; + in.positionData.defaultY = 12.0; + in.positionData.horizontalAlignment = HorizontalAlignment::left; + in.fontData.style = FontStyle::italic; + in.justify = HorizontalAlignment::center; + in.printObject = Bool::no; + in.id = std::string{"tempo1"}; - const auto out = roundTrip(score); + const auto out = roundTripTempo(in); - const auto &directions = out.parts.back().measures.back().staves.back().directions; - CHECK_EQUAL(1, static_cast(directions.size())); - if (directions.empty() || directions.back().tempos.empty()) - { - return; - } - const auto &outTempo = directions.back().tempos.back(); - CHECK(outTempo.positionData.isDefaultYSpecified); - CHECK(HorizontalAlignment::left == outTempo.positionData.horizontalAlignment); - CHECK(FontStyle::italic == outTempo.fontData.style); - CHECK(HorizontalAlignment::center == outTempo.justify); - CHECK(Bool::no == outTempo.printObject); - CHECK(outTempo.id.has_value()); - if (outTempo.id.has_value()) + CHECK(out.positionData.isDefaultYSpecified); + CHECK(HorizontalAlignment::left == out.positionData.horizontalAlignment); + CHECK(FontStyle::italic == out.fontData.style); + CHECK(HorizontalAlignment::center == out.justify); + CHECK(Bool::no == out.printObject); + CHECK(out.id.has_value()); + if (out.id.has_value()) { - CHECK(std::string{"tempo1"} == *outTempo.id); + CHECK(std::string{"tempo1"} == *out.id); } } -T_END +T_END; #endif diff --git a/src/private/mxtest/api/roundtrip-baseline.txt b/src/private/mxtest/api/roundtrip-baseline.txt index 8f491bf54..eea92380a 100644 --- a/src/private/mxtest/api/roundtrip-baseline.txt +++ b/src/private/mxtest/api/roundtrip-baseline.txt @@ -495,3 +495,16 @@ synthetic/wood.4.0.xml synthetic/metronome.3.0.xml synthetic/metronome.3.1.xml synthetic/metronome.4.0.xml + +# The metronome-note form (metric relationships drawn with note figures: +# metronome-note, -relation, -beam, -dot, -tied, -tuplet, -arrows) and +# beat-unit-tied now round-trip (second metronome slice of #324). +synthetic/beat-unit-tied.3.1.xml +synthetic/metronome-arrows.3.1.xml +synthetic/metronome-beam.3.0.xml +synthetic/metronome-dot.3.0.xml +synthetic/metronome-note.3.0.xml +synthetic/metronome-relation.3.0.xml +synthetic/metronome-tied.3.1.xml +synthetic/metronome-tuplet.3.0.xml +synthetic/metronome-type.3.0.xml