@@ -362,6 +362,7 @@ ON CONFLICT(id) DO UPDATE SET
|
||||
THEN excluded.updated_at_ms
|
||||
ELSE songs.updated_at_ms
|
||||
END
|
||||
WHERE songs.catalog_managed = 1
|
||||
)sql",
|
||||
std::move(params));
|
||||
if (!inserted) {
|
||||
@@ -417,6 +418,7 @@ ON CONFLICT(id) DO UPDATE SET
|
||||
THEN excluded.updated_at_ms
|
||||
ELSE practice_exercises.updated_at_ms
|
||||
END
|
||||
WHERE practice_exercises.catalog_managed = 1
|
||||
)sql",
|
||||
std::move(params));
|
||||
if (!inserted) {
|
||||
|
||||
+4
-309
@@ -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 {
|
||||
|
||||
@@ -0,0 +1,347 @@
|
||||
#include <vamp_zero/notation.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <filesystem>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <numeric>
|
||||
#include <set>
|
||||
#include <stdexcept>
|
||||
#include <boost/json.hpp>
|
||||
#include <boost/rational.hpp>
|
||||
#include <boost/multiprecision/cpp_int.hpp>
|
||||
#include <vamp_zero/errors.hpp>
|
||||
|
||||
namespace {
|
||||
using object = boost::json::object;
|
||||
using value = boost::json::value;
|
||||
using exact_time = boost::rational<boost::multiprecision::cpp_int>;
|
||||
auto exact(vamp_zero::score_time t) -> exact_time { return {t.numerator, t.denominator}; }
|
||||
constexpr auto types = std::array<std::string_view, 14>{"maxima", "long", "breve", "whole", "half", "quarter", "eighth", "16th", "32nd", "64th", "128th", "256th", "512th", "1024th"};
|
||||
|
||||
auto notation_error(std::string const& message) -> void { throw std::invalid_argument(message); }
|
||||
auto text(object const& o, char const* key, std::string fallback = {}) -> std::string {
|
||||
auto p = o.if_contains(key);
|
||||
return p == nullptr ? fallback : std::string{p->as_string()};
|
||||
}
|
||||
auto integer(value const& v) -> std::int64_t { return boost::json::value_to<std::int64_t>(v); }
|
||||
auto integer(object const& o, char const* key, std::int64_t fallback = 0) -> std::int64_t {
|
||||
auto p = o.if_contains(key);
|
||||
return p == nullptr ? fallback : integer(*p);
|
||||
}
|
||||
auto boolean(object const& o, char const* key) -> bool {
|
||||
auto p = o.if_contains(key);
|
||||
return p != nullptr && p->as_bool();
|
||||
}
|
||||
auto time(value const& v) -> vamp_zero::score_time {
|
||||
auto const& o = v.as_object();
|
||||
auto n = integer(o.at("numerator"));
|
||||
auto d = integer(o.at("denominator"));
|
||||
if (o.size() != 2 || n < 0 || d <= 0 || std::gcd(n, d) != 1) notation_error("invalid or unreduced musical time");
|
||||
return {n, d};
|
||||
}
|
||||
auto written(value const& v) -> vamp_zero::score_written_duration {
|
||||
auto const& o = v.as_object();
|
||||
auto name = text(o, "type");
|
||||
auto dots = integer(o, "dots");
|
||||
if (std::find(types.begin(), types.end(), name) == types.end() || dots < 0 || dots > 16) notation_error("invalid written duration");
|
||||
auto result = vamp_zero::score_written_duration{.type = name, .dots = static_cast<std::int32_t>(dots)};
|
||||
if (auto ratio = o.if_contains("ratio")) {
|
||||
result.ratio = time(*ratio);
|
||||
if (result.ratio.numerator == 0) notation_error("duration ratio must be positive");
|
||||
}
|
||||
if (auto normal = o.if_contains("normal")) {
|
||||
auto parsed = written(*normal);
|
||||
result.normal_type = parsed.type;
|
||||
result.normal_dots = parsed.dots;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
auto metric_length(vamp_zero::score_written_duration const& written) -> exact_time {
|
||||
auto index = std::distance(types.begin(), std::find(types.begin(), types.end(), written.type));
|
||||
auto dots = std::int64_t{1} << written.dots;
|
||||
return exact_time{32, std::int64_t{1} << index} * exact_time{2*dots-1, dots} * exact(written.ratio);
|
||||
}
|
||||
auto clef(value const& v) -> std::string {
|
||||
auto const& o = v.as_object();
|
||||
auto sign = text(o, "sign");
|
||||
auto line = integer(o, "line");
|
||||
auto octave = integer(o, "octave_change");
|
||||
if ((sign != "G" && sign != "F" && sign != "C") || line < 1 || line > 5 || octave < -3 || octave > 3) notation_error("invalid pitched clef");
|
||||
return sign + ":" + std::to_string(line) + ":" + std::to_string(octave);
|
||||
}
|
||||
auto alterations(value const& v) -> std::array<std::int32_t, 7> {
|
||||
auto const& values = v.as_object().at("alterations").as_array();
|
||||
if (values.size() != 7) notation_error("key must have seven diatonic alterations");
|
||||
auto result = std::array<std::int32_t, 7>{};
|
||||
for (auto i = std::size_t{}; i < 7; ++i) {
|
||||
auto a = integer(values[i]);
|
||||
if (a < -3 || a > 3) notation_error("key alteration outside supported engraving range");
|
||||
result[i] = static_cast<std::int32_t>(a);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
auto pitch(value const& v) -> vamp_zero::score_pitch {
|
||||
auto const& o = v.as_object();
|
||||
auto letter = text(o, "letter");
|
||||
auto alter = integer(o.at("alter"));
|
||||
auto octave = integer(o.at("octave"));
|
||||
if (letter.size() != 1 || std::string_view{"cdefgab"}.find(letter[0]) == std::string_view::npos || alter < -3 || alter > 3 || octave < -4 || octave > 16) notation_error("invalid spelled pitch");
|
||||
auto result = vamp_zero::score_pitch{.letter = letter[0], .octave = static_cast<std::int32_t>(octave), .accidental = alter == 0 ? "n" : std::string(static_cast<std::size_t>(std::abs(alter)), alter > 0 ? '#' : 'b'), .id = text(o, "id"), .alteration = static_cast<std::int32_t>(alter), .duration = time(o.at("duration")), .written = written(o.at("written")), .tie_to = text(o, "tie_to"), .tie_from = text(o, "tie_from")};
|
||||
if (auto a = o.if_contains("accidental_display")) {
|
||||
result.explicit_accidental = true;
|
||||
result.accidental_parentheses = boolean(a->as_object(), "parentheses");
|
||||
result.accidental_bracket = boolean(a->as_object(), "bracket");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
auto note(value const& v) -> vamp_zero::score_note {
|
||||
auto const& o = v.as_object();
|
||||
auto result = vamp_zero::score_note{};
|
||||
result.id = text(o, "id");
|
||||
result.kind = text(o, "kind");
|
||||
if (result.kind != "note" && result.kind != "rest" && result.kind != "spacer") notation_error("invalid event kind");
|
||||
result.hidden = boolean(o, "hidden") || result.kind == "spacer";
|
||||
result.onset = time(o.at("onset"));
|
||||
result.duration = time(o.at("duration"));
|
||||
result.written = written(o.at("written"));
|
||||
// Layout coordinates are floating point; the parsed rational remains authoritative.
|
||||
result.duration_ticks = result.duration.quarters() * 4096.0;
|
||||
result.display_ticks = vamp_zero::written_quarters(result.written) * 4096.0;
|
||||
result.lyric = text(o, "lyric");
|
||||
result.notehead = text(o, "notehead");
|
||||
result.stem = text(o, "stem");
|
||||
result.cue = boolean(o, "cue");
|
||||
result.measure_rest = boolean(o, "measure_rest");
|
||||
if (auto position = o.if_contains("rest_position")) {
|
||||
auto const& p = position->as_object();
|
||||
auto letter = text(p,"letter");
|
||||
auto octave = integer(p,"octave");
|
||||
if (letter.size()!=1 || std::string_view{"cdefgab"}.find(letter[0])==std::string_view::npos || octave < -4 || octave > 16) notation_error("invalid rest position");
|
||||
result.rest_position = vamp_zero::score_pitch{.letter=letter[0], .octave=static_cast<std::int32_t>(octave)};
|
||||
}
|
||||
if (auto grace = o.if_contains("grace")) {
|
||||
result.grace = true;
|
||||
result.grace_slash = boolean(grace->as_object(), "slash");
|
||||
}
|
||||
if (result.grace != (result.duration.numerator == 0)) notation_error("only grace notes may have zero metric duration");
|
||||
if (!result.grace && !result.measure_rest && exact(result.duration) != metric_length(result.written)) notation_error("metric and written event durations disagree");
|
||||
for (auto const& p : o.at("pitches").as_array()) {
|
||||
result.pitches.push_back(pitch(p));
|
||||
if (!result.grace && result.pitches.back().duration.numerator == 0) notation_error("metric chord tones require positive duration");
|
||||
if (!result.grace && exact(result.pitches.back().duration) != metric_length(result.pitches.back().written)) notation_error("metric and written chord-tone durations disagree");
|
||||
if (exact(result.pitches.back().duration) > exact(result.duration)) notation_error("chord tone exceeds its anchor duration");
|
||||
}
|
||||
if (result.kind != "note" && !result.pitches.empty()) notation_error("rests/spacers cannot contain pitches");
|
||||
if (result.kind == "note" && result.pitches.empty() && result.notehead != "slash") notation_error("pitched event has no pitches");
|
||||
result.tie_to_next = std::any_of(result.pitches.begin(), result.pitches.end(), [](auto const& p) { return !p.tie_to.empty(); });
|
||||
result.tie_from_previous = std::all_of(result.pitches.begin(), result.pitches.end(), [](auto const& p) { return !p.tie_from.empty(); });
|
||||
if (auto tuplets = o.if_contains("tuplet_ids")) for (auto const& id : tuplets->as_array()) result.tuplet_ids.emplace_back(id.as_string());
|
||||
if (!result.tuplet_ids.empty()) result.tuplet_id = result.tuplet_ids.back();
|
||||
if (auto beams = o.if_contains("beams")) for (auto const& b : beams->as_array()) {
|
||||
auto const& beam = b.as_object();
|
||||
auto level = integer(beam, "level");
|
||||
auto type = text(beam, "type");
|
||||
if (level < 1 || level > 8 || (type != "begin" && type != "continue" && type != "end" && type != "forward hook" && type != "backward hook")) notation_error("invalid beam");
|
||||
result.beams.push_back({static_cast<std::int32_t>(level), type});
|
||||
}
|
||||
if (auto tremolo = o.if_contains("tremolo")) {
|
||||
auto marks = integer(tremolo->as_object(), "marks");
|
||||
if (marks < 0 || marks > 8) notation_error("invalid measured tremolo");
|
||||
result.tremolo_marks = static_cast<std::int32_t>(marks);
|
||||
result.tremolo_type = text(tremolo->as_object(), "type");
|
||||
if (result.tremolo_type != "single" && result.tremolo_type != "start" && result.tremolo_type != "stop") notation_error("invalid tremolo type");
|
||||
result.tremolo_partner = text(tremolo->as_object(), "partner");
|
||||
}
|
||||
if (auto lyric = o.if_contains("lyric_notation")) result.lyric_extend = text(lyric->as_object(), "extend");
|
||||
return result;
|
||||
}
|
||||
auto beats(value const& v) -> std::vector<vamp_zero::score_time> {
|
||||
auto result = std::vector<vamp_zero::score_time>{};
|
||||
if (v.as_object().at("groups").as_array().empty() && !boolean(v.as_object(), "unmetered")) notation_error("meter requires beat groups or senza misura");
|
||||
for (auto const& group : v.as_object().at("groups").as_array()) {
|
||||
auto unit = integer(group.as_object().at("unit"));
|
||||
auto const& counts = group.as_object().at("beats").as_array();
|
||||
if (unit <= 0 || unit > 4096 || counts.empty()) notation_error("invalid meter group");
|
||||
for (auto const& count : counts) {
|
||||
auto n = integer(count);
|
||||
if (n <= 0 || n > 1024) notation_error("invalid meter count");
|
||||
auto compound = counts.size() == 1 && unit >= 8 && n > 3 && n % 3 == 0;
|
||||
auto repetitions = counts.size() > 1 ? 1 : compound ? n / 3 : n;
|
||||
auto numerator = counts.size() > 1 ? n * 4 : compound ? 12 : 4;
|
||||
auto divisor = std::gcd(numerator, unit);
|
||||
for (auto i = std::int64_t{}; i < repetitions; ++i) result.push_back({numerator / divisor, unit / divisor});
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
auto meter_text(value const& v) -> std::string {
|
||||
auto const& meter = v.as_object();
|
||||
auto symbol = text(meter,"symbol","normal");
|
||||
if (symbol == "common" || symbol == "cut") return symbol;
|
||||
if (boolean(meter,"unmetered")) return "senza misura";
|
||||
auto result = std::string{};
|
||||
for (auto const& group : meter.at("groups").as_array()) {
|
||||
if (!result.empty()) result += '+';
|
||||
auto numerator = std::string{};
|
||||
for (auto const& count : group.as_object().at("beats").as_array()) {
|
||||
if (!numerator.empty()) numerator += '+';
|
||||
numerator += std::to_string(integer(count));
|
||||
}
|
||||
result += numerator + '/' + std::to_string(integer(group.as_object().at("unit")));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
auto measure(value const& v) -> vamp_zero::score_measure {
|
||||
auto const& o = v.as_object();
|
||||
auto result = vamp_zero::score_measure{.id = text(o, "id"), .section_label = text(o, "section_label"), .time_signature = text(o, "time_signature"), .key_signature = text(o, "key_signature")};
|
||||
result.duration = time(o.at("duration"));
|
||||
if (result.duration.numerator == 0) notation_error("measure has zero duration");
|
||||
result.clef = clef(o.at("clef"));
|
||||
result.key_alterations = alterations(o.at("key"));
|
||||
result.structured_key = true;
|
||||
result.beats = beats(o.at("meter"));
|
||||
result.time_signature = meter_text(o.at("meter"));
|
||||
result.implicit = boolean(o, "implicit");
|
||||
auto const& voices = o.at("voices").as_array();
|
||||
if (voices.size() != 1) notation_error("exactly one voice is supported; voices cannot be flattened");
|
||||
auto cursor = exact_time{};
|
||||
for (auto const& item : voices.front().as_object().at("notes").as_array()) {
|
||||
auto n = note(item);
|
||||
if (exact(n.onset) + exact(n.duration) > exact(result.duration)) notation_error("event extends beyond measure");
|
||||
if (!n.grace) {
|
||||
if (exact(n.onset) < cursor) notation_error("events overlap within one rhythmic voice");
|
||||
cursor = exact(n.onset) + exact(n.duration);
|
||||
}
|
||||
n.clef = result.clef;
|
||||
result.notes.push_back(std::move(n));
|
||||
}
|
||||
if (auto harmonies = o.if_contains("harmonies")) for (auto const& item : harmonies->as_array()) {
|
||||
auto const& h = item.as_object();
|
||||
auto onset = time(h.at("onset"));
|
||||
if (exact(onset) >= exact(result.duration)) notation_error("harmony outside measure");
|
||||
result.harmonies.push_back({text(h,"id"), onset, text(h,"symbol"), text(h,"kind")});
|
||||
}
|
||||
if (auto attrs = o.if_contains("attributes")) for (auto const& item : attrs->as_array()) {
|
||||
auto const& a = item.as_object();
|
||||
auto change = vamp_zero::score_attribute{.onset = time(a.at("onset"))};
|
||||
if (auto c = a.if_contains("clef")) change.clef = clef(*c);
|
||||
if (auto k = a.if_contains("key")) change.key_alterations = alterations(*k);
|
||||
if (auto m = a.if_contains("meter")) { change.time_signature = meter_text(*m); change.beats = beats(*m); }
|
||||
if (exact(change.onset) >= exact(result.duration)) notation_error("attribute change outside measure");
|
||||
result.attributes.push_back(std::move(change));
|
||||
}
|
||||
if (auto bars = o.if_contains("barlines")) result.right_barline = text(bars->as_object(), "right", "regular");
|
||||
for (auto& n : result.notes) {
|
||||
for (auto const& a : result.attributes)
|
||||
if (exact(a.onset) <= exact(n.onset) && !a.clef.empty()) n.clef = a.clef;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
auto vamp_zero::written_quarters(score_written_duration const& written) -> double {
|
||||
auto found = std::find(types.begin(), types.end(), written.type);
|
||||
if (found == types.end()) return 0.0;
|
||||
return std::ldexp(32.0, -static_cast<int>(std::distance(types.begin(), found))) * (2.0 - std::ldexp(1.0, -written.dots));
|
||||
}
|
||||
|
||||
auto vamp_zero::parse_notation_document(std::string_view json, std::string source_path) -> std::expected<score_document, modulus::error> {
|
||||
try {
|
||||
auto parsed = boost::json::parse(json);
|
||||
auto const& root = parsed.as_object();
|
||||
if (integer(root, "schema_version") != 2) notation_error("unsupported score schema; regenerate assets with the current Product Vamp generator");
|
||||
auto const& meta = root.at("metadata").as_object();
|
||||
auto const& staves = root.at("staves").as_array();
|
||||
if (staves.size() != 1) notation_error("exactly one staff is currently supported");
|
||||
auto const& staff = staves.front().as_object();
|
||||
auto result = score_document{};
|
||||
result.title = text(meta, "title");
|
||||
if (result.title.empty()) notation_error("score title is required");
|
||||
result.composer = text(meta, "composer");
|
||||
result.key_signature = text(meta, "key_signature");
|
||||
result.time_signature = text(meta, "time_signature");
|
||||
result.clef = clef(staff.at("clef"));
|
||||
result.arrangement_part_type = text(staff, "arrangement_part_type", "practice");
|
||||
result.part = text(staff, "name");
|
||||
result.source_path = source_path;
|
||||
result.source_json = std::string{json};
|
||||
result.id = std::filesystem::path(result.source_path).stem().string();
|
||||
if (result.id.empty()) result.id = result.title + "-" + result.part;
|
||||
for (auto const& item : staff.at("measures").as_array()) result.measures.push_back(measure(item));
|
||||
if (result.measures.empty()) notation_error("score has no measures");
|
||||
auto ids = std::set<std::string>{};
|
||||
auto events = std::map<std::string, score_note const*>{};
|
||||
auto event_order = std::map<std::string, std::size_t>{};
|
||||
struct tone_ref { score_pitch const* pitch; exact_time start; };
|
||||
auto tones = std::map<std::string, tone_ref>{};
|
||||
auto absolute = exact_time{};
|
||||
for (auto const& m : result.measures) {
|
||||
for (auto const& n : m.notes) {
|
||||
if (n.id.empty() || !ids.insert(n.id).second) notation_error("duplicate or empty event ID");
|
||||
event_order.emplace(n.id, events.size());
|
||||
events.emplace(n.id, &n);
|
||||
for (auto const& p : n.pitches) {
|
||||
if (p.id.empty() || !tones.emplace(p.id, tone_ref{&p, absolute+exact(n.onset)}).second) notation_error("duplicate or empty pitch ID");
|
||||
}
|
||||
}
|
||||
absolute += exact(m.duration);
|
||||
}
|
||||
constexpr auto natural = std::array<int,7>{0,2,4,5,7,9,11};
|
||||
auto midi = [&](score_pitch const& p) { return 12*(p.octave+1)+natural[std::string_view{"cdefgab"}.find(p.letter)]+p.alteration.value_or(0); };
|
||||
for (auto const& [id, ref] : tones) {
|
||||
auto const& p = *ref.pitch;
|
||||
if (!p.tie_to.empty()) {
|
||||
auto next = tones.find(p.tie_to);
|
||||
if (next == tones.end() || next->second.pitch->tie_from != id || midi(p) != midi(*next->second.pitch) || ref.start+exact(p.duration) != next->second.start) notation_error("invalid or dangling pitch tie");
|
||||
}
|
||||
if (!p.tie_from.empty()) {
|
||||
auto prev = tones.find(p.tie_from);
|
||||
if (prev == tones.end() || prev->second.pitch->tie_to != id) notation_error("dangling incoming pitch tie");
|
||||
}
|
||||
}
|
||||
auto span_ids = std::set<std::string>{};
|
||||
if (auto spans = staff.if_contains("tuplets")) for (auto const& item : spans->as_array()) {
|
||||
auto const& t = item.as_object();
|
||||
if (integer(t,"note_count") <= 0 || integer(t,"note_count") > 1000000 || integer(t,"space_count") <= 0 || integer(t,"space_count") > 1000000 || integer(t,"depth") < 0 || integer(t,"depth") > 16) notation_error("invalid tuplet ratio or nesting depth");
|
||||
auto span = score_tuplet{.id = text(t,"id"), .note_count = static_cast<std::int32_t>(integer(t,"note_count")), .space_count = static_cast<std::int32_t>(integer(t,"space_count")), .start = text(t,"start"), .end = text(t,"end"), .depth = static_cast<std::int32_t>(integer(t,"depth")), .bracket = text(t,"bracket","auto"), .show_number = text(t,"show_number","actual")};
|
||||
if (span.id.empty() || !span_ids.insert(span.id).second || !ids.contains(span.start) || !ids.contains(span.end) || event_order.at(span.start) > event_order.at(span.end)) notation_error("invalid tuplet span");
|
||||
if ((span.bracket != "auto" && span.bracket != "yes" && span.bracket != "no") || (span.show_number != "none" && span.show_number != "actual" && span.show_number != "both")) notation_error("invalid tuplet appearance");
|
||||
for (auto const& [id, n] : events) {
|
||||
auto member = std::find(n->tuplet_ids.begin(), n->tuplet_ids.end(), span.id) != n->tuplet_ids.end();
|
||||
if ((id == span.start || id == span.end) && !member) notation_error("tuplet endpoint is not a member");
|
||||
if (member && (event_order.at(id) < event_order.at(span.start) || event_order.at(id) > event_order.at(span.end))) notation_error("tuplet member outside span");
|
||||
}
|
||||
for (auto& m : result.measures) if (std::any_of(m.notes.begin(), m.notes.end(), [&](auto const& n) { return std::find(n.tuplet_ids.begin(), n.tuplet_ids.end(), span.id) != n.tuplet_ids.end(); })) m.tuplets.push_back(span);
|
||||
}
|
||||
for (auto const& [id, n] : events) {
|
||||
for (auto const& span_id : n->tuplet_ids) if (!span_ids.contains(span_id)) notation_error("event refers to an unknown tuplet");
|
||||
if (n->tremolo_type == "start" || n->tremolo_type == "stop") {
|
||||
auto other = events.find(n->tremolo_partner);
|
||||
auto expected = n->tremolo_type == "start" ? "stop" : "start";
|
||||
if (other == events.end() || other->second->tremolo_partner != id || other->second->tremolo_type != expected || other->second->tremolo_marks != n->tremolo_marks) notation_error("invalid paired tremolo");
|
||||
}
|
||||
}
|
||||
if (auto spans = staff.if_contains("octave_shifts")) for (auto const& item : spans->as_array()) {
|
||||
auto const& span = item.as_object();
|
||||
if (integer(span,"octaves") < -3 || integer(span,"octaves") > 3 || exact(time(span.at("end"))) <= exact(time(span.at("start"))) || exact(time(span.at("end"))) > absolute) notation_error("invalid octave-shift span");
|
||||
result.octave_shifts.push_back({time(span.at("start")), time(span.at("end")), static_cast<std::int32_t>(integer(span,"octaves"))});
|
||||
}
|
||||
auto start = exact_time{};
|
||||
for (auto& m : result.measures) {
|
||||
for (auto& n : m.notes) {
|
||||
for (auto const& span : result.octave_shifts) {
|
||||
if (exact(span.end) <= exact(span.start) || exact(span.end) > absolute || span.octaves < -3 || span.octaves > 3) notation_error("invalid octave-shift span");
|
||||
if (start+exact(n.onset) >= exact(span.start) && start+exact(n.onset) < exact(span.end)) n.display_octaves += span.octaves;
|
||||
}
|
||||
}
|
||||
start += exact(m.duration);
|
||||
}
|
||||
return result;
|
||||
} catch (std::exception const& error) {
|
||||
return std::unexpected(make_error(error_code::INVALID_ARGUMENT, (source_path.empty() ? std::string{} : source_path + ": ") + "score notation: " + error.what()));
|
||||
}
|
||||
}
|
||||
+519
-197
File diff suppressed because it is too large
Load Diff
+14
-1
@@ -60,6 +60,19 @@ struct vamp_zero::score_draw_cache {
|
||||
}
|
||||
|
||||
std::unordered_map<std::string, entry> entries{};
|
||||
std::shared_ptr<vamp_zero::score_document const> page_document{};
|
||||
std::size_t page_index{};
|
||||
std::optional<vamp_zero::score_page_layout> page{};
|
||||
|
||||
auto draw(luma::draw_context& context, luma::rect bounds, std::shared_ptr<vamp_zero::score_document const> document, std::size_t index) -> void {
|
||||
if (!page || page_document != document || index != page_index
|
||||
|| page->bounds.x != bounds.x || page->bounds.y != bounds.y || page->bounds.width != bounds.width || page->bounds.height != bounds.height) {
|
||||
page = vamp_zero::layout_score_page(bounds, *document, index);
|
||||
page_document = std::move(document);
|
||||
page_index = index;
|
||||
}
|
||||
vamp_zero::draw_score_layout(context, *page);
|
||||
}
|
||||
};
|
||||
|
||||
namespace {
|
||||
@@ -353,7 +366,7 @@ auto draw_score(luma::draw_context& context, luma::rect bounds, vamp_zero::app_s
|
||||
.width = bounds.width - 48.0F,
|
||||
.height = bounds.height - 36.0F,
|
||||
};
|
||||
vamp_zero::draw_score_page(context, paper_bounds, **parsed, state.page_index);
|
||||
score_cache.draw(context, paper_bounds, *parsed, state.page_index);
|
||||
}
|
||||
|
||||
class vamp_root_control final : public luma::control {
|
||||
|
||||
Reference in New Issue
Block a user