Git Product home page Git Product logo

mx's Introduction

MusicXML Class Library

  • Author: Matthew James Briggs
  • License: MIT
  • Version: 1.0
  • Supported MusicXML Version: 3.0
  • Language: C++17

CircleCI

Introduction

This project is a C++ library for working with MusicXML.

Build

Building and running tests should be as simple as:

git clone https://github.com/webern/mx.git mx
mkdir build
cd build
cmake ../mx -DMX_BUILD_TESTS=on -DMX_BUILD_CORE_TESTS=off -DMX_BUILD_EXAMPLES=on
make -j6
./mxtest

Cmake Options

There are three cmake options:

    -DMX_BUILD_TESTS=on
    -DMX_BUILD_CORE_TESTS=off
    -DMX_BUILD_EXAMPLES=on

The configuration shown above is the recommended configuration for development. If you just need the lib then turn off all three of the cmake options. The 'core tests' take a long time to compile. You only need to run them if you make changes in the mx::core namespace.

Build Tenets

  • mx should not depend on any outside libraries (no deps).
  • mx third-party code should be kept to a minimum.
  • mx third-party code should be checked into the mx repo and compiled as part of the mx library.
  • mx should not depened on any package manager, though it may be published into any package manager.

Using mx in a Cmake Project

The following script demonstrates how you can start a new cmake project that uses mx by commiting its sourcecode into your project:

#!/bin/bash
se -eou pipefail

# this script demonstrates how to depend on mx by including it in your
# sourcecode tree.

# if given, the first argument is a path to directory where the new
# project will be created.
REPO="${1:-/tmp/$(uuidgen)}"
echo "Creating a new project in: ${REPO}"

# create a new git repository for your project
mkdir -p "${REPO}"
cd "${REPO}"
git init
# bring the mx sourcecode into your project into a temporary location
git clone https://github.com/webern/mx.git mxtemp

# copy only what we need.  all we need is the Sourcode directory, the
# cmake file, the license, and the .gitignore file (helpful since there
# is one generated file.)
mkdir mx
mv mxtemp/Sourcecode mx/Sourcecode
mv mxtemp/.gitignore mx/.gitignore
mv mxtemp/LICENSE.txt mx/LICENSE.txt
mv mxtemp/CMakeLists.txt mx/CMakeLists.txt
# we don't need the test code, either
rm -rf mx/Sourcecode/private/mxtest
rm -rf mxtemp

# commit the mx sourcecode to our project repo
git add --all && git commit -m'mx sourcecode'

# create a main.cpp file
cat <<- "EOF" > main.cpp
#include <iostream>
#include "mx/api/ScoreData.h"
#include "mx/api/DocumentManager.h"

int main () {
    using namespace mx::api;
    ScoreData score{};
    score.workTitle = "Hello World";
    NoteData note{};
    note.durationData.durationName = DurationName::quarter;
    note.pitchData.step = Step::d;
    VoiceData voiceData{};
    voiceData.notes.push_back(note);
    StaffData staff{};
    staff.voices[0] = voiceData;
    MeasureData measure{};
    measure.staves.push_back(staff);
    PartData part{};
    part.measures.push_back(measure);
    score.parts.push_back(part);
    auto& mgr = DocumentManager::getInstance();
    const auto id = mgr.createFromScore(score);
    mgr.writeToStream(id, std::cout);
    mgr.destroyDocument(id);
}
EOF

# create a cmake file
cat <<- "EOF" > CMakeLists.txt
cmake_minimum_required(VERSION 3.17)
project(my-musicxml-proj)
set(CMAKE_CXX_STANDARD 17)
set(CPP_VERSION 17)

add_executable(my-musicxml-proj main.cpp)
add_subdirectory(mx)
target_link_libraries(my-musicxml-proj mx)
target_include_directories(my-musicxml-proj PRIVATE mx/Sourcecode/include)
EOF

# create a .gitignore file to ignore a build directory
cat <<- "EOF" > .gitignore
build/
EOF

git add --all && git commit -m'musicxml hello world'

# create a build directory
mkdir build

# build your project
cd build
cmake .. && make -j10
# run your executable
./my-musicxml-proj

Xcode Project

The Xcode project (checked-in to the repo) has targets for iOS and macOS frameworks and dylibs. These are not specified in the cmake file. Contributors are not required to keep the Xcode project up-to-date. If you add, move or remove files from the codebase, it is likely that the Xcode CI run will fail. This will not prevent a contribution from being merged, the maintainer will fix the project after-the-fact.

Using mx

API

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.

Writing MusicXML with mx::api

#include <string>
#include <iostream>
#include <cstdint>
#include <sstream>

#include "mx/api/DocumentManager.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.beats = 4;
    measure.timeSignature.beatType = 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 );

    // the document manager is the liaison between our score data and the MusicXML DOM.
    // it completely hides the MusicXML DOM from us when using mx::api
    auto& mgr = DocumentManager::getInstance();
    const auto documentID = mgr.createFromScore( score );

    // write to the console
    #if MX_WRITE_THIS_TO_THE_CONSOLE
    mgr.writeToStream( documentID, std::cout );
    std::cout << std::endl;
    #endif

    // write to a file
    mgr.writeToFile( documentID, "./example.musicxml" );

    // we need to explicitly delete the object held by the manager
    mgr.destroyDocument( documentID );

    return 0;
}

Reading MusicXML with mx::api

#include "mx/api/DocumentManager.h"
#include "mx/api/ScoreData.h"

#include <string>
#include <iostream>
#include <cstdint>
#include <sstream>

#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;

    // create a reference to the singleton which holds documents in memory for us
    auto& mgr = DocumentManager::getInstance();

    // place the xml from above into a stream object
    std::istringstream istr{ xml };

    // ask the document manager to parse the xml into memory for us, returns a document ID.
    const auto documentID = mgr.createFromStream( istr );

    // get the structural representation of the score from the document manager
    const auto score = mgr.getData( documentID );

    // we need to explicitly destroy the document from memory
    mgr.destroyDocument(documentID);

    // 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;
}

Implementation Details

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.

Namespaces
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 macros
using namespace ezxml;      // generic serialization and deserialization of xml
mx::api

The mx::api namespace is a set of 'plain old data' structs that represent a simplified model of MusicXML. For example, here is the ScoreData.h, which represents the top level of the object heirarchy:

class ScoreData
{
public:
    MusicXmlVersion musicXmlVersion;
    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;
    EncodingData encoding;
    std::vector<PageTextData> pageTextItems;
    DefaultsData defaults;
    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 struct allow us to simplify things like specifying a note's tick time position, and allowing the serialization process to take care of details such as <forward> <backup> elements.

mx::core

The mx::core namespace contains the MusicXML representation objects such as elements and attributes. mx::core was mostly generated from musicxml.xsd with plenty of intervention by hand.

XML Choices and Groups

In the musicxml.xsd there are many cases of xs:choice or xs:group being used. These constructs are typically represented in the mx::core class structure the same way that they are found in the musicxml.xsd specification. The interfaces in this namespace are relatively stable, however they are tightly bound to MusicXML's specification and thus they will change when it comes time to support a future version of MusicXML.

mx::impl

mx::impl is the translation layer between mx::api and mx::core.

mx::utility

This namespace is small. It mostly contains macros and small, generic functions.

ezxml

The ezxml namespace contains generic XML DOM functionality. Under the hood pugixml is being used. See the XML DOM section for more information. Note that, even though ezxml can stand alone as a useful abstraction, we build it as if it were entirely owned by the mx project. Additionally, we check the pugixml library in and build it as if it were part of the mx project. This is in keeping with the build tenets above

Partwise vs. Timewise

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 nonresistant. Nonetheless MusicXML Class Library implements both Timewise and Partwise. The class mx::core::Document can hold either a Partwise or a Timewise score. Note that it actually holds both, but only one or the other is 'active' (this is similar to how xsd choice constructs are handled). You can check the inner document type with the getChoice function. You can convert between Partwise and Timewise with the convertContents function.

Elements

Each XML element is represented by a class which derives from ElementInterface. Elements are created and used by way of shared pointers. Each element comes with a set of using/typedef statements as well as a convenience function for making the shared pointers.

Shared Pointers

Many elements contain other elements. When they do, these data members will also be shared pointers. Get/set functions will allow access to the data members by accepting and returning shared pointers. If you attempt to set a data member to a nullptr, the setter function will silently do nothing. Thus we can be reasonably assured our objects will never return nullptr.

For example

std::shared_ptr<Foo> foo; /* nullptr! */
bar->setFoo( foo );       /* no-op because you passed a nullptr */
auto x = bar->getFoo();   /* guaranteed not to be null */
x->somefuntion();         /* OK to dereference without checking for nullptr */
Optional Member Data

Many of the elements in MusicXML are optional. In these cases there is a bool which indicates whether or not the element is present. The bool serves as a flag indicating whether or not the optional element will be output when you stream out your MusicXML document. The bool has no side-effect on the element who's presence/absence it represents. So for example we may set some data:

foo->setValue( "hello" );
bar->setFoo( foo );

But in this example, if Foo is an optional member of Bar, then we must also set hasFoo to true or else foo will not be in the XML output.

bar->toStream(...); /* Foo is not in the output! */
bar->setHasFoo( true );
bar->toStream(...); /* Now we see <foo>hello</foo> in the output. */

Also note that setting HasFoo to false does not mean that Foo's value is gone.

bar->setHasFoo( false ); /* The XML document no longer has a Foo */
bar->getFoo()->getValue() == "hello"; /* True! The value still exists but is not present in the XML. */
Optional Member Data with Unbounded Occurrences

Sometimes an element may contain zero, one, or many occurrences of another element. For example

<xs:element name="key" type="key" minOccurs="0" maxOccurs="unbounded">

In this case there will be a collection of Key objects and the getter/setters will look like this, where KeySet is a typedef of std::vector<Key>.

const KeySet& getKeySet() const;
void addKey( const KeyPtr& value );
void removeKey( const KeySetIterConst& value );
void clearKeySet();
KeyPtr getKey( const KeySetIterConst& setIterator ) const;
Required Member Data with Unbounded Occurrences

Sometimes an element is required, but you may optionally have more than one. For example

<xs:element name="direction-type" type="direction-type" maxOccurs="unbounded"/>

In this case, minOccurs="1" (by default per XSD language rules). The functions will look just like the previous example, but they will behave differently

const DirectionTypeSet& getDirectionTypeSet() const;
void addDirectionType( const DirectionTypePtr& value );
void removeDirectionType( const DirectionTypeSetIterConst& value );
void clearDirectionTypeSet();
DirectionTypePtr getDirectionType( const DirectionTypeSetIterConst& setIterator ) const;

When the containing element is constructed, a single DirectionType will be default constructed and pushed onto the vector. Thus you will have one default constructed DirectionType in the set upon construction.

If you try to call removeDirectionType with only one DirectionType in the set (size==1) nothing will happen. You will still have a single DirectionType in the collection.

When you call clearDirectionTypeSet vector.clear() will be called but it will follow up by pushing a default constructed DirectionType onto the vector so you will still have size==1.

As it turns out, this design choice tends to be annoying in practice. On the upside, it does guarantee that your MusicXML document will be valid, even if you forget to add a required element. The downside is that it means you have to deal with the fact that a default constructed element always exists in the set, so you must replace or remove the first element. Furthermore, you cannot remove the existing element until another one has been added. Here are the two patterns I have used for this (pseudocode).

Pattern 1: Replace the first element by dereferencing the begin() iterator:

bool isFirstAdded = false;
for( auto stuffElement : stuffElementsIWantToAdd )
{
    if( !isFirstAdded )
    {
        *( myElementIWantToAddThemTo->getStuffSet().begin() ) = stuffElement;
        isFirstAdded = true;
    }
    else
    {
        myElementIWantToAddThemTo->addStuff( stuffElement );
    }
}

Pattern 2: Remove the default element After adding a replacement:

bool isFirstAdded = false;
for( auto stuffElement : stuffIWantToAdd )
{
    myElementIWantToAddThemTo->addStuff( stuffElement );
    if( !isFirstAdded )
    {
        myElementIWantToAddThemTo->removeStuff( myElementIWantToAddThemTo->getStuffSet().cbegin() )
        isFirstAdded = true;
    }
}

Pattern 1 always works, even if you're not sure whether or not the minOccurs="1" or "0". Pattern 2 only works when minOccurs="1". There are no cases where minOccurs is greater than 1.

Member Data with Bounded maxOccurs
<xs:element name="beam" type="beam" minOccurs="0" maxOccurs="8"/>

In this case if you call addBeam when there are already 8 beams in the vector, nothing will happen.

xs:groups

For an xs:group there is usually a single 'element' class which represents the group of elements. For example this XSD snippet:

<xs:group name="editorial">
	<xs:sequence>
		<xs:group ref="footnote" minOccurs="0"/>
		<xs:group ref="level" minOccurs="0"/>
	</xs:sequence>
</xs:group>

is represented by this class:

class EditorialGroup : public ElementInterface
{
public:
    EditorialGroup();

    /* ... other stuff ... */

    /* _________ Footnote minOccurs = 0, maxOccurs = 1 _________ */
    FootnotePtr getFootnote() const;
    void setFootnote( const FootnotePtr& value );
    bool getHasFootnote() const;
    void setHasFootnote( const bool value );

    /* _________ Level minOccurs = 0, maxOccurs = 1 _________ */
    LevelPtr getLevel() const;
    void setLevel( const LevelPtr& value );
    bool getHasLevel() const;
    void setHasLevel( const bool value );
    
    bool fromXElement( std::ostream& message, xml::XElement& xelement );

private:
    FootnotePtr myFootnote;
    bool myHasFootnote;
    LevelPtr myLevel;
    bool myHasLevel;
};
xs:choices

There are a few exceptions (mistakes) but for the most part, xs:choice constructs are represented by a class with a name ending in 'Choice'. The element will have an enum named 'Choice' in the public scope of the class. Each of the possible 'choices' will exist as data members of the class, but only one of them will be 'active' (was present in, or will be written to, XML). For example, this xsd construct:

<xs:choice minOccurs="0">
    <xs:element name="pre-bend" type="empty">
        <xs:annotation>
            <xs:documentation>The pre-bend element indicates that this is a pre-bend rather than a normal bend or a release.</xs:documentation>
        </xs:annotation>
    </xs:element>
    <xs:element name="release" type="empty">
        <xs:annotation>
            <xs:documentation>The release element indicates that this is a release rather than a normal bend or pre-bend.</xs:documentation>
        </xs:annotation>
    </xs:element>
</xs:choice>

Is represented by this class:

class BendChoice : public ElementInterface
{
public:
    enum class Choice
    {
        preBend = 1,
        release = 2
    };
    BendChoice();

	/* ... other stuff ... */
    
    BendChoice::Choice getChoice() const;
    void setChoice( BendChoice::Choice value );

    /* _________ PreBend minOccurs = 1, maxOccurs = 1 _________ */
    PreBendPtr getPreBend() const;
    void setPreBend( const PreBendPtr& value );

    /* _________ Release minOccurs = 1, maxOccurs = 1 _________ */
    ReleasePtr getRelease() const;
    void setRelease( const ReleasePtr& value );

    bool fromXElement( std::ostream& message, xml::XElement& xelement );

private:
    Choice myChoice;
    PreBendPtr myPreBend;
    ReleasePtr myRelease;
};

When getChoice() == BendChoice::Choice::preBend then we will see <pre-bend/> in the XML, but when getChoice() == BendChoice::Choice::postBend then we will see <post-bend/> in the XML.

XML DOM (::ezxml::)

Any XML document can be read and manipulated with the classes in the ::ezxml:: namespace. Most notably, look at the following pure virtual interfaces XDoc, XElement, XAttribute. Also look at the STL-compliant iterators XElementIterator and XAttributeIterator.

These interfaces are designed to wrap any underlying XML DOM software so that mx::core does not care or know about the XML DOM code. A set of implementation classes wrapping pugixml are provided, but if you need to use, say Xerces or RapidXML, you can look at the PugiElement, PugiDoc, etc classes and wrap whatever library you need.

Here's how you can read a MusicXML document into mx::core classes by way of ::ezxml::.

#include "mx/core/Document.h"
#include "mx/utility/Utility.h"
#include "functions.h"
#include "ezxml/XFactory.h"
#include "ezxml/XDoc.h"

#include <iostream>
#include <string>
#include <sstream>

int main(int argc, const char *argv[])
{
    // allocate the objects
    mx::core::DocumentPtr mxDoc = makeDocument();
    ::ezxml::::XDocPtr xmlDoc = ::ezxml::::XFactory::makeXDoc();
    
    // read a MusicXML file into the XML DOM structure
    xmlDoc->loadFile( "music.xml" );

    // create an ostream to receive any parsing messages
    std::stringstream parseMessages;
    
    // convert the XML DOM into MusicXML Classes
    bool isSuccess = mxDoc->fromXDoc( parseMessages, *xmlDoc );
    
    if( !isSuccess )
    {
        std::cout << "Parsing of the MusicXML document failed with the following message(s):" << std::endl;
        std::cout << parseMessages.str() << std::endl;
        return -1;
    }
    
    // maybe the document was timewise document. if so, convert it to partwise
    if( mxDoc->getChoice() == mx::core::DocumentChoice::timewise )
    {
        mxDoc->convertContents();
    }
    
    // get the root
    auto scorePartwise = mxDoc->getScorePartwise();
    
    // change the title
    scorePartwise->getScoreHeaderGroup()->setHasWork( true );
    scorePartwise->getScoreHeaderGroup()->getWork()->setHasWorkTitle( true );
    scorePartwise->getScoreHeaderGroup()->getWork()->getWorkTitle()->setValue( mx::core::XsString( "New Title" ) );
    
    // write it back out to disk
    mxDoc->toXDoc( *xmlDoc );
    xmlDoc->write( "newtitle.xml" );
    
    return 0;
}

Hello World using mx::core

On the MusicXML home page there is an example of a "Hello World" simple MusicXML file. Here is a main function that would output this "Hello World" MusicXML example to std::cout.

#include <iostream>
#include "DocumentPartwise.h"
#include "Elements.h"

using namespace mx::core;
using namespace std;

int main(int argc, const char * argv[])
{
    auto doc = makeDocumentPartwise();
    auto s = doc->getScorePartwise();
    s->getAttributes()->hasVersion = true;
    s->getAttributes()->version = XsToken( "3.0" );
    auto header = s->getScoreHeaderGroup();
    header->getPartList()->getScorePart()->getAttributes()->id = XsID( "P1" );
    header->getPartList()->getScorePart()->getPartName()->setValue( XsString( "Music" ) );
    auto part = *( s->getPartwisePartSet().cbegin() );
    part->getAttributes()->id = XsIDREF( "P1" );
    auto measure = *( part->getPartwiseMeasureSet().cbegin() );
    measure->getAttributes()->number = XsToken( "1" );
    auto propertiesChoice = makeMusicDataChoice();
    propertiesChoice->setChoice( MusicDataChoice::Choice::properties );
    auto properties = propertiesChoice->getProperties();
    properties->setHasDivisions( true );
    properties->getDivisions()->setValue( PositiveDivisionsValue( 1 ) );
    properties->addKey( makeKey() );
    auto time = makeTime();
    time->getTimeChoice()->setChoice( TimeChoice::Choice::timeSignature );
    time->getTimeChoice()->getTimeSignature()->getBeats()->setValue( XsString( "4" ) );
    time->getTimeChoice()->getTimeSignature()->getBeatType()->setValue( XsString( "4" ) );
    properties->addTime( time );
    auto clef = makeClef();
    clef->getSign()->setValue( ClefSign::g );
    clef->setHasLine( true );
    clef->getLine()->setValue( StaffLine( 2 ) );
    properties->addClef( clef );
    measure->getMusicDataGroup()->addMusicDataChoice( propertiesChoice );
    auto noteData = makeMusicDataChoice();
    noteData->setChoice( MusicDataChoice::Choice::note );
    noteData->getNote()->getNoteChoice()->setChoice( NoteChoice::Choice::normal );
    noteData->getNote()->getNoteChoice()->getNormalNoteGroup()->getFullNoteGroup()->getFullNoteTypeChoice()->setChoice( FullNoteTypeChoice::Choice::pitch );
    noteData->getNote()->getNoteChoice()->getNormalNoteGroup()->getFullNoteGroup()->getFullNoteTypeChoice()->getPitch()->getStep()->setValue( StepEnum::c );
    noteData->getNote()->getNoteChoice()->getNormalNoteGroup()->getFullNoteGroup()->getFullNoteTypeChoice()->getPitch()->getOctave()->setValue( OctaveValue( 4 ) );
    noteData->getNote()->getNoteChoice()->getNormalNoteGroup()->getDuration()->setValue( PositiveDivisionsValue( 4 ) );
    noteData->getNote()->getType()->setValue( NoteTypeValue::whole );
    measure->getMusicDataGroup()->addMusicDataChoice( noteData );
    
    doc->toStream( cout ); /* print Hello World MusicXML document to console */
    return 0;
}

Unit Test Framework

An executable program named mxtest is also included in the project. mxtest utilizes the Catch2 test framework. The core tests are slow to compile, see the cmake options section for more info on how to skip compilation of the tests.

mx's People

Contributors

gergol avatar outcue avatar robinka avatar semitonegene avatar webern avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

mx's Issues

[Q] Mixing ::core and ::api structures.

Is it possible to mix core and api data structures? From what I can tell, I don't believe it is. At least I couldn't find it quickly.

My use case :
I must add a theoretical key signature to an mx::api::Measure (d flat minor). As such, I cannot use the mx::api::KeyData, since it cuts off values not present in the signature circle. I think mx::core::NonTraditionalKey is meant to build just that.

I really like the mx::api workflow, so I'm hoping I don't need to change my whole project to use mx::core. Is there a way to retro-fit the NonTraditionalKey into an api::measure?

Thanks!

README: Min Occurs 1 examples

This is suspicious, fix the example or the claim.

Pattern 1 always works, even if you're not sure whether or not the minOccurs="1" or "0".

[feature] Enum classes "count".

I don't know how much effort this would take, but if it is too much don't worry about it. I understand you maintain this for fun on your free time.

One of my only gripes with mx is the lack of count enum class members.

In my experience, it is often best practice to add a trailing count enum, which enables you to write generalized loops on enums without ever worrying about size problems. For ex.

enum class potato {
    russet,
    yukon,
    sweet,
    count, // This will always be equal to the number of enum elements, if they aren't specified values.
};

So I can do.

for (size_t i = 0; i < size_t(potato::count); ++i) {
    potato my_potato = potato(i); //etc
}

So, in mx. I wish there was a count where it makes sense. For ex with Step.

    	enum class Step
    	{
    		c = 0,
    		d = 1,
    		e = 2,
    		f = 3,
    		g = 4,
    		a = 5,
    		b = 6,
    		count,
            unspecified = -1
    	};

Cheers

README: Usage Section

Rename the section currently labeled Usage as Core, prepend it with a new section called Usage which describes the mx::api namespace.

Prevent Pugi Symbol Conflicts

Per @p-groarke in #53

Embedding pugixml in your repo like you did will make mx unusable to anyone who links directly with pugixml in their project (duplicate symbols). You can google "dependency hell" for more info (yes, that's the real name). To fix that, you could use something super robust like conan to install pugi when you build mx. Or you can use the less robust ExternalProject_Add as well. I would be happy to send a PR with any of those solutions.

True, embedding could be retained but this could be fixed by patching the pugi namespace to something else.

See webern/ezxml#6

Make Objects Copyable

There is no easy way to "deep copy" anything. Each element and attributesclass needs a "clone" function. Alternatively each makeSomething() could be overloaded to create a clone.

ci testing fanciness

See issue #23

When we are on any branch other than master (e.g. when we are on develop) the circleci should build with MX_TEST_BUILD=light

When we are on master circleci should build MX_TEST_BUILD=all

The Encoding Element should allow multiple choices

class Encoding in Elements.h is implemented incorrectly, it should allow for an unbounded number of Encoding Choices.

        <xs:choice minOccurs="0" maxOccurs="unbounded">
            <xs:element name="encoding-date" type="yyyy-mm-dd"/>
            <xs:element name="encoder" type="typed-text"/>
            <xs:element name="software" type="xs:string"/>
            <xs:element name="encoding-description" type="xs:string"/>
            <xs:element name="supports" type="supports"/>
        </xs:choice>

Currently I have this implemented as maxOccurs=1

Remove install_package for ezxml

ezxml is a transparent, built-in part of the mx code base that users need not concern themselves with. We should not install it when installing mx.

Note: maybe it doesn't get installed when installing mx, I'm not sure because it is in a child directory and is statically linked into the mx library.

Words Color

Color is missing from WordsAttributes and does not save from the api::WordsData struct

Support UTF-16 and wstring

Support building the MusicXML Class Library in wstring mode. Support import and export of UTF-16 files and strings.

NonTraditional key : C key

By using the KeyComponent vector empty to check whether a key is in non-traditional mode, it makes it impossible to create C keys.

Integration Test Paths are Hard Coded

Use cmake to dynamically produce Path.h so that the integration tests will work on any system when cmake is used.

The checked-in version of Path.h should make some assumptions and use a relative path to the mx.xcworkspace directory since, in the absence of cmake, it is likely the developer is using the xcworkspace.

Create an Self-Contained Example that relies on the installation of Mx

Somewhere, probably at the top-level, an Examples directory could be created.
In there a sub directory could contain a small main function along with a CMakeLists.txt file. In that CMakeLists.txt file we should relay on this installation of mx and use find_package( mx ). During continuous integration runs, we should add a step where we make install then cd to the example and build it. If mx installation isn't working, that build should fail. If mx installation is working, that build should succeed.

Use RAII for the DocumentManager

The DocumentManager is a weird concept. Explain why it exists and how to use it early in the Readme and in the code itself.

For extra credit, return an RAII object instead of a document ID so that leaks won't ever happen.

e.g. std::unique_ptr<mx::api::Document> createFromFile( "/some/file.xml" ).

[feature] mx::api NonTraditionalKey equivalent

We've discussed about this in the past, and I've found mixing mx::core with mx::api to be a pain! :)

So, I'm just adding this here in case someday you'd want to add this feature to mx::api.

Cheers

Support Pages

The current ScoreData looks like this, with no way to specify page layout (as far as I can tell/remember).

        class ScoreData
        {
        public:
            ScoreData();
            MusicXmlVersion musicXmlVersion;
            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;
            EncodingData encoding;
            std::vector<PageTextData> pageTextItems;
            LayoutData layout;
            std::vector<PartData> parts;
            std::vector<PartGroupData> partGroups;
            int ticksPerQuarter;
            std::set<SystemData> systems;

            int getNumMeasures() const;
            int getNumStavesPerSystem() const;

            /// sorts all of the events, directions, etc.
            /// it is good to call this before writing to xml.
            void sort();
        };

Implement page layout in a manner consistent with the SystemData approach.

Open questions: should page breaks be specified by measure index, like systems are, or should page breaks be specified with system index?

KeyComponent : Uninitialized.

Right now, there is no default constructor provided for KeyComponent. This means, if you aren't using cents, it will corrupt the output.

When exporting, in Converter::convertToAlter, the cents won't be equal to 0 and so the generated alter is gibberish.

          <key-step>E</key-step>
          <key-alter>-9223372036854775809</key-alter>
          <key-accidental>flat</key-accidental>

Publish mx with Cocoapods

Respecting #72, mx will remain a zero-dependency library that does not require a package manager to build. There is no reason why we can not publish it into package managers though. So let's publish it with Cocoapods.

Remove empty cpp files

file: libMx.a(MiscData.cpp.o) has no symbols
file: libMx.a(WordsData.cpp.o) has no symbols
file: libMx.a(Elements.cpp.o) has no symbols
file: libMx.a(AttributesIterface.cpp.o) has no symbols
file: libMx.a(MiscData.cpp.o) has no symbols
file: libMx.a(WordsData.cpp.o) has no symbols
file: libMx.a(Elements.cpp.o) has no symbols
file: libMx.a(AttributesIterface.cpp.o) has no symbols
``

Add Tenets to the readme

Add tenants to the readme explaining why we don't use a package manager and why we don't use find_package in the CMakeLists.txt files.

Import MusicXML

Add the feature to import MusicXML files into the MusicXML-Class-Library object structure.

Pitch step and alter not set when creating grace note

Here is how I am configuring the NoteParams. It works fine for rests and normal notes, but the Pitch properties are lost when creating a cue note.
`
if (item->isTemporal()) {
// Configure the note (also can be a rest)
const TemporalPtr temporal = std::dynamic_pointer_cast(item);
NoteParams noteParams;
noteParams.staffNumber = temporal->hand() + 1;
noteParams.voiceNumber = temporal->voice() + 1;
noteParams.duration = temporal->getDuration();
noteParams.durationType = temporal->XMLDurationType();

                        if (item->isNote()) {
                            const NotePtr note = std::dynamic_pointer_cast<Note>(item);
                            if (note->hasGrace()) {
                                noteParams.noteChoice = NoteChoice::Choice::grace;
                            }
                            noteParams.isRest = false;
                            noteParams.isChord = note->partOfChord(tIndex, mIndex) && !note->chordMain(tIndex, mIndex);
                            noteParams.stemDirection = note->stemUp() ? mx::utility::UpDown::up : mx::utility::UpDown::down;
                            noteParams.durationDots = note->dots();
                            noteParams.showAccidental = note->accidental();
                            std::string name;
                            note->XMLPitch(name, noteParams.step, noteParams.alter, noteParams.octave);
                            // std::vector<t::BeamValue> beams;

                        } else if (item->isRest()) {
                            const RestPtr rest = std::dynamic_pointer_cast<Rest>(item);
                            noteParams.isRest = true;
                        }

                        MusicDataChoicePtr noteData = createNote(noteParams);
                        partwiseMeasure->getMusicDataGroup()->addMusicDataChoice(noteData);
                    }

`

Publish mx with Conan

Respecting #72, mx will remain a zero-dependency library that does not require a package manager to build. There is no reason why we can not publish it into package managers though. So let's publish it with Conan.

mx::core Support a newer version of MusicXML

We are stuck on MusicXML 3.0 because it’s a big job to upgrade to 3.1 (or whichever version is out when we get to it).

Do this without breaking mx::api

  • Create a program that parses the XSD. (In Progress)
    • parse xs:annotation
    • parse xs:attribute
    • parse xs:attributeGroup
    • parse xs:choice
    • parse xs:complexContent
    • parse xs:complexType
    • parse xs:documentation
    • parse xs:element
    • parse xs:enumeration
    • parse xs:extension
    • parse xs:group
    • parse xs:import
    • parse xs:maxInclusive
    • parse xs:minExclusive
    • parse xs:minInclusive
    • parse xs:pattern
    • parse xs:restriction
    • parse xs:schema
    • parse xs:sequence
    • parse xs:simpleContent
    • parse xs:simpleType
    • parse xs:union
  • Create a program that flattens and links the XSD.
  • Create a program that converts the XSD into programming friendly types (Class, Enum, etc).
  • Regenerate Enums
  • Regenerate other SimpleTypes
  • Regenerate unions
  • Check for changes to custom simple types during generating.
  • Regenerate Attributes
  • Regenerate Choices
  • Regenerate Groups
  • Regenerate Elements
  • Create a program that re-generates mx::core for MusicXML 3.0 without too much breakage.
  • Use the program to generate mx::core for MusicXML 3.1 (or 3.2 or 4.0, w3c/musicxml#314) without breakage.
  • Expose some new functionality from mx::core to mx:api.

Create a Back Door to `mx::core::DocumentPtr`

A user may want to use mx::api for most things, but need to get at mx::core::DocumentPtr for some tweaking. Provide a way to do this without exposing mx::core symbols in any mx::api headers.

Requested here #44 but leaving that issue to be about the specific key signature issue.

Properly hide mx::core

mx::core is currently hoisted into the mx::api namespace. Ensure that client code can use the library while referencing only the mx::api headers.

Create an example program that demonstrates that this is working and add it to the CI build so that it doesn't regress.

cmake options to avoid building tests

set up cmake to take options so that, by default we

Build NO test binary.

But we can choose to build
Light tests

Or we can choose to build
Full tests

e.g.
maybe
MX_TEST_BUILD=none
MX_TEST_BUILD=light
MX_TEST_BUILD=all

[feature] Constructor with defaults.

Hi, I hope you are well and good amidst the current pandemic.

I've been hitting a minor snag when trying to store mx::api types in various const/global containers. It would be nice if PitchData offered a flexible value constructor to remedy this.

For example:
PitchData(Step step = Step::c, int alter = 0, double cents = 0.0, Accidental c = Accidental::none, bool = false, bool = false, bool = false, bool = false, int octave = 4);

This would enable a user to create pitches like this: PitchData p{Step::c, -1}; which is nice.

A real world use case, I am building an unordered_map to map a root to a mx::api::KeyData fifth.

namespace mx {
using namespace api;
}

namespace std {
template <>
struct hash<mx::PitchData> {
	constexpr inline size_t operator()(const mx::PitchData& p) const {
		size_t h1 = size_t(p.step) << 32;
		size_t h2 = size_t(p.alter);
		return h1 ^ h2;
	}
};
} // namespace std


namespace {
std::unordered_map<mx::PitchData, int> root_to_key_circle {
    { mx::PitchData{/* you cannot do that :( */ }, -1 },
    { mx::PitchData{/* you cannot do that :( */ }, 0 },
    { mx::PitchData{/* you cannot do that :( */ }, 1 },
// etc
};
} // namespace

Now, you may want to have different or other types of constructors, but the general idea is the same. It would be nice to be able to construct a PitchData with a few arguments.

Take care and stay safe!

Mx Backup Staff Change Voice Increment Bug

If Mx encounters a <backup> or <forward>, but this is the last thing that happens before switching to another hand (staff), then the voice number should not be incremented.

Issues with extraneous <forward> element.

While building some measures with 2 staves (treble & bass), 2 voices, I get an extraneous <forward> element at the beginning of the second measure. The export doesn't seem to know the "playhead" is already at the correct position.

I'm curious if there is something to watch out for when dealing with multiple staves/voices?

Snippets of the generating code based on your example, writing a test scale.

Adding notes to measures.

	int current_time = 0;
	for (const mx::NoteData& n : scale) {
		ret.back().staves[0].voices[treb_voice_id].notes.push_back(n);
		mx::NoteData bass_n = n;
		bass_n.pitchData.octave -= 1;
		ret.back().staves[1].voices[bass_voice_id].notes.push_back(bass_n);

		current_time += n.durationData.durationTimeTicks;

		if (current_time >= ret.back().timeSignature.beats
						* ret.back().timeSignature.beatType) {
			ret.push_back(build_measure());
			current_time = 0;
		}
	}

How the measure is built.

mx::MeasureData build_measure(bool first = false) {
	mx::MeasureData ret;
	mx::StaffData staff_treb;
	mx::StaffData staff_bass;

	ret.timeSignature.beats = qticks;
	ret.timeSignature.beatType = qticks;

	if (first) {
		ret.timeSignature.isImplicit = false;

		// set the clef
		mx::ClefData clef_treb;
		clef_treb.setTreble();
		staff_treb.clefs.push_back(clef_treb);

		mx::ClefData clef_bass;
		clef_bass.setBass();
		staff_bass.clefs.push_back(clef_bass);

		mx::KeyData key;
		key.fifths = 4; //test
		ret.keys.push_back(key);
	}


	// add a voice
	mx::VoiceData voice_treb;
	mx::VoiceData voice_bass;

	staff_treb.voices[treb_voice_id] = voice_treb;
	staff_bass.voices[bass_voice_id] = voice_bass;

	// add a staff
	ret.staves.push_back(staff_treb);
	ret.staves.push_back(staff_bass);

	return ret;
}

[Question] new-system

Hi, me again :)

I am trying to add line-breaks every 4 measures. From what I read on musicxml, I need the print new-system attribute. https://usermanuals.musicxml.com/MusicXML/Content/CT-MusicXML-print.htm

I am trying to create new systems like this (I have 6 measures right now).

	for (size_t i = 0; i < part.measures.size(); i += 4) {
		mx::api::SystemData system;
		system.measureIndex = int(i);
		score.systems.insert(system);
	}

	score.sort();
	mx::DocumentManager& mgr = mx::DocumentManager::getInstance();
	const int documentID = mgr.createFromScore(score);

But all I get is the first new-system, not the second. Any idea what I am doing wrong?
Thx

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.