Skip to content
Merged
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
74 changes: 71 additions & 3 deletions src/private/mx/impl/MeasureReader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ MeasureReader::MeasureReader(const core::PartwiseMeasure &inPartwiseMeasureRef,
const MeasureCursor &previousMeasureCursor, DiagnosticsContext diagnostics)
: myMutex{}, myPartwiseMeasure{inPartwiseMeasureRef}, myConverter{}, myDiagnostics{diagnostics}, myOutMeasureData{},
myCurrentCursor{cursor}, myPreviousMeasureCursor{previousMeasureCursor}, myHistory{}, myCrossStaffHomes{},
myPreviousNoteBucketStaffIndex{-1}
myPreviousNoteBucketStaffIndex{-1}, myVoiceGroups{}, myNextVoiceBucket{0}
{
HistoryRecord initialCursorRecord;
initialCursorRecord.amount = 0;
Expand Down Expand Up @@ -224,6 +224,8 @@ std::pair<api::MeasureData, std::optional<api::TransposeData>> MeasureReader::ge

myCrossStaffHomes.clear();
myPreviousNoteBucketStaffIndex = -1;
myVoiceGroups.clear();
myNextVoiceBucket = 0;
scanForCrossStaffBeamGroups();

const auto mdcSpan = myPartwiseMeasure.musicData();
Expand Down Expand Up @@ -537,8 +539,74 @@ void MeasureReader::parseNote(const core::Note &inMxNote, const core::Note *next
MX_ASSERT(bucketStaffIndex >= 0);
MX_ASSERT(static_cast<size_t>(bucketStaffIndex) < myOutMeasureData.staves.size());
myPreviousNoteBucketStaffIndex = bucketStaffIndex;
insertNoteData(std::move(noteData), myCurrentCursor.staffIndex, myCurrentCursor.voiceIndex,
noteReader.getVoiceLabel());
const int voiceBucket = findVoiceBucket(bucketStaffIndex, noteReader.getIsChord(), noteReader.getVoiceLabel());
insertNoteData(std::move(noteData), bucketStaffIndex, voiceBucket, noteReader.getVoiceLabel());
}

// A <backup> starts another voice. Between backups, MusicXML can also change voice from one note to
// the next with nothing but the <voice> text, the next note starting where the last one ended.
//
// This returns the voice a note goes into, as its key in the staff's voices map. It looks at the
// notes of the same staff since the last <backup> in the measure, or since the start of the measure
// when there has not been one. Each different <voice> text among those notes gets a voice of its
// own, and the note goes into the voice for its text. For example, three notes labelled 4, 5 and 4
// make two voices: the first and third notes are in one, and the middle note is in the other. When
// those notes name at most one voice (they all have the same text, or none of them has a <voice>),
// they are all one voice. The header calls each of these voices a group.
int MeasureReader::findVoiceBucket(int staff, bool isChordContinuation, const std::string &voiceText) const
{
auto &staffGroups = myVoiceGroups[staff];

if (staffGroups.run != myCurrentCursor.voiceIndex)
{
staffGroups = StaffVoiceGroups{};
staffGroups.run = myCurrentCursor.voiceIndex;
}

auto &groups = staffGroups.groups;
const auto findGroup = [&](auto isMatch) { return std::find_if(groups.begin(), groups.end(), isMatch); };
auto group = groups.end();

if (isChordContinuation || voiceText.empty())
{
// A <chord/> note sounds with the note before it and so is in its voice, whatever its own
// <voice> says. A note without a <voice> continues the voice of the note before it.
group = findGroup([&](const VoiceGroup &g) { return g.bucket == staffGroups.previousNoteBucket; });
}
else
{
group = findGroup([&](const VoiceGroup &g) { return g.name == voiceText; });

// While the run's notes so far have not named their voice, this note names it. Otherwise
// a name the run has not seen yet starts another voice.
if (group == groups.end() && !(groups.size() == 1 && groups.front().name.empty()))
{
groups.push_back(VoiceGroup{voiceText, myNextVoiceBucket});
++myNextVoiceBucket;
group = std::prev(groups.end());
}
else if (group == groups.end())
{
group = groups.begin();
}
}

if (group == groups.end())
{
// the first note of the run
groups.push_back(VoiceGroup{"", myNextVoiceBucket});
++myNextVoiceBucket;
group = std::prev(groups.end());
}

// As with VoiceData::label, the first note that names the voice names it for all of them.
if (group->name.empty())
{
group->name = voiceText;
}

staffGroups.previousNoteBucket = group->bucket;
return group->bucket;
}

void MeasureReader::scanForCrossStaffBeamGroups() const
Expand Down
26 changes: 26 additions & 0 deletions src/private/mx/impl/MeasureReader.h
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,31 @@ class MeasureReader
// chord cannot be split across staff buckets; -1 means "no previous note"
mutable int myPreviousNoteBucketStaffIndex;

// The notes of one staff between two <backup>s are split into voices by the text of their
// <voice> element (see findVoiceBucket). A group is one such voice: the <voice> text that
// named it, empty until a note names it, and the voice bucket its notes go into.
struct VoiceGroup
{
std::string name;
int bucket = 0;
};

// The voice groups of one staff since the last <backup>. run is the cursor's backup count
// when they were made; a later count means a <backup> came since, and the groups start over.
struct StaffVoiceGroups
{
int run = -1;
std::vector<VoiceGroup> groups;
int previousNoteBucket = -1;
};

// keyed by staff bucket
mutable std::map<int, StaffVoiceGroups> myVoiceGroups;

// The next voice bucket to hand out. Buckets count up through the measure, so on each staff
// they sort by backup run first and by order of appearance within the run.
mutable int myNextVoiceBucket;

private:
static api::FigureData parseFigure(const core::Figure &figure);
static int figuredBassStaffIndex(const MeasureCursor &cursor, const api::MeasureData &measure,
Expand Down Expand Up @@ -123,6 +148,7 @@ class MeasureReader
void importStaffDetails(const core::Attributes &inMxAttributes) const;
void importClefs(std::span<const core::Clef> inClefs) const;
void importClef(const core::Clef &inClef) const;
int findVoiceBucket(int staff, bool isChordContinuation, const std::string &voiceText) const;
void insertNoteData(api::NoteData &&noteData, int staff, int voice, const std::string &voiceLabel) const;
void insertClef(api::ClefData &&clefData, int staff) const;
void consolidateVoicesForAllStaves() const;
Expand Down
28 changes: 18 additions & 10 deletions src/private/mx/impl/MeasureWriter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
#include "mx/impl/NoteWriter.h"
#include "mx/impl/PositionFunctions.h"
#include "mx/impl/ScoreWriter.h"
#include "mx/impl/VoicePass.h"
#include "mx/utility/Throw.h"

#include <limits>
Expand Down Expand Up @@ -614,28 +615,36 @@ void MeasureWriter::writeVoices(const api::StaffData &inStaff)
auto directionIter = inStaff.directions.cbegin();
auto directionEnd = inStaff.directions.cend();

for (const auto &voice : inStaff.voices)
for (const auto &pass : makeVoicePasses(inStaff))
{
bool isFirstNote = true;
int currentChordTickPosition = -1;
int previousChordTickPosition = -2;
bool previousChordNoteWasGrace = false;
myHistory.setVoiceIndex(voice.first);
auto noteIter = voice.second.notes.cbegin();
auto noteEnd = voice.second.notes.cend();
myHistory.setVoiceIndex(pass.voices.front()->first);
const auto &notes = pass.notes();
auto noteIter = notes.cbegin();
auto noteEnd = notes.cend();

if (directionIter != directionEnd)
{
backupToLeadingDirection(directionIter, directionEnd, noteIter, noteEnd);
writeDirections(directionIter, directionEnd, noteIter, std::cbegin(voice.second.notes), noteEnd);
writeDirections(directionIter, directionEnd, noteIter, std::cbegin(notes), noteEnd);
}

for (; noteIter != noteEnd; ++noteIter)
{
bool isStartOfChord = false;
const auto &written = pass.written.at(static_cast<size_t>(noteIter - notes.cbegin()));
const auto &voice = *written.voice;
const auto &apiNote = *written.note;

if (voice.first != myHistory.getCursor().voiceIndex)
{
myHistory.setVoiceIndex(voice.first);
}

myHistory.setChord(noteIter->isChord);
const auto &apiNote = *noteIter;

// Middle barlines are checked before this note's <backup> or <forward>, so that one at the
// end of a voice stays ahead of the <backup> into the next voice, and again after it, for
Expand Down Expand Up @@ -705,7 +714,7 @@ void MeasureWriter::writeVoices(const api::StaffData &inStaff)
}

myPropertiesWriter->flushBuffer();
writeDirections(directionIter, directionEnd, noteIter, std::cbegin(voice.second.notes), noteEnd);
writeDirections(directionIter, directionEnd, noteIter, std::cbegin(notes), noteEnd);

NoteWriter writer{
apiNote, myHistory.getCursor(), myScoreWriter, myPreviousCursor.isChordActive, voice.second.notes,
Expand All @@ -719,10 +728,9 @@ void MeasureWriter::writeVoices(const api::StaffData &inStaff)

} // foreach note

writeDirections(directionIter, directionEnd, std::cend(voice.second.notes), std::cbegin(voice.second.notes),
std::cend(voice.second.notes));
writeDirections(directionIter, directionEnd, std::cend(notes), std::cbegin(notes), std::cend(notes));

} // foreach voice
} // foreach pass

std::vector<api::NoteData> fakeEmptyNoteVector;
const auto iterBegin = std::cbegin(fakeEmptyNoteVector);
Expand Down
9 changes: 5 additions & 4 deletions src/private/mx/impl/SpannerResolver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include "mx/impl/SpannerResolver.h"
#include "mx/api/GlissandoData.h"
#include "mx/impl/OttavaFunctions.h"
#include "mx/impl/VoicePass.h"
#include "mx/impl/WriteRefusal.h"
#include "mx/utility/Throw.h"

Expand Down Expand Up @@ -638,12 +639,12 @@ void SpannerResolver::resolvePart(const api::PartData &inPart, int partIndex, Di
for (const auto &staff : measure.staves)
{
collector.setStaff(staffIndex);
for (const auto &voicePair : staff.voices)
for (const auto &pass : makeVoicePasses(staff))
{
collector.setVoice(voicePair.first);
for (const auto &note : voicePair.second.notes)
for (const auto &written : pass.written)
{
collector.addNote(note);
collector.setVoice(written.voice->first);
collector.addNote(*written.note);
}
}
for (const auto &direction : staff.directions)
Expand Down
14 changes: 8 additions & 6 deletions src/private/mx/impl/SpannerResolver.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,14 @@ namespace impl
// 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,
// glissando/slide stops then starts, tuplet starts then stops, wavy-line
// stops/continues/starts, per note -- mirroring NotationsWriter within each
// class; the order between classes does not matter, since each class has its
// own pool), and each staff's directions in
// MeasureWriter serializes it: measures in order, staves in order, each
// staff's notes in the order makeVoicePasses gives them (voices ascending,
// except where a voice is written in the gaps of the voice before it), and
// within a note curve stops/continues/starts, glissando/slide stops then
// starts, tuplet starts then stops, wavy-line stops/continues/starts
// (mirroring NotationsWriter within each class; the order between classes
// does not matter, since each class has its own pool), 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
Expand Down
Loading
Loading