Music XML
ci / checks (push) Successful in 42m24s

This commit is contained in:
khodowany
2026-09-10 22:01:18 -05:00
parent ea4e0bbba1
commit f84717990b
47 changed files with 16739 additions and 8391 deletions
+4 -309
View File
@@ -20,6 +20,7 @@
#include <vamp_zero/errors.hpp>
#include <vamp_zero/score.hpp>
#include <vamp_zero/notation.hpp>
namespace {
@@ -27,72 +28,6 @@ auto invalid(std::string message) -> modulus::error {
return vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, std::move(message));
}
auto required_string(boost::json::object const& object, std::char_t const* field) -> std::expected<std::string, modulus::error> {
auto found = object.find(field);
if (found == object.end() || !found->value().is_string()) {
return std::unexpected(invalid(std::string{field} + " must be a string"));
}
auto value = std::string{found->value().as_string()};
if (value.empty()) {
return std::unexpected(invalid(std::string{field} + " must not be empty"));
}
return value;
}
auto optional_string_value(boost::json::object const& object, std::char_t const* field, std::string fallback = {}) -> std::expected<std::string, modulus::error> {
auto found = object.find(field);
if (found == object.end()) {
return fallback;
}
if (!found->value().is_string()) {
return std::unexpected(invalid(std::string{field} + " must be a string"));
}
return std::string{found->value().as_string()};
}
auto optional_bool_value(boost::json::object const& object, std::char_t const* field, std::bool_t fallback = false) -> std::expected<std::bool_t, modulus::error> {
auto found = object.find(field);
if (found == object.end()) {
return fallback;
}
if (!found->value().is_bool()) {
return std::unexpected(invalid(std::string{field} + " must be a boolean"));
}
return found->value().as_bool();
}
auto optional_int64_value(boost::json::object const& object, std::char_t const* field, std::int64_t fallback = 0) -> std::expected<std::int64_t, modulus::error> {
auto found = object.find(field);
if (found == object.end()) {
return fallback;
}
if (found->value().is_int64()) {
return found->value().as_int64();
}
if (found->value().is_uint64()) {
auto value = found->value().as_uint64();
if (value <= static_cast<std::uint64_t>(std::numeric_limits<std::int64_t>::max())) {
return static_cast<std::int64_t>(value);
}
}
return std::unexpected(invalid(std::string{field} + " must be an integer"));
}
auto require_object(boost::json::value const& value, std::string_view label) -> std::expected<boost::json::object const*, modulus::error> {
if (!value.is_object()) {
return std::unexpected(invalid(std::string{label} + " must be an object"));
}
return &value.as_object();
}
auto require_array(boost::json::object const& object, std::char_t const* field) -> std::expected<boost::json::array const*, modulus::error> {
auto found = object.find(field);
if (found == object.end() || !found->value().is_array()) {
return std::unexpected(invalid(std::string{field} + " must be an array"));
}
return &found->value().as_array();
}
auto human_label(std::string value) -> std::string {
if (value.empty()) {
return "Score";
@@ -116,164 +51,6 @@ auto practice_type_label(std::string const& value) -> std::string {
return human_label(value) + " Practice";
}
auto generated_source_id(std::string const& source_path, std::string const& title, std::string const& part) -> std::string {
if (!source_path.empty()) {
auto stem = std::filesystem::path{source_path}.stem().string();
if (!stem.empty()) {
return stem;
}
}
auto value = title + "-" + part;
for (auto& ch : value) {
if (ch >= 'A' && ch <= 'Z') {
ch = static_cast<std::char_t>(ch + ('a' - 'A'));
} else if (!((ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9'))) {
ch = '-';
}
}
return value.empty() ? "score" : value;
}
auto parse_pitch(boost::json::value const& value) -> std::expected<vamp_zero::score_pitch, modulus::error> {
auto object = require_object(value, "pitch");
if (!object) {
return std::unexpected(object.error());
}
auto letter = optional_string_value(**object, "letter", "c");
auto octave = optional_int64_value(**object, "octave", 4);
auto accidental = optional_string_value(**object, "accidental", {});
if (!letter || !octave || !accidental) {
return std::unexpected(!letter ? letter.error() : (!octave ? octave.error() : accidental.error()));
}
return vamp_zero::score_pitch{
.letter = letter->empty() ? 'c' : (*letter)[0],
.octave = static_cast<std::int32_t>(*octave),
.accidental = std::move(*accidental),
};
}
auto parse_note(boost::json::value const& value) -> std::expected<vamp_zero::score_note, modulus::error> {
auto object = require_object(value, "note");
if (!object) {
return std::unexpected(object.error());
}
auto id = optional_string_value(**object, "id", {});
auto kind = optional_string_value(**object, "kind", "note");
auto hidden = optional_bool_value(**object, "hidden", false);
auto duration = optional_int64_value(**object, "duration_ticks", 0);
auto display = std::expected<std::int64_t, modulus::error>{0};
if (duration) {
display = optional_int64_value(**object, "display_ticks", *duration);
}
auto notehead = optional_string_value(**object, "notehead", {});
auto chord = optional_string_value(**object, "chord_symbol", {});
auto lyric = optional_string_value(**object, "lyric", {});
auto tie_to_next = optional_bool_value(**object, "tie_to_next", false);
auto tie_from_previous = optional_bool_value(**object, "tie_from_previous", false);
auto tuplet_id = optional_string_value(**object, "tuplet_id", {});
if (!id || !kind || !hidden || !duration || !display || !notehead || !chord || !lyric || !tie_to_next || !tie_from_previous || !tuplet_id) {
return std::unexpected(!id ? id.error() : (!kind ? kind.error() : (!hidden ? hidden.error() : (!duration ? duration.error() : (!display ? display.error() : (!notehead ? notehead.error() : (!chord ? chord.error() : (!lyric ? lyric.error() : (!tie_to_next ? tie_to_next.error() : (!tie_from_previous ? tie_from_previous.error() : tuplet_id.error()))))))))));
}
auto note = vamp_zero::score_note{
.id = std::move(*id),
.kind = std::move(*kind),
.hidden = *hidden,
.duration_ticks = *duration,
.display_ticks = *display,
.notehead = std::move(*notehead),
.chord_symbol = std::move(*chord),
.lyric = std::move(*lyric),
.tie_to_next = *tie_to_next,
.tie_from_previous = *tie_from_previous,
.tuplet_id = std::move(*tuplet_id),
};
if (auto found = (*object)->find("pitches"); found != (*object)->end()) {
if (!found->value().is_array()) {
return std::unexpected(invalid("pitches must be an array"));
}
for (auto const& pitch_value : found->value().as_array()) {
auto pitch = parse_pitch(pitch_value);
if (!pitch) {
return std::unexpected(pitch.error());
}
note.pitches.push_back(std::move(*pitch));
}
}
return note;
}
auto parse_tuplet(boost::json::value const& value) -> std::expected<vamp_zero::score_tuplet, modulus::error> {
auto object = require_object(value, "tuplet");
if (!object) {
return std::unexpected(object.error());
}
auto id = required_string(**object, "id");
auto note_count = optional_int64_value(**object, "note_count", 0);
auto space_count = optional_int64_value(**object, "space_count", 0);
if (!id || !note_count || !space_count) {
return std::unexpected(!id ? id.error() : (!note_count ? note_count.error() : space_count.error()));
}
return vamp_zero::score_tuplet{
.id = std::move(*id),
.note_count = static_cast<std::int32_t>(*note_count),
.space_count = static_cast<std::int32_t>(*space_count),
};
}
auto parse_measure(boost::json::value const& value) -> std::expected<vamp_zero::score_measure, modulus::error> {
auto object = require_object(value, "measure");
if (!object) {
return std::unexpected(object.error());
}
auto id = optional_string_value(**object, "id", {});
auto section = optional_string_value(**object, "section_label", {});
auto time = optional_string_value(**object, "time_signature", {});
auto key = optional_string_value(**object, "key_signature", {});
if (!id || !section || !time || !key) {
return std::unexpected(!id ? id.error() : (!section ? section.error() : (!time ? time.error() : key.error())));
}
auto measure = vamp_zero::score_measure{
.id = std::move(*id),
.section_label = std::move(*section),
.time_signature = std::move(*time),
.key_signature = std::move(*key),
};
auto voices = require_array(**object, "voices");
if (!voices) {
return std::unexpected(voices.error());
}
for (auto const& voice_value : **voices) {
auto voice = require_object(voice_value, "voice");
if (!voice) {
return std::unexpected(voice.error());
}
auto notes = require_array(**voice, "notes");
if (!notes) {
return std::unexpected(notes.error());
}
for (auto const& note_value : **notes) {
auto note = parse_note(note_value);
if (!note) {
return std::unexpected(note.error());
}
measure.notes.push_back(std::move(*note));
}
if (auto tuplets_value = (*voice)->find("tuplets"); tuplets_value != (*voice)->end()) {
if (!tuplets_value->value().is_array()) {
return std::unexpected(invalid("tuplets must be an array"));
}
for (auto const& tuplet_value : tuplets_value->value().as_array()) {
auto tuplet = parse_tuplet(tuplet_value);
if (!tuplet) {
return std::unexpected(tuplet.error());
}
measure.tuplets.push_back(std::move(*tuplet));
}
}
}
return measure;
}
auto load_score_documents(std::filesystem::path const& directory, std::bool_t required) -> std::expected<std::vector<vamp_zero::score_document>, modulus::error> {
auto error = std::error_code{};
if (!std::filesystem::exists(directory, error)) {
@@ -334,7 +111,8 @@ auto key_rank(std::string const& key) -> std::size_t {
"Bb",
"B",
};
auto found = std::find(keys.begin(), keys.end(), key);
auto tonic = key.substr(0, key.find(' '));
auto found = std::find(keys.begin(), keys.end(), tonic);
return found == keys.end() ? keys.size() : static_cast<std::size_t>(std::distance(keys.begin(), found));
}
@@ -367,90 +145,7 @@ auto same_practice_exercise(vamp_zero::practice_exercise const& left, vamp_zero:
}
auto vamp_zero::parse_score_document(std::string_view json, std::string source_path) -> std::expected<vamp_zero::score_document, modulus::error> {
auto error = boost::system::error_code{};
auto parsed = boost::json::parse(json, error);
if (error) {
return std::unexpected(invalid("score JSON is invalid: " + error.message()));
}
auto root = require_object(parsed, "score root");
if (!root) {
return std::unexpected(root.error());
}
auto metadata_value = (*root)->find("metadata");
if (metadata_value == (*root)->end()) {
return std::unexpected(invalid("score metadata is required"));
}
auto metadata = require_object(metadata_value->value(), "metadata");
if (!metadata) {
return std::unexpected(metadata.error());
}
auto title = required_string(**metadata, "title");
auto composer = optional_string_value(**metadata, "composer", {});
auto key = optional_string_value(**metadata, "key_signature", {});
auto time = optional_string_value(**metadata, "time_signature", {});
if (!title || !composer || !key || !time) {
return std::unexpected(!title ? title.error() : (!composer ? composer.error() : (!key ? key.error() : time.error())));
}
auto staves = require_array(**root, "staves");
if (!staves) {
return std::unexpected(staves.error());
}
if ((*staves)->empty() || !(*staves)->front().is_object()) {
return std::unexpected(invalid("score must contain at least one staff"));
}
auto const& staff = (*staves)->front().as_object();
auto part = optional_string_value(staff, "arrangement_part_type", {});
if (!part || part->empty()) {
part = optional_string_value(staff, "role", {});
}
if (!part || part->empty()) {
part = optional_string_value(staff, "name", "score");
}
if (!part) {
return std::unexpected(part.error());
}
auto clef = optional_string_value(staff, "clef", "treble");
if (!clef) {
return std::unexpected(clef.error());
}
auto measures = require_array(staff, "measures");
if (!measures) {
return std::unexpected(measures.error());
}
auto document = vamp_zero::score_document{
.id = generated_source_id(source_path, *title, *part),
.title = std::move(*title),
.composer = std::move(*composer),
.key_signature = std::move(*key),
.time_signature = std::move(*time),
.clef = clef->empty() ? std::string{"treble"} : std::move(*clef),
.part = human_label(*part),
.arrangement_part_type = std::move(*part),
.source_path = std::move(source_path),
.source_json = std::string{json},
};
for (auto const& measure_value : **measures) {
auto measure = parse_measure(measure_value);
if (!measure) {
return std::unexpected(measure.error());
}
if (document.key_signature.empty() && !measure->key_signature.empty()) {
document.key_signature = measure->key_signature;
}
if (document.time_signature.empty() && !measure->time_signature.empty()) {
document.time_signature = measure->time_signature;
}
document.measures.push_back(std::move(*measure));
}
if (document.key_signature.empty()) {
document.key_signature = "C";
}
if (document.time_signature.empty()) {
document.time_signature = "4/4";
}
return document;
return vamp_zero::parse_notation_document(json, std::move(source_path));
}
auto vamp_zero::group_library_parts(std::vector<vamp_zero::library_part> parts) -> vamp_zero::library_state {