| llm-edit | false |
|---|
- Author: Matthew James Briggs
- License: MIT
- Supported MusicXML Version: 4.0
- Language: C++20
This project is a C++ library for working with MusicXML.
The mx::core typed model is generated by a language-agnostic code generator (gen/) which reads
the MusicXML 4.0 XSD and emits mx::core (see the Code Generation section). The
simplified mx::api layer provides a modestly simplified data model, which is mapped to MusicXML by
mx::impl.
A top-level Makefile wraps CMake and encodes the build/test configurations this project uses. It
is a convenience layer, not a replacement for CMake.
Building and running the core roundtrip suite should be as simple as:
git clone https://github.com/webern/mx.git mx
cd mx
make core-roundtrip-test
Run make (or make help) to list every target. There is a Dockerfile which defines mx-sdk,
ostensibly intended to make builds reproducible and match CI, but it is easy to work around if you
prefer not to use it (MX_TOOLCHAIN=native).
mxshould not depend on any outside libraries (no deps).mxthird-party code should be kept to a minimum.mxthird-party code should be checked into themxrepo and compiled as part ofmx.mxshould not depend on a package manager.
The above build tenets lead to the vendoring of pugixml. However, it is possible to use your own
copy of pugixml or to substitute a different XML backend. See src/private/mx/xml/ and the CMake
option MX_XML_BACKEND. cmake/backend-pugixml/CMakeLists.txt is a complete example.
mx is designed such that invalid scalar values are typically supported by clamping the invalid
value to a valid one. For example, in test files I have discovered many cases where MuseScore wrote
-1.11 as a dynamics value. This is invalid according to the spec, which says:
<xs:attribute name="dynamics" type="non-negative-decimal"/>So in this case, mx will load the file and replace -1.11 with 0. Diagnostic messages are recorded
for such cases, which can be inspected after reading and writing. See
src/include/mx/api/Diagnostics.h.
The following script demonstrates how you can start a new CMake project that uses mx by committing
its source code into your project.
src/private/mx/core/generated/ is generated by the gen/ code generator; do not edit it directly.
Regenerate with make gen-cpp. See gen/README.md and gen/AGENTS.md.
The mx::api namespace is intended to be a simplified structural representation of MusicXML. It
should be more intuitive than manipulating the DOM representation directly. In particular, voices
and time positions are more explicitly managed. Some complexities, on the other hand, are retained
in mx::api, such as the need to manage beam starts and stops explicitly.
#include <string>
#include <iostream>
#include <cstdint>
#include <sstream>
#include <utility>
#include "mx/api/MusicXml.h"
#include "mx/api/ScoreData.h"
// set this to 1 if you want to see the XML in your console
#define MX_WRITE_THIS_TO_THE_CONSOLE 0
int main(int argc, const char * argv[])
{
using namespace mx::api;
const auto qticks = 4;
// create a score
auto score = ScoreData{};
score.workTitle = "Mx Example";
score.composer = "Matthew James Briggs";
score.copyright = "Copyright (c) 2019";
score.ticksPerQuarter = qticks;
// create a part
score.parts.emplace_back( PartData{} );
auto& part = score.parts.back();
// give the part a name
part.name = "Flute";
part.abbreviation = "Fl.";
part.displayName = "Flute";
part.displayAbbreviation = "Fl.";
// give the part an instrument
part.instrumentData.soundID = SoundID::windFlutesFlute;
part.instrumentData.midiData.channel = 1;
part.instrumentData.midiData.program = 74;
// add a measure
part.measures.emplace_back( MeasureData{} );
auto& measure = part.measures.back();
measure.timeSignature = TimeChoice( TimeSignatureData{ "4", "4" } );
measure.timeSignature.isImplicit = false;
// add a staff
measure.staves.emplace_back( StaffData{} );
auto& staff = measure.staves.back();
// set the clef
auto clef = ClefData{};
clef.setTreble();
staff.clefs.emplace_back( clef );
// add a voice
staff.voices[0] = VoiceData{};
auto& voice = staff.voices.at( 0 );
const auto quarter = qticks;
const auto half = qticks * 2;
const auto eighth = qticks / 2;
// add a few notes
auto currentTime = 0;
auto note = NoteData{};
note.pitchData.step = Step::d;
note.pitchData.alter = 1;
note.pitchData.octave = 5;
note.pitchData.accidental = Accidental::sharp;
note.durationData.durationName = DurationName::half;
note.durationData.durationTimeTicks = half;
note.tickTimePosition = currentTime;
voice.notes.push_back( note );
// advance our time
currentTime += half;
note.pitchData.step = Step::e;
note.pitchData.alter = 0;
note.pitchData.octave = 5;
note.pitchData.accidental = Accidental::none;
note.durationData.durationName = DurationName::eighth;
note.durationData.durationTimeTicks = eighth;
note.tickTimePosition = currentTime;
// beams are handled explicitly in MusicXML
note.beams.push_back( Beam::begin ); // start an eighth-note beam
voice.notes.push_back( note );
currentTime += eighth;
note.pitchData.step = Step::f;
note.pitchData.alter = 0;
note.pitchData.octave = 5;
note.pitchData.accidental = Accidental::none;
note.durationData.durationName = DurationName::eighth;
note.tickTimePosition = currentTime;
note.durationData.durationTimeTicks = eighth;
note.beams.clear();
note.beams.push_back( Beam::end ); // end the eighth-note beam
voice.notes.push_back( note );
currentTime += eighth;
note.pitchData.step = Step::e;
note.pitchData.alter = 0;
note.pitchData.octave = 5;
note.pitchData.accidental = Accidental::none;
note.durationData.durationName = DurationName::quarter;
note.durationData.durationTimeTicks = quarter;
note.tickTimePosition = currentTime;
note.beams.clear();
voice.notes.push_back( note );
// a MusicXml document is created from the score data and owns the
// underlying MusicXML model, which is hidden from us when using mx::api
auto docResult = fromScore( score );
if( !docResult.ok() ) { return 1; }
const auto document = std::move( docResult ).value();
// write to the console
#if MX_WRITE_THIS_TO_THE_CONSOLE
document.writeToStream( std::cout );
std::cout << std::endl;
#endif
// write to a file. argv[1] overrides the default output path so the build
// system can send the file to a gitignored location (see issue #150).
const std::string outputPath = ( argc > 1 ) ? argv[1] : "./example.musicxml";
const auto writeResult = document.writeToFile( outputPath );
return writeResult.ok() ? 0 : 1;
}#include "mx/api/MusicXml.h"
#include "mx/api/ScoreData.h"
#include <string>
#include <iostream>
#include <cstdint>
#include <sstream>
#include <utility>
#define MX_IS_A_SUCCESS 0
#define MX_IS_A_FAILURE 1
constexpr const char* const xml = R"(
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE score-partwise PUBLIC
"-//Recordare//DTD MusicXML 3.1 Partwise//EN"
"http://www.musicxml.org/dtds/partwise.dtd">
<score-partwise version="3.1">
<part-list>
<score-part id="P1">
<part-name>Music</part-name>
</score-part>
</part-list>
<part id="P1">
<measure number="1">
<attributes>
<divisions>1</divisions>
<key>
<fifths>0</fifths>
</key>
<time>
<beats>4</beats>
<beat-type>4</beat-type>
</time>
<clef>
<sign>G</sign>
<line>2</line>
</clef>
</attributes>
<note>
<pitch>
<step>C</step>
<octave>4</octave>
</pitch>
<duration>4</duration>
<type>whole</type>
</note>
</measure>
</part>
</score-partwise>
)";
int main(int argc, const char * argv[])
{
using namespace mx::api;
// place the XML from above into a stream object
std::istringstream istr{ xml };
// parse the XML into a MusicXml document that we own
auto docResult = MusicXml::fromStream( istr );
if( !docResult.ok() ) { return MX_IS_A_FAILURE; }
// take the score out of the document. intoScore also consumes the
// document, so its memory is freed as the function returns
const auto scoreResult = intoScore( std::move( docResult ).value() );
if( !scoreResult.ok() ) { return MX_IS_A_FAILURE; }
const auto& score = scoreResult.value();
// make sure we have exactly one part
if( score.parts.size() != 1 )
{
return MX_IS_A_FAILURE;
}
// drill down into the data structure to retrieve the note
const auto& part = score.parts.at( 0 );
const auto& measure = part.measures.at( 0 );
const auto& staff = measure.staves.at( 0 );
const auto& voice = staff.voices.at( 0 );
const auto& note = voice.notes.at( 0 );
if( note.durationData.durationName != DurationName::whole )
{
return MX_IS_A_FAILURE;
}
if( note.pitchData.step != Step::c )
{
return MX_IS_A_FAILURE;
}
return MX_IS_A_SUCCESS;
}The MusicXML classes in mx::core are tightly bound to the musicxml.xsd specification. MusicXML
can be challenging to use, and the mx::core class structure mirrors the complexity of the MusicXML
specification. A simplified representation is available in mx::api. It is possible to work with a
subset of MusicXML using only mx::api, without delving into mx::core.
using namespace mx::api; // an easier interface for reading and writing MusicXML
using namespace mx::core; // a direct representation of a MusicXML document in C++ classes
using namespace mx::impl; // the logic that translates between mx::api and mx::core
using namespace mx::utility; // a typical catch-all for generic stuff like logging macrosThe mx::api namespace is a set of 'plain old data' structs that represent a simplified model of
MusicXML. For example, here is ScoreData from ScoreData.h, which represents the top level of the
object hierarchy:
class ScoreData
{
public:
MusicXmlVersion musicXmlVersion;
std::string declaredMusicXmlVersion;
std::string musicXmlType;
std::string workTitle;
std::string workNumber;
std::string movementTitle;
std::string movementNumber;
std::string composer;
std::string lyricist;
std::string arranger;
std::string publisher;
std::string copyright;
std::optional<std::string> copyrightType;
std::optional<std::string> source;
EncodingData encoding;
std::vector<PageTextData> pageTextItems;
std::vector<PageImageData> pageImageItems;
DefaultsData defaults;
bool concertScore;
std::vector<PartData> parts;
std::vector<PartGroupData> partGroups;
int ticksPerQuarter;
std::map<MeasureIndex, LayoutData> layout;
};mx::api and mx::core are kept completely separate.
That is, mx::api data is serialized into mx::core data, which is then serialized into MusicXML.
The mx::api structs allow us to simplify things like specifying a note's tick time position and
allow the serialization process to take care of details such as <forward> and <backup> elements.
The mx::core namespace contains the MusicXML representation objects such as elements and
attributes. mx::core is generated from the MusicXML 4.0 XSD by the generator in gen/.
The core model is valid-by-construction, with the exception of ID uniqueness and references (which are validated during serialization and deserialization).
mx::impl is the translation layer between mx::api and mx::core.
There are two types of MusicXML documents: partwise and timewise. A partwise document consists
of a set of parts which contain measures. A timewise document consists of a set of measures which
contain parts. Partwise is used more often by MusicXML applications, while timewise documents seem
to be rare or even nonexistent. mx implements both timewise and partwise, though mx::api always
exists in a partwise orientation.