diff --git a/.circleci/Dockerfile.test b/.circleci/Dockerfile.test index cf4c4e461..0071a1157 100644 --- a/.circleci/Dockerfile.test +++ b/.circleci/Dockerfile.test @@ -3,5 +3,13 @@ FROM matthewjamesbriggs/mxci:v001 ENV BASEDIR="/mx" WORKDIR $BASEDIR COPY . . -CMD cmake -DMX_BUILD_TESTS=on -DMX_BUILD_CORE_TESTS=on . && make -j12 && ./MxTest +CMD cmake \ + -DMX_BUILD_TESTS=on \ + -DMX_BUILD_CORE_TESTS=on \ + -DMX_BUILD_EXAMPLES=on \ + . && \ + make -j12 && \ + ./MxTest && \ + ./MxRead && \ + ./MxWrite # CMD cmake --version \ No newline at end of file diff --git a/.circleci/config.yml b/.circleci/config.yml index 63c11796a..f7cee6eaf 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -47,13 +47,17 @@ jobs: name: Cmake command: | [ "$(git rev-parse --abbrev-ref HEAD)" == "master" ] && \ - cmake -DMX_BUILD_TESTS=on -DMX_BUILD_CORE_TESTS=on . || \ - cmake -DMX_BUILD_TESTS=on -DMX_BUILD_CORE_TESTS=off . + cmake -DMX_BUILD_EXAMPLES=on -DMX_BUILD_TESTS=on -DMX_BUILD_CORE_TESTS=on . || \ + cmake -DMX_BUILD_EXAMPLES=on -DMX_BUILD_TESTS=on -DMX_BUILD_CORE_TESTS=off . - run: name: Build command: make + - run: + name: Run Examples + command: ./MxRead && ./MxWrite + - run: name: Test command: ./MxTest diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 000000000..a6a59ca01 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +Mx \ No newline at end of file diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 000000000..31c5ccdce --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 000000000..79ee123c2 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 000000000..8822db8f1 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 000000000..3c89db64a --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/mx.iml b/.idea/mx.iml new file mode 100644 index 000000000..f08604bb6 --- /dev/null +++ b/.idea/mx.iml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 000000000..94a25f7f4 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index ec7caa949..8a11fe605 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -91,3 +91,22 @@ if(MX_BUILD_TESTS) else() message("tests will not be compiled") endif() + +# MxExamples +if(MX_BUILD_EXAMPLES) + message("examples will be compiled") + find_package( Threads ) + add_executable(MxRead ${SOURCE}/mx/examples/Read.cpp) + add_executable(MxWrite ${SOURCE}/mx/examples/Write.cpp) + target_link_libraries(MxRead Mx ${CMAKE_THREAD_LIBS_INIT}) + target_link_libraries(MxWrite Mx ${CMAKE_THREAD_LIBS_INIT}) + set_property(TARGET MxRead PROPERTY CXX_STANDARD 14) + set_property(TARGET MxWrite PROPERTY CXX_STANDARD 14) + + # TODO - create a proper 'include' directory + target_include_directories(MxRead PRIVATE ${SOURCE}/mx/api) + target_include_directories(MxWrite PRIVATE ${SOURCE}/mx/api) + +else() + message("examples will not be compiled") +endif() diff --git a/README.md b/README.md index 27b92ac87..ae9162387 100755 --- a/README.md +++ b/README.md @@ -56,6 +56,242 @@ Visual Studio will open with a solution file and be ready to compile. ``` ## Usage + +The `mx::api` namespace is intended to be a (somewhat) simplified wrapping `api` for MusicXML. It should be slightly more intuitive than manipulating the DOM representation directly. + +#### Writing MusicXML with `mx::api` +```C++ +#include +#include +#include +#include + +#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 1 + +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" ); + + return 0; +} +``` + +#### Reading MusicXML with `mx::api` + +```C++ +#include "mx/api/DocumentManager.h" +#include "mx/api/ScoreData.h" + +#include +#include +#include +#include + +#define MX_IS_A_SUCCESS 0 +#define MX_IS_A_FAILURE 1 + +constexpr const char* const xml = R"( + + + + + + Music + + + + + + 1 + + 0 + + + + G + 2 + + + + + C + 4 + + 4 + whole + + + + +)"; + +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; +} +``` + +## Core Implementation Details + The MusicXML classes are tightly bound to the musicxml.xsd specification. MusicXML can be challenging to use and the mx class structure mirrors the complexity of the MusicXML specification. A facade or api for simplifying the interactions with MusicXML documents is planned for future development. ##### Namespaces @@ -417,24 +653,22 @@ int main(int argc, const char * argv[]) ### Unit Test Framework -An executable program named MxTest is also included in the project. MxTest utilizes the CppUnitLite macro library by Michael Feathers. Licensing of this library is not clear, here is a link to the source of this library http://c2.com/cgi/wiki?CppUnitLite +An executable program named MxTest is also included in the project. MxTest utilizes the CppUnitLite macro library by Michael Feathers. Licensing of this library is not clear, [here is a link](http://c2.com/cgi/wiki?CppUnitLite) to the source of this library. CppUnitLite appears to be abandonware, but it is very useful. Here are some additional CppUnitLite links -http://www.objectmentor.com/resources/downloads.html -https://github.com/smikes/CppUnitLite + * [http://www.objectmentor.com/resources/downloads.html](http://www.objectmentor.com/resources/downloads.html) + * [https://github.com/smikes/CppUnitLite](https://github.com/smikes/CppUnitLite) + * [https://github.com/webern/CppUnitLite](https://github.com/webern/CppUnitLite) The tests are slow to compile, see the *Compiling* section for more info on how to skip compilation of the tests. -### Known Issues and Todo List -- There is no easy way to "deep copy" anything. Each element and attributes class needs a "clone" function. -- XsID uniqueness is not constrained. Should it be? -- XsIDREF's, especially in the ScorePartList are not constrained to relate to the PartList XsID's. Thus it is possible to create an unparsable MusicXML document. A feature could be added to enforce this one-to-one constraint. Should this feature be added in the core model? Maybe not. -- A simplified and stable facade API should be added to make it much, much easier to understand and interact with MusicXML documents. +### Release Notes -### Historical Notes and Release Notes -**Historical Note: October 6, 2016:** Significant progress has been made on the `api` namespace, which is a simplified set of data structures to represent a MusicXML document. These are being implemented as mostly-POD structs. Currently the importing of data into these structures is well-underway, but the exporting from these data structures has not been implemented. + * **Version 1.0** April 2019: A more-or-less stable version of `mx::api` + * **Version 0.3** Early 2019 + * **Version 0.2** August 21, 2016 Adds the ability to import MusicXML documents into MusicXML Classes. -**Release: Version 0.2 August 21, 2016** Adds the ability to import MusicXML documents into MusicXML Classes. +**Historical Note: October 6, 2016:** Significant progress has been made on the `api` namespace, which is a simplified set of data structures to represent a MusicXML document. These are being implemented as mostly-POD structs. Currently the importing of data into these structures is well-underway, but the exporting from these data structures has not been implemented. **Historical Note: August 16, 2016:** All tests are passing (core, xml and import). The remaining items to do on the ximport feature are - search for all `\\TODO's` and fix those that can be fixed diff --git a/Sourcecode/mx/examples/Read.cpp b/Sourcecode/mx/examples/Read.cpp new file mode 100644 index 000000000..2a60ff8f6 --- /dev/null +++ b/Sourcecode/mx/examples/Read.cpp @@ -0,0 +1,102 @@ +#include +#include +#include +#include + +// TODO - create a proper include folder +// normally we would want to include like this +// #include "mx/api/DocumentManager.h" +// however because our cmake system is a little bit +// broken, we are importing from the mx/api directory +// directly. + +#include "DocumentManager.h" +#include "ScoreData.h" + +#define MX_IS_A_SUCCESS 0 +#define MX_IS_A_FAILURE 1 + +constexpr const char* const xml = R"( + + + + + + Music + + + + + + 1 + + 0 + + + + G + 2 + + + + + C + 4 + + 4 + whole + + + + +)"; + +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; +} \ No newline at end of file diff --git a/Sourcecode/mx/examples/Write.cpp b/Sourcecode/mx/examples/Write.cpp new file mode 100644 index 000000000..2dc1cf05d --- /dev/null +++ b/Sourcecode/mx/examples/Write.cpp @@ -0,0 +1,134 @@ +#include +#include +#include +#include + +// TODO - create a proper include folder +// normally we would want to include like this +// #include "mx/api/DocumentManager.h" +// however because our cmake system is a little bit +// broken, we are importing from the mx/api directory +// directly. + +#include "DocumentManager.h" +#include "ScoreData.h" + +// set this to 1 if you want to see the xml in your console +#define MX_WRITE_THIS_TO_THE_CONSOLE 1 + +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" ); + + return 0; +} \ No newline at end of file diff --git a/Sourcecode/mxtest/file/PathRoot.h b/Sourcecode/mxtest/file/PathRoot.h index b2de5688b..d078801ca 100644 --- a/Sourcecode/mxtest/file/PathRoot.h +++ b/Sourcecode/mxtest/file/PathRoot.h @@ -1,13 +1,13 @@ -// This file is auto generated -// To override these paths, pass in these macro definitions to the compiler. +// This file is auto generated by CMake +// To override these paths, pass in macro definitions to the compiler. #pragma once // The absolute path to the root of the repository. #ifndef MX_REPO_ROOT_PATH -#define MX_REPO_ROOT_PATH "/Users/mjb/Documents/repos/mx" +#define MX_REPO_ROOT_PATH "/Users/mjb/repos/mx" #endif // The absolute path to the binary output directory. #ifndef MX_BINARY_OUTPUT_PATH -#define MX_BINARY_OUTPUT_PATH "/Users/mjb/Documents/repos/mx/cmake-build-debug" +#define MX_BINARY_OUTPUT_PATH "/Users/mjb/repos/mx/cmake-build-debug" #endif \ No newline at end of file