Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion data/api.features.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1208,8 +1208,9 @@
<attribute name="type" support="yes"/>
<attribute name="number" support="yes"/>
<attribute name="spread" support="yes"/>
<attribute name="niente" support="yes"/>
</attributes>
<notes>api::WedgeStart/WedgeStop; DirectionReader.parseWedge (stop/continue routed structurally, not via WedgeType enum).</notes>
<notes>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.</notes>
</feature>
<feature name="with-bar" support="none">
<notes>Part of bend; not modeled.</notes>
Expand Down
13 changes: 12 additions & 1 deletion src/include/mx/api/WedgeData.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this could imply an n instead of a circle, even if the spec calls it niente, so lets disambiguate here.

Suggested change
Bool niente;
Bool nienteCircle;


// The <wedge> element's id attribute (see Id.h).
std::optional<Id> 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{}
{
}
};
Expand Down Expand Up @@ -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);
Expand Down
96 changes: 93 additions & 3 deletions src/private/mx/impl/DirectionReader.cpp

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛑 I'm concerned about this PR. It's a lot of code churn in many files for something that doesn't seem to appear in the wild corpus at all. Holding on this one for further consideration.

Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -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"
Expand All @@ -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<OpenWedge> 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));
Expand Down Expand Up @@ -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;
}
Expand All @@ -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())
Expand Down
31 changes: 30 additions & 1 deletion src/private/mx/impl/DirectionReader.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,53 @@
#include "mx/impl/PositionFunctions.h"

#include <mutex>
#include <unordered_map>
#include <unordered_set>
#include <vector>

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<const core::Wedge *, api::Bool> diminuendoNiente;

// Every stop that closes a diminuendo. niente on any other stop is not at the hairpin's point.
std::unordered_set<const core::Wedge *> 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:
Expand All @@ -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);
Expand Down
14 changes: 14 additions & 0 deletions src/private/mx/impl/DirectionWriter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,14 @@ void DirectionWriter::emitWedgeStop(const api::WedgeStop &wedgeStop, const void
{
wedge.setSpread(core::Tenths{core::Decimal{static_cast<double>(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{};
Expand Down Expand Up @@ -416,6 +424,12 @@ void DirectionWriter::emitWedgeStart(const api::WedgeStart &wedgeStart, const vo
wedge.setSpread(core::Tenths{core::Decimal{static_cast<double>(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)
Expand Down
10 changes: 6 additions & 4 deletions src/private/mx/impl/MeasureReader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion src/private/mx/impl/MeasureReader.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<api::MeasureData, std::optional<api::TransposeData>> getMeasureData() const;
impl::MeasureCursor getCursor() const;
Expand All @@ -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;
Expand Down
6 changes: 5 additions & 1 deletion src/private/mx/impl/PartReader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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();
Expand Down
Loading
Loading