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
15 changes: 15 additions & 0 deletions src/include/mx/api/NoteData.h
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,19 @@ class NoteData
std::optional<std::string> noteheadSmufl;

PitchData pitchData; // step, alter, octave, accidental, etc

// How hard this note is struck on playback, as a percentage of the default forte level: 100
// is that default and 50 is half as hard. This is MusicXML's dynamics attribute on <note>,
// and MIDI's Note On velocity. It describes one performance of this note. The dynamic
// marking printed above the staff is a separate thing -- a MarkData in noteAttachmentData --
// so leave this empty and playback follows the printed marking.
std::optional<double> velocityStart;

// How hard this note is released on playback, on the same percentage scale as velocityStart.
// This is MusicXML's end-dynamics attribute on <note>, and MIDI's Note Off velocity. Few
// instruments respond to it, so leave it empty unless the release really matters.
std::optional<double> velocityStop;

int userRequestedVoiceNumber;

// Most users can ignore this; leave it unspecified. It only controls whether the note's
Expand Down Expand Up @@ -230,6 +243,8 @@ MXAPI_EQUALS_MEMBER(notehead)
MXAPI_EQUALS_MEMBER(noteheadFilled)
MXAPI_EQUALS_MEMBER(noteheadSmufl)
MXAPI_EQUALS_MEMBER(pitchData)
MXAPI_EQUALS_MEMBER(velocityStart)
MXAPI_EQUALS_MEMBER(velocityStop)
MXAPI_EQUALS_MEMBER(userRequestedVoiceNumber)
MXAPI_EQUALS_MEMBER(writeStaffNumber)
MXAPI_EQUALS_MEMBER(crossStaffIndex)
Expand Down
7 changes: 4 additions & 3 deletions src/private/mx/api/NoteData.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ 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}, noteheadFilled{Bool::unspecified}, noteheadSmufl{}, pitchData{},
userRequestedVoiceNumber{VALUE_UNSPECIFIED}, writeStaffNumber{Bool::unspecified}, stem{Stem::unspecified},
tickTimePosition{0}, durationData{}, beams{}, positionData{}, printData{}, noteAttachmentData{}, lyrics{}
notehead{Notehead::normal}, noteheadFilled{Bool::unspecified}, noteheadSmufl{}, pitchData{}, velocityStart{},
velocityStop{}, userRequestedVoiceNumber{VALUE_UNSPECIFIED}, writeStaffNumber{Bool::unspecified},
stem{Stem::unspecified}, tickTimePosition{0}, durationData{}, beams{}, positionData{}, printData{},
noteAttachmentData{}, lyrics{}
{
}
} // namespace api
Expand Down
11 changes: 11 additions & 0 deletions src/private/mx/impl/NoteFunctions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,17 @@ api::NoteData NoteFunctions::parseNote() const
}

myOutNoteData.pitchData.octave = reader.getOctave();

if (myNote.dynamics().has_value())
{
myOutNoteData.velocityStart = myNote.dynamics()->value().value();
}

if (myNote.endDynamics().has_value())
{
myOutNoteData.velocityStop = myNote.endDynamics()->value().value();
}

myOutNoteData.userRequestedVoiceNumber = reader.getVoiceNumber();

// Auto rule (see NoteData::writeStaffNumber): <staff> is included on a multi-staff part and
Expand Down
12 changes: 12 additions & 0 deletions src/private/mx/impl/NoteWriter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// Distributed under the MIT License

#include "mx/impl/NoteWriter.h"
#include "mx/core/Decimal.h"
#include "mx/core/NameToken.h"
#include "mx/core/generated/Accidental.h"
#include "mx/core/generated/BeamLevel.h"
Expand All @@ -17,6 +18,7 @@
#include "mx/core/generated/GraceNoteGroup.h"
#include "mx/core/generated/LyricChoice.h"
#include "mx/core/generated/LyricTextGroup.h"
#include "mx/core/generated/NonNegativeDecimal.h"
#include "mx/core/generated/NormalNoteGroup.h"
#include "mx/core/generated/Pitch.h"
#include "mx/core/generated/Rest.h"
Expand Down Expand Up @@ -94,6 +96,16 @@ core::Note NoteWriter::getNote(bool isStartOfChord) const
myOutNote.setPrintObject(myConverter.convert(myNoteData.printData.printObject));
}

if (myNoteData.velocityStart.has_value())
{
myOutNote.setDynamics(core::NonNegativeDecimal{core::Decimal{*myNoteData.velocityStart}});
}

if (myNoteData.velocityStop.has_value())
{
myOutNote.setEndDynamics(core::NonNegativeDecimal{core::Decimal{*myNoteData.velocityStop}});
}

// The tie <notations> come first (as in the old writer, where they were
// created during setNoteChoiceAndFullNoteGroup).
if (!myOutTieNotationsChoices.empty())
Expand Down
93 changes: 93 additions & 0 deletions src/private/mxtest/api/NoteVelocityApiTest.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// MusicXML Class Library
// Copyright (c) by Matthew James Briggs
// Distributed under the MIT License

#include "mxtest/control/CompileControl.h"
#ifdef MX_COMPILE_API_TESTS

#include "cpul/cpulTestHarness.h"
#include "mx/api/DocumentManager.h"
#include "mxtest/api/RoundTrip.h"
#include "mxtest/api/TestHelpers.h"

using namespace std;
using namespace mx::api;
using namespace mxtest;

// A one-measure, one-note score. The caller fills in the note's velocities.
ScoreData noteVelocityMakeScore()
{
ScoreData score;
score.ticksPerQuarter = 4;
score.parts.emplace_back();
auto &part = score.parts.back();
part.measures.emplace_back();
auto &measure = part.measures.back();
measure.staves.emplace_back();
auto &staff = measure.staves.back();
auto &voice = staff.voices[0];
voice.notes.emplace_back();
auto &note = voice.notes.back();
note.durationData.durationName = DurationName::quarter;
note.durationData.durationTimeTicks = 4;
return score;
}

const NoteData &noteVelocityFirstNote(const ScoreData &score)
{
return score.parts.at(0).measures.at(0).staves.at(0).voices.at(0).notes.at(0);
}

TEST(velocitiesSurviveRoundTrip, NoteVelocity)
{
auto score = noteVelocityMakeScore();
auto &note = score.parts.at(0).measures.at(0).staves.at(0).voices.at(0).notes.at(0);
note.velocityStart = 71.5;
note.velocityStop = 40.0;

const auto out = roundTrip(score);
const auto &outNote = noteVelocityFirstNote(out);
REQUIRE(outNote.velocityStart.has_value());
REQUIRE(outNote.velocityStop.has_value());
CHECK_DOUBLES_EQUAL(71.5, *outNote.velocityStart, 0.0001);
CHECK_DOUBLES_EQUAL(40.0, *outNote.velocityStop, 0.0001);
}

TEST(velocitiesWriteMusicXmlAttributes, NoteVelocity)
{
auto score = noteVelocityMakeScore();
auto &note = score.parts.at(0).measures.at(0).staves.at(0).voices.at(0).notes.at(0);
note.velocityStart = 100.0;
note.velocityStop = 60.0;

const auto xml = toXml(score);
CHECK(xml.find("dynamics=\"100\"") != std::string::npos);
CHECK(xml.find("end-dynamics=\"60\"") != std::string::npos);
}

TEST(absentVelocitiesWriteNothing, NoteVelocity)
{
const auto score = noteVelocityMakeScore();
const auto xml = toXml(score);
CHECK(xml.find("dynamics=") == std::string::npos);

const auto out = roundTrip(score);
const auto &outNote = noteVelocityFirstNote(out);
CHECK(!outNote.velocityStart.has_value());
CHECK(!outNote.velocityStop.has_value());
}

TEST(velocitiesAreReadFromMusicXml, NoteVelocity)
{
auto score = noteVelocityMakeScore();
auto &note = score.parts.at(0).measures.at(0).staves.at(0).voices.at(0).notes.at(0);
note.velocityStart = 12.5;

const auto in = fromXml(toXml(score));
const auto &inNote = noteVelocityFirstNote(in);
REQUIRE(inNote.velocityStart.has_value());
CHECK_DOUBLES_EQUAL(12.5, *inNote.velocityStart, 0.0001);
CHECK(!inNote.velocityStop.has_value());
}

#endif
17 changes: 17 additions & 0 deletions src/private/mxtest/api/roundtrip-baseline.txt
Original file line number Diff line number Diff line change
Expand Up @@ -669,3 +669,20 @@ synthetic/wavy-line.4.0.xml
# earlier work and simply had not been pinned yet.
lysuite/ly71c_ChordsFrets.xml
musuite/testHarmony3.xml

# Unblocked by note playback velocity: the api had no home for the <note> dynamics
# attribute, so a note that named its own playback velocity lost it on write. It is
# now NoteData::velocityStart (with velocityStop for end-dynamics).
foundsuite/Invention 2.xml
foundsuite/Invention 3.xml
foundsuite/Invention_10.xml
foundsuite/Invention_11.xml
foundsuite/Invention_12.xml
foundsuite/Invention_13.xml
foundsuite/Invention_14.xml
foundsuite/Invention_15.xml
foundsuite/Invention_4.xml
foundsuite/Invention_6.xml
foundsuite/Invention_7.xml
foundsuite/Invention_8.xml
foundsuite/Invention_9.xml
Loading