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
4 changes: 2 additions & 2 deletions docs/ai/api-feature-audit.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,8 @@ so these boxes are status markers, not clickable; the live checklist is the pare
| [x] | `<credit>` gaps (credit-image, no-words credits, multiple credit-type) | 2(2) | #187 |
| [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 |
| [x] | round-trip `<figured-bass>` | 2(5) | #190 |
| [x] | harmony `inversion`, `function`, `numeral` | 2(6) | #191 |
| [x] | write `midi-name` on output | 3 | #192 |

Parent tracker: #159. The section-2 lower-use gaps are intentionally not tracked yet.
64 changes: 64 additions & 0 deletions src/include/mx/api/ChordData.h
Original file line number Diff line number Diff line change
Expand Up @@ -191,19 +191,72 @@ MXAPI_EQUALS_MEMBER(notes)
MXAPI_EQUALS_END;
MXAPI_NOT_EQUALS_AND_VECTORS(FrameData);

// A harmony-chord identifies its pitch source one of three mutually exclusive ways (the MusicXML
// harmony-chord choice): a 'root' (pop-style chord symbol, the default), a 'numeral' (Roman numeral
// / Nashville number for functional harmony, MusicXML 4.0), or the deprecated 'function' string.
enum class HarmonyChordSource
{
root,
numeral,
function
};

// The mode used to interpret a numeral when it differs from the key signature
// (the MusicXML numeral-mode element). MusicXML requires a numeral-mode inside every numeral-key,
// so 'unspecified' is only meaningful when there is no numeral-key; combined with hasNumeralKey it
// is written out as 'major' (see DirectionWriter and the numeralMode field below).
enum class NumeralMode
{
unspecified,
major,
minor,
naturalMinor,
melodicMinor,
harmonicMinor
};

class ChordData
{
public:
ChordData();

public:
// Which of the mutually exclusive harmony-chord alternatives is in use. Defaults to 'root',
// which preserves the historical behavior; 'root', 'rootAlter', 'numeral*', and 'functionText'
// are interpreted according to this value.
HarmonyChordSource harmonyChordSource;

Step root;
int rootAlter;

// The deprecated MusicXML <function> element: Roman numeral text such as "V". Used only when
// harmonyChordSource == function.
std::string functionText;

// The MusicXML 4.0 <numeral>. Used only when harmonyChordSource == numeral.
// numeralRoot is a scale degree 1-7; numeralRootText is its optional display text (e.g. "V").
int numeralRoot;
std::string numeralRootText;
// The numeral-alter (chromatic alteration of the numeral root), in semitones.
int numeralAlter;
bool hasNumeralAlter;
// The optional <numeral-key> (key local to the numeral): numeral-fifths plus numeral-mode.
// When hasNumeralKey is true, numeralMode must name a concrete mode: numeral-mode is required by
// MusicXML inside numeral-key, so leaving it 'unspecified' is a contradiction that the writer
// resolves by emitting 'major'.
bool hasNumeralKey;
int numeralKeyFifths;
NumeralMode numeralMode;

ChordKind chordKind;
std::string text;
Bool useSymbols;
Step bass;
int bassAlter;
// The MusicXML <inversion> (0 = root position, 1 = first inversion, ...). Present only when
// hasInversion is true.
int inversion;
bool hasInversion;
std::vector<Extension> extensions;
std::vector<MiscData> miscData;
bool hasFrameData;
Expand All @@ -212,13 +265,24 @@ class ChordData
};

MXAPI_EQUALS_BEGIN(ChordData)
MXAPI_EQUALS_MEMBER(harmonyChordSource)
MXAPI_EQUALS_MEMBER(root)
MXAPI_EQUALS_MEMBER(rootAlter)
MXAPI_EQUALS_MEMBER(functionText)
MXAPI_EQUALS_MEMBER(numeralRoot)
MXAPI_EQUALS_MEMBER(numeralRootText)
MXAPI_EQUALS_MEMBER(numeralAlter)
MXAPI_EQUALS_MEMBER(hasNumeralAlter)
MXAPI_EQUALS_MEMBER(hasNumeralKey)
MXAPI_EQUALS_MEMBER(numeralKeyFifths)
MXAPI_EQUALS_MEMBER(numeralMode)
MXAPI_EQUALS_MEMBER(chordKind)
MXAPI_EQUALS_MEMBER(text)
MXAPI_EQUALS_MEMBER(useSymbols)
MXAPI_EQUALS_MEMBER(bass)
MXAPI_EQUALS_MEMBER(bassAlter)
MXAPI_EQUALS_MEMBER(inversion)
MXAPI_EQUALS_MEMBER(hasInversion)
MXAPI_EQUALS_MEMBER(extensions)
MXAPI_EQUALS_MEMBER(miscData)
MXAPI_EQUALS_MEMBER(hasFrameData)
Expand Down
6 changes: 5 additions & 1 deletion src/include/mx/api/DirectionData.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include "mx/api/ApiCommon.h"
#include "mx/api/ChordData.h"
#include "mx/api/CodaData.h"
#include "mx/api/FiguredBassData.h"
#include "mx/api/MarkData.h"
#include "mx/api/OttavaData.h"
#include "mx/api/RehearsalData.h"
Expand Down Expand Up @@ -115,6 +116,7 @@ struct DirectionData
std::vector<SpannerStop> pedalStops;
std::vector<WordsData> words;
std::vector<ChordData> chords;
std::vector<FiguredBassData> figuredBasses;
std::vector<SegnoData> segnos;
std::vector<CodaData> codas;
std::vector<RehearsalData> rehearsals;
Expand All @@ -138,7 +140,8 @@ 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.isSoundDataSpecified &&
directionData.segnos.size() == 0 && directionData.codas.size() == 0 &&
directionData.figuredBasses.size() == 0 && !directionData.isSoundDataSpecified &&
directionData.orderedComponents.size() == 0;
}

Expand All @@ -163,6 +166,7 @@ MXAPI_EQUALS_MEMBER(pedalStarts)
MXAPI_EQUALS_MEMBER(pedalStops)
MXAPI_EQUALS_MEMBER(words)
MXAPI_EQUALS_MEMBER(chords)
MXAPI_EQUALS_MEMBER(figuredBasses)
MXAPI_EQUALS_MEMBER(segnos)
MXAPI_EQUALS_MEMBER(codas)
MXAPI_EQUALS_MEMBER(orderedComponents)
Expand Down
64 changes: 64 additions & 0 deletions src/include/mx/api/FiguredBassData.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// MusicXML Class Library
// Copyright (c) by Matthew James Briggs
// Distributed under the MIT License

#pragma once

#include "mx/api/ApiCommon.h"

#include <string>
#include <vector>

namespace mx
{
namespace api
{
// A single <figure> within a <figured-bass>. Each part is optional; an empty string means the
// corresponding child element (<prefix>, <figure-number>, <suffix>) is absent.
class FigureData
{
public:
std::string prefix;
std::string figureNumber;
std::string suffix;

FigureData() : prefix{}, figureNumber{}, suffix{}
{
}
};

MXAPI_EQUALS_BEGIN(FigureData)
MXAPI_EQUALS_MEMBER(prefix)
MXAPI_EQUALS_MEMBER(figureNumber)
MXAPI_EQUALS_MEMBER(suffix)
MXAPI_EQUALS_END;
MXAPI_NOT_EQUALS_AND_VECTORS(FigureData);

// The MusicXML <figured-bass> element: an ordered list of figures plus optional parentheses and an
// optional duration. Figured bass takes its position from the first regular note that follows it in
// score order, so it carries no explicit tick position of its own here; it is attached to the
// DirectionData whose tickTimePosition locates it.
class FiguredBassData
{
public:
std::vector<FigureData> figures;
// parentheses around the whole figured-bass group. unspecified means the attribute is absent
// (MusicXML treats absent as "no").
Bool parentheses;
// The optional <duration>, in ticks. A value less than 0 means 'unspecified' (no duration child).
int durationTimeTicks;

FiguredBassData() : figures{}, parentheses{Bool::unspecified}, durationTimeTicks{-1}
{
}
};

MXAPI_EQUALS_BEGIN(FiguredBassData)
MXAPI_EQUALS_MEMBER(figures)
MXAPI_EQUALS_MEMBER(parentheses)
MXAPI_EQUALS_MEMBER(durationTimeTicks)
MXAPI_EQUALS_END;
MXAPI_NOT_EQUALS_AND_VECTORS(FiguredBassData);

} // namespace api
} // namespace mx
7 changes: 5 additions & 2 deletions src/private/mx/api/ChordData.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,11 @@ FrameData::FrameData() : stringCount{6}, fretCount{4}, firstFret{1}, isFirstFret
}

ChordData::ChordData()
: root{Step::c}, rootAlter{0}, chordKind{ChordKind::unspecified}, text{}, useSymbols{Bool::unspecified},
bass{Step::unspecified}, bassAlter{0}, extensions{}, miscData{}, hasFrameData{false}, frameData{}, positionData{}
: harmonyChordSource{HarmonyChordSource::root}, root{Step::c}, rootAlter{0}, functionText{}, numeralRoot{0},
numeralRootText{}, numeralAlter{0}, hasNumeralAlter{false}, hasNumeralKey{false}, numeralKeyFifths{0},
numeralMode{NumeralMode::unspecified}, chordKind{ChordKind::unspecified}, text{}, useSymbols{Bool::unspecified},
bass{Step::unspecified}, bassAlter{0}, inversion{0}, hasInversion{false}, extensions{}, miscData{},
hasFrameData{false}, frameData{}, positionData{}
{
}

Expand Down
92 changes: 84 additions & 8 deletions src/private/mx/impl/DirectionReader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,15 @@
#include "mx/core/generated/HarmonyChordGroupChoice.h"
#include "mx/core/generated/HarpPedals.h"
#include "mx/core/generated/Image.h"
#include "mx/core/generated/Inversion.h"
#include "mx/core/generated/Kind.h"
#include "mx/core/generated/KindValue.h"
#include "mx/core/generated/Metronome.h"
#include "mx/core/generated/Numeral.h"
#include "mx/core/generated/NumeralKey.h"
#include "mx/core/generated/NumeralMode.h"
#include "mx/core/generated/NumeralRoot.h"
#include "mx/core/generated/NumeralValue.h"
#include "mx/core/generated/OctaveShift.h"
#include "mx/core/generated/Offset.h"
#include "mx/core/generated/OtherDirection.h"
Expand All @@ -50,6 +56,7 @@
#include "mx/core/generated/Step.h"
#include "mx/core/generated/String.h"
#include "mx/core/generated/StringMute.h"
#include "mx/core/generated/StyleText.h"
#include "mx/core/generated/UpDownStopContinue.h"
#include "mx/core/generated/Wedge.h"
#include "mx/core/generated/WedgeType.h"
Expand Down Expand Up @@ -169,10 +176,7 @@ void DirectionReader::parseValues()
{
for (const auto &hcg : myHarmony->harmonyChord())
{
if (hcg.choice().isRoot())
{
parseHarmony(*myHarmony, hcg);
}
parseHarmony(*myHarmony, hcg);
}
}
}
Expand Down Expand Up @@ -837,12 +841,78 @@ void DirectionReader::parseOtherDirection(const core::DirectionType &directionTy
void DirectionReader::parseHarmony(const core::Harmony &inHarmony, const core::HarmonyChordGroup &inGrp)
{
mx::api::ChordData chord;
const auto &root = inGrp.choice().asRoot();
chord.root = myConverter.convert(root.rootStep().value());
const auto &choice = inGrp.choice();

if (root.rootAlter().has_value())
switch (choice.kind())
{
chord.rootAlter = mx::utility::roundTo<double, int>(root.rootAlter()->value().value().value());
case core::HarmonyChordGroupChoice::Kind::root: {
chord.harmonyChordSource = api::HarmonyChordSource::root;
const auto &root = choice.asRoot();
chord.root = myConverter.convert(root.rootStep().value());

if (root.rootAlter().has_value())
{
chord.rootAlter = mx::utility::roundTo<double, int>(root.rootAlter()->value().value().value());
}
break;
}
case core::HarmonyChordGroupChoice::Kind::numeral: {
chord.harmonyChordSource = api::HarmonyChordSource::numeral;
const auto &numeral = choice.asNumeral();
chord.numeralRoot = numeral.numeralRoot().value().value();

if (numeral.numeralRoot().text().has_value())
{
chord.numeralRootText = *numeral.numeralRoot().text();
}

if (numeral.numeralAlter().has_value())
{
chord.hasNumeralAlter = true;
chord.numeralAlter = mx::utility::roundTo<double, int>(numeral.numeralAlter()->value().value().value());
}

if (numeral.numeralKey().has_value())
{
chord.hasNumeralKey = true;
const auto &numeralKey = *numeral.numeralKey();
chord.numeralKeyFifths = numeralKey.numeralFifths().value();

switch (numeralKey.numeralMode().tag())
{
case core::NumeralMode::Tag::major:
chord.numeralMode = api::NumeralMode::major;
break;
case core::NumeralMode::Tag::minor:
chord.numeralMode = api::NumeralMode::minor;
break;
case core::NumeralMode::Tag::naturalMinor:
chord.numeralMode = api::NumeralMode::naturalMinor;
break;
case core::NumeralMode::Tag::melodicMinor:
chord.numeralMode = api::NumeralMode::melodicMinor;
break;
case core::NumeralMode::Tag::harmonicMinor:
chord.numeralMode = api::NumeralMode::harmonicMinor;
break;
default:
// A core numeral mode we do not model yet: leave numeralMode unspecified rather
// than guess. No -Wswitch guard exists to flag a newly added core tag.
break;
}
}
break;
}
case core::HarmonyChordGroupChoice::Kind::function: {
chord.harmonyChordSource = api::HarmonyChordSource::function;
chord.functionText = choice.asFunction().value();
break;
}
default:
// A core harmony-chord kind we do not model yet: leave harmonyChordSource at its default
// (root) with no pitch data rather than silently misreading. No -Wswitch guard exists to
// flag a newly added core kind.
break;
}

const auto &kind = inGrp.kind();
Expand Down Expand Up @@ -877,6 +947,12 @@ void DirectionReader::parseHarmony(const core::Harmony &inHarmony, const core::H
}
}

if (inGrp.inversion().has_value())
{
chord.hasInversion = true;
chord.inversion = inGrp.inversion()->value();
}

const auto &degrees = inGrp.degree();

for (const auto &degree : degrees)
Expand Down
Loading
Loading