diff --git a/data/api.features.xml b/data/api.features.xml
index 8d98ced92..413e9c0af 100644
--- a/data/api.features.xml
+++ b/data/api.features.xml
@@ -1208,8 +1208,9 @@
+
- api::WedgeStart/WedgeStop; DirectionReader.parseWedge (stop/continue routed structurally, not via WedgeType enum).
+ api::WedgeStart/WedgeStop; DirectionReader.parseWedge (stop/continue routed structurally, not via WedgeType enum). niente lives on WedgeStart for either direction; a diminuendo's niente is read from and written to its stop.
Part of bend; not modeled.
diff --git a/src/include/mx/api/WedgeData.h b/src/include/mx/api/WedgeData.h
index dbdd5a4f0..826035e91 100644
--- a/src/include/mx/api/WedgeData.h
+++ b/src/include/mx/api/WedgeData.h
@@ -36,12 +36,22 @@ struct WedgeStart
bool isColorSpecified;
ColorData colorData;
+ // Whether a small circle is drawn at the point of the hairpin (niente): a crescendo that grows
+ // out of nothing, or a diminuendo that fades away to nothing. Set it here on the start for
+ // either direction; yes draws the circle, and no or unspecified leave a plain point. MusicXML
+ // states it at the point, on the start of a crescendo and on the stop of a diminuendo, and mx
+ // writes it there for you. So a diminuendo's circle is written only when the diminuendo has a
+ // WedgeStop that ends it; without one the circle is left out and a diagnostic says so. A
+ // hairpin whose wedgeType is unspecified is written as a crescendo, with the circle on its
+ // start.
+ Bool niente;
+
// The element's id attribute (see Id.h).
std::optional id;
WedgeStart()
: number{}, wedgeType{WedgeType::unspecified}, isSpreadSpecified{false}, spread{0.0}, lineData{},
- positionData{}, isColorSpecified{false}, colorData{}, id{}
+ positionData{}, isColorSpecified{false}, colorData{}, niente{Bool::unspecified}, id{}
{
}
};
@@ -70,6 +80,7 @@ MXAPI_EQUALS_MEMBER(lineData)
MXAPI_EQUALS_MEMBER(positionData)
MXAPI_EQUALS_MEMBER(isColorSpecified)
MXAPI_EQUALS_MEMBER(colorData)
+MXAPI_EQUALS_MEMBER(niente)
MXAPI_EQUALS_MEMBER(id)
MXAPI_EQUALS_END;
MXAPI_NOT_EQUALS_AND_VECTORS(WedgeStart);
diff --git a/src/private/mx/impl/DirectionReader.cpp b/src/private/mx/impl/DirectionReader.cpp
index bbe979242..12b653608 100644
--- a/src/private/mx/impl/DirectionReader.cpp
+++ b/src/private/mx/impl/DirectionReader.cpp
@@ -42,6 +42,7 @@
#include "mx/core/generated/Membrane.h"
#include "mx/core/generated/Metal.h"
#include "mx/core/generated/Metronome.h"
+#include "mx/core/generated/MusicDataChoice.h"
#include "mx/core/generated/Numeral.h"
#include "mx/core/generated/NumeralKey.h"
#include "mx/core/generated/NumeralMode.h"
@@ -53,6 +54,8 @@
#include "mx/core/generated/OnOff.h"
#include "mx/core/generated/OtherDirection.h"
#include "mx/core/generated/OtherText.h"
+#include "mx/core/generated/PartwiseMeasure.h"
+#include "mx/core/generated/PartwisePart.h"
#include "mx/core/generated/Pedal.h"
#include "mx/core/generated/PedalTuning.h"
#include "mx/core/generated/PedalType.h"
@@ -90,6 +93,7 @@
#include "mx/impl/PrintFunctions.h"
#include "mx/impl/SoundFunctions.h"
#include "mx/impl/SpannerFunctions.h"
+#include "mx/impl/SpannerPairing.h"
#include "mx/impl/TuningFunctions.h"
#include "mx/utility/Round.h"
#include "mx/utility/StringToInt.h"
@@ -100,18 +104,70 @@ namespace mx
namespace impl
{
DirectionReader::DirectionReader(const core::Direction &inDirection, MeasureCursor inCursor,
- DiagnosticsContext diagnostics)
+ DiagnosticsContext diagnostics, const WedgeNiente *inWedgeNiente)
: myDirection{&inDirection}, myHarmony{nullptr}, myCursor{inCursor}, myConverter{}, myDiagnostics{diagnostics},
- myOutDirectionData{}
+ myWedgeNiente{inWedgeNiente}, myOutDirectionData{}
{
}
DirectionReader::DirectionReader(const core::Harmony &inHarmony, MeasureCursor inCursor, DiagnosticsContext diagnostics)
: myDirection{nullptr}, myHarmony{&inHarmony}, myCursor{inCursor}, myConverter{}, myDiagnostics{diagnostics},
- myOutDirectionData{}
+ myWedgeNiente{nullptr}, myOutDirectionData{}
{
}
+WedgeNiente DirectionReader::scanWedgeNiente(const core::PartwisePart &inPart)
+{
+ // Every wedge that is not a stop opens. A continue segment is read as a WedgeStart, which is
+ // written as a crescendo start, so it opens here too and a stop pairs with the same start on
+ // read as on write.
+ struct OpenWedge
+ {
+ const core::Wedge *wedge;
+ bool isDiminuendo;
+ };
+
+ const Converter converter{};
+ WedgeNiente result;
+ SpannerPairing pairing;
+ for (const auto &measure : inPart.measure())
+ {
+ for (const auto &musicData : measure.musicData())
+ {
+ if (musicData.kind() != core::MusicDataChoice::Kind::direction)
+ {
+ continue;
+ }
+ for (const auto &directionType : musicData.asDirection().directionType())
+ {
+ if (directionType.choice().kind() != core::DirectionTypeChoice::Kind::wedge)
+ {
+ continue;
+ }
+ const auto &wedge = directionType.choice().asWedge();
+ const auto number =
+ wedge.number().has_value() ? api::SpannerNumber(wedge.number()->value()) : api::SpannerNumber{};
+ const auto tag = wedge.type().tag();
+ if (tag != core::WedgeType::Tag::stop)
+ {
+ pairing.open(number, OpenWedge{&wedge, tag == core::WedgeType::Tag::diminuendo});
+ continue;
+ }
+ const auto start = pairing.close(number);
+ if (start.has_value() && start->isDiminuendo)
+ {
+ result.diminuendoStops.insert(&wedge);
+ if (wedge.niente().has_value())
+ {
+ result.diminuendoNiente[start->wedge] = converter.convert(*wedge.niente());
+ }
+ }
+ }
+ }
+ }
+ return result;
+}
+
void DirectionReader::report(api::Severity severity, api::DiagnosticCode code, std::string message) const
{
myDiagnostics.report(severity, code, measureLocation(myCursor), std::move(message));
@@ -546,6 +602,13 @@ void DirectionReader::parseWedge(const core::DirectionType &directionType)
}
stop.positionData = positionData;
stop.id = getId(wedge);
+ const bool closesDiminuendo = myWedgeNiente && myWedgeNiente->diminuendoStops.count(&wedge) > 0;
+ if (wedge.niente().has_value() && !closesDiminuendo)
+ {
+ report(api::Severity::error, api::DiagnosticCode::droppedData,
+ "wedge niente is only read on the start of a crescendo or the stop of a diminuendo; "
+ "niente on a stop that does not close a diminuendo is not read");
+ }
myOutDirectionData.directionTypes.emplace_back(api::DirectionChoice{std::move(stop)});
return;
}
@@ -566,11 +629,38 @@ void DirectionReader::parseWedge(const core::DirectionType &directionType)
start.lineData = lineData;
start.isColorSpecified = isColorSpecified;
start.colorData = colorData;
+ start.niente = parseWedgeStartNiente(wedge);
start.id = getId(wedge);
myOutDirectionData.directionTypes.emplace_back(api::DirectionChoice{std::move(start)});
}
}
+api::Bool DirectionReader::parseWedgeStartNiente(const core::Wedge &wedge) const
+{
+ const auto tag = wedge.type().tag();
+ if (tag == core::WedgeType::Tag::crescendo)
+ {
+ return wedge.niente().has_value() ? myConverter.convert(*wedge.niente()) : api::Bool::unspecified;
+ }
+
+ if (wedge.niente().has_value())
+ {
+ report(api::Severity::error, api::DiagnosticCode::droppedData,
+ tag == core::WedgeType::Tag::diminuendo
+ ? "wedge niente is only read on the start of a crescendo or the stop of a diminuendo; niente on "
+ "the start of a diminuendo is not read"
+ : "wedge niente is only read on the start of a crescendo or the stop of a diminuendo; niente on "
+ "a continue segment is not read");
+ }
+
+ if (tag != core::WedgeType::Tag::diminuendo || !myWedgeNiente)
+ {
+ return api::Bool::unspecified;
+ }
+ const auto iter = myWedgeNiente->diminuendoNiente.find(&wedge);
+ return iter == myWedgeNiente->diminuendoNiente.cend() ? api::Bool::unspecified : iter->second;
+}
+
void DirectionReader::parseDynamics(const core::DirectionType &directionType)
{
for (const auto &dynamic : directionType.choice().asDynamics().items())
diff --git a/src/private/mx/impl/DirectionReader.h b/src/private/mx/impl/DirectionReader.h
index b28f26f3d..5bcb22f8a 100644
--- a/src/private/mx/impl/DirectionReader.h
+++ b/src/private/mx/impl/DirectionReader.h
@@ -17,25 +17,53 @@
#include "mx/impl/PositionFunctions.h"
#include
+#include
+#include
#include
namespace mx
{
+namespace core
+{
+class PartwisePart;
+class Wedge;
+} // namespace core
+
namespace impl
{
+// MusicXML writes a diminuendo's niente on the stop that closes it, which comes later in the
+// part than the start that api::WedgeStart::niente lives on. This is what a scan of the whole
+// part learns about that pairing before the part's directions are read.
+struct WedgeNiente
+{
+ // The niente stated by the stop of each diminuendo whose stop states one.
+ std::unordered_map diminuendoNiente;
+
+ // Every stop that closes a diminuendo. niente on any other stop is not at the hairpin's point.
+ std::unordered_set diminuendoStops;
+};
+
class DirectionReader
{
public:
- DirectionReader(const core::Direction &inDirection, MeasureCursor inCursor, DiagnosticsContext diagnostics = {});
+ // inWedgeNiente is the scan of the part this direction belongs to (see scanWedgeNiente). When
+ // it is null, a diminuendo's niente is not read.
+ DirectionReader(const core::Direction &inDirection, MeasureCursor inCursor, DiagnosticsContext diagnostics = {},
+ const WedgeNiente *inWedgeNiente = nullptr);
DirectionReader(const core::Harmony &inHarmony, MeasureCursor inCursor, DiagnosticsContext diagnostics = {});
api::DirectionData getDirectionData();
+ // Pairs the part's wedge stops with the starts they close, in one pass in document order and
+ // by the rule in SpannerPairing.h, and records what each diminuendo's stop says about niente.
+ static WedgeNiente scanWedgeNiente(const core::PartwisePart &inPart);
+
private:
const core::Direction *const myDirection;
const core::Harmony *const myHarmony;
const MeasureCursor myCursor;
const Converter myConverter;
DiagnosticsContext myDiagnostics;
+ const WedgeNiente *const myWedgeNiente;
api::DirectionData myOutDirectionData;
private:
@@ -55,6 +83,7 @@ class DirectionReader
void parseWordsRun(const core::DirectionType &directionType);
void parseCoda(const core::DirectionType &directionType);
void parseWedge(const core::DirectionType &directionType);
+ api::Bool parseWedgeStartNiente(const core::Wedge &wedge) const;
void parseDynamics(const core::DirectionType &directionType);
void parseDashes(const core::DirectionType &directionType);
void parseBracket(const core::DirectionType &directionType);
diff --git a/src/private/mx/impl/DirectionWriter.cpp b/src/private/mx/impl/DirectionWriter.cpp
index 544942469..e88be9976 100644
--- a/src/private/mx/impl/DirectionWriter.cpp
+++ b/src/private/mx/impl/DirectionWriter.cpp
@@ -383,6 +383,14 @@ void DirectionWriter::emitWedgeStop(const api::WedgeStop &wedgeStop, const void
{
wedge.setSpread(core::Tenths{core::Decimal{static_cast(wedgeStop.spread)}});
}
+
+ // a diminuendo's niente comes from its start and is written here, at the hairpin's point
+ const auto niente = mySpannerResolver.wedgeStopNiente(inIdentity);
+ if (niente != api::Bool::unspecified)
+ {
+ wedge.setNiente(myConverter.convert(niente));
+ }
+
setAttributesFromPositionData(wedgeStop.positionData, wedge);
setId(wedgeStop.id, wedge, myDiagnostics, cursorLocation(myCursor));
core::DirectionType dt{};
@@ -416,6 +424,12 @@ void DirectionWriter::emitWedgeStart(const api::WedgeStart &wedgeStart, const vo
wedge.setSpread(core::Tenths{core::Decimal{static_cast(wedgeStart.spread)}});
}
+ // a crescendo's point is at its start; a diminuendo's niente is written on its stop
+ if (wedgeStart.wedgeType != api::WedgeType::diminuendo && wedgeStart.niente != api::Bool::unspecified)
+ {
+ wedge.setNiente(myConverter.convert(wedgeStart.niente));
+ }
+
setAttributesFromPositionData(wedgeStart.positionData, wedge);
setAttributesFromLineData(wedgeStart.lineData, wedge);
if (wedgeStart.isColorSpecified)
diff --git a/src/private/mx/impl/MeasureReader.cpp b/src/private/mx/impl/MeasureReader.cpp
index 88f8a4e84..1771e769d 100644
--- a/src/private/mx/impl/MeasureReader.cpp
+++ b/src/private/mx/impl/MeasureReader.cpp
@@ -163,9 +163,11 @@ int MeasureReader::figuredBassStaffIndex(const MeasureCursor &cursor, const api:
}
MeasureReader::MeasureReader(const core::PartwiseMeasure &inPartwiseMeasureRef, const MeasureCursor &cursor,
- const MeasureCursor &previousMeasureCursor, DiagnosticsContext diagnostics)
- : myMutex{}, myPartwiseMeasure{inPartwiseMeasureRef}, myConverter{}, myDiagnostics{diagnostics}, myOutMeasureData{},
- myCurrentCursor{cursor}, myPreviousMeasureCursor{previousMeasureCursor}, myHistory{}, myCrossStaffHomes{},
+ const MeasureCursor &previousMeasureCursor, DiagnosticsContext diagnostics,
+ const WedgeNiente *inWedgeNiente)
+ : myMutex{}, myPartwiseMeasure{inPartwiseMeasureRef}, myConverter{}, myDiagnostics{diagnostics},
+ myWedgeNiente{inWedgeNiente}, myOutMeasureData{}, myCurrentCursor{cursor},
+ myPreviousMeasureCursor{previousMeasureCursor}, myHistory{}, myCrossStaffHomes{},
myPreviousNoteBucketStaffIndex{-1}
{
HistoryRecord initialCursorRecord;
@@ -714,7 +716,7 @@ void MeasureReader::parseForward(const core::Forward &inMxForward) const
void MeasureReader::parseDirection(const core::Direction &inDirection) const
{
- DirectionReader reader{inDirection, myCurrentCursor, myDiagnostics};
+ DirectionReader reader{inDirection, myCurrentCursor, myDiagnostics, myWedgeNiente};
auto directionData = reader.getDirectionData();
// make an adjustment if the directionData refers to a non-existent staff
diff --git a/src/private/mx/impl/MeasureReader.h b/src/private/mx/impl/MeasureReader.h
index 85e9e67f8..fcf5810cd 100644
--- a/src/private/mx/impl/MeasureReader.h
+++ b/src/private/mx/impl/MeasureReader.h
@@ -39,12 +39,15 @@ class ForPart;
namespace impl
{
class NoteReader;
+struct WedgeNiente;
class MeasureReader
{
public:
+ // inWedgeNiente is the part's wedge scan (see DirectionReader::scanWedgeNiente), or null.
MeasureReader(const core::PartwiseMeasure &inPartwiseMeasureRef, const MeasureCursor &cursor,
- const MeasureCursor &previousMeasureCursor, DiagnosticsContext diagnostics = {});
+ const MeasureCursor &previousMeasureCursor, DiagnosticsContext diagnostics = {},
+ const WedgeNiente *inWedgeNiente = nullptr);
std::pair> getMeasureData() const;
impl::MeasureCursor getCursor() const;
@@ -54,6 +57,7 @@ class MeasureReader
const core::PartwiseMeasure &myPartwiseMeasure;
const Converter myConverter;
DiagnosticsContext myDiagnostics;
+ const WedgeNiente *const myWedgeNiente;
mutable api::MeasureData myOutMeasureData;
mutable MeasureCursor myCurrentCursor;
diff --git a/src/private/mx/impl/PartReader.cpp b/src/private/mx/impl/PartReader.cpp
index ceadee241..683c9cddd 100644
--- a/src/private/mx/impl/PartReader.cpp
+++ b/src/private/mx/impl/PartReader.cpp
@@ -22,6 +22,7 @@
#include "mx/core/generated/ScorePartMIDIGroup.h"
#include "mx/core/generated/ScorePartwise.h"
#include "mx/impl/Converter.h"
+#include "mx/impl/DirectionReader.h"
#include "mx/impl/MeasureReader.h"
#include "mx/impl/NameDisplayFunctions.h"
#include "mx/impl/PositionFunctions.h"
@@ -105,9 +106,12 @@ api::PartData PartReader::getPartData()
myPreviousCursor = myCurrentCursor;
+ // a diminuendo's niente is stated on its stop, later in the part, so the wedges are paired first
+ const auto wedgeNiente = DirectionReader::scanWedgeNiente(myPartwisePart);
+
for (const auto &mxMeasure : myPartwisePart.measure())
{
- MeasureReader reader{mxMeasure, myCurrentCursor, myPreviousCursor, myDiagnostics};
+ MeasureReader reader{mxMeasure, myCurrentCursor, myPreviousCursor, myDiagnostics, &wedgeNiente};
// the reader returns the measure data and any data that needs to be written at
// the part-level (e.g. transposition). currently this is done as a pair.
auto measureDataPair = reader.getMeasureData();
diff --git a/src/private/mx/impl/SpannerPairing.h b/src/private/mx/impl/SpannerPairing.h
new file mode 100644
index 000000000..b1ab7ce86
--- /dev/null
+++ b/src/private/mx/impl/SpannerPairing.h
@@ -0,0 +1,79 @@
+// MusicXML Class Library
+// Copyright (c) by Matthew James Briggs
+// Distributed under the MIT License
+
+#pragma once
+
+#include "mx/api/SpannerNumber.h"
+
+#include