diff --git a/.circleci/config.yml b/.circleci/config.yml index fb86fabe1..754cd32c2 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -10,6 +10,10 @@ jobs: steps: - checkout + - run: + name: Toolchain Versions + command: | + c++ --version && cmake --version && git --version && cat /etc/os-release - run: name: Cmake command: | diff --git a/.circleci/dockerfile-test-entrypoint.sh b/.circleci/dockerfile-test-entrypoint.sh index 698a7fec7..3f4c5237a 100644 --- a/.circleci/dockerfile-test-entrypoint.sh +++ b/.circleci/dockerfile-test-entrypoint.sh @@ -7,9 +7,7 @@ sep() { sep echo "version info" -g++ --version -cmake --version -cat /etc/os-release +c++ --version && cmake --version && git --version && cat /etc/os-release sep cmake \ diff --git a/README.md b/README.md index d83563183..7f3e982bb 100755 --- a/README.md +++ b/README.md @@ -9,10 +9,7 @@ MusicXML Class Library ----------------------------------------- -![CircleCI](https://circleci.com/gh/webern/mx.svg?style=svg&circle-token=2f4d1a33a0825d9634b458a2306ed22482314661) - -Badges seem to be [broken](https://github.com/github/markup/issues/224), go to the [CircleCI project](https://circleci.com/gh/webern/mx) for build status. - +[![CircleCI](https://circleci.com/gh/webern/mx.svg?style=svg)](https://circleci.com/gh/webern/mx) ## Introduction @@ -21,9 +18,10 @@ This project is a C++ library for working with MusicXML files. MusicXML files a ## Compiling The project is tested with the following: -* Xcode 10.1 Apple LLVM version 10.0.0 (clang-1000.11.45.5) +* Clang (Xcode 10 and higher) * g++ (Debian 6.3.0-18+deb9u1) 6.3.0 20170516 * cmake version 3.7.2 +* MSVC2019 Visual Studio should be very close to working. A good first pull request would be to compile with VS and add the version to the list above. diff --git a/Sourcecode/include/mx/api/NoteData.h b/Sourcecode/include/mx/api/NoteData.h index 9aa8a0f58..1c1539b02 100755 --- a/Sourcecode/include/mx/api/NoteData.h +++ b/Sourcecode/include/mx/api/NoteData.h @@ -101,9 +101,14 @@ namespace mx PitchData pitchData; // step, alter, octave, accidental, etc int userRequestedVoiceNumber; Stem stem; - - // the location of the note, timewise, within the measure - // denominated in ticksPerQuarter as defined by ScoreData + + // the time location of the note, within the measure, denominated in ticksPerQuarter which + // is defined in ScoreData. in each measure, the note with tickTimePosition 0 is located at + // the start of the measure. if ScoreData defines ticksPerQuarter as N, then the note + // located at tickTimePosition N will be located one quarter note after the start of the + // measure. MusicXML's and tags will be automatically placed into the + // MusicXML as required to facilitate the correct timing of notes based on their + // tickTimePosition values. int tickTimePosition; DurationData durationData; diff --git a/Sourcecode/include/mx/api/PitchData.h b/Sourcecode/include/mx/api/PitchData.h index 0d3303f0b..03a776a7d 100644 --- a/Sourcecode/include/mx/api/PitchData.h +++ b/Sourcecode/include/mx/api/PitchData.h @@ -66,29 +66,55 @@ namespace mx struct PitchData { + // default construction is middle c (c4) PitchData(); + // the note name. i.e. c, d, e, f, g, a, b Step step; + + // the alteration (number of semitones of pitch distance) from the step. for example, if step is 'c' and + // alter is 1, then the sounding pitch is c#. if step is 'd' and alter is -2, then the sounding pitch is + // 'c' (i.e. d double flat). alter only affects the sounding pitch of the note. accidentals are applied + // independently. alter is always required to produce the correct sounding pitch, regardless of key + // signature or accidentals. int alter; + + // additional alteration to the sounding pitch (in hundredths of a semitone). the MusicXML alter value is + // a floating point number to facilitate microtonal music. however for mx::api we wanted the simplicity of + // dealing with integrals for the more common case on non-microtonal music. in order to still support + // microtones without resording to a floating-point alter value, we break out microtonal adjustments to a + // separate 'cents' field, which will be addeded to the alter integral: + // = (double)alter + (cents / 100.0) + double cents; + + // in MusicXML, the accidental is completely independent of the sounding pitch and is only present when you + // actually want to show the accidental in the notated music. i.e. accidental is purely visual. for example, + // if you have a measure consisting of repeated c# notes, you would typically notate this with an accidental + // on the first note only. the rest of the notes of the measure would be 'sharped' by virtue of the first + // note's sharp. in MusicXML, the first note should have an accidental of 'sharp' and an alter of '1', and + // the remaining notes of the measure should have an accidental of 'none' and an alter of '1'. Accidental accidental; bool isAccidentalParenthetical; bool isAccidentalCautionary; bool isAccidentalEditorial; bool isAccidentalBracketed; + + // which octave the note is located in. middle c is in octave 4. int octave; - // automatically set the Accidental enum value by - // parsing the alter value + // automatically set the Accidental enum value by parsing the alter value (does not consider the value of + // cents). this is a convenience function that simply adds the correct accidental given the current value + // of alter. for example, if alter is 1, then showAccidental will set the accidental field to 'sharp'. void showAccidental(); - // set the accidental value to 'none' and clear out the - // other accidental-related values + // set the accidental value to 'none' and clear out the other accidental-related values void hideAccidental(); }; MXAPI_EQUALS_BEGIN( PitchData ) MXAPI_EQUALS_MEMBER( step ) MXAPI_EQUALS_MEMBER( alter ) + MXAPI_DOUBLES_EQUALS_MEMBER( cents ) MXAPI_EQUALS_MEMBER( accidental ) MXAPI_EQUALS_MEMBER( isAccidentalParenthetical ) MXAPI_EQUALS_MEMBER( isAccidentalCautionary ) diff --git a/Sourcecode/private/mx/api/PitchData.cpp b/Sourcecode/private/mx/api/PitchData.cpp index d4b932da6..b253f7779 100644 --- a/Sourcecode/private/mx/api/PitchData.cpp +++ b/Sourcecode/private/mx/api/PitchData.cpp @@ -11,6 +11,7 @@ namespace mx PitchData::PitchData() : step{ Step::c } , alter{ 0 } + , cents{ 0.0 } , accidental{Accidental::none} , isAccidentalParenthetical{ false } , isAccidentalCautionary{ false } diff --git a/Sourcecode/private/mx/impl/NoteFunctions.cpp b/Sourcecode/private/mx/impl/NoteFunctions.cpp index 0d92d4076..2565ab7e3 100755 --- a/Sourcecode/private/mx/impl/NoteFunctions.cpp +++ b/Sourcecode/private/mx/impl/NoteFunctions.cpp @@ -110,6 +110,7 @@ namespace mx auto converter = Converter{}; myOutNoteData.pitchData.step = converter.convert( reader.getStep() ); myOutNoteData.pitchData.alter = reader.getAlter(); + myOutNoteData.pitchData.cents = reader.getCents(); myOutNoteData.pitchData.accidental = api::Accidental::none; diff --git a/Sourcecode/private/mx/impl/NoteReader.cpp b/Sourcecode/private/mx/impl/NoteReader.cpp index 5faf400ba..4295ea35e 100644 --- a/Sourcecode/private/mx/impl/NoteReader.cpp +++ b/Sourcecode/private/mx/impl/NoteReader.cpp @@ -43,6 +43,7 @@ #include "mx/utility/StringToInt.h" #include +#include "mx/api/PitchData.h" namespace mx { @@ -64,6 +65,7 @@ namespace mx , myDurationValue( 0.0L ) , myStep( core::StepEnum::c ) , myAlter( 0 ) + , myCents( 0.0 ) , myOctave( 4 ) , myStaffNumber( 0 ) , myVoiceNumber( 0 ) @@ -212,7 +214,17 @@ namespace mx const auto& pitch = *fullNoteTypeChoice.getPitch(); myStep = pitch.getStep()->getValue(); myOctave = pitch.getOctave()->getValue().getValue(); - myAlter = static_cast( std::ceil( pitch.getAlter()->getValue().getValue() - 0.5 ) ); + const auto xmlAlter = pitch.getAlter()->getValue().getValue(); + const auto intAlter = static_cast( xmlAlter ); + myAlter = intAlter; + const auto micro = xmlAlter - static_cast ( intAlter ); + const auto microDistance = std::abs( micro ); + if( microDistance >= 0.000000000001 ) + { + const auto theCents = micro * 100.0; + const auto theNarrowCents = static_cast( theCents ); + myCents = theNarrowCents; + } break; } diff --git a/Sourcecode/private/mx/impl/NoteReader.h b/Sourcecode/private/mx/impl/NoteReader.h index 013dc08e6..3701d2e24 100644 --- a/Sourcecode/private/mx/impl/NoteReader.h +++ b/Sourcecode/private/mx/impl/NoteReader.h @@ -50,6 +50,7 @@ namespace mx inline long double getDurationValue() const { return myDurationValue; } inline core::StepEnum getStep() const { return myStep; } inline int getAlter() const { return myAlter; } + inline double getCents() const { return myCents; } inline int getOctave() const { return myOctave; } inline int getStaffNumber() const { return myStaffNumber; } inline int getVoiceNumber() const { return myVoiceNumber; } @@ -89,6 +90,7 @@ namespace mx long double myDurationValue; core::StepEnum myStep; int myAlter; + double myCents; int myOctave; int myStaffNumber; int myVoiceNumber; diff --git a/Sourcecode/private/mx/impl/NoteWriter.cpp b/Sourcecode/private/mx/impl/NoteWriter.cpp index 7ab32181b..c0003bb39 100644 --- a/Sourcecode/private/mx/impl/NoteWriter.cpp +++ b/Sourcecode/private/mx/impl/NoteWriter.cpp @@ -372,8 +372,13 @@ namespace mx pitch->getStep()->setValue( myConverter.convert( myNoteData.pitchData.step ) ); if( myNoteData.pitchData.alter != 0 ) { + core::DecimalType microtones = 0.0; + if( myNoteData.pitchData.cents != 0.0 ) { + microtones = static_cast( myNoteData.pitchData.cents / 100.0 ); + } + const auto alter = static_cast( myNoteData.pitchData.alter ) + microtones; pitch->setHasAlter( true ); - pitch->getAlter()->setValue( core::Semitones{ static_cast( myNoteData.pitchData.alter ) } ); + pitch->getAlter()->setValue( core::Semitones{ alter } ); } pitch->getOctave()->setValue( core::OctaveValue{ myNoteData.pitchData.octave } ); } diff --git a/Sourcecode/private/mxtest/api/PitchDataTest.cpp b/Sourcecode/private/mxtest/api/PitchDataTest.cpp new file mode 100644 index 000000000..a2c3b0536 --- /dev/null +++ b/Sourcecode/private/mxtest/api/PitchDataTest.cpp @@ -0,0 +1,274 @@ +// MusicXML Class Library +// Copyright (c) by Matthew James Briggs +// Distributed under the MIT License + +#include +#include "mxtest/control/CompileControl.h" +#ifdef MX_COMPILE_API_TESTS + +#include "cpul/cpulTestHarness.h" +#include "mx/api/DocumentManager.h" +#include "mx/core/Document.h" +#include "mx/core/elements/ScorePartwise.h" +#include "mx/core/elements/PartwisePart.h" +#include "mx/core/elements/PartwiseMeasure.h" +#include "mx/core/elements/MusicDataGroup.h" +#include "mx/core/elements/Note.h" +#include "mx/core/elements/NoteChoice.h" +#include "mx/core/elements/NormalNoteGroup.h" +#include "mx/core/elements/FullNoteGroup.h" +#include "mx/core/elements/FullNoteTypeChoice.h" +#include "mx/core/elements/Pitch.h" +#include "mx/core/elements/Notations.h" +#include "mx/core/elements/NotationsChoice.h" +#include "mx/core/elements/Tied.h" +#include "mx/core/elements/MusicDataChoice.h" +#include "mx/core/elements/Direction.h" +#include "mx/core/elements/DirectionType.h" +#include "mx/core/elements/Offset.h" +#include "mx/core/elements/Pedal.h" +#include "ezxml/ezxml.h" + +using namespace std; +using namespace mx::api; + +namespace +{ + ezxml::XElementPtr bruteForceFindFirstElement( const ezxml::XElementPtr root, const std::string& inElementName ) + { + if( !root ) + { + throw std::runtime_error{ "bug in bruteForceFindFirstElement" }; + } + + if( root->getName() == inElementName ) + { + return root; + } + + auto iter = root->begin(); + const auto end = root->end(); + + for( ; iter != end; ++iter ) + { + const auto possible = bruteForceFindFirstElement( iter->clone(), inElementName ); + if( possible && possible->getName() == inElementName ) + { + return possible; + } + } + + return nullptr; + } + + struct Input + { + Step step; + int alter; + double cents; + Accidental accidental; + }; + + struct Output + { + Step step; + int alter; + double cents; + Accidental accidental; + std::string alterString; + std::string secondAlterString; + }; + + Output pitchDataTest( const Input& input ) + { + ScoreData score; + score.parts.emplace_back(); + auto& part = score.parts.back(); + part.measures.emplace_back(); + auto& measure = part.measures.back(); + measure.staves.emplace_back(); + auto& staff = measure.staves.back(); + auto& voice = staff.voices[0]; + voice.notes.emplace_back(); + auto& note = voice.notes.back(); + note.pitchData.step = input.step; + note.pitchData.accidental = input.accidental; + note.pitchData.alter = input.alter; + note.pitchData.cents = input.cents; + + // round trip it through xml + auto& mgr = DocumentManager::getInstance(); + auto docId = mgr.createFromScore( score ); + std::stringstream ss; + mgr.writeToStream( docId, ss ); + mgr.destroyDocument( docId ); + + // check the alter value that was written to xml + const auto xdoc = ezxml::XFactory::makeXDoc(); + xdoc->loadStream( ss ); + auto elem = xdoc->getRoot(); + elem = bruteForceFindFirstElement( elem, "alter" ); + const auto alterString = elem->getValue(); + Output output; + output.alterString = alterString; + +// deserialize back to ScoreData + const std::string xml = ss.str(); + std::istringstream iss{ xml }; + docId = mgr.createFromStream( iss ); + auto oscore = mgr.getData( docId ); + mgr.destroyDocument( docId ); + const auto& opart = oscore.parts.back(); + const auto& omeasure = opart.measures.back(); + const auto& ostaff = omeasure.staves.back(); + const auto& ovoice = ostaff.voices.at( 0 ); + const auto& onote = ovoice.notes.back(); + output.step = onote.pitchData.step; + output.alter = onote.pitchData.alter; + output.cents = onote.pitchData.cents; + output.accidental = onote.pitchData.accidental; + + // serialize a second time and check the alter string again + docId = mgr.createFromScore( score ); + ss.str( "" ); + mgr.writeToStream( docId, ss ); + mgr.destroyDocument( docId ); + + // check the alter value that was written to xml + const auto xdoc2 = ezxml::XFactory::makeXDoc(); + xdoc2->loadStream( ss ); + auto elem2 = xdoc->getRoot(); + elem2 = bruteForceFindFirstElement( elem2, "alter" ); + const auto alterString2 = elem->getValue(); + output.secondAlterString = alterString2; + return output; + } +} + +TEST( ThreeQuarterSharp, PitchData ) +{ + auto input = Input{}; + input.step = Step::f; + input.alter = 1; + input.cents = 50.0; + input.accidental = Accidental::threeQuartersSharp; + const std::string expectedAlterString = "1.5"; + const int expectedAlter = input.alter; + const double expectedCents = input.cents; + const Accidental expectedAccidental = input.accidental; + const auto output = pitchDataTest( input ); + + CHECK_EQUAL( expectedAlterString, output.alterString ); + CHECK_EQUAL( expectedAlterString, output.secondAlterString ); + CHECK_EQUAL( expectedAlter, output.alter ); + CHECK_DOUBLES_EQUAL( expectedCents, output.cents, MX_API_EQUALITY_EPSILON ); + CHECK( expectedAccidental == output.accidental ); +} +T_END; + +TEST( ThreeQuarterFlat, PitchData ) +{ + auto input = Input{}; + input.step = Step::b; + input.alter = -1; + input.cents = -50.0; + input.accidental = Accidental::threeQuartersFlat; + const std::string expectedAlterString = "-1.5"; + const int expectedAlter = input.alter; + const double expectedCents = input.cents; + const Accidental expectedAccidental = input.accidental; + const auto output = pitchDataTest( input ); + + CHECK_EQUAL( expectedAlterString, output.alterString ); + CHECK_EQUAL( expectedAlterString, output.secondAlterString ); + CHECK_EQUAL( expectedAlter, output.alter ); + CHECK_DOUBLES_EQUAL( expectedCents, output.cents, MX_API_EQUALITY_EPSILON ); + CHECK( expectedAccidental == output.accidental ); +} +T_END; + +TEST( AlmostDoubleSharp, PitchData ) +{ + auto input = Input{}; + input.step = Step::g; + input.alter = 1; + input.cents = 99.999; + input.accidental = Accidental::doubleSharp; + const std::string expectedAlterString = "1.99999"; + const int expectedAlter = input.alter; + const double expectedCents = input.cents; + const Accidental expectedAccidental = input.accidental; + const auto output = pitchDataTest( input ); + + CHECK_EQUAL( expectedAlterString, output.alterString ); + CHECK_EQUAL( expectedAlterString, output.secondAlterString ); + CHECK_EQUAL( expectedAlter, output.alter ); + CHECK_DOUBLES_EQUAL( expectedCents, output.cents, MX_API_EQUALITY_EPSILON ); + CHECK( expectedAccidental == output.accidental ); +} +T_END; + +TEST( AlmostDoubleFlat, PitchData ) +{ + auto input = Input{}; + input.step = Step::a; + input.alter = -1; + input.cents = -99.9999; + input.accidental = Accidental::flatFlat; + const std::string expectedAlterString = "-1.999999"; + const int expectedAlter = input.alter; + const double expectedCents = input.cents; + const Accidental expectedAccidental = input.accidental; + const auto output = pitchDataTest( input ); + + CHECK_EQUAL( expectedAlterString, output.alterString ); + CHECK_EQUAL( expectedAlterString, output.secondAlterString ); + CHECK_EQUAL( expectedAlter, output.alter ); + CHECK_DOUBLES_EQUAL( expectedCents, output.cents, MX_API_EQUALITY_EPSILON ); + CHECK( expectedAccidental == output.accidental ); +} +T_END; + +TEST( CrazyEdgeCase1, PitchData ) +{ + auto input = Input{}; + input.step = Step::g; + input.alter = 1; + input.cents = -123456789; + input.accidental = Accidental::none; + const std::string expectedAlterString = "-1234566.89"; + const int expectedAlter = -1234566; + const double expectedCents = -89.0; + const Accidental expectedAccidental = input.accidental; + const auto output = pitchDataTest( input ); + + CHECK_EQUAL( expectedAlterString, output.alterString ); + CHECK_EQUAL( expectedAlterString, output.secondAlterString ); + CHECK_EQUAL( expectedAlter, output.alter ); + CHECK_DOUBLES_EQUAL( expectedCents, output.cents, MX_API_EQUALITY_EPSILON ); + CHECK( expectedAccidental == output.accidental ); +} +T_END; + +TEST( CrazyEdgeCase2, PitchData ) +{ + auto input = Input{}; + input.step = Step::e; + input.alter = 21; + input.cents = 100.01; + input.accidental = Accidental::sori; + const std::string expectedAlterString = "22.0001"; + const int expectedAlter = 22; + const double expectedCents = 0.01; + const Accidental expectedAccidental = input.accidental; + const auto output = pitchDataTest( input ); + + CHECK_EQUAL( expectedAlterString, output.alterString ); + CHECK_EQUAL( expectedAlterString, output.secondAlterString ); + CHECK_EQUAL( expectedAlter, output.alter ); + CHECK_DOUBLES_EQUAL( expectedCents, output.cents, MX_API_EQUALITY_EPSILON ); + CHECK( expectedAccidental == output.accidental ); +} +T_END; + +#endif diff --git a/Sourcecode/private/mxtest/api/RoundTrip.h b/Sourcecode/private/mxtest/api/RoundTrip.h index 75049e28f..01a221f44 100644 --- a/Sourcecode/private/mxtest/api/RoundTrip.h +++ b/Sourcecode/private/mxtest/api/RoundTrip.h @@ -5,7 +5,6 @@ #pragma once #include "mxtest/control/CompileControl.h" -#ifdef MX_COMPILE_API_ROUNDTRIP #include "mx/api/DocumentManager.h" #include "mxtest/file/MxFileRepository.h" @@ -45,4 +44,3 @@ namespace mxtest } } -#endif diff --git a/Sourcecode/private/mxtest/control/CompileControl.h b/Sourcecode/private/mxtest/control/CompileControl.h index 3e606fd96..a6ce3b23f 100755 --- a/Sourcecode/private/mxtest/control/CompileControl.h +++ b/Sourcecode/private/mxtest/control/CompileControl.h @@ -13,7 +13,6 @@ #define MX_COMPILE_UTILTIY_TESTS #define MX_COMPILE_XML_TESTS -// use this to restrict the size of the files that -// the test run will open (compile-time constant). +// use this to restrict the size of the files that will be processed during the test run. // 0 indicates no limit -constexpr const int MX_COMPILE_MAX_FILE_SIZE_BYTES = 0; // 1024 * 3; // (1024 * 1024); +constexpr const int MX_COMPILE_MAX_FILE_SIZE_BYTES = 0;