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
2 changes: 1 addition & 1 deletion docs/ai/api-feature-audit.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ so these boxes are status markers, not clickable; the live checklist is the pare
| [ ] | technical marks with payloads (`fingering`, `pluck`, `bend`, ...) | 1c | #185 |
| [x] | read `<print>` per-measure layout | 2(1) | #186 |
| [x] | `<credit>` gaps (credit-image, no-words credits, multiple credit-type) | 2(2) | #187 |
| [ ] | read and write `<sound>` | 2(3) | #188 |
| [x] | read and write `<sound>` | 2(3) | #188 |
| [x] | defaults fonts (`word-font`, `lyric-font`, `music-font`) | 2(4) | #189 |
| [ ] | round-trip `<figured-bass>` | 2(5) | #190 |
| [ ] | harmony `inversion`, `function`, `numeral` | 2(6) | #191 |
Expand Down
19 changes: 14 additions & 5 deletions src/include/mx/api/DirectionData.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include "mx/api/OttavaData.h"
#include "mx/api/RehearsalData.h"
#include "mx/api/SegnoData.h"
#include "mx/api/SoundData.h"
#include "mx/api/TempoData.h"
#include "mx/api/WedgeData.h"
#include "mx/api/WordsData.h"
Expand Down Expand Up @@ -93,7 +94,12 @@ struct DirectionData
// only relevant for Directions placed on staff zero, it is otherwise ignored.
bool isStaffValueSpecified;

// TODO - sound element
// The <sound> element carried by this direction. It is only meaningful when
// isSoundDataSpecified is true. A direction whose only content is a sound round-trips as a
// standalone <sound> within the measure; a direction with other content writes the sound as a
// child of the <direction> element.
bool isSoundDataSpecified;
SoundData soundData;

std::vector<TempoData> tempos;
std::vector<MarkData> marks;
Expand All @@ -115,9 +121,10 @@ struct DirectionData
std::vector<DirectionComponent> orderedComponents;

DirectionData()
: tickTimePosition{0}, placement{Placement::unspecified}, voice{-1}, isStaffValueSpecified{true}, marks{},
wedgeStarts{}, wedgeStops{}, ottavaStarts{}, ottavaStops{}, bracketStarts{}, bracketStops{}, dashesStarts{},
dashesStops{}, pedalStarts{}, pedalStops{}, words{}, chords{}, segnos{}
: tickTimePosition{0}, placement{Placement::unspecified}, voice{-1}, isStaffValueSpecified{true},
isSoundDataSpecified{false}, soundData{}, marks{}, wedgeStarts{}, wedgeStops{}, ottavaStarts{}, ottavaStops{},
bracketStarts{}, bracketStops{}, dashesStarts{}, dashesStops{}, pedalStarts{}, pedalStops{}, words{},
chords{}, segnos{}
{
}
};
Expand All @@ -131,7 +138,7 @@ inline bool isDirectionDataEmpty(const DirectionData &directionData)
directionData.pedalStarts.size() == 0 && directionData.pedalStops.size() == 0 &&
directionData.tempos.size() == 0 && directionData.ottavaStarts.size() == 0 &&
directionData.ottavaStops.size() == 0 && directionData.words.size() == 0 &&
directionData.segnos.size() == 0 && directionData.codas.size() == 0 &&
directionData.segnos.size() == 0 && directionData.codas.size() == 0 && !directionData.isSoundDataSpecified &&
directionData.orderedComponents.size() == 0;
}

Expand All @@ -140,6 +147,8 @@ MXAPI_EQUALS_MEMBER(tickTimePosition)
MXAPI_EQUALS_MEMBER(placement)
MXAPI_EQUALS_MEMBER(voice)
MXAPI_EQUALS_MEMBER(isStaffValueSpecified)
MXAPI_EQUALS_MEMBER(isSoundDataSpecified)
MXAPI_EQUALS_MEMBER(soundData)
MXAPI_EQUALS_MEMBER(tempos)
MXAPI_EQUALS_MEMBER(marks)
MXAPI_EQUALS_MEMBER(wedgeStarts)
Expand Down
83 changes: 83 additions & 0 deletions src/include/mx/api/SoundData.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// MusicXML Class Library
// Copyright (c) by Matthew James Briggs
// Distributed under the MIT License

#pragma once

#include "mx/api/ApiCommon.h"

#include <string>

namespace mx
{
namespace api
{
// MusicXML Documentation: The sound element contains general playback parameters. They can stand
// alone within a part/measure, or be a component element within a direction.
//
// mx::api models the commonly-used scalar attributes of <sound>. The nested child elements
// (<midi-instrument>, <midi-device>, <play>, <swing>, <offset>) are intentionally not modeled.
//
// A SoundData is carried on DirectionData. When a DirectionData holds a SoundData but no other
// direction content, it round-trips as a standalone <sound> element within the measure. When a
// DirectionData holds other direction content in addition to the SoundData, the <sound> is written
// as a child of the <direction> element.
struct SoundData
{
// tempo in quarter notes per minute. A value less than 0 means 'unspecified'.
double tempo;

// dynamics (MIDI velocity) as a percentage of the default forte value. A value less than 0
// means 'unspecified'.
double dynamics;

// Dacapo indicates to go back to the beginning of the movement. When used it always has the
// value 'yes'.
Bool dacapo;

// forward-repeat indicates that a forward repeat sign is implied but not displayed. When used
// it always has the value 'yes'.
Bool forwardRepeat;

// Pizzicato in a sound element effects all following notes. 'yes' indicates pizzicato, 'no'
// indicates arco.
Bool pizzicato;

// Segno and dalsegno are used for backwards jumps to a segno sign; coda and tocoda are used for
// forward jumps to a coda sign. The fine attribute follows the final note or rest in a movement
// with a da capo or dal segno direction. These are strings; an empty string means 'unspecified'.
std::string segno;
std::string dalsegno;
std::string coda;
std::string tocoda;
std::string fine;

SoundData()
: tempo{-1.0}, dynamics{-1.0}, dacapo{Bool::unspecified}, forwardRepeat{Bool::unspecified},
pizzicato{Bool::unspecified}, segno{}, dalsegno{}, coda{}, tocoda{}, fine{}
{
}

bool isSpecified() const
{
return tempo >= 0.0 || dynamics >= 0.0 || dacapo != Bool::unspecified || forwardRepeat != Bool::unspecified ||
pizzicato != Bool::unspecified || !segno.empty() || !dalsegno.empty() || !coda.empty() ||
!tocoda.empty() || !fine.empty();
}
};

MXAPI_EQUALS_BEGIN(SoundData)
MXAPI_DOUBLES_EQUALS_MEMBER(tempo)
MXAPI_DOUBLES_EQUALS_MEMBER(dynamics)
MXAPI_EQUALS_MEMBER(dacapo)
MXAPI_EQUALS_MEMBER(forwardRepeat)
MXAPI_EQUALS_MEMBER(pizzicato)
MXAPI_EQUALS_MEMBER(segno)
MXAPI_EQUALS_MEMBER(dalsegno)
MXAPI_EQUALS_MEMBER(coda)
MXAPI_EQUALS_MEMBER(tocoda)
MXAPI_EQUALS_MEMBER(fine)
MXAPI_EQUALS_END;
MXAPI_NOT_EQUALS_AND_VECTORS(SoundData);
} // namespace api
} // namespace mx
12 changes: 12 additions & 0 deletions src/private/mx/impl/DirectionReader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
#include "mx/core/generated/RootStep.h"
#include "mx/core/generated/Scordatura.h"
#include "mx/core/generated/Segno.h"
#include "mx/core/generated/Sound.h"
#include "mx/core/generated/StartStop.h"
#include "mx/core/generated/StartStopContinue.h"
#include "mx/core/generated/Step.h"
Expand All @@ -57,6 +58,7 @@
#include "mx/impl/MarkDataFunctions.h"
#include "mx/impl/MetronomeReader.h"
#include "mx/impl/PrintFunctions.h"
#include "mx/impl/SoundFunctions.h"
#include "mx/impl/SpannerFunctions.h"
#include "mx/utility/Round.h"
#include "mx/utility/Unused.h"
Expand Down Expand Up @@ -152,6 +154,16 @@ void DirectionReader::parseValues()
{
parseDirectionType(dt);
}

if (myDirection->sound().has_value())
{
auto soundData = readSoundData(*myDirection->sound());
if (soundData.isSpecified())
{
myOutDirectionData.isSoundDataSpecified = true;
myOutDirectionData.soundData = std::move(soundData);
}
}
}
else if (myHarmony)
{
Expand Down
26 changes: 26 additions & 0 deletions src/private/mx/impl/DirectionWriter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
#include "mx/core/generated/Root.h"
#include "mx/core/generated/RootStep.h"
#include "mx/core/generated/Semitones.h"
#include "mx/core/generated/Sound.h"
#include "mx/core/generated/StartStop.h"
#include "mx/core/generated/StartStopContinue.h"
#include "mx/core/generated/String.h"
Expand All @@ -56,6 +57,7 @@
#include "mx/impl/LineFunctions.h"
#include "mx/impl/MarkDataFunctions.h"
#include "mx/impl/PrintFunctions.h"
#include "mx/impl/SoundFunctions.h"
#include "mx/impl/SpannerFunctions.h"
#include "mx/utility/Throw.h"
#include "mx/utility/Unused.h"
Expand Down Expand Up @@ -393,8 +395,32 @@ std::vector<core::MusicDataChoice> DirectionWriter::getDirectionLikeThings()

if (myIsFirstDirectionTypeAdded)
{
// The direction has other content; attach the <sound> as a child of the <direction>.
if (myDirectionData.isSoundDataSpecified && myDirectionData.soundData.isSpecified())
{
core::Sound sound{};
writeSoundData(myDirectionData.soundData, sound);
direction.setSound(std::move(sound));
}

output.push_back(core::MusicDataChoice::direction(direction));
}
else if (myDirectionData.isSoundDataSpecified && myDirectionData.soundData.isSpecified())
{
// The direction has no other content; emit a standalone <sound> element.
core::Sound sound{};
writeSoundData(myDirectionData.soundData, sound);

if (offset != 0)
{
core::Offset coreOffset{};
coreOffset.setValue(core::Divisions{core::Decimal{static_cast<double>(offset)}});
coreOffset.setSound(core::YesNo::yes());
sound.setOffset(coreOffset);
}

output.push_back(core::MusicDataChoice::sound(std::move(sound)));
}

auto harmonyMdcs = createHarmonyElements(offset);
addMusicDataChoices(harmonyMdcs, output);
Expand Down
24 changes: 22 additions & 2 deletions src/private/mx/impl/MeasureReader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
#include "mx/impl/DirectionReader.h"
#include "mx/impl/NoteFunctions.h"
#include "mx/impl/NoteReader.h"
#include "mx/impl/SoundFunctions.h"
#include "mx/impl/TimeReader.h"
#include "mx/utility/Throw.h"
#include "mx/utility/Unused.h"
Expand Down Expand Up @@ -622,8 +623,27 @@ void MeasureReader::parsePrint(const core::Print &inMxPrint) const

void MeasureReader::parseSound(const core::Sound &inMxSound) const
{
MX_UNUSED(inMxSound);
// std::cout << "sound is not supported" << std::endl;
auto soundData = readSoundData(inMxSound);

if (!soundData.isSpecified())
{
return;
}

if (myOutMeasureData.staves.empty())
{
return;
}

// A standalone <sound> has no <staff>; place it on staff 0 with isStaffValueSpecified = false
// and no other direction content, so it round-trips as a standalone <sound> element.
auto directionData = api::DirectionData{};
directionData.tickTimePosition = myCurrentCursor.tickTimePosition;
directionData.isStaffValueSpecified = false;
directionData.isSoundDataSpecified = true;
directionData.soundData = std::move(soundData);

myOutMeasureData.staves.at(0).directions.emplace_back(std::move(directionData));
}

void MeasureReader::parseBarline(const core::Barline &inMxBarline) const
Expand Down
127 changes: 127 additions & 0 deletions src/private/mx/impl/SoundFunctions.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// MusicXML Class Library
// Copyright (c) by Matthew James Briggs
// Distributed under the MIT License

#include "mx/impl/SoundFunctions.h"
#include "mx/core/Decimal.h"
#include "mx/core/generated/NonNegativeDecimal.h"
#include "mx/core/generated/Sound.h"
#include "mx/core/generated/YesNo.h"

namespace mx
{
namespace impl
{
namespace
{
api::Bool toApiBool(const std::optional<core::YesNo> &value)
{
if (!value.has_value())
{
return api::Bool::unspecified;
}

return value->tag() == core::YesNo::Tag::yes ? api::Bool::yes : api::Bool::no;
}
} // namespace

api::SoundData readSoundData(const core::Sound &inSound)
{
api::SoundData out{};

if (inSound.tempo().has_value())
{
out.tempo = inSound.tempo()->value().value();
}

if (inSound.dynamics().has_value())
{
out.dynamics = inSound.dynamics()->value().value();
}

out.dacapo = toApiBool(inSound.dacapo());
out.forwardRepeat = toApiBool(inSound.forwardRepeat());
out.pizzicato = toApiBool(inSound.pizzicato());

if (inSound.segno().has_value())
{
out.segno = *inSound.segno();
}

if (inSound.dalsegno().has_value())
{
out.dalsegno = *inSound.dalsegno();
}

if (inSound.coda().has_value())
{
out.coda = *inSound.coda();
}

if (inSound.tocoda().has_value())
{
out.tocoda = *inSound.tocoda();
}

if (inSound.fine().has_value())
{
out.fine = *inSound.fine();
}

return out;
}

void writeSoundData(const api::SoundData &inSoundData, core::Sound &outSound)
{
if (inSoundData.tempo >= 0.0)
{
outSound.setTempo(core::NonNegativeDecimal{core::Decimal{inSoundData.tempo}});
}

if (inSoundData.dynamics >= 0.0)
{
outSound.setDynamics(core::NonNegativeDecimal{core::Decimal{inSoundData.dynamics}});
}

if (inSoundData.dacapo != api::Bool::unspecified)
{
outSound.setDacapo(inSoundData.dacapo == api::Bool::yes ? core::YesNo::yes() : core::YesNo::no());
}

if (inSoundData.forwardRepeat != api::Bool::unspecified)
{
outSound.setForwardRepeat(inSoundData.forwardRepeat == api::Bool::yes ? core::YesNo::yes() : core::YesNo::no());
}

if (inSoundData.pizzicato != api::Bool::unspecified)
{
outSound.setPizzicato(inSoundData.pizzicato == api::Bool::yes ? core::YesNo::yes() : core::YesNo::no());
}

if (!inSoundData.segno.empty())
{
outSound.setSegno(inSoundData.segno);
}

if (!inSoundData.dalsegno.empty())
{
outSound.setDalsegno(inSoundData.dalsegno);
}

if (!inSoundData.coda.empty())
{
outSound.setCoda(inSoundData.coda);
}

if (!inSoundData.tocoda.empty())
{
outSound.setTocoda(inSoundData.tocoda);
}

if (!inSoundData.fine.empty())
{
outSound.setFine(inSoundData.fine);
}
}
} // namespace impl
} // namespace mx
Loading
Loading