diff --git a/CMakeLists.txt b/CMakeLists.txt index 4262dd6ee..e7a0a9736 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,9 +14,17 @@ set(LIBRARY_OUTPUT_PATH ${CMAKE_BINARY_DIR}) # Emscripten disables exception catching by default; mx's internal MX_THROW # (Throw.h) needs it, so DocumentManager can actually catch and convert it. +# +# Emscripten's default wasm stack is 64 KiB. mx's write path is a deep, +# unoptimized (Debug) C++ call chain -- DocumentManager -> ScoreWriter -> +# PartWriter -> MeasureWriter -> NoteWriter -> NotationsWriter, several of +# which carry their own sizable locals -- and it overflows that default with +# only a little headroom to spare (issue #389 hit this by adding two more +# spanner-writing loops). 1 MiB was measured to leave a comfortable margin +# for further growth in that same call chain. if(EMSCRIPTEN) add_compile_options(-fexceptions) - add_link_options(-fexceptions) + add_link_options(-fexceptions -sSTACK_SIZE=1048576) endif() option(MX_CORE_DEV "Build the core roundtrip (corert) test binary against the regenerated mx/core." OFF) diff --git a/src/include/mx/api/GlissandoData.h b/src/include/mx/api/GlissandoData.h new file mode 100644 index 000000000..c6d387b68 --- /dev/null +++ b/src/include/mx/api/GlissandoData.h @@ -0,0 +1,104 @@ +// MusicXML Class Library +// Copyright (c) by Matthew James Briggs +// Distributed under the MIT License + +#pragma once + +#include "mx/api/ApiCommon.h" +#include "mx/api/Id.h" +#include "mx/api/LineData.h" +#include "mx/api/PositionData.h" +#include "mx/api/PrintData.h" +#include "mx/api/SpannerNumber.h" + +#include +#include + +namespace mx +{ +namespace api +{ +// A glissando and a slide are notated the same way and carry the same attributes; they differ +// only in the pitch motion implied between the two notes they connect. A glissando sounds the +// discrete pitches in between (its line defaults to wavy); a slide is a continuous portamento +// (its line defaults to solid). +enum class GlissandoType +{ + unspecified, + glissando, + slide +}; + +struct GlissandoStart +{ + GlissandoType glissandoType; + SpannerNumber number; + + // Text printed alongside the line, e.g. "gliss." -- the / element value. + std::string text; + + PositionData positionData; + PrintData printData; // font and color + LineData lineData; // line-type, dash-length, space-length + + // Playback timing (MusicXML's trill-sound attributes). These exist on only -- a + // glissando has no accelerate/beats/first-beat/last-beat -- and are left at their defaults + // when glissandoType is glissando. + Bool accelerate; + std::optional beats; + std::optional firstBeat; + std::optional lastBeat; + + // The id attribute of the element this spanner end is written as -- or + // (see Id.h). + std::optional id; + + GlissandoStart(GlissandoType inGlissandoType) + : glissandoType{inGlissandoType}, number{}, text{}, positionData{}, printData{}, lineData{}, + accelerate{Bool::unspecified}, beats{std::nullopt}, firstBeat{std::nullopt}, lastBeat{std::nullopt}, id{} + { + } +}; + +struct GlissandoStop +{ + GlissandoType glissandoType; + SpannerNumber number; + PositionData positionData; + LineData lineData; + + // The id attribute of the element this spanner end is written as -- or + // (see Id.h). + std::optional id; + + GlissandoStop(GlissandoType inGlissandoType) + : glissandoType{inGlissandoType}, number{}, positionData{}, lineData{}, id{} + { + } +}; + +MXAPI_EQUALS_BEGIN(GlissandoStart) +MXAPI_EQUALS_MEMBER(glissandoType) +MXAPI_EQUALS_MEMBER(number) +MXAPI_EQUALS_MEMBER(text) +MXAPI_EQUALS_MEMBER(positionData) +MXAPI_EQUALS_MEMBER(printData) +MXAPI_EQUALS_MEMBER(lineData) +MXAPI_EQUALS_MEMBER(accelerate) +MXAPI_EQUALS_MEMBER(beats) +MXAPI_EQUALS_MEMBER(firstBeat) +MXAPI_EQUALS_MEMBER(lastBeat) +MXAPI_EQUALS_MEMBER(id) +MXAPI_EQUALS_END; +MXAPI_NOT_EQUALS_AND_VECTORS(GlissandoStart); + +MXAPI_EQUALS_BEGIN(GlissandoStop) +MXAPI_EQUALS_MEMBER(glissandoType) +MXAPI_EQUALS_MEMBER(number) +MXAPI_EQUALS_MEMBER(positionData) +MXAPI_EQUALS_MEMBER(lineData) +MXAPI_EQUALS_MEMBER(id) +MXAPI_EQUALS_END; +MXAPI_NOT_EQUALS_AND_VECTORS(GlissandoStop); +} // namespace api +} // namespace mx diff --git a/src/include/mx/api/MarkData.h b/src/include/mx/api/MarkData.h index bff6c117b..938b68700 100644 --- a/src/include/mx/api/MarkData.h +++ b/src/include/mx/api/MarkData.h @@ -60,7 +60,6 @@ enum class MarkType invertedVerticalTurn, haydn, shake, - wavyLine, mordent, invertedMordent, schleifer, diff --git a/src/include/mx/api/NoteAttachmentData.h b/src/include/mx/api/NoteAttachmentData.h index a09dac921..df864c3d3 100644 --- a/src/include/mx/api/NoteAttachmentData.h +++ b/src/include/mx/api/NoteAttachmentData.h @@ -5,8 +5,10 @@ #pragma once #include "mx/api/CurveData.h" +#include "mx/api/GlissandoData.h" #include "mx/api/MarkData.h" #include "mx/api/TupletData.h" +#include "mx/api/WavyLineData.h" namespace mx { @@ -21,8 +23,11 @@ class NoteAttachmentData std::vector curveStops; std::vector tupletStarts; std::vector tupletStops; - // std::vector spannerStarts; - // std::vector spannerStops; + std::vector glissandoStarts; + std::vector glissandoStops; + std::vector wavyLineStarts; + std::vector wavyLineContinuations; + std::vector wavyLineStops; }; MXAPI_EQUALS_BEGIN(NoteAttachmentData) @@ -32,8 +37,11 @@ MXAPI_EQUALS_MEMBER(curveContinuations) MXAPI_EQUALS_MEMBER(curveStops) MXAPI_EQUALS_MEMBER(tupletStarts) MXAPI_EQUALS_MEMBER(tupletStops) -// MXAPI_EQUALS_MEMBER( spannerStarts ) -// MXAPI_EQUALS_MEMBER( spannerStops ) +MXAPI_EQUALS_MEMBER(glissandoStarts) +MXAPI_EQUALS_MEMBER(glissandoStops) +MXAPI_EQUALS_MEMBER(wavyLineStarts) +MXAPI_EQUALS_MEMBER(wavyLineContinuations) +MXAPI_EQUALS_MEMBER(wavyLineStops) MXAPI_EQUALS_END; MXAPI_NOT_EQUALS_AND_VECTORS(NoteAttachmentData); } // namespace api diff --git a/src/include/mx/api/WavyLineData.h b/src/include/mx/api/WavyLineData.h new file mode 100644 index 000000000..bef25c17a --- /dev/null +++ b/src/include/mx/api/WavyLineData.h @@ -0,0 +1,134 @@ +// MusicXML Class Library +// Copyright (c) by Matthew James Briggs +// Distributed under the MIT License + +#pragma once + +#include "mx/api/ApiCommon.h" +#include "mx/api/ColorData.h" +#include "mx/api/PositionData.h" +#include "mx/api/SpannerNumber.h" + +#include +#include + +namespace mx +{ +namespace api +{ +// Which of the two notes flanking a trill or vibrato the wavy line's playback starts on. +enum class WavyLineStartNote +{ + unspecified, + upper, + main, + below +}; + +// The written pitch step of the auxiliary (upper) note the trill alternates with. +enum class WavyLineTrillStep +{ + unspecified, + whole, + half, + unison +}; + +// The interval of a trill's closing turn, or that it has none. +enum class WavyLineTwoNoteTurn +{ + unspecified, + whole, + half, + none +}; + +// A wavy line indicates a trill extension or a vibrato line. Unlike glissando and slide it can +// have a continue point (for a line that crosses a system or page break), so it is a +// start/continue/stop spanner rather than a start/stop one. It lives inside a note's . +struct WavyLineStart +{ + SpannerNumber number; + PositionData positionData; // placement rides in positionData.placement + bool isColorSpecified; + ColorData colorData; + + // A specific SMuFL glyph from the Multi-segment lines range (e.g. "wiggleTrill"), overriding + // the default trill/vibrato glyph. + std::optional smufl; + + // Playback (MusicXML's trill-sound attributes). + WavyLineStartNote startNote; + WavyLineTrillStep trillStep; + WavyLineTwoNoteTurn twoNoteTurn; + Bool accelerate; + std::optional beats; + std::optional secondBeat; + std::optional lastBeat; + + WavyLineStart() + : number{}, positionData{}, isColorSpecified{false}, colorData{}, smufl{std::nullopt}, + startNote{WavyLineStartNote::unspecified}, trillStep{WavyLineTrillStep::unspecified}, + twoNoteTurn{WavyLineTwoNoteTurn::unspecified}, accelerate{Bool::unspecified}, beats{std::nullopt}, + secondBeat{std::nullopt}, lastBeat{std::nullopt} + { + } +}; + +struct WavyLineContinue +{ + SpannerNumber number; + PositionData positionData; + bool isColorSpecified; + ColorData colorData; + + WavyLineContinue() : number{}, positionData{}, isColorSpecified{false}, colorData{} + { + } +}; + +struct WavyLineStop +{ + SpannerNumber number; + PositionData positionData; + bool isColorSpecified; + ColorData colorData; + + WavyLineStop() : number{}, positionData{}, isColorSpecified{false}, colorData{} + { + } +}; + +MXAPI_EQUALS_BEGIN(WavyLineStart) +MXAPI_EQUALS_MEMBER(number) +MXAPI_EQUALS_MEMBER(positionData) +MXAPI_EQUALS_MEMBER(isColorSpecified) +MXAPI_EQUALS_MEMBER(colorData) +MXAPI_EQUALS_MEMBER(smufl) +MXAPI_EQUALS_MEMBER(startNote) +MXAPI_EQUALS_MEMBER(trillStep) +MXAPI_EQUALS_MEMBER(twoNoteTurn) +MXAPI_EQUALS_MEMBER(accelerate) +MXAPI_EQUALS_MEMBER(beats) +MXAPI_EQUALS_MEMBER(secondBeat) +MXAPI_EQUALS_MEMBER(lastBeat) +MXAPI_EQUALS_END; +MXAPI_NOT_EQUALS_AND_VECTORS(WavyLineStart); + +MXAPI_EQUALS_BEGIN(WavyLineContinue) +MXAPI_EQUALS_MEMBER(number) +MXAPI_EQUALS_MEMBER(positionData) +MXAPI_EQUALS_MEMBER(isColorSpecified) +MXAPI_EQUALS_MEMBER(colorData) +MXAPI_EQUALS_END; +MXAPI_NOT_EQUALS_AND_VECTORS(WavyLineContinue); + +MXAPI_EQUALS_BEGIN(WavyLineStop) +MXAPI_EQUALS_MEMBER(number) +MXAPI_EQUALS_MEMBER(positionData) +MXAPI_EQUALS_MEMBER(isColorSpecified) +MXAPI_EQUALS_MEMBER(colorData) +MXAPI_EQUALS_END; +MXAPI_NOT_EQUALS_AND_VECTORS(WavyLineStop); +} // namespace api +} // namespace mx diff --git a/src/private/mx/api/MarkData.cpp b/src/private/mx/api/MarkData.cpp index 03bdda73f..cd4a7a4d0 100644 --- a/src/private/mx/api/MarkData.cpp +++ b/src/private/mx/api/MarkData.cpp @@ -41,15 +41,15 @@ bool isMarkOrnament(MarkType markType) return (markType == MarkType::trillMark) || (markType == MarkType::turn) || (markType == MarkType::delayedTurn) || (markType == MarkType::invertedTurn) || (markType == MarkType::delayedInvertedTurn) || (markType == MarkType::verticalTurn) || (markType == MarkType::invertedVerticalTurn) || - (markType == MarkType::haydn) || (markType == MarkType::shake) || (markType == MarkType::wavyLine) || - (markType == MarkType::mordent) || (markType == MarkType::invertedMordent) || - (markType == MarkType::schleifer) || (markType == MarkType::tremoloSingleOne) || - (markType == MarkType::tremoloSingleTwo) || (markType == MarkType::tremoloSingleThree) || - (markType == MarkType::tremoloSingleFour) || (markType == MarkType::tremoloSingleFive) || - (markType == MarkType::tremoloSingleSix) || (markType == MarkType::tremoloSingleSeven) || - (markType == MarkType::tremoloSingleEight) || (markType == MarkType::tremoloStart) || - (markType == MarkType::tremoloStop) || (markType == MarkType::tremoloUnmeasured) || - (markType == MarkType::otherOrnament) || (markType == MarkType::unknownOrnament); + (markType == MarkType::haydn) || (markType == MarkType::shake) || (markType == MarkType::mordent) || + (markType == MarkType::invertedMordent) || (markType == MarkType::schleifer) || + (markType == MarkType::tremoloSingleOne) || (markType == MarkType::tremoloSingleTwo) || + (markType == MarkType::tremoloSingleThree) || (markType == MarkType::tremoloSingleFour) || + (markType == MarkType::tremoloSingleFive) || (markType == MarkType::tremoloSingleSix) || + (markType == MarkType::tremoloSingleSeven) || (markType == MarkType::tremoloSingleEight) || + (markType == MarkType::tremoloStart) || (markType == MarkType::tremoloStop) || + (markType == MarkType::tremoloUnmeasured) || (markType == MarkType::otherOrnament) || + (markType == MarkType::unknownOrnament); } bool isMarkFermata(MarkType markType) diff --git a/src/private/mx/impl/Converter.cpp b/src/private/mx/impl/Converter.cpp index f6e847fb4..eea1b0af7 100644 --- a/src/private/mx/impl/Converter.cpp +++ b/src/private/mx/impl/Converter.cpp @@ -230,7 +230,6 @@ const Converter::EnumMap Conver {core::OrnamentsGroupChoice::Kind::invertedVerticalTurn, api::MarkType::invertedVerticalTurn}, {core::OrnamentsGroupChoice::Kind::haydn, api::MarkType::haydn}, {core::OrnamentsGroupChoice::Kind::shake, api::MarkType::shake}, - {core::OrnamentsGroupChoice::Kind::wavyLine, api::MarkType::wavyLine}, {core::OrnamentsGroupChoice::Kind::mordent, api::MarkType::mordent}, {core::OrnamentsGroupChoice::Kind::invertedMordent, api::MarkType::invertedMordent}, {core::OrnamentsGroupChoice::Kind::schleifer, api::MarkType::schleifer}, diff --git a/src/private/mx/impl/GlissandoFunctions.h b/src/private/mx/impl/GlissandoFunctions.h new file mode 100644 index 000000000..50936db94 --- /dev/null +++ b/src/private/mx/impl/GlissandoFunctions.h @@ -0,0 +1,166 @@ +// MusicXML Class Library +// Copyright (c) by Matthew James Briggs +// Distributed under the MIT License + +#pragma once + +#include "mx/api/GlissandoData.h" +#include "mx/api/NoteAttachmentData.h" +#include "mx/core/Decimal.h" +#include "mx/core/generated/Glissando.h" +#include "mx/core/generated/NumberLevel.h" +#include "mx/core/generated/Percent.h" +#include "mx/core/generated/Slide.h" +#include "mx/core/generated/StartStop.h" +#include "mx/core/generated/TrillBeats.h" +#include "mx/impl/Converter.h" +#include "mx/impl/IdFunctions.h" +#include "mx/impl/LineFunctions.h" +#include "mx/impl/PositionFunctions.h" +#include "mx/impl/PrintFunctions.h" +#include "mx/impl/SpannerFunctions.h" + +#include +#include + +namespace mx +{ +namespace impl +{ +// and are distinct core types with an identical shared attribute set; deduce +// which one inElement is the same way curveTypeOf<> distinguishes slur from tie in +// CurveFunctions.h. +template constexpr api::GlissandoType glissandoTypeOf() +{ + return std::is_same_v, core::Glissando> ? api::GlissandoType::glissando + : api::GlissandoType::slide; +} + +template +api::GlissandoStart parseGlissandoStart(const GLISSANDO_OR_SLIDE_TYPE &inElement) +{ + api::GlissandoStart start{glissandoTypeOf()}; + + if (inElement.number().has_value()) + { + start.number = api::SpannerNumber(checkNumber(&inElement)); + } + + start.text = inElement.value(); + start.positionData = getPositionData(inElement); + start.printData = getPrintData(inElement); + start.lineData = getLineData(inElement); + start.id = getId(inElement); + + // Playback (trill-sound) attributes exist on only. + if constexpr (std::is_same_v, core::Slide>) + { + if (inElement.accelerate().has_value()) + { + Converter converter; + start.accelerate = converter.convert(*inElement.accelerate()); + } + if (inElement.beats().has_value()) + { + start.beats = static_cast(inElement.beats()->value().value()); + } + if (inElement.firstBeat().has_value()) + { + start.firstBeat = static_cast(inElement.firstBeat()->value().value()); + } + if (inElement.lastBeat().has_value()) + { + start.lastBeat = static_cast(inElement.lastBeat()->value().value()); + } + } + + return start; +} + +template +api::GlissandoStop parseGlissandoStop(const GLISSANDO_OR_SLIDE_TYPE &inElement) +{ + api::GlissandoStop stop{glissandoTypeOf()}; + + if (inElement.number().has_value()) + { + stop.number = api::SpannerNumber(checkNumber(&inElement)); + } + + stop.positionData = getPositionData(inElement); + stop.lineData = getLineData(inElement); + stop.id = getId(inElement); + return stop; +} + +// Reads a or element into whichever start/stop vector on outAttachments +// matches its type attribute. +template +void parseGlissandoOrSlide(const GLISSANDO_OR_SLIDE_TYPE &inElement, api::NoteAttachmentData &outAttachments) +{ + if (core::StartStop::start() == inElement.type()) + { + outAttachments.glissandoStarts.emplace_back(parseGlissandoStart(inElement)); + } + else + { + outAttachments.glissandoStops.emplace_back(parseGlissandoStop(inElement)); + } +} + +// The number written is the one the SpannerNumberResolver pass resolved for the object -- mirrors +// writeAttributesFromCurveStart/Stop in CurveFunctions.h. +template +void writeAttributesFromGlissandoStart(const api::GlissandoStart &inStart, GLISSANDO_OR_SLIDE_TYPE &outElement, + const std::optional &inResolvedNumber) +{ + outElement.setType(core::StartStop::start()); + setId(inStart.id, outElement); + setAttributesFromPositionData(inStart.positionData, outElement); + setAttributesFromPrintData(inStart.printData, outElement); + setAttributesFromLineData(inStart.lineData, outElement); + outElement.setValue(inStart.text); + + if (inResolvedNumber.has_value()) + { + outElement.setNumber(core::NumberLevel{*inResolvedNumber}); + } + + if constexpr (std::is_same_v, core::Slide>) + { + if (inStart.accelerate != api::Bool::unspecified) + { + Converter converter; + outElement.setAccelerate(converter.convert(inStart.accelerate)); + } + if (inStart.beats.has_value()) + { + outElement.setBeats(core::TrillBeats{core::Decimal{*inStart.beats}}); + } + if (inStart.firstBeat.has_value()) + { + outElement.setFirstBeat(core::Percent{core::Decimal{*inStart.firstBeat}}); + } + if (inStart.lastBeat.has_value()) + { + outElement.setLastBeat(core::Percent{core::Decimal{*inStart.lastBeat}}); + } + } +} + +template +void writeAttributesFromGlissandoStop(const api::GlissandoStop &inStop, GLISSANDO_OR_SLIDE_TYPE &outElement, + const std::optional &inResolvedNumber) +{ + outElement.setType(core::StartStop::stop()); + setId(inStop.id, outElement); + setAttributesFromPositionData(inStop.positionData, outElement); + setAttributesFromLineData(inStop.lineData, outElement); + + if (inResolvedNumber.has_value()) + { + outElement.setNumber(core::NumberLevel{*inResolvedNumber}); + } +} +} // namespace impl +} // namespace mx diff --git a/src/private/mx/impl/NotationsWriter.cpp b/src/private/mx/impl/NotationsWriter.cpp index 2e97934d9..bd0026a01 100644 --- a/src/private/mx/impl/NotationsWriter.cpp +++ b/src/private/mx/impl/NotationsWriter.cpp @@ -62,10 +62,12 @@ #include "mx/impl/Converter.h" #include "mx/impl/CurveFunctions.h" #include "mx/impl/DynamicsWriter.h" +#include "mx/impl/GlissandoFunctions.h" #include "mx/impl/IdFunctions.h" #include "mx/impl/MarkDataFunctions.h" #include "mx/impl/PositionFunctions.h" #include "mx/impl/ScoreWriter.h" +#include "mx/impl/WavyLineFunctions.h" #include @@ -181,6 +183,8 @@ core::Notations NotationsWriter::getNotations() const outNotations.addChoice(core::NotationsChoice::tied(tied)); } + addGlissandoAndSlide(outNotations); + for (const auto &tupletStop : myNoteData.noteAttachmentData.tupletStops) { core::Tuplet tuplet; @@ -259,6 +263,12 @@ core::Notations NotationsWriter::getNotations() const outNotations.addChoice(core::NotationsChoice::tuplet(tuplet)); } + // Wavy lines live inside , alongside trill-mark/shake/etc. Stops and continues are + // emitted before the mark-derived ornaments below (stop-before-start, as with glissando/slide + // above); starts are emitted after, matching the common + // shape real files use. + addWavyLineStopsAndContinues(ornaments); + for (const auto &mark : myNoteData.noteAttachmentData.marks) { if (isMarkArticulation(mark.markType) || isMarkCustom(mark.markType)) @@ -433,6 +443,8 @@ core::Notations NotationsWriter::getNotations() const } } + addWavyLineStarts(ornaments); + if (!articulations.choice().empty()) { outNotations.addChoice(core::NotationsChoice::articulations(articulations)); @@ -451,6 +463,81 @@ core::Notations NotationsWriter::getNotations() const return outNotations; } +void NotationsWriter::addGlissandoAndSlide(core::Notations &outNotations) const +{ + const auto &numberResolver = myScoreWriter.getSpannerNumberResolver(); + + // Glissando and slide are top-level children, like slur/tie. Stops are emitted + // before starts so a chain of glissandi on one note keeps score order (see #139). + for (const auto &glissandoStop : myNoteData.noteAttachmentData.glissandoStops) + { + const auto resolvedNumber = numberResolver.emittedNumber(glissandoStop.number, &glissandoStop); + if (glissandoStop.glissandoType == api::GlissandoType::slide) + { + core::Slide slide; + writeAttributesFromGlissandoStop(glissandoStop, slide, resolvedNumber); + outNotations.addChoice(core::NotationsChoice::slide(slide)); + } + else + { + core::Glissando glissando; + writeAttributesFromGlissandoStop(glissandoStop, glissando, resolvedNumber); + outNotations.addChoice(core::NotationsChoice::glissando(glissando)); + } + } + + for (const auto &glissandoStart : myNoteData.noteAttachmentData.glissandoStarts) + { + const auto resolvedNumber = numberResolver.emittedNumber(glissandoStart.number, &glissandoStart); + if (glissandoStart.glissandoType == api::GlissandoType::slide) + { + core::Slide slide; + writeAttributesFromGlissandoStart(glissandoStart, slide, resolvedNumber); + outNotations.addChoice(core::NotationsChoice::slide(slide)); + } + else + { + core::Glissando glissando; + writeAttributesFromGlissandoStart(glissandoStart, glissando, resolvedNumber); + outNotations.addChoice(core::NotationsChoice::glissando(glissando)); + } + } +} + +void NotationsWriter::addWavyLineStopsAndContinues(core::Ornaments &outOrnaments) const +{ + const auto &numberResolver = myScoreWriter.getSpannerNumberResolver(); + + for (const auto &wavyLineStop : myNoteData.noteAttachmentData.wavyLineStops) + { + const auto resolvedNumber = numberResolver.emittedNumber(wavyLineStop.number, &wavyLineStop); + core::OrnamentsGroup group; + group.setChoice(core::OrnamentsGroupChoice::wavyLine(writeWavyLineStop(wavyLineStop, resolvedNumber))); + outOrnaments.addGroup(group); + } + + for (const auto &wavyLineContinue : myNoteData.noteAttachmentData.wavyLineContinuations) + { + const auto resolvedNumber = numberResolver.emittedNumber(wavyLineContinue.number, &wavyLineContinue); + core::OrnamentsGroup group; + group.setChoice(core::OrnamentsGroupChoice::wavyLine(writeWavyLineContinue(wavyLineContinue, resolvedNumber))); + outOrnaments.addGroup(group); + } +} + +void NotationsWriter::addWavyLineStarts(core::Ornaments &outOrnaments) const +{ + const auto &numberResolver = myScoreWriter.getSpannerNumberResolver(); + + for (const auto &wavyLineStart : myNoteData.noteAttachmentData.wavyLineStarts) + { + const auto resolvedNumber = numberResolver.emittedNumber(wavyLineStart.number, &wavyLineStart); + core::OrnamentsGroup group; + group.setChoice(core::OrnamentsGroupChoice::wavyLine(writeWavyLineStart(wavyLineStart, resolvedNumber))); + outOrnaments.addGroup(group); + } +} + core::NotationsChoice NotationsWriter::makeArticulationsNotationsChoice() const { return core::NotationsChoice::articulations(core::Articulations{}); @@ -689,12 +776,6 @@ void NotationsWriter::addOrnament(const api::MarkData &mark, core::Ornaments &ou group.setChoice(core::OrnamentsGroupChoice::shake(ets)); break; } - case core::OrnamentsGroupChoice::Kind::wavyLine: { - core::WavyLine wl; - setAttributesFromPositionData(mark.positionData, wl); - group.setChoice(core::OrnamentsGroupChoice::wavyLine(wl)); - break; - } case core::OrnamentsGroupChoice::Kind::mordent: { core::Mordent m; setAttributesFromPositionData(mark.positionData, m); diff --git a/src/private/mx/impl/NotationsWriter.h b/src/private/mx/impl/NotationsWriter.h index e930f4bc5..595bd3827 100644 --- a/src/private/mx/impl/NotationsWriter.h +++ b/src/private/mx/impl/NotationsWriter.h @@ -37,6 +37,15 @@ class NotationsWriter void addArticulation(const api::MarkData &markData, core::Articulations &outArticulations) const; void addOrnament(const api::MarkData &markData, core::Ornaments &outOrnaments) const; void addTechnical(const api::MarkData &markData, core::Technical &outTechnical) const; + + // Kept as their own (non-inlined) methods rather than loops inlined in getNotations(), the + // same reason addArticulation/addOrnament/addTechnical are separate: getNotations() is + // already a very large function, and folding more per-branch locals directly into its own + // stack frame doesn't scale -- it overflowed the default wasm stack (issue found while + // fixing #389's Emscripten smoke build). + void addGlissandoAndSlide(core::Notations &outNotations) const; + void addWavyLineStopsAndContinues(core::Ornaments &outOrnaments) const; + void addWavyLineStarts(core::Ornaments &outOrnaments) const; }; } // namespace impl } // namespace mx diff --git a/src/private/mx/impl/NoteFunctions.cpp b/src/private/mx/impl/NoteFunctions.cpp index 28b95c9f2..c98fa382f 100644 --- a/src/private/mx/impl/NoteFunctions.cpp +++ b/src/private/mx/impl/NoteFunctions.cpp @@ -11,6 +11,7 @@ #include "mx/impl/CurveFunctions.h" #include "mx/impl/DynamicsReader.h" #include "mx/impl/FermataFunctions.h" +#include "mx/impl/GlissandoFunctions.h" #include "mx/impl/IdFunctions.h" #include "mx/impl/MarkDataFunctions.h" #include "mx/impl/NonArpeggiateFunctions.h" @@ -18,7 +19,6 @@ #include "mx/impl/OrnamentsFunctions.h" #include "mx/impl/PositionFunctions.h" #include "mx/impl/PrintFunctions.h" -#include "mx/impl/SlideFunctions.h" #include "mx/impl/TechnicalFunctions.h" #include "mx/impl/TimeReader.h" #include "mx/impl/TupletReader.h" @@ -249,18 +249,16 @@ void NoteFunctions::parseNotations() const break; } case core::NotationsChoice::Kind::slide: { - // TODO - import slide - // SlideFunctions funcs{ notationsChoice.asSlide(), myCursor }; - // myOutNoteData.noteAttachmentData.marks.emplace_back( funcs.parseSlide() ); + parseGlissandoOrSlide(notationsChoice.asSlide(), myOutNoteData.noteAttachmentData); break; } case core::NotationsChoice::Kind::glissando: { + parseGlissandoOrSlide(notationsChoice.asGlissando(), myOutNoteData.noteAttachmentData); break; } case core::NotationsChoice::Kind::ornaments: { - // TODO - some ornaments should be treated as spanners instead of marks OrnamentsFunctions funcs{notationsChoice.asOrnaments(), myCursor}; - funcs.parseOrnaments(myOutNoteData.noteAttachmentData.marks); + funcs.parseOrnaments(myOutNoteData.noteAttachmentData); break; } case core::NotationsChoice::Kind::technical: { diff --git a/src/private/mx/impl/OrnamentsFunctions.cpp b/src/private/mx/impl/OrnamentsFunctions.cpp index 8cb77abb9..1579188cc 100644 --- a/src/private/mx/impl/OrnamentsFunctions.cpp +++ b/src/private/mx/impl/OrnamentsFunctions.cpp @@ -11,6 +11,7 @@ #include "mx/core/generated/TremoloType.h" #include "mx/impl/AccidentalMarkFunctions.h" #include "mx/impl/MarkDataFunctions.h" +#include "mx/impl/WavyLineFunctions.h" namespace mx { @@ -44,17 +45,26 @@ OrnamentsFunctions::OrnamentsFunctions(const core::Ornaments &inOrnaments, impl: { } -void OrnamentsFunctions::parseOrnaments(std::vector &outMarks) const +void OrnamentsFunctions::parseOrnaments(api::NoteAttachmentData &outAttachments) const { - parseOrnamentsSet(outMarks); - parseAccidentalMarkSet(outMarks); + parseOrnamentsSet(outAttachments); + parseAccidentalMarkSet(outAttachments.marks); } -void OrnamentsFunctions::parseOrnamentsSet(std::vector &outMarks) const +void OrnamentsFunctions::parseOrnamentsSet(api::NoteAttachmentData &outAttachments) const { for (const auto &group : myOrnaments.group()) { const auto &choiceObj = group.choice(); + + // A wavy line is a start/continue/stop spanner, not a mark -- it goes to its own vectors + // rather than through the markType-based path below. + if (choiceObj.kind() == core::OrnamentsGroupChoice::Kind::wavyLine) + { + parseWavyLine(choiceObj.asWavyLine(), outAttachments); + continue; + } + Converter converter; const auto markType = converter.convertOrnament(choiceObj.kind()); auto markData = api::MarkData{}; @@ -64,7 +74,7 @@ void OrnamentsFunctions::parseOrnamentsSet(std::vector &outMarks) if (markData.markType != api::MarkType::unknownOrnament) { - outMarks.emplace_back(std::move(markData)); + outAttachments.marks.emplace_back(std::move(markData)); } } } @@ -125,11 +135,6 @@ void OrnamentsFunctions::parseOrnament(const core::OrnamentsGroupChoice &choiceO parseMarkDataAttributes(choiceObj.asShake(), outMark); break; } - case core::OrnamentsGroupChoice::Kind::wavyLine: { - outMark.name = "wavy-line"; - parseMarkDataAttributes(choiceObj.asWavyLine(), outMark); - break; - } case core::OrnamentsGroupChoice::Kind::mordent: { outMark.name = "mordent"; const auto &m = choiceObj.asMordent(); diff --git a/src/private/mx/impl/OrnamentsFunctions.h b/src/private/mx/impl/OrnamentsFunctions.h index c1da6c3b1..acb1459e9 100644 --- a/src/private/mx/impl/OrnamentsFunctions.h +++ b/src/private/mx/impl/OrnamentsFunctions.h @@ -5,6 +5,7 @@ #pragma once #include "mx/api/MarkData.h" +#include "mx/api/NoteAttachmentData.h" #include "mx/impl/Converter.h" #include "mx/impl/Cursor.h" @@ -25,14 +26,14 @@ class OrnamentsFunctions ~OrnamentsFunctions() = default; OrnamentsFunctions(const OrnamentsFunctions &) = default; OrnamentsFunctions(OrnamentsFunctions &&) = default; - void parseOrnaments(std::vector &outMarks) const; + void parseOrnaments(api::NoteAttachmentData &outAttachments) const; private: const core::Ornaments &myOrnaments; const impl::Cursor myCursor; private: - void parseOrnamentsSet(std::vector &outMarks) const; + void parseOrnamentsSet(api::NoteAttachmentData &outAttachments) const; void parseAccidentalMarkSet(std::vector &outMarks) const; void parseOrnament(const core::OrnamentsGroupChoice &choiceObj, api::MarkData &outMark) const; }; diff --git a/src/private/mx/impl/SlideFunctions.cpp b/src/private/mx/impl/SlideFunctions.cpp deleted file mode 100644 index 6a8572f85..000000000 --- a/src/private/mx/impl/SlideFunctions.cpp +++ /dev/null @@ -1,44 +0,0 @@ -// MusicXML Class Library -// Copyright (c) by Matthew James Briggs -// Distributed under the MIT License - -#include "mx/impl/SlideFunctions.h" -#include "mx/api/SpannerData.h" -#include "mx/core/generated/Slide.h" -#include "mx/impl/MarkDataFunctions.h" - -namespace mx -{ -namespace impl -{ - -SlideFunctions::SlideFunctions(const core::Slide &inSlide, impl::Cursor inCursor) : mySlide{inSlide}, myCursor{inCursor} -{ -} - -void SlideFunctions::parseSlide(api::NoteAttachmentData & /*inNoteAttachmentData*/) const -{ - (void)mySlide; - // const auto& attr = mySlide.getAttributes(); - // const auto slideType = attr->type; - // - // api::SpannerStart spannerData; - // spannerData.spannerType = api::SpannerType::slide; - // spannerData.tickTimePosition = myCursor.tickTimePosition; - // - // if( attr->hasNumber ) - // { - // spannerData.numberLevel = attr->number.getValue(); - // } - // - // if( slideType == core::StartStop::start ) - // { - // inNoteAttachmentData.noteAttachmentData.spannerStarts.emplace_back( std::move( spannerData ) ); - // } - // else - // { - // inNoteAttachmentData.noteAttachmentData.spannerStops.emplace_back( std::move( spannerData ) ); - // } -} -} // namespace impl -} // namespace mx diff --git a/src/private/mx/impl/SlideFunctions.h b/src/private/mx/impl/SlideFunctions.h deleted file mode 100644 index 790b2c5ea..000000000 --- a/src/private/mx/impl/SlideFunctions.h +++ /dev/null @@ -1,34 +0,0 @@ -// MusicXML Class Library -// Copyright (c) by Matthew James Briggs -// Distributed under the MIT License - -#pragma once - -#include "mx/api/NoteAttachmentData.h" -#include "mx/impl/Converter.h" -#include "mx/impl/Cursor.h" - -namespace mx -{ -namespace core -{ -class Slide; -} - -namespace impl -{ -class SlideFunctions -{ - public: - SlideFunctions(const core::Slide &inSlide, impl::Cursor inCursor); - ~SlideFunctions() = default; - SlideFunctions(const SlideFunctions &) = default; - SlideFunctions(SlideFunctions &&) = default; - void parseSlide(api::NoteAttachmentData &inNoteAttachmentData) const; - - private: - const core::Slide &mySlide; - const impl::Cursor myCursor; -}; -} // namespace impl -} // namespace mx diff --git a/src/private/mx/impl/SpannerNumberResolver.cpp b/src/private/mx/impl/SpannerNumberResolver.cpp index b564a7f78..884414dd6 100644 --- a/src/private/mx/impl/SpannerNumberResolver.cpp +++ b/src/private/mx/impl/SpannerNumberResolver.cpp @@ -3,6 +3,7 @@ // Distributed under the MIT License #include "mx/impl/SpannerNumberResolver.h" +#include "mx/api/GlissandoData.h" #include "mx/utility/Throw.h" #include @@ -21,6 +22,8 @@ namespace impl // distinguishes slur from tied; the SpannerStart/Stop structs serve several // elements (octave-shift, bracket, dashes) whose number attributes are // independent of each other in MusicXML, so each gets its own pool. +// GlissandoType similarly distinguishes from , two distinct +// elements with independent number attributes. enum class SpannerNumberClass { slur, @@ -28,7 +31,10 @@ enum class SpannerNumberClass wedge, octaveShift, bracket, - dashes + dashes, + glissando, + slide, + wavyLine }; // One start/continue/stop occurrence at its position in the serialized stream. The number is @@ -80,6 +86,31 @@ class SpannerNumberEventCollector { addCurve(start.curveType, &start, start.number, true, false); } + + // NotationsWriter emits glissando/slide stops, then starts (see #139 stop-before-start). + for (const auto &stop : attachments.glissandoStops) + { + addGlissando(stop.glissandoType, &stop, stop.number, false, true); + } + for (const auto &start : attachments.glissandoStarts) + { + addGlissando(start.glissandoType, &start, start.number, true, false); + } + + // NotationsWriter emits wavy-line stops, then continues, then (after any mark-derived + // ornaments, which carry no number) starts. + for (const auto &stop : attachments.wavyLineStops) + { + add(SpannerNumberClass::wavyLine, &stop, stop.number, false, true); + } + for (const auto &wavyLineContinue : attachments.wavyLineContinuations) + { + add(SpannerNumberClass::wavyLine, &wavyLineContinue, wavyLineContinue.number, false, false); + } + for (const auto &start : attachments.wavyLineStarts) + { + add(SpannerNumberClass::wavyLine, &start, start.number, true, false); + } } // Mirrors DirectionWriter::emitDirectionTypes: one pass over the ordered direction-type @@ -156,6 +187,14 @@ class SpannerNumberEventCollector } } + void addGlissando(api::GlissandoType inGlissandoType, const void *inObject, const api::SpannerNumber &inNumber, + bool inOpens, bool inCloses) + { + const auto spannerClass = + inGlissandoType == api::GlissandoType::slide ? SpannerNumberClass::slide : SpannerNumberClass::glissando; + add(spannerClass, inObject, inNumber, inOpens, inCloses); + } + int myPosition = 0; std::map> myEvents; }; diff --git a/src/private/mx/impl/SpannerNumberResolver.h b/src/private/mx/impl/SpannerNumberResolver.h index 4df1af6f7..d5a28484b 100644 --- a/src/private/mx/impl/SpannerNumberResolver.h +++ b/src/private/mx/impl/SpannerNumberResolver.h @@ -24,19 +24,20 @@ namespace impl // concurrently-open identity spanner is never handed the same number. // // Numbers come from a pool of 1..16 per part and per spanner class (slur, -// tied, wedge, octave-shift, bracket, and dashes each have their own pool; a -// slur numbered 1 and a wedge numbered 1 do not conflict). The number-level -// documentation scopes concurrency to the part, never the staff: two spanners -// conflict exactly when they overlap in the order a streaming reader -// encounters them, even when they sit on different staves of the part. So -// resolvePart walks the part in the exact order MeasureWriter serializes it: -// measures in order, staves in order, voices ascending, notes in vector order -// (curve stops, then continues, then starts per note -- mirroring -// NotationsWriter), and each staff's directions in vector order (mirroring -// DirectionWriter's per-direction emission order). An identity spanner takes -// the lowest number that is free across its whole serialized extent -- from -// its first event to its last, whichever of start/stop comes first in the -// stream -- and releases it afterward. +// tied, wedge, octave-shift, bracket, dashes, glissando, slide, and wavy-line +// each have their own pool; a slur numbered 1 and a wedge numbered 1 do not +// conflict). The number-level documentation scopes concurrency to the part, +// never the staff: two spanners conflict exactly when they overlap in the +// order a streaming reader encounters them, even when they sit on different +// staves of the part. So resolvePart walks the part in the exact order +// MeasureWriter serializes it: measures in order, staves in order, voices +// ascending, notes in vector order (curve stops/continues/starts, then +// glissando/slide stops then starts, then wavy-line stops/continues/starts, +// per note -- mirroring NotationsWriter), and each staff's directions in +// vector order (mirroring DirectionWriter's per-direction emission order). An +// identity spanner takes the lowest number that is free across its whole +// serialized extent -- from its first event to its last, whichever of +// start/stop comes first in the stream -- and releases it afterward. // // Identity ids are scoped per part and per spanner class: events in the same // part sharing a class and id are one logical spanner, even across staves. diff --git a/src/private/mx/impl/WavyLineFunctions.cpp b/src/private/mx/impl/WavyLineFunctions.cpp new file mode 100644 index 000000000..b7173a84c --- /dev/null +++ b/src/private/mx/impl/WavyLineFunctions.cpp @@ -0,0 +1,291 @@ +// MusicXML Class Library +// Copyright (c) by Matthew James Briggs +// Distributed under the MIT License + +#include "mx/impl/WavyLineFunctions.h" +#include "mx/core/Decimal.h" +#include "mx/core/generated/NumberLevel.h" +#include "mx/core/generated/Percent.h" +#include "mx/core/generated/SmuflWavyLineGlyphName.h" +#include "mx/core/generated/StartNote.h" +#include "mx/core/generated/StartStopContinue.h" +#include "mx/core/generated/TrillBeats.h" +#include "mx/core/generated/TrillStep.h" +#include "mx/core/generated/TwoNoteTurn.h" +#include "mx/core/generated/WavyLine.h" +#include "mx/core/generated/YesNo.h" +#include "mx/impl/Converter.h" +#include "mx/impl/PositionFunctions.h" +#include "mx/impl/PrintFunctions.h" +#include "mx/impl/SpannerFunctions.h" + +namespace mx +{ +namespace impl +{ +api::WavyLineStartNote wavyLineFunctionsConvertStartNoteFromCore(core::StartNote inValue) +{ + switch (inValue.tag()) + { + case core::StartNote::Tag::upper: + return api::WavyLineStartNote::upper; + case core::StartNote::Tag::main: + return api::WavyLineStartNote::main; + case core::StartNote::Tag::below: + return api::WavyLineStartNote::below; + } + return api::WavyLineStartNote::unspecified; +} + +core::StartNote wavyLineFunctionsConvertStartNoteToCore(api::WavyLineStartNote inValue) +{ + switch (inValue) + { + case api::WavyLineStartNote::main: + return core::StartNote::main(); + case api::WavyLineStartNote::below: + return core::StartNote::below(); + case api::WavyLineStartNote::upper: + case api::WavyLineStartNote::unspecified: + default: + return core::StartNote::upper(); + } +} + +api::WavyLineTrillStep wavyLineFunctionsConvertTrillStepFromCore(core::TrillStep inValue) +{ + switch (inValue.tag()) + { + case core::TrillStep::Tag::whole: + return api::WavyLineTrillStep::whole; + case core::TrillStep::Tag::half: + return api::WavyLineTrillStep::half; + case core::TrillStep::Tag::unison: + return api::WavyLineTrillStep::unison; + } + return api::WavyLineTrillStep::unspecified; +} + +core::TrillStep wavyLineFunctionsConvertTrillStepToCore(api::WavyLineTrillStep inValue) +{ + switch (inValue) + { + case api::WavyLineTrillStep::half: + return core::TrillStep::half(); + case api::WavyLineTrillStep::unison: + return core::TrillStep::unison(); + case api::WavyLineTrillStep::whole: + case api::WavyLineTrillStep::unspecified: + default: + return core::TrillStep::whole(); + } +} + +api::WavyLineTwoNoteTurn wavyLineFunctionsConvertTwoNoteTurnFromCore(core::TwoNoteTurn inValue) +{ + switch (inValue.tag()) + { + case core::TwoNoteTurn::Tag::whole: + return api::WavyLineTwoNoteTurn::whole; + case core::TwoNoteTurn::Tag::half: + return api::WavyLineTwoNoteTurn::half; + case core::TwoNoteTurn::Tag::none: + return api::WavyLineTwoNoteTurn::none; + } + return api::WavyLineTwoNoteTurn::unspecified; +} + +core::TwoNoteTurn wavyLineFunctionsConvertTwoNoteTurnToCore(api::WavyLineTwoNoteTurn inValue) +{ + switch (inValue) + { + case api::WavyLineTwoNoteTurn::half: + return core::TwoNoteTurn::half(); + case api::WavyLineTwoNoteTurn::none: + return core::TwoNoteTurn::none(); + case api::WavyLineTwoNoteTurn::whole: + case api::WavyLineTwoNoteTurn::unspecified: + default: + return core::TwoNoteTurn::whole(); + } +} + +void parseWavyLine(const core::WavyLine &inWavyLine, api::NoteAttachmentData &outAttachments) +{ + api::SpannerNumber number; + if (inWavyLine.number().has_value()) + { + number = api::SpannerNumber(checkNumber(&inWavyLine)); + } + + const api::PositionData positionData = getPositionData(inWavyLine); + const bool isColorSpecified = checkHasColor(&inWavyLine); + api::ColorData colorData; + if (isColorSpecified) + { + colorData = getColor(inWavyLine); + } + + const auto type = inWavyLine.type(); + + if (core::StartStopContinue::start() == type) + { + api::WavyLineStart start; + start.number = number; + start.positionData = positionData; + start.isColorSpecified = isColorSpecified; + start.colorData = colorData; + + if (inWavyLine.smufl().has_value()) + { + start.smufl = inWavyLine.smufl()->toString(); + } + if (inWavyLine.startNote().has_value()) + { + start.startNote = wavyLineFunctionsConvertStartNoteFromCore(*inWavyLine.startNote()); + } + if (inWavyLine.trillStep().has_value()) + { + start.trillStep = wavyLineFunctionsConvertTrillStepFromCore(*inWavyLine.trillStep()); + } + if (inWavyLine.twoNoteTurn().has_value()) + { + start.twoNoteTurn = wavyLineFunctionsConvertTwoNoteTurnFromCore(*inWavyLine.twoNoteTurn()); + } + if (inWavyLine.accelerate().has_value()) + { + Converter converter; + start.accelerate = converter.convert(*inWavyLine.accelerate()); + } + if (inWavyLine.beats().has_value()) + { + start.beats = static_cast(inWavyLine.beats()->value().value()); + } + if (inWavyLine.secondBeat().has_value()) + { + start.secondBeat = static_cast(inWavyLine.secondBeat()->value().value()); + } + if (inWavyLine.lastBeat().has_value()) + { + start.lastBeat = static_cast(inWavyLine.lastBeat()->value().value()); + } + + outAttachments.wavyLineStarts.emplace_back(std::move(start)); + } + else if (core::StartStopContinue::continue_() == type) + { + api::WavyLineContinue continueItem; + continueItem.number = number; + continueItem.positionData = positionData; + continueItem.isColorSpecified = isColorSpecified; + continueItem.colorData = colorData; + outAttachments.wavyLineContinuations.emplace_back(std::move(continueItem)); + } + else + { + api::WavyLineStop stop; + stop.number = number; + stop.positionData = positionData; + stop.isColorSpecified = isColorSpecified; + stop.colorData = colorData; + outAttachments.wavyLineStops.emplace_back(std::move(stop)); + } +} + +core::WavyLine writeWavyLineStart(const api::WavyLineStart &inStart, const std::optional &inResolvedNumber) +{ + core::WavyLine wavyLine; + wavyLine.setType(core::StartStopContinue::start()); + + if (inResolvedNumber.has_value()) + { + wavyLine.setNumber(core::NumberLevel{*inResolvedNumber}); + } + + if (inStart.smufl.has_value()) + { + wavyLine.setSmufl(core::SmuflWavyLineGlyphName::parse(*inStart.smufl)); + } + + setAttributesFromPositionData(inStart.positionData, wavyLine); + + if (inStart.isColorSpecified) + { + setAttributesFromColorData(inStart.colorData, wavyLine); + } + + if (inStart.startNote != api::WavyLineStartNote::unspecified) + { + wavyLine.setStartNote(wavyLineFunctionsConvertStartNoteToCore(inStart.startNote)); + } + if (inStart.trillStep != api::WavyLineTrillStep::unspecified) + { + wavyLine.setTrillStep(wavyLineFunctionsConvertTrillStepToCore(inStart.trillStep)); + } + if (inStart.twoNoteTurn != api::WavyLineTwoNoteTurn::unspecified) + { + wavyLine.setTwoNoteTurn(wavyLineFunctionsConvertTwoNoteTurnToCore(inStart.twoNoteTurn)); + } + if (inStart.accelerate != api::Bool::unspecified) + { + Converter converter; + wavyLine.setAccelerate(converter.convert(inStart.accelerate)); + } + if (inStart.beats.has_value()) + { + wavyLine.setBeats(core::TrillBeats{core::Decimal{*inStart.beats}}); + } + if (inStart.secondBeat.has_value()) + { + wavyLine.setSecondBeat(core::Percent{core::Decimal{*inStart.secondBeat}}); + } + if (inStart.lastBeat.has_value()) + { + wavyLine.setLastBeat(core::Percent{core::Decimal{*inStart.lastBeat}}); + } + + return wavyLine; +} + +core::WavyLine writeWavyLineContinue(const api::WavyLineContinue &inContinue, + const std::optional &inResolvedNumber) +{ + core::WavyLine wavyLine; + wavyLine.setType(core::StartStopContinue::continue_()); + + if (inResolvedNumber.has_value()) + { + wavyLine.setNumber(core::NumberLevel{*inResolvedNumber}); + } + + setAttributesFromPositionData(inContinue.positionData, wavyLine); + + if (inContinue.isColorSpecified) + { + setAttributesFromColorData(inContinue.colorData, wavyLine); + } + + return wavyLine; +} + +core::WavyLine writeWavyLineStop(const api::WavyLineStop &inStop, const std::optional &inResolvedNumber) +{ + core::WavyLine wavyLine; + wavyLine.setType(core::StartStopContinue::stop()); + + if (inResolvedNumber.has_value()) + { + wavyLine.setNumber(core::NumberLevel{*inResolvedNumber}); + } + + setAttributesFromPositionData(inStop.positionData, wavyLine); + + if (inStop.isColorSpecified) + { + setAttributesFromColorData(inStop.colorData, wavyLine); + } + + return wavyLine; +} +} // namespace impl +} // namespace mx diff --git a/src/private/mx/impl/WavyLineFunctions.h b/src/private/mx/impl/WavyLineFunctions.h new file mode 100644 index 000000000..b20b6c0b9 --- /dev/null +++ b/src/private/mx/impl/WavyLineFunctions.h @@ -0,0 +1,31 @@ +// MusicXML Class Library +// Copyright (c) by Matthew James Briggs +// Distributed under the MIT License + +#pragma once + +#include "mx/api/NoteAttachmentData.h" +#include "mx/api/WavyLineData.h" + +#include + +namespace mx +{ +namespace core +{ +class WavyLine; +} + +namespace impl +{ +// Reads one element (a child of a note's ) into whichever +// start/continue/stop vector on outAttachments matches its type attribute. +void parseWavyLine(const core::WavyLine &inWavyLine, api::NoteAttachmentData &outAttachments); + +// The number written is the one the SpannerNumberResolver pass resolved for the object. +core::WavyLine writeWavyLineStart(const api::WavyLineStart &inStart, const std::optional &inResolvedNumber); +core::WavyLine writeWavyLineContinue(const api::WavyLineContinue &inContinue, + const std::optional &inResolvedNumber); +core::WavyLine writeWavyLineStop(const api::WavyLineStop &inStop, const std::optional &inResolvedNumber); +} // namespace impl +} // namespace mx diff --git a/src/private/mxtest/api/GlissandoApiTest.cpp b/src/private/mxtest/api/GlissandoApiTest.cpp new file mode 100644 index 000000000..190a02d1f --- /dev/null +++ b/src/private/mxtest/api/GlissandoApiTest.cpp @@ -0,0 +1,303 @@ +// MusicXML Class Library +// Copyright (c) by Matthew James Briggs +// Distributed under the MIT License + +// Coverage for issue #389: glissando and slide are note-attached start/stop spanners that read and +// write as top-level children, go through the SpannerNumber identity mechanism, and +// carry printed text, line appearance, and (for slide) playback timing. + +#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" +#include "mxtest/file/MxFileRepository.h" +#include "pugixml.hpp" + +using namespace mx::api; +using namespace mxtest; + +namespace glissandoApiTest +{ +inline NoteData makeGraceNote(Step inStep, int inOctave) +{ + NoteData note; + note.isGrace = true; + note.pitchData.step = inStep; + note.pitchData.octave = inOctave; + note.durationData.durationName = DurationName::eighth; + note.durationData.durationTimeTicks = 0; + return note; +} +} // namespace glissandoApiTest + +TEST(startStopRoundTrip, Glissando) +{ + using namespace glissandoApiTest; + ScoreData score; + score.parts.emplace_back(); + auto &part = score.parts.back(); + part.measures.emplace_back(); + auto &measure = part.measures.back(); + measure.staves.emplace_back(); + auto &staff = measure.staves.back(); + auto &voice = staff.voices[0]; + + voice.notes.push_back(makeGraceNote(Step::c, 4)); + GlissandoStart start{GlissandoType::glissando}; + start.number = SpannerNumber(1); + start.text = "gliss."; + start.lineData.lineType = LineType::dashed; + voice.notes.back().noteAttachmentData.glissandoStarts.push_back(start); + + voice.notes.push_back(makeGraceNote(Step::g, 4)); + GlissandoStop stop{GlissandoType::glissando}; + stop.number = SpannerNumber(1); + voice.notes.back().noteAttachmentData.glissandoStops.push_back(stop); + + const auto roundTripped = roundTrip(score); + REQUIRE(roundTripped.parts.size() == 1); + const auto ¬es = roundTripped.parts.at(0).measures.at(0).staves.at(0).voices.at(0).notes; + REQUIRE(notes.size() == 2); + + REQUIRE(notes.at(0).noteAttachmentData.glissandoStarts.size() == 1); + const auto &outStart = notes.at(0).noteAttachmentData.glissandoStarts.at(0); + CHECK(outStart.glissandoType == GlissandoType::glissando); + CHECK(SpannerNumber(1) == outStart.number); + CHECK_EQUAL(std::string{"gliss."}, outStart.text); + CHECK(outStart.lineData.lineType == LineType::dashed); + + REQUIRE(notes.at(1).noteAttachmentData.glissandoStops.size() == 1); + const auto &outStop = notes.at(1).noteAttachmentData.glissandoStops.at(0); + CHECK(outStop.glissandoType == GlissandoType::glissando); + CHECK(SpannerNumber(1) == outStop.number); +} + +T_END + +TEST(slidePlaybackRoundTrips, Glissando) +{ + using namespace glissandoApiTest; + ScoreData score; + score.parts.emplace_back(); + auto &part = score.parts.back(); + part.measures.emplace_back(); + auto &measure = part.measures.back(); + measure.staves.emplace_back(); + auto &staff = measure.staves.back(); + auto &voice = staff.voices[0]; + + voice.notes.push_back(makeGraceNote(Step::c, 4)); + GlissandoStart start{GlissandoType::slide}; + start.number = SpannerNumber(1); + start.text = "port."; + start.accelerate = Bool::yes; + start.beats = 2.0; + start.firstBeat = 25.0; + start.lastBeat = 75.0; + voice.notes.back().noteAttachmentData.glissandoStarts.push_back(start); + + voice.notes.push_back(makeGraceNote(Step::e, 4)); + GlissandoStop stop{GlissandoType::slide}; + stop.number = SpannerNumber(1); + voice.notes.back().noteAttachmentData.glissandoStops.push_back(stop); + + const auto roundTripped = roundTrip(score); + const auto ¬es = roundTripped.parts.at(0).measures.at(0).staves.at(0).voices.at(0).notes; + + REQUIRE(notes.at(0).noteAttachmentData.glissandoStarts.size() == 1); + const auto &outStart = notes.at(0).noteAttachmentData.glissandoStarts.at(0); + CHECK(outStart.glissandoType == GlissandoType::slide); + CHECK_EQUAL(std::string{"port."}, outStart.text); + CHECK(outStart.accelerate == Bool::yes); + REQUIRE(outStart.beats.has_value()); + CHECK_DOUBLES_EQUAL(2.0, *outStart.beats, 0.00001); + REQUIRE(outStart.firstBeat.has_value()); + CHECK_DOUBLES_EQUAL(25.0, *outStart.firstBeat, 0.00001); + REQUIRE(outStart.lastBeat.has_value()); + CHECK_DOUBLES_EQUAL(75.0, *outStart.lastBeat, 0.00001); + + REQUIRE(notes.at(1).noteAttachmentData.glissandoStops.size() == 1); + CHECK(notes.at(1).noteAttachmentData.glissandoStops.at(0).glissandoType == GlissandoType::slide); +} + +T_END + +// Requirement 7: and are top-level children, not nested inside +// (unlike wavy-line). +TEST(writesAsTopLevelNotationsChild, Glissando) +{ + using namespace glissandoApiTest; + ScoreData score; + score.parts.emplace_back(); + auto &part = score.parts.back(); + part.measures.emplace_back(); + auto &measure = part.measures.back(); + measure.staves.emplace_back(); + auto &staff = measure.staves.back(); + auto &voice = staff.voices[0]; + + voice.notes.push_back(makeGraceNote(Step::c, 4)); + GlissandoStart start{GlissandoType::glissando}; + voice.notes.back().noteAttachmentData.glissandoStarts.push_back(start); + + const auto xml = toXml(score); + pugi::xml_document xdoc; + REQUIRE(xdoc.load_string(xml.c_str())); + + const auto notations = xdoc.document_element().child("part").child("measure").child("note").child("notations"); + REQUIRE(notations); + const auto firstChild = notations.first_child(); + REQUIRE(firstChild); + CHECK_EQUAL(std::string{"glissando"}, std::string{firstChild.name()}); +} + +T_END + +// Requirement 8: when a note carries both an incoming stop and an outgoing start for the same +// element, the stop must precede the start in document order (mirrors the #139 rule for slurs). +TEST(stopPrecedesStartOnAChainedNote, Glissando) +{ + using namespace glissandoApiTest; + ScoreData score; + score.parts.emplace_back(); + auto &part = score.parts.back(); + part.measures.emplace_back(); + auto &measure = part.measures.back(); + measure.staves.emplace_back(); + auto &staff = measure.staves.back(); + auto &voice = staff.voices[0]; + + voice.notes.push_back(makeGraceNote(Step::c, 4)); + voice.notes.push_back(makeGraceNote(Step::d, 4)); + voice.notes.push_back(makeGraceNote(Step::e, 4)); + + GlissandoStart firstStart{GlissandoType::glissando}; + firstStart.number = SpannerNumber(1); + voice.notes.at(0).noteAttachmentData.glissandoStarts.push_back(firstStart); + + // The middle note both ends glissando 1 and begins a new glissando -- pushed start-before-stop + // into the api vectors on purpose, to prove the writer reorders regardless of insertion order. + GlissandoStart secondStart{GlissandoType::glissando}; + secondStart.number = SpannerNumber(1); + voice.notes.at(1).noteAttachmentData.glissandoStarts.push_back(secondStart); + GlissandoStop firstStop{GlissandoType::glissando}; + firstStop.number = SpannerNumber(1); + voice.notes.at(1).noteAttachmentData.glissandoStops.push_back(firstStop); + + GlissandoStop secondStop{GlissandoType::glissando}; + secondStop.number = SpannerNumber(1); + voice.notes.at(2).noteAttachmentData.glissandoStops.push_back(secondStop); + + const auto xml = toXml(score); + pugi::xml_document xdoc; + REQUIRE(xdoc.load_string(xml.c_str())); + + const auto notes = xdoc.document_element().child("part").child("measure"); + auto noteIter = notes.children("note").begin(); + ++noteIter; // the middle note + + const auto notations = noteIter->child("notations"); + REQUIRE(notations); + auto glissandoIter = notations.children("glissando").begin(); + REQUIRE(glissandoIter != notations.children("glissando").end()); + CHECK_EQUAL(std::string{"stop"}, std::string{glissandoIter->attribute("type").value()}); + ++glissandoIter; + REQUIRE(glissandoIter != notations.children("glissando").end()); + CHECK_EQUAL(std::string{"start"}, std::string{glissandoIter->attribute("type").value()}); +} + +T_END + +// Requirement 2: identity spanners draw from the writer-side SpannerNumberResolver, and glissando +// and slide draw from separate pools even when open at the same time. +TEST(identityNumbersAssignedPerElementPool, Glissando) +{ + using namespace glissandoApiTest; + ScoreData score; + score.parts.emplace_back(); + auto &part = score.parts.back(); + part.measures.emplace_back(); + auto &measure = part.measures.back(); + measure.staves.emplace_back(); + auto &staff = measure.staves.back(); + auto &voice = staff.voices[0]; + + // A glissando and a slide open back to back and stay open across the same span: if they + // shared a number pool the second would be forced to skip the first's number. + voice.notes.push_back(makeGraceNote(Step::c, 4)); + GlissandoStart glissandoStart{GlissandoType::glissando}; + glissandoStart.number = SpannerNumber("g"); + voice.notes.back().noteAttachmentData.glissandoStarts.push_back(glissandoStart); + + voice.notes.push_back(makeGraceNote(Step::e, 4)); + GlissandoStart slideStart{GlissandoType::slide}; + slideStart.number = SpannerNumber("s"); + voice.notes.back().noteAttachmentData.glissandoStarts.push_back(slideStart); + + voice.notes.push_back(makeGraceNote(Step::g, 4)); + GlissandoStop glissandoStop{GlissandoType::glissando}; + glissandoStop.number = SpannerNumber("g"); + voice.notes.back().noteAttachmentData.glissandoStops.push_back(glissandoStop); + + voice.notes.push_back(makeGraceNote(Step::b, 4)); + GlissandoStop slideStop{GlissandoType::slide}; + slideStop.number = SpannerNumber("s"); + voice.notes.back().noteAttachmentData.glissandoStops.push_back(slideStop); + + const auto roundTripped = roundTrip(score); + const auto ¬es = roundTripped.parts.at(0).measures.at(0).staves.at(0).voices.at(0).notes; + + REQUIRE(notes.at(0).noteAttachmentData.glissandoStarts.size() == 1); + REQUIRE(notes.at(1).noteAttachmentData.glissandoStarts.size() == 1); + CHECK(SpannerNumber(1) == notes.at(0).noteAttachmentData.glissandoStarts.at(0).number); + CHECK(SpannerNumber(1) == notes.at(1).noteAttachmentData.glissandoStarts.at(0).number); + + REQUIRE(notes.at(2).noteAttachmentData.glissandoStops.size() == 1); + REQUIRE(notes.at(3).noteAttachmentData.glissandoStops.size() == 1); + CHECK(SpannerNumber(1) == notes.at(2).noteAttachmentData.glissandoStops.at(0).number); + CHECK(SpannerNumber(1) == notes.at(3).noteAttachmentData.glissandoStops.at(0).number); +} + +T_END + +// Requirement 9: the synthetic fixtures parse and surface pairing plus the shared attributes. +TEST(readsSyntheticGlissandoFixture, Glissando) +{ + using namespace mxtest; + const auto score = mxtest::MxFileRepository::loadFile("glissando.3.0.xml"); + REQUIRE(score.parts.size() == 1); + const auto ¬e = score.parts.at(0).measures.at(0).staves.at(0).voices.at(0).notes.at(0); + REQUIRE(note.noteAttachmentData.glissandoStarts.size() == 1); + const auto &start = note.noteAttachmentData.glissandoStarts.at(0); + CHECK(start.glissandoType == GlissandoType::glissando); + CHECK(SpannerNumber(1) == start.number); + CHECK(start.lineData.lineType == LineType::solid); + CHECK_EQUAL(std::string{"x"}, start.text); +} + +T_END + +TEST(readsSyntheticSlideFixture, Glissando) +{ + using namespace mxtest; + const auto score = mxtest::MxFileRepository::loadFile("slide.3.0.xml"); + REQUIRE(score.parts.size() == 1); + const auto ¬e = score.parts.at(0).measures.at(0).staves.at(0).voices.at(0).notes.at(0); + REQUIRE(note.noteAttachmentData.glissandoStarts.size() == 1); + const auto &start = note.noteAttachmentData.glissandoStarts.at(0); + CHECK(start.glissandoType == GlissandoType::slide); + CHECK(start.accelerate == Bool::yes); + REQUIRE(start.beats.has_value()); + CHECK_DOUBLES_EQUAL(2.0, *start.beats, 0.00001); + REQUIRE(start.firstBeat.has_value()); + CHECK_DOUBLES_EQUAL(1.0, *start.firstBeat, 0.00001); + REQUIRE(start.lastBeat.has_value()); + CHECK_DOUBLES_EQUAL(1.0, *start.lastBeat, 0.00001); +} + +T_END + +#endif diff --git a/src/private/mxtest/api/NoteDataTest.cpp b/src/private/mxtest/api/NoteDataTest.cpp index 51228ace0..97cda8019 100644 --- a/src/private/mxtest/api/NoteDataTest.cpp +++ b/src/private/mxtest/api/NoteDataTest.cpp @@ -980,7 +980,7 @@ TEST(ornaments, NoteData) note.noteAttachmentData.marks.back().positionData.isDefaultXSpecified = true; note.noteAttachmentData.marks.back().positionData.defaultX = 123.0; - note.noteAttachmentData.marks.emplace_back(Placement::above, MarkType::wavyLine); + note.noteAttachmentData.marks.emplace_back(Placement::above, MarkType::shake); note.noteAttachmentData.marks.back().positionData.isDefaultYSpecified = true; note.noteAttachmentData.marks.back().positionData.defaultY = -456.0; @@ -1019,7 +1019,7 @@ TEST(ornaments, NoteData) ++oIter; md = *oIter; - CHECK(md.markType == MarkType::wavyLine); + CHECK(md.markType == MarkType::shake); CHECK(!md.positionData.isDefaultXSpecified); CHECK(md.positionData.isDefaultYSpecified); CHECK_DOUBLES_EQUAL(-456.0, md.positionData.defaultY, 0.00001); diff --git a/src/private/mxtest/api/WavyLineApiTest.cpp b/src/private/mxtest/api/WavyLineApiTest.cpp new file mode 100644 index 000000000..88b00e659 --- /dev/null +++ b/src/private/mxtest/api/WavyLineApiTest.cpp @@ -0,0 +1,265 @@ +// MusicXML Class Library +// Copyright (c) by Matthew James Briggs +// Distributed under the MIT License + +// Coverage for issue #389: a wavy line is a start/continue/stop spanner that lives inside a +// note's , is numbered like any other spanner, and carries a SMuFL glyph override and +// trill-sound playback on its start. + +#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" +#include "mxtest/file/MxFileRepository.h" +#include "pugixml.hpp" + +using namespace mx::api; +using namespace mxtest; + +namespace wavyLineApiTest +{ +inline NoteData makeGraceNote(Step inStep, int inOctave) +{ + NoteData note; + note.isGrace = true; + note.pitchData.step = inStep; + note.pitchData.octave = inOctave; + note.durationData.durationName = DurationName::eighth; + note.durationData.durationTimeTicks = 0; + return note; +} +} // namespace wavyLineApiTest + +TEST(startStopRoundTrip, WavyLine) +{ + using namespace wavyLineApiTest; + ScoreData score; + score.parts.emplace_back(); + auto &part = score.parts.back(); + part.measures.emplace_back(); + auto &measure = part.measures.back(); + measure.staves.emplace_back(); + auto &staff = measure.staves.back(); + auto &voice = staff.voices[0]; + + voice.notes.push_back(makeGraceNote(Step::c, 4)); + WavyLineStart start; + start.number = SpannerNumber(1); + start.positionData.placement = Placement::above; + start.smufl = "wiggleTrill"; + start.startNote = WavyLineStartNote::upper; + start.trillStep = WavyLineTrillStep::whole; + start.twoNoteTurn = WavyLineTwoNoteTurn::whole; + start.accelerate = Bool::yes; + start.beats = 2.0; + start.secondBeat = 25.0; + start.lastBeat = 75.0; + voice.notes.back().noteAttachmentData.wavyLineStarts.push_back(start); + + voice.notes.push_back(makeGraceNote(Step::g, 4)); + WavyLineStop stop; + stop.number = SpannerNumber(1); + voice.notes.back().noteAttachmentData.wavyLineStops.push_back(stop); + + const auto roundTripped = roundTrip(score); + REQUIRE(roundTripped.parts.size() == 1); + const auto ¬es = roundTripped.parts.at(0).measures.at(0).staves.at(0).voices.at(0).notes; + REQUIRE(notes.size() == 2); + + REQUIRE(notes.at(0).noteAttachmentData.wavyLineStarts.size() == 1); + const auto &outStart = notes.at(0).noteAttachmentData.wavyLineStarts.at(0); + CHECK(SpannerNumber(1) == outStart.number); + CHECK(outStart.positionData.placement == Placement::above); + REQUIRE(outStart.smufl.has_value()); + CHECK_EQUAL(std::string{"wiggleTrill"}, *outStart.smufl); + CHECK(outStart.startNote == WavyLineStartNote::upper); + CHECK(outStart.trillStep == WavyLineTrillStep::whole); + CHECK(outStart.twoNoteTurn == WavyLineTwoNoteTurn::whole); + CHECK(outStart.accelerate == Bool::yes); + REQUIRE(outStart.beats.has_value()); + CHECK_DOUBLES_EQUAL(2.0, *outStart.beats, 0.00001); + REQUIRE(outStart.secondBeat.has_value()); + CHECK_DOUBLES_EQUAL(25.0, *outStart.secondBeat, 0.00001); + REQUIRE(outStart.lastBeat.has_value()); + CHECK_DOUBLES_EQUAL(75.0, *outStart.lastBeat, 0.00001); + + REQUIRE(notes.at(1).noteAttachmentData.wavyLineStops.size() == 1); + CHECK(SpannerNumber(1) == notes.at(1).noteAttachmentData.wavyLineStops.at(0).number); +} + +T_END + +// Requirement 1: two wavy lines on one note -- a stop for the incoming trill extension and a +// start for a new one -- must be expressible and keep score order (stop before start). +TEST(twoWavyLinesOnOneNote, WavyLine) +{ + using namespace wavyLineApiTest; + ScoreData score; + score.parts.emplace_back(); + auto &part = score.parts.back(); + part.measures.emplace_back(); + auto &measure = part.measures.back(); + measure.staves.emplace_back(); + auto &staff = measure.staves.back(); + auto &voice = staff.voices[0]; + + voice.notes.push_back(makeGraceNote(Step::c, 4)); + WavyLineStart firstStart; + firstStart.number = SpannerNumber(1); + voice.notes.back().noteAttachmentData.wavyLineStarts.push_back(firstStart); + + voice.notes.push_back(makeGraceNote(Step::d, 4)); + // Pushed start-before-stop on purpose, to prove the writer reorders regardless of insertion + // order (mirrors the #139 stop-before-start rule). + WavyLineStart secondStart; + secondStart.number = SpannerNumber(1); + voice.notes.back().noteAttachmentData.wavyLineStarts.push_back(secondStart); + WavyLineStop firstStop; + firstStop.number = SpannerNumber(1); + voice.notes.back().noteAttachmentData.wavyLineStops.push_back(firstStop); + + voice.notes.push_back(makeGraceNote(Step::e, 4)); + WavyLineStop secondStop; + secondStop.number = SpannerNumber(1); + voice.notes.back().noteAttachmentData.wavyLineStops.push_back(secondStop); + + const auto roundTripped = roundTrip(score); + const auto ¬es = roundTripped.parts.at(0).measures.at(0).staves.at(0).voices.at(0).notes; + CHECK(notes.at(0).noteAttachmentData.wavyLineStarts.size() == 1); + CHECK(notes.at(1).noteAttachmentData.wavyLineStarts.size() == 1); + CHECK(notes.at(1).noteAttachmentData.wavyLineStops.size() == 1); + CHECK(notes.at(2).noteAttachmentData.wavyLineStops.size() == 1); + + const auto xml = toXml(score); + pugi::xml_document xdoc; + REQUIRE(xdoc.load_string(xml.c_str())); + auto noteIter = xdoc.document_element().child("part").child("measure").children("note").begin(); + ++noteIter; // the middle note, carrying both a stop and a start + + const auto ornaments = noteIter->child("notations").child("ornaments"); + REQUIRE(ornaments); + auto wavyIter = ornaments.children("wavy-line").begin(); + REQUIRE(wavyIter != ornaments.children("wavy-line").end()); + CHECK_EQUAL(std::string{"stop"}, std::string{wavyIter->attribute("type").value()}); + ++wavyIter; + REQUIRE(wavyIter != ornaments.children("wavy-line").end()); + CHECK_EQUAL(std::string{"start"}, std::string{wavyIter->attribute("type").value()}); +} + +T_END + +// A continuation point (for a wavy line crossing a system/page break) round-trips its number and +// position. +TEST(continuationRoundTrips, WavyLine) +{ + using namespace wavyLineApiTest; + ScoreData score; + score.parts.emplace_back(); + auto &part = score.parts.back(); + part.measures.emplace_back(); + auto &measure = part.measures.back(); + measure.staves.emplace_back(); + auto &staff = measure.staves.back(); + auto &voice = staff.voices[0]; + + voice.notes.push_back(makeGraceNote(Step::c, 4)); + WavyLineStart start; + start.number = SpannerNumber(1); + voice.notes.back().noteAttachmentData.wavyLineStarts.push_back(start); + + voice.notes.push_back(makeGraceNote(Step::d, 4)); + WavyLineContinue wavyLineContinue; + wavyLineContinue.number = SpannerNumber(1); + wavyLineContinue.isColorSpecified = true; + wavyLineContinue.colorData.red = 0xAA; + wavyLineContinue.colorData.green = 0xBB; + wavyLineContinue.colorData.blue = 0xCC; + voice.notes.back().noteAttachmentData.wavyLineContinuations.push_back(wavyLineContinue); + + voice.notes.push_back(makeGraceNote(Step::e, 4)); + WavyLineStop stop; + stop.number = SpannerNumber(1); + voice.notes.back().noteAttachmentData.wavyLineStops.push_back(stop); + + const auto roundTripped = roundTrip(score); + const auto ¬es = roundTripped.parts.at(0).measures.at(0).staves.at(0).voices.at(0).notes; + REQUIRE(notes.at(1).noteAttachmentData.wavyLineContinuations.size() == 1); + const auto &outContinue = notes.at(1).noteAttachmentData.wavyLineContinuations.at(0); + CHECK(SpannerNumber(1) == outContinue.number); + CHECK(outContinue.isColorSpecified); + CHECK_EQUAL(0xAA, static_cast(outContinue.colorData.red)); + CHECK_EQUAL(0xBB, static_cast(outContinue.colorData.green)); + CHECK_EQUAL(0xCC, static_cast(outContinue.colorData.blue)); +} + +T_END + +// A wavy line lives inside , unlike glissando/slide which are top-level +// children. +TEST(writesInsideOrnaments, WavyLine) +{ + using namespace wavyLineApiTest; + ScoreData score; + score.parts.emplace_back(); + auto &part = score.parts.back(); + part.measures.emplace_back(); + auto &measure = part.measures.back(); + measure.staves.emplace_back(); + auto &staff = measure.staves.back(); + auto &voice = staff.voices[0]; + + voice.notes.push_back(makeGraceNote(Step::c, 4)); + WavyLineStart start; + voice.notes.back().noteAttachmentData.wavyLineStarts.push_back(start); + + const auto xml = toXml(score); + pugi::xml_document xdoc; + REQUIRE(xdoc.load_string(xml.c_str())); + const auto wavyLine = xdoc.document_element() + .child("part") + .child("measure") + .child("note") + .child("notations") + .child("ornaments") + .child("wavy-line"); + REQUIRE(wavyLine); +} + +T_END + +// Requirement 9: the synthetic fixtures parse and surface pairing plus the shared attributes. +TEST(readsSyntheticWavyLineFixture, WavyLine) +{ + const auto score = mxtest::MxFileRepository::loadFile("wavy-line.3.0.xml"); + REQUIRE(score.parts.size() == 1); + const auto ¬e = score.parts.at(0).measures.at(0).staves.at(0).voices.at(0).notes.at(0); + REQUIRE(note.noteAttachmentData.wavyLineStarts.size() == 1); + const auto &start = note.noteAttachmentData.wavyLineStarts.at(0); + CHECK(SpannerNumber(1) == start.number); + CHECK(start.positionData.placement == Placement::above); + CHECK(start.startNote == WavyLineStartNote::upper); + CHECK(start.trillStep == WavyLineTrillStep::whole); + CHECK(start.twoNoteTurn == WavyLineTwoNoteTurn::whole); + CHECK(start.accelerate == Bool::yes); + REQUIRE(start.beats.has_value()); + CHECK_DOUBLES_EQUAL(2.0, *start.beats, 0.00001); +} + +T_END + +TEST(readsSyntheticWavyLineFixtureWithSmufl, WavyLine) +{ + const auto score = mxtest::MxFileRepository::loadFile("wavy-line.4.0.xml"); + REQUIRE(score.parts.size() == 1); + const auto ¬e = score.parts.at(0).measures.at(0).staves.at(0).voices.at(0).notes.at(0); + REQUIRE(note.noteAttachmentData.wavyLineStarts.size() == 1); + REQUIRE(note.noteAttachmentData.wavyLineStarts.at(0).smufl.has_value()); + CHECK_EQUAL(std::string{"wiggleTrill"}, *note.noteAttachmentData.wavyLineStarts.at(0).smufl); +} + +T_END + +#endif diff --git a/src/private/mxtest/api/roundtrip-baseline.txt b/src/private/mxtest/api/roundtrip-baseline.txt index 26ce9b255..a0b6f8eb6 100644 --- a/src/private/mxtest/api/roundtrip-baseline.txt +++ b/src/private/mxtest/api/roundtrip-baseline.txt @@ -644,3 +644,22 @@ lysuite/ly45f_Repeats_InvalidEndings.xml # reaches the api on SpannerStart/SpannerStop, so it is no longer dropped. synthetic/bracket.3.1.xml synthetic/dashes.3.1.xml + +# Unblocked by #389 (glissando/slide/wavy-line spanners). Glissando and slide were +# dropped entirely on read and could not be authored; wavy-line read and wrote as an +# unpaired mark that always wrote type="start", so every wavy line lost its pairing. +# All three now round-trip as start/stop (glissando, slide) or start/continue/stop +# (wavy-line) spanners. +ksuite/k005a_Spanners_Slides.xml +lysuite/ly33f_Trill_EndingOnGraceNote.xml +lysuite/ly33h_Spanners_Glissando.xml +musuite/testArpGliss1.xml +musuite/testArpGliss2.xml +musuite/testLines1.xml +musuite/testLines1_ref.xml +synthetic/glissando.3.0.xml +synthetic/glissando.3.1.xml +synthetic/slide.3.0.xml +synthetic/slide.3.1.xml +synthetic/wavy-line.3.0.xml +synthetic/wavy-line.4.0.xml diff --git a/src/private/mxtest/file/MxFileRepositoy.cpp b/src/private/mxtest/file/MxFileRepositoy.cpp index 89a7e76ee..87fabb317 100644 --- a/src/private/mxtest/file/MxFileRepositoy.cpp +++ b/src/private/mxtest/file/MxFileRepositoy.cpp @@ -479,5 +479,9 @@ void MxFileRepository::initializeNameSubdirectoryMap() myNameSubdirectoryMap.emplace("coda.3.0.xml", "synthetic"); myNameSubdirectoryMap.emplace("coda.3.1.xml", "synthetic"); myNameSubdirectoryMap.emplace("kind.3.0.xml", "synthetic"); + myNameSubdirectoryMap.emplace("glissando.3.0.xml", "synthetic"); + myNameSubdirectoryMap.emplace("slide.3.0.xml", "synthetic"); + myNameSubdirectoryMap.emplace("wavy-line.3.0.xml", "synthetic"); + myNameSubdirectoryMap.emplace("wavy-line.4.0.xml", "synthetic"); } } // namespace mxtest