diff --git a/src/include/mx/api/DirectionData.h b/src/include/mx/api/DirectionData.h index 487247566..ef74f2af3 100644 --- a/src/include/mx/api/DirectionData.h +++ b/src/include/mx/api/DirectionData.h @@ -16,6 +16,7 @@ #include "mx/api/MarkData.h" #include "mx/api/OtherDirectionData.h" #include "mx/api/OttavaData.h" +#include "mx/api/PedalLineData.h" #include "mx/api/PercussionData.h" #include "mx/api/PrincipalVoiceData.h" #include "mx/api/RehearsalData.h" @@ -44,8 +45,7 @@ enum class DirectionComponentKind bracketStop, dashesStart, dashesStop, - pedalStart, - pedalStop, + pedal, words, chord, segno, @@ -135,8 +135,7 @@ struct DirectionData std::vector bracketStops; std::vector dashesStarts; std::vector dashesStops; - std::vector pedalStarts; - std::vector pedalStops; + std::vector pedals; std::vector words; std::vector chords; std::vector figuredBasses; @@ -165,8 +164,7 @@ struct DirectionData DirectionData() : tickTimePosition{0}, placement{Placement::unspecified}, voice{VALUE_UNSPECIFIED}, isStaffValueSpecified{true}, isSoundDataSpecified{false}, soundData{}, marks{}, wedgeStarts{}, wedgeStops{}, ottavaStarts{}, ottavaStops{}, - bracketStarts{}, bracketStops{}, dashesStarts{}, dashesStops{}, pedalStarts{}, pedalStops{}, words{}, - chords{}, segnos{} + bracketStarts{}, bracketStops{}, dashesStarts{}, dashesStops{}, pedals{}, words{}, chords{}, segnos{} { } }; @@ -177,10 +175,9 @@ inline bool isDirectionDataEmpty(const DirectionData &directionData) directionData.wedgeStarts.size() == 0 && directionData.wedgeStops.size() == 0 && directionData.bracketStarts.size() == 0 && directionData.bracketStops.size() == 0 && directionData.dashesStarts.size() == 0 && directionData.dashesStops.size() == 0 && - directionData.pedalStarts.size() == 0 && directionData.pedalStops.size() == 0 && - directionData.tempos.size() == 0 && directionData.ottavaStarts.size() == 0 && - directionData.ottavaStops.size() == 0 && directionData.words.size() == 0 && - directionData.segnos.size() == 0 && directionData.codas.size() == 0 && + directionData.pedals.size() == 0 && directionData.tempos.size() == 0 && + directionData.ottavaStarts.size() == 0 && directionData.ottavaStops.size() == 0 && + directionData.words.size() == 0 && directionData.segnos.size() == 0 && directionData.codas.size() == 0 && directionData.rehearsals.size() == 0 && directionData.damps.size() == 0 && directionData.dampAlls.size() == 0 && directionData.eyeglasses.size() == 0 && directionData.stringMutes.size() == 0 && directionData.staffDivides.size() == 0 && @@ -208,8 +205,7 @@ MXAPI_EQUALS_MEMBER(bracketStarts) MXAPI_EQUALS_MEMBER(bracketStops) MXAPI_EQUALS_MEMBER(dashesStarts) MXAPI_EQUALS_MEMBER(dashesStops) -MXAPI_EQUALS_MEMBER(pedalStarts) -MXAPI_EQUALS_MEMBER(pedalStops) +MXAPI_EQUALS_MEMBER(pedals) MXAPI_EQUALS_MEMBER(words) MXAPI_EQUALS_MEMBER(chords) MXAPI_EQUALS_MEMBER(figuredBasses) diff --git a/src/include/mx/api/MarkData.h b/src/include/mx/api/MarkData.h index a5489d072..b3a57b6dd 100644 --- a/src/include/mx/api/MarkData.h +++ b/src/include/mx/api/MarkData.h @@ -207,8 +207,8 @@ enum class MarkType fermataSquareInverted, // square unknownFermata, // bad state, unknown type, error - pedal, // the typical piano pedal mark - damp, // the typical piano end-pedal mark + pedal, // the typical piano pedal mark. for pedal marks with lines, see PedalLineData + damp, // the typical piano end-pedal mark. for pedal marks with lines, see PedalLineData // arpeggiate arpeggiate, diff --git a/src/include/mx/api/PedalLineData.h b/src/include/mx/api/PedalLineData.h new file mode 100644 index 000000000..53c5c6c64 --- /dev/null +++ b/src/include/mx/api/PedalLineData.h @@ -0,0 +1,69 @@ +// MusicXML Class Library +// Copyright (c) by Matthew James Briggs +// Distributed under the MIT License + +#pragma once + +#include "mx/api/ApiCommon.h" +#include "mx/api/PositionData.h" + +namespace mx +{ +namespace api +{ +// The kind of piano-pedal event, MusicXML's pedal/@type. A damper or sostenuto pedal line is +// written as a run of these events on one staff, ordered by tickTimePosition. The three +// "opening" kinds begin a line, the two "closing" kinds end one, and change/continueLine occur +// mid-line: +// +// start Begin a damper-pedal line with the usual downstroke. +// sostenuto Begin a sostenuto-pedal line (the middle piano pedal). +// resume Begin a line with no downstroke -- used to pick a line back up after a +// discontinue, or to start one that a text or symbol already introduced. +// change A pedal lift and immediate retake, drawn as an inverted "V" in the line. +// continueLine A formatting-only continuation point, used to break the line neatly across a +// system break or to shape a complex pedaling line. It changes nothing musically. +// stop End the line with an explicit lift (the upstroke at the end of the line). +// discontinue End the line with no explicit lift -- the counterpart to resume. +// +// mx::api models only pedal lines here (MusicXML pedal/@line="yes"). The alternative sign form +// (Ped. and * marks, pedal/@line="no") is expressed through MarkData as MarkType::pedal and +// MarkType::damp instead. +enum class PedalLineKind +{ + unspecified, + start, + stop, + sostenuto, + change, + continueLine, + discontinue, + resume +}; + +// One event in a piano-pedal line (see PedalKind). Put a sequence of these in +// DirectionData::pedals to describe an entire pedaling; each event stands on its own and is +// placed at its own tickTimePosition. +struct PedalLineData +{ + PedalLineKind kind; + int tickTimePosition; + PositionData positionData; + + PedalLineData() : kind{PedalLineKind::unspecified}, tickTimePosition{0}, positionData{} + { + } + + PedalLineData(PedalLineKind inKind) : kind{inKind}, tickTimePosition{0}, positionData{} + { + } +}; + +MXAPI_EQUALS_BEGIN(PedalLineData) +MXAPI_EQUALS_MEMBER(kind) +MXAPI_EQUALS_MEMBER(tickTimePosition) +MXAPI_EQUALS_MEMBER(positionData) +MXAPI_EQUALS_END; +MXAPI_NOT_EQUALS_AND_VECTORS(PedalLineData); +} // namespace api +} // namespace mx diff --git a/src/private/mx/impl/DirectionReader.cpp b/src/private/mx/impl/DirectionReader.cpp index 7f9d6cfe2..b62cc8753 100644 --- a/src/private/mx/impl/DirectionReader.cpp +++ b/src/private/mx/impl/DirectionReader.cpp @@ -703,17 +703,32 @@ void DirectionReader::parseBracket(const core::DirectionType &directionType) } } +api::PedalLineKind pedalKindForTag(core::PedalType::Tag tag) +{ + switch (tag) + { + case core::PedalType::Tag::start: + return api::PedalLineKind::start; + case core::PedalType::Tag::stop: + return api::PedalLineKind::stop; + case core::PedalType::Tag::sostenuto: + return api::PedalLineKind::sostenuto; + case core::PedalType::Tag::change: + return api::PedalLineKind::change; + case core::PedalType::Tag::continue_: + return api::PedalLineKind::continueLine; + case core::PedalType::Tag::discontinue: + return api::PedalLineKind::discontinue; + case core::PedalType::Tag::resume: + return api::PedalLineKind::resume; + } + return api::PedalLineKind::unspecified; +} + void DirectionReader::parsePedal(const core::DirectionType &directionType) { const auto &pedal = directionType.choice().asPedal(); - - // sostenuto/change/continue/discontinue/resume are unmodeled (#324) -- pedalStarts/ - // pedalStops only model the damper start/stop pair. A (etc.) is - // dropped here rather than misrepresented as a plain start or stop. - if (pedal.type().tag() != core::PedalType::Tag::start && pedal.type().tag() != core::PedalType::Tag::stop) - { - return; - } + const auto tag = pedal.type().tag(); const auto placement = (myDirection && myDirection->placement().has_value()) @@ -723,40 +738,30 @@ void DirectionReader::parsePedal(const core::DirectionType &directionType) myOutDirectionData.placement = placement; - if (pedal.line().has_value() && pedal.line()->tag() == core::YesNo::Tag::yes) - { - if (pedal.type().tag() == core::PedalType::Tag::start) - { - api::SpannerStart start; - start.tickTimePosition = myOutDirectionData.tickTimePosition; - start.positionData = getPositionData(pedal); - start.positionData.placement = placement; - start.lineData.lineType = api::LineType::solid; - start.lineData.lineHook = api::LineHook::none; - myOutDirectionData.pedalStarts.emplace_back(std::move(start)); - appendOrderedComponent(api::DirectionComponentKind::pedalStart, - static_cast(myOutDirectionData.pedalStarts.size()) - 1); - } - else if (pedal.type().tag() == core::PedalType::Tag::stop) - { - api::SpannerStop stop; - stop.tickTimePosition = myOutDirectionData.tickTimePosition; - stop.positionData = getPositionData(pedal); - stop.positionData.placement = placement; - stop.lineData.lineType = api::LineType::solid; - stop.lineData.lineHook = api::LineHook::down; - stop.lineData.isStopLengthSpecified = true; - stop.lineData.endLength = 10.0; - myOutDirectionData.pedalStops.emplace_back(std::move(stop)); - appendOrderedComponent(api::DirectionComponentKind::pedalStop, - static_cast(myOutDirectionData.pedalStops.size()) - 1); - } + // A pedal line (line="yes"). sostenuto/change/continue/discontinue/resume are inherently + // line-formatting types (the spec only defines them with line="yes"), so they are treated as + // line pedals even when the line attribute is omitted. start/stop with line != "yes" are the + // sign form (Ped./* marks) and fall through to the MarkData path below. + const bool isLine = (pedal.line().has_value() && pedal.line()->tag() == core::YesNo::Tag::yes) || + tag == core::PedalType::Tag::sostenuto || tag == core::PedalType::Tag::change || + tag == core::PedalType::Tag::continue_ || tag == core::PedalType::Tag::discontinue || + tag == core::PedalType::Tag::resume; + + if (isLine) + { + api::PedalLineData pedalData{pedalKindForTag(tag)}; + pedalData.tickTimePosition = myOutDirectionData.tickTimePosition; + pedalData.positionData = getPositionData(pedal); + pedalData.positionData.placement = placement; + myOutDirectionData.pedals.emplace_back(std::move(pedalData)); + appendOrderedComponent(api::DirectionComponentKind::pedal, + static_cast(myOutDirectionData.pedals.size()) - 1); return; } auto pedalType = api::MarkType::pedal; - if (pedal.type().tag() == core::PedalType::Tag::stop) + if (tag == core::PedalType::Tag::stop) { pedalType = api::MarkType::damp; } diff --git a/src/private/mx/impl/DirectionWriter.cpp b/src/private/mx/impl/DirectionWriter.cpp index cb8ce34b4..d295121ee 100644 --- a/src/private/mx/impl/DirectionWriter.cpp +++ b/src/private/mx/impl/DirectionWriter.cpp @@ -282,25 +282,42 @@ void DirectionWriter::emitMark(api::MarkData mark, core::Direction &direction) } } -void DirectionWriter::emitPedalStart(const api::SpannerStart &pedalStart, core::Direction &direction) +core::PedalType corePedalType(api::PedalLineKind kind) { - core::Pedal pedal{}; - pedal.setType(core::PedalType::start()); - pedal.setLine(core::YesNo::yes()); - pedal.setSign(core::YesNo::yes()); - setAttributesFromPositionData(pedalStart.positionData, pedal); - core::DirectionType dt{}; - dt.setChoice(core::DirectionTypeChoice::pedal(pedal)); - addDirectionType(std::move(dt), direction); + switch (kind) + { + case api::PedalLineKind::start: + return core::PedalType::start(); + case api::PedalLineKind::stop: + return core::PedalType::stop(); + case api::PedalLineKind::sostenuto: + return core::PedalType::sostenuto(); + case api::PedalLineKind::change: + return core::PedalType::change(); + case api::PedalLineKind::continueLine: + return core::PedalType::continue_(); + case api::PedalLineKind::discontinue: + return core::PedalType::discontinue(); + case api::PedalLineKind::resume: + return core::PedalType::resume(); + case api::PedalLineKind::unspecified: + break; + } + return core::PedalType::start(); } -void DirectionWriter::emitPedalStop(const api::SpannerStop &pedalStop, core::Direction &direction) +void DirectionWriter::emitPedal(const api::PedalLineData &item, core::Direction &direction) { + // An unspecified kind describes no pedal event; emit nothing rather than a guessed default. + if (item.kind == api::PedalLineKind::unspecified) + { + return; + } + core::Pedal pedal{}; - pedal.setType(core::PedalType::stop()); + pedal.setType(corePedalType(item.kind)); pedal.setLine(core::YesNo::yes()); - pedal.setSign(core::YesNo::yes()); - setAttributesFromPositionData(pedalStop.positionData, pedal); + setAttributesFromPositionData(item.positionData, pedal); core::DirectionType dt{}; dt.setChoice(core::DirectionTypeChoice::pedal(pedal)); addDirectionType(std::move(dt), direction); @@ -1241,14 +1258,9 @@ void DirectionWriter::emitFixedOrder(core::Direction &direction) emitMark(mark, direction); } - for (const auto &item : myDirectionData.pedalStarts) - { - emitPedalStart(item, direction); - } - - for (const auto &item : myDirectionData.pedalStops) + for (const auto &item : myDirectionData.pedals) { - emitPedalStop(item, direction); + emitPedal(item, direction); } for (const auto &item : myDirectionData.wedgeStops) @@ -1391,17 +1403,10 @@ void DirectionWriter::emitOrderedComponents(core::Direction &direction) } break; - case api::DirectionComponentKind::pedalStart: - if (i >= 0 && static_cast(i) < myDirectionData.pedalStarts.size()) - { - emitPedalStart(myDirectionData.pedalStarts.at(i), direction); - } - break; - - case api::DirectionComponentKind::pedalStop: - if (i >= 0 && static_cast(i) < myDirectionData.pedalStops.size()) + case api::DirectionComponentKind::pedal: + if (i >= 0 && static_cast(i) < myDirectionData.pedals.size()) { - emitPedalStop(myDirectionData.pedalStops.at(i), direction); + emitPedal(myDirectionData.pedals.at(i), direction); } break; diff --git a/src/private/mx/impl/DirectionWriter.h b/src/private/mx/impl/DirectionWriter.h index 0bc4c6d7b..b25279e78 100644 --- a/src/private/mx/impl/DirectionWriter.h +++ b/src/private/mx/impl/DirectionWriter.h @@ -38,8 +38,7 @@ class DirectionWriter std::vector &ioOutputSet); void emitMark(api::MarkData mark, core::Direction &direction); - void emitPedalStart(const api::SpannerStart &pedalStart, core::Direction &direction); - void emitPedalStop(const api::SpannerStop &pedalStop, core::Direction &direction); + void emitPedal(const api::PedalLineData &pedal, core::Direction &direction); void emitWedgeStop(const api::WedgeStop &wedgeStop, core::Direction &direction); void emitWedgeStart(const api::WedgeStart &wedgeStart, core::Direction &direction); void emitOttavaStop(const api::OttavaStop &ottavaStop, core::Direction &direction); diff --git a/src/private/mxtest/api/DirectionMarksRoundTripTest.cpp b/src/private/mxtest/api/DirectionMarksRoundTripTest.cpp index 01dcb2ce3..527eee59a 100644 --- a/src/private/mxtest/api/DirectionMarksRoundTripTest.cpp +++ b/src/private/mxtest/api/DirectionMarksRoundTripTest.cpp @@ -238,6 +238,44 @@ TEST(HarpPedals, DirectionMarksRoundTrip) T_END; +TEST(PedalAllKinds, DirectionMarksRoundTrip) +{ + // Every pedal-line type must survive a round-trip; before #324 only start/stop did and the + // rest (sostenuto/change/continueLine/discontinue/resume) were silently dropped. + const PedalLineKind kinds[] = { + PedalLineKind::start, PedalLineKind::stop, PedalLineKind::sostenuto, PedalLineKind::change, + PedalLineKind::continueLine, PedalLineKind::discontinue, PedalLineKind::resume, + }; + + for (const auto kind : kinds) + { + DirectionData direction; + direction.pedals.emplace_back(kind); + const auto directions = roundTripDirectionData(direction); + REQUIRE(directions.size() == 1); + REQUIRE(directions.front().pedals.size() == 1); + CHECK(directions.front().pedals.front().kind == kind); + } +} + +T_END; + +TEST(PedalPlacement, DirectionMarksRoundTrip) +{ + DirectionData direction; + direction.placement = Placement::below; + PedalLineData pedal{PedalLineKind::start}; + pedal.positionData.placement = Placement::below; + direction.pedals.push_back(pedal); + const auto directions = roundTripDirectionData(direction); + REQUIRE(directions.size() == 1); + REQUIRE(directions.front().pedals.size() == 1); + CHECK(directions.front().pedals.front().kind == PedalLineKind::start); + CHECK(directions.front().placement == Placement::below); +} + +T_END; + TEST(Scordatura, DirectionMarksRoundTrip) { DirectionData direction;