Skip to content
Open
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
5 changes: 3 additions & 2 deletions .claude/skills/mx-api-doctrine/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,9 @@ Responses to wrong api usage, in order of preference:
1. Unrepresentable: shape the type so the wrong state cannot be expressed (choice types below;
merged fields, principle 3).
2. Defined fallback: document a harmless result and return it. A wrong-kind choice accessor
returns a default-constructed copy; the writer emits nothing for a meaningless combination
(cue-note ties are silently dropped). No signal to the caller.
returns a default-constructed copy; the writer drops the half of an encoding that is
meaningless for the note it is on (a tie on a silent cue note is written as `<tied>`
notation only, never as a sound-level `<tie>`). No signal to the caller.
3. `Result<T>` (`Result.h`): the error channel of last resort. It exists for the
`DocumentManager` I/O boundary, where failure is real (unreadable file, unparseable XML). Do
not spread it into the data model.
Expand Down
160 changes: 90 additions & 70 deletions data/corpus.xml

Large diffs are not rendered by default.

87 changes: 87 additions & 0 deletions data/synthetic/tied.cue.4.0.features.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated by `python3 -m audit files`. The element/attribute surface of the
sibling corpus file. Do not edit by hand; regenerate with the audit tool. -->
<feature-audit>
<audited>synthetic/tied.cue.4.0.xml</audited>
<audited-version>4.0</audited-version>
<features>
<feature>
<name>attributes</name>
</feature>
<feature>
<name>cue</name>
</feature>
<feature>
<name>divisions</name>
</feature>
<feature>
<name>duration</name>
</feature>
<feature>
<name>grace</name>
</feature>
<feature>
<name>measure</name>
<attributes>
<attribute>number</attribute>
</attributes>
</feature>
<feature>
<name>notations</name>
</feature>
<feature>
<name>note</name>
</feature>
<feature>
<name>octave</name>
</feature>
<feature>
<name>part</name>
<attributes>
<attribute>id</attribute>
</attributes>
</feature>
<feature>
<name>part-list</name>
</feature>
<feature>
<name>part-name</name>
</feature>
<feature>
<name>pitch</name>
</feature>
<feature>
<name>score-part</name>
<attributes>
<attribute>id</attribute>
</attributes>
</feature>
<feature>
<name>score-partwise</name>
<attributes>
<attribute>version</attribute>
</attributes>
</feature>
<feature>
<name>step</name>
</feature>
<feature>
<name>tie</name>
<attributes>
<attribute>type</attribute>
</attributes>
</feature>
<feature>
<name>tied</name>
<attributes>
<attribute>type</attribute>
</attributes>
</feature>
<feature>
<name>type</name>
</feature>
<feature>
<name>voice</name>
</feature>
</features>
</feature-audit>
104 changes: 104 additions & 0 deletions data/synthetic/tied.cue.4.0.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<score-partwise version="4.0">
<part-list>
<score-part id="id1">
<part-name>x</part-name>
</score-part>
</part-list>
<part id="id1">
<measure number="1">
<attributes>
<divisions>4</divisions>
</attributes>
<note>
<cue />
<pitch>
<step>C</step>
<octave>4</octave>
</pitch>
<duration>4</duration>
<voice>1</voice>
<type>quarter</type>
<notations>
<tied type="start" />
</notations>
</note>
<note>
<cue />
<pitch>
<step>C</step>
<octave>4</octave>
</pitch>
<duration>4</duration>
<voice>1</voice>
<type>quarter</type>
<notations>
<tied type="stop" />
</notations>
</note>
<note>
<grace />
<cue />
<pitch>
<step>D</step>
<octave>4</octave>
</pitch>
<voice>1</voice>
<type>eighth</type>
<notations>
<tied type="start" />
</notations>
</note>
<note>
<cue />
<pitch>
<step>D</step>
<octave>4</octave>
</pitch>
<duration>8</duration>
<voice>1</voice>
<type>half</type>
<notations>
<tied type="stop" />
</notations>
</note>
</measure>
<measure number="2">
<note>
<grace />
<pitch>
<step>F</step>
<octave>4</octave>
</pitch>
<tie type="start" />
<voice>1</voice>
<type>eighth</type>
<notations>
<tied type="start" />
</notations>
</note>
<note>
<pitch>
<step>F</step>
<octave>4</octave>
</pitch>
<duration>8</duration>
<tie type="stop" />
<voice>1</voice>
<type>half</type>
<notations>
<tied type="stop" />
</notations>
</note>
<note>
<pitch>
<step>E</step>
<octave>4</octave>
</pitch>
<duration>8</duration>
<voice>1</voice>
<type>half</type>
</note>
</measure>
</part>
</score-partwise>
10 changes: 7 additions & 3 deletions src/include/mx/api/NoteData.h
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,11 @@ class NoteData
// tag, but subsequent chord notes do have the tag).
bool isChord;

// One field, two encodings: on write these emit both <tie> (sound) and
// <tied> (notation), so the two can never contradict each other.
// One field, two encodings: MusicXML states a tie twice, once for sound
// (<tie>) and once for notation (<tied>), so setting one of these emits
// both and the two can never contradict each other. On a cue or grace-cue
// note only the notation is emitted -- those notes are silent, and the
// schema gives them no <tie> -- so the tie is visible but not played.
bool isTieStart;
bool isTieStop;

Expand All @@ -125,7 +128,8 @@ class NoteData
// are the four combinations of these two independent flags. Schema facts:
// a grace note carries no <duration> on the wire (durationTimeTicks reads
// as 0 and is ignored on write), and cue notes -- including grace-cue
// notes -- cannot carry <tie> (ties on them are silently dropped on write).
// notes -- cannot carry <tie>, so a tie on one is notation only (see
// isTieStart / isTieStop).
bool isGrace;
// <grace>'s slash attribute. Only meaningful when isGrace is true.
Bool graceSlash;
Expand Down
19 changes: 13 additions & 6 deletions src/private/mx/api/ScoreData.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

#include "mx/api/ScoreData.h"

#include <algorithm>

namespace mx
{
namespace api
Expand Down Expand Up @@ -45,6 +47,11 @@ int ScoreData::getNumStavesPerSystem() const
return numStaves;
}

// Sorts by tick position only, so items sharing a tick keep the order they were
// given. That order is meaningful and must not be disturbed: the members of a
// chord all sit at one tick, and MusicXML encodes a chord by omitting <chord>
// from its first note. A plain std::sort is unstable, so the chord's spelling
// would depend on the standard library implementation.
void ScoreData::sort()
{
for (auto &part : parts)
Expand All @@ -54,25 +61,25 @@ void ScoreData::sort()
for (auto &staff : measure.staves)
{

const auto clefCompare = [&](ClefData &inLeft, ClefData &inRight) {
const auto clefCompare = [](const ClefData &inLeft, const ClefData &inRight) {
return inLeft.tickTimePosition < inRight.tickTimePosition;
};

std::sort(std::begin(staff.clefs), std::end(staff.clefs), clefCompare);
std::stable_sort(std::begin(staff.clefs), std::end(staff.clefs), clefCompare);

const auto directionCompare = [&](DirectionData &inLeft, DirectionData &inRight) {
const auto directionCompare = [](const DirectionData &inLeft, const DirectionData &inRight) {
return inLeft.tickTimePosition < inRight.tickTimePosition;
};

std::sort(std::begin(staff.directions), std::end(staff.directions), directionCompare);
std::stable_sort(std::begin(staff.directions), std::end(staff.directions), directionCompare);

for (auto &voice : staff.voices)
{
const auto noteCompare = [&](NoteData &inLeft, NoteData &inRight) {
const auto noteCompare = [](const NoteData &inLeft, const NoteData &inRight) {
return inLeft.tickTimePosition < inRight.tickTimePosition;
};

std::sort(std::begin(voice.second.notes), std::end(voice.second.notes), noteCompare);
std::stable_sort(std::begin(voice.second.notes), std::end(voice.second.notes), noteCompare);
}
}
}
Expand Down
38 changes: 37 additions & 1 deletion src/private/mx/impl/NoteReader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@
#include "mx/core/generated/LyricChoice.h"
#include "mx/core/generated/LyricSyllableGroup.h"
#include "mx/core/generated/LyricTextGroup.h"
#include "mx/core/generated/Notations.h"
#include "mx/core/generated/NotationsChoice.h"
#include "mx/core/generated/Syllabic.h"
#include "mx/core/generated/TextElementData.h"
#include "mx/core/generated/Tied.h"
#include "mx/impl/FontFunctions.h"
#include "mx/impl/PositionFunctions.h"
#include "mx/impl/PrintFunctions.h"
Expand Down Expand Up @@ -221,15 +224,19 @@ void NoteReader::setNormalGraceCueItems()
else
{
// <grace/> + <cue/>: a grace note inside a cue passage. The
// grace-cue group carries no <tie> in the schema.
// grace-cue group has no <tie> slot in the schema, so the tie can
// only be stated as a <tied> notation.
myIsCue = true;
setTieFromNotations();
}
break;
}
case core::NoteChoice::Kind::cueNoteGroup: {
myIsCue = true;
const auto &noteGuts = myNoteChoice.asCueNoteGroup();
myDurationValue = noteGuts.duration().value().value();
// A cue note is silent and has no <tie> slot either; same as grace-cue.
setTieFromNotations();
break;
}
default:
Expand Down Expand Up @@ -478,6 +485,35 @@ void NoteReader::setTie(std::span<const core::Tie> tieSet)
}
}

// Cue and grace-cue notes have no <tie> slot in the schema, so their tie can
// only be stated as a <tied> notation. Reads the start/stop flags from there.
// (A lone <tied type="let-ring"> is a different thing and is read elsewhere,
// into NoteData::tieLetRing.)
void NoteReader::setTieFromNotations()
{
for (const auto &notations : myNote.notations())
{
for (const auto &notationsChoice : notations.choice())
{
if (notationsChoice.kind() != core::NotationsChoice::Kind::tied)
{
continue;
}

const auto type = notationsChoice.asTied().type();

if (type == core::TiedType::start())
{
myIsTieStart = true;
}
else if (type == core::TiedType::stop())
{
myIsTieStop = true;
}
}
}
}

void NoteReader::setLyric()
{
const auto lyricSet = myNote.lyric();
Expand Down
1 change: 1 addition & 0 deletions src/private/mx/impl/NoteReader.h
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,7 @@ class NoteReader
void setAccidental();
void setStem();
void setTie(std::span<const core::Tie> tieSet);
void setTieFromNotations();
void setLyric();
};
} // namespace impl
Expand Down
Loading
Loading