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
7 changes: 7 additions & 0 deletions src/include/mx/api/NoteData.h
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,12 @@ class NoteData
PitchData pitchData; // step, alter, octave, accidental, etc
int userRequestedVoiceNumber;

// Whether the source note carried an explicit <staff> element. mx::api normally only
// writes <staff> when the part has more than one staff (where it is structurally
// required); this preserves a source's redundant-but-legal <staff> on a single-staff
// part instead of silently dropping it. Mirrors DirectionData::isStaffValueSpecified.
bool isStaffValueSpecified;

// The zero-based index of the staff on which this note is displayed, for the rare case
// that it differs from the staff that contains the note (cross-staff notation, e.g. a
// beamed piano run or a chord that dips onto the other staff). The note stays in its
Expand Down Expand Up @@ -189,6 +195,7 @@ MXAPI_EQUALS_MEMBER(isCue)
MXAPI_EQUALS_MEMBER(graceSlash)
MXAPI_EQUALS_MEMBER(pitchData)
MXAPI_EQUALS_MEMBER(userRequestedVoiceNumber)
MXAPI_EQUALS_MEMBER(isStaffValueSpecified)
MXAPI_EQUALS_MEMBER(crossStaffIndex)
MXAPI_EQUALS_MEMBER(stem)
MXAPI_EQUALS_MEMBER(stemPositionData)
Expand Down
5 changes: 3 additions & 2 deletions src/private/mx/api/NoteData.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ namespace api
NoteData::NoteData()
: isRest{false}, isMeasureRest{false}, isUnpitched{false}, isDisplayStepOctaveSpecified{false}, isChord{false},
isTieStart{false}, isTieStop{false}, tieLetRing{}, isGrace{false}, graceSlash{Bool::unspecified}, isCue{false},
notehead{Notehead::normal}, pitchData{}, userRequestedVoiceNumber{VALUE_UNSPECIFIED}, stem{Stem::unspecified},
tickTimePosition{0}, durationData{}, beams{}, positionData{}, printData{}, noteAttachmentData{}, lyrics{}
notehead{Notehead::normal}, pitchData{}, userRequestedVoiceNumber{VALUE_UNSPECIFIED},
isStaffValueSpecified{false}, stem{Stem::unspecified}, tickTimePosition{0}, durationData{}, beams{},
positionData{}, printData{}, noteAttachmentData{}, lyrics{}
{
}
} // namespace api
Expand Down
1 change: 1 addition & 0 deletions src/private/mx/impl/NoteFunctions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ api::NoteData NoteFunctions::parseNote() const

myOutNoteData.pitchData.octave = reader.getOctave();
myOutNoteData.userRequestedVoiceNumber = reader.getVoiceNumber();
myOutNoteData.isStaffValueSpecified = reader.getIsStaffSpecified();

myOutNoteData.notehead = converter.convert(reader.getNoteheadValue());

Expand Down
7 changes: 4 additions & 3 deletions src/private/mx/impl/NoteReader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,8 @@ NoteReader::NoteReader(const core::Note &mxNote)
: myNote(mxNote), myNoteChoice(myNote.choice()), myFullNoteGroup(findFullNoteGroup(myNoteChoice)),
myIsNormal(false), myIsGrace(false), myIsCue(false), myIsRest(false), myIsChord(false), myIsMeasureRest(false),
myIsUnpitched(false), myIsPitch(false), myIsDisplayStepOctaveSpecified(false), myDurationValue(0.0),
myStep(core::Step::c()), myAlter(0), myCents(0.0), myOctave(4), myStaffNumber(0), myVoiceNumber(0),
myNoteheadValue(core::NoteheadValue::normal()), myDurationType(core::NoteTypeValue::maxima()),
myStep(core::Step::c()), myAlter(0), myCents(0.0), myOctave(4), myStaffNumber(0), myIsStaffSpecified(false),
myVoiceNumber(0), myNoteheadValue(core::NoteheadValue::normal()), myDurationType(core::NoteTypeValue::maxima()),
myIsDurationTypeSpecified(false), myNumDots(0), myBeams(), myTimeModificationActualNotes(-1),
myTimeModificationNormalNotes(-1), myTimeModificationNormalType(core::NoteTypeValue::maxima()),
myTimeModificationNormalTypeDots(0), myHasAccidental(false), myAccidental(core::AccidentalValue::natural()),
Expand Down Expand Up @@ -304,7 +304,8 @@ void NoteReader::setChord()

void NoteReader::setStaffNumber()
{
if (myNote.staff().has_value())
myIsStaffSpecified = myNote.staff().has_value();
if (myIsStaffSpecified)
{
myStaffNumber = *myNote.staff();
}
Expand Down
6 changes: 6 additions & 0 deletions src/private/mx/impl/NoteReader.h
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,11 @@ class NoteReader
return myStaffNumber;
}

inline bool getIsStaffSpecified() const
{
return myIsStaffSpecified;
}

inline int getVoiceNumber() const
{
return myVoiceNumber;
Expand Down Expand Up @@ -251,6 +256,7 @@ class NoteReader
double myCents;
int myOctave;
int myStaffNumber;
bool myIsStaffSpecified;
int myVoiceNumber;
core::NoteheadValue myNoteheadValue;
core::NoteTypeValue myDurationType;
Expand Down
2 changes: 1 addition & 1 deletion src/private/mx/impl/NoteWriter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,7 @@ void NoteWriter::setStaffAndVoice() const
// position, but the displayed staff comes from the override (NoteData.h)
myOutNote.setStaff(*myNoteData.crossStaffIndex + 1);
}
else if (myCursor.getNumStaves() > 1 && myCursor.staffIndex >= 0)
else if (myCursor.staffIndex >= 0 && (myCursor.getNumStaves() > 1 || myNoteData.isStaffValueSpecified))
{
myOutNote.setStaff(myCursor.staffIndex + 1);
}
Expand Down
60 changes: 60 additions & 0 deletions src/private/mxtest/api/NoteDataTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include "mx/core/generated/Document.h"
#include "mx/core/generated/MusicDataChoice.h"
#include "mxtest/api/RoundTrip.h"
#include "mxtest/api/TestHelpers.h"
#include "mxtest/file/Path.h"

using namespace std;
Expand Down Expand Up @@ -1480,4 +1481,63 @@ TEST(strongAccentDirection, NoteData)

T_END;

TEST(explicitStaffOnSingleStaffPartRoundTrips, NoteData)
{
const std::string xml = R"(<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<score-partwise version="3.0">
<part-list>
<score-part id="P1">
<part-name>x</part-name>
</score-part>
</part-list>
<part id="P1">
<measure number="1">
<attributes>
<divisions>1</divisions>
</attributes>
<note>
<pitch><step>C</step><octave>4</octave></pitch>
<duration>1</duration>
<voice>1</voice>
<type>quarter</type>
<staff>1</staff>
</note>
</measure>
</part>
</score-partwise>
)";

const auto score = fromXml(xml);
const auto &notes = score.parts.back().measures.back().staves.back().voices.at(0).notes;
REQUIRE(notes.size() == 1);
CHECK(notes.at(0).isStaffValueSpecified);

const auto outXml = toXml(score);
CHECK(outXml.find("<staff>1</staff>") != std::string::npos);
}

T_END;

TEST(implicitStaffOnSingleStaffPartOmitsElement, NoteData)
{
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 &voice = measure.staves.back().voices[0];

NoteData note;
note.tickTimePosition = 0;
note.durationData.durationTimeTicks = score.ticksPerQuarter;
note.durationData.durationName = DurationName::quarter;
voice.notes.push_back(note);

const auto xml = toXml(score);
CHECK(xml.find("<staff>") == std::string::npos);
}

T_END;

#endif
5 changes: 5 additions & 0 deletions src/private/mxtest/api/roundtrip-baseline.txt
Original file line number Diff line number Diff line change
Expand Up @@ -298,3 +298,8 @@ musuite/testAccidentals2.xml
# MeasureWriter: the field and its Converter existed but neither side ever
# called it, so <measure-numbering> was silently dropped on round-trip.
ksuite/k015a_System_Layout.xml

# Unblocked by NoteData::isStaffValueSpecified: NoteWriter only wrote <staff>
# for multi-staff parts, dropping a source's explicit (but structurally
# redundant) <staff> on a single-staff part.
lysuite/ly24c_GraceNote_MeasureEnd.xml
Loading