@@ -0,0 +1,587 @@
|
||||
#include <vamp_zero/library.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <expected>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <limits>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <boost/json/array.hpp>
|
||||
#include <boost/json/object.hpp>
|
||||
#include <boost/json/parse.hpp>
|
||||
#include <boost/json/value.hpp>
|
||||
#include <boost/system/error_code.hpp>
|
||||
|
||||
#include <vamp_zero/errors.hpp>
|
||||
|
||||
namespace {
|
||||
|
||||
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";
|
||||
}
|
||||
auto capitalize_next = true;
|
||||
for (auto& ch : value) {
|
||||
if (ch == '-' || ch == '_') {
|
||||
ch = ' ';
|
||||
capitalize_next = true;
|
||||
continue;
|
||||
}
|
||||
if (capitalize_next && ch >= 'a' && ch <= 'z') {
|
||||
ch = static_cast<std::char_t>(ch - ('a' - 'A'));
|
||||
}
|
||||
capitalize_next = false;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
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);
|
||||
if (!id || !kind || !hidden || !duration || !display || !notehead || !chord || !lyric || !tie_to_next || !tie_from_previous) {
|
||||
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.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,
|
||||
};
|
||||
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_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));
|
||||
}
|
||||
}
|
||||
return measure;
|
||||
}
|
||||
|
||||
auto score_page_count(std::size_t measure_count) -> std::size_t {
|
||||
constexpr auto measures_per_page = std::size_t{12};
|
||||
return measure_count == 0U ? 1U : (measure_count + measures_per_page - 1U) / measures_per_page;
|
||||
}
|
||||
|
||||
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)) {
|
||||
if (required) {
|
||||
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::NOT_FOUND, "score asset directory not found: " + directory.string()));
|
||||
}
|
||||
return std::vector<vamp_zero::score_document>{};
|
||||
}
|
||||
|
||||
auto paths = std::vector<std::filesystem::path>{};
|
||||
for (auto const& entry : std::filesystem::directory_iterator{directory, error}) {
|
||||
if (error) {
|
||||
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::UNAVAILABLE, "failed to scan score assets: " + error.message()));
|
||||
}
|
||||
if (!entry.is_regular_file() || entry.path().extension() != ".json") {
|
||||
continue;
|
||||
}
|
||||
paths.push_back(entry.path());
|
||||
}
|
||||
std::sort(paths.begin(), paths.end());
|
||||
|
||||
auto documents = std::vector<vamp_zero::score_document>{};
|
||||
for (auto const& path : paths) {
|
||||
auto input = std::ifstream{path, std::ios::binary};
|
||||
if (!input) {
|
||||
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::NOT_FOUND, "failed to open score JSON: " + path.string()));
|
||||
}
|
||||
auto text = std::string{std::istreambuf_iterator<std::char_t>{input}, std::istreambuf_iterator<std::char_t>{}};
|
||||
auto document = vamp_zero::parse_score_document(text, path.string());
|
||||
if (!document) {
|
||||
return std::unexpected(document.error());
|
||||
}
|
||||
documents.push_back(std::move(*document));
|
||||
}
|
||||
return documents;
|
||||
}
|
||||
|
||||
auto path_parts(std::filesystem::path const& path) -> std::vector<std::string> {
|
||||
auto parts = std::vector<std::string>{};
|
||||
for (auto const& part : path) {
|
||||
parts.push_back(part.string());
|
||||
}
|
||||
return parts;
|
||||
}
|
||||
|
||||
auto key_rank(std::string const& key) -> std::size_t {
|
||||
static auto const keys = std::vector<std::string>{
|
||||
"C",
|
||||
"Db",
|
||||
"D",
|
||||
"Eb",
|
||||
"E",
|
||||
"F",
|
||||
"Gb",
|
||||
"G",
|
||||
"Ab",
|
||||
"A",
|
||||
"Bb",
|
||||
"B",
|
||||
};
|
||||
auto found = std::find(keys.begin(), keys.end(), key);
|
||||
return found == keys.end() ? keys.size() : static_cast<std::size_t>(std::distance(keys.begin(), found));
|
||||
}
|
||||
|
||||
auto sync_primary_variant(vamp_zero::practice_exercise& exercise) -> void {
|
||||
std::sort(exercise.variants.begin(), exercise.variants.end(), [](auto const& left, auto const& right) {
|
||||
auto left_rank = key_rank(left.key_signature);
|
||||
auto right_rank = key_rank(right.key_signature);
|
||||
if (left_rank != right_rank) {
|
||||
return left_rank < right_rank;
|
||||
}
|
||||
return left.id < right.id;
|
||||
});
|
||||
if (exercise.variants.empty()) {
|
||||
return;
|
||||
}
|
||||
auto const& primary = exercise.variants.front();
|
||||
exercise.key_signature = primary.key_signature;
|
||||
exercise.source_path = primary.source_path;
|
||||
exercise.source_json = primary.source_json;
|
||||
exercise.measure_count = primary.measure_count;
|
||||
exercise.total_pages = primary.total_pages;
|
||||
}
|
||||
|
||||
auto same_practice_exercise(vamp_zero::practice_exercise const& left, vamp_zero::practice_exercise const& right) -> std::bool_t {
|
||||
return left.id == right.id
|
||||
&& left.practice_type_id == right.practice_type_id
|
||||
&& left.lesson_group_id == right.lesson_group_id;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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", {});
|
||||
if (!title || !composer || !key) {
|
||||
return std::unexpected(!title ? title.error() : (!composer ? composer.error() : key.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 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),
|
||||
.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());
|
||||
}
|
||||
document.measures.push_back(std::move(*measure));
|
||||
}
|
||||
return document;
|
||||
}
|
||||
|
||||
auto vamp_zero::group_library_parts(std::vector<vamp_zero::library_part> parts) -> vamp_zero::library_state {
|
||||
std::sort(parts.begin(), parts.end(), [](auto const& left, auto const& right) {
|
||||
if (left.title != right.title) {
|
||||
return left.title < right.title;
|
||||
}
|
||||
return left.part < right.part;
|
||||
});
|
||||
|
||||
auto groups = std::vector<vamp_zero::song_group>{};
|
||||
for (auto const& part : parts) {
|
||||
if (groups.empty() || groups.back().title != part.title || groups.back().composer != part.composer) {
|
||||
groups.push_back({
|
||||
.title = part.title,
|
||||
.composer = part.composer,
|
||||
});
|
||||
}
|
||||
groups.back().parts.push_back(part);
|
||||
}
|
||||
return vamp_zero::library_state{
|
||||
.parts = std::move(parts),
|
||||
.groups = std::move(groups),
|
||||
};
|
||||
}
|
||||
|
||||
auto vamp_zero::group_practice_exercises(std::vector<vamp_zero::practice_exercise> exercises) -> vamp_zero::practice_library {
|
||||
std::sort(exercises.begin(), exercises.end(), [](auto const& left, auto const& right) {
|
||||
if (left.practice_type_id != right.practice_type_id) {
|
||||
return left.practice_type_id < right.practice_type_id;
|
||||
}
|
||||
if (left.lesson_group_id != right.lesson_group_id) {
|
||||
return left.lesson_group_id < right.lesson_group_id;
|
||||
}
|
||||
if (left.id != right.id) {
|
||||
return left.id < right.id;
|
||||
}
|
||||
if (key_rank(left.key_signature) != key_rank(right.key_signature)) {
|
||||
return key_rank(left.key_signature) < key_rank(right.key_signature);
|
||||
}
|
||||
return left.source_path < right.source_path;
|
||||
});
|
||||
|
||||
auto merged = std::vector<vamp_zero::practice_exercise>{};
|
||||
for (auto& exercise : exercises) {
|
||||
if (!merged.empty() && same_practice_exercise(merged.back(), exercise)) {
|
||||
merged.back().variants.insert(
|
||||
merged.back().variants.end(),
|
||||
std::make_move_iterator(exercise.variants.begin()),
|
||||
std::make_move_iterator(exercise.variants.end())
|
||||
);
|
||||
sync_primary_variant(merged.back());
|
||||
continue;
|
||||
}
|
||||
sync_primary_variant(exercise);
|
||||
merged.push_back(std::move(exercise));
|
||||
}
|
||||
|
||||
auto types = std::vector<vamp_zero::practice_type_group>{};
|
||||
for (auto const& exercise : merged) {
|
||||
auto type = std::find_if(types.begin(), types.end(), [&](auto const& item) {
|
||||
return item.id == exercise.practice_type_id;
|
||||
});
|
||||
if (type == types.end()) {
|
||||
types.push_back({
|
||||
.id = exercise.practice_type_id,
|
||||
.title = exercise.practice_type_title,
|
||||
});
|
||||
type = std::prev(types.end());
|
||||
}
|
||||
auto lesson = std::find_if(type->lesson_groups.begin(), type->lesson_groups.end(), [&](auto const& item) {
|
||||
return item.id == exercise.lesson_group_id;
|
||||
});
|
||||
if (lesson == type->lesson_groups.end()) {
|
||||
type->lesson_groups.push_back({
|
||||
.id = exercise.lesson_group_id,
|
||||
.title = exercise.lesson_group_title,
|
||||
});
|
||||
lesson = std::prev(type->lesson_groups.end());
|
||||
}
|
||||
lesson->exercises.push_back(exercise);
|
||||
}
|
||||
return vamp_zero::practice_library{
|
||||
.exercises = std::move(merged),
|
||||
.types = std::move(types),
|
||||
};
|
||||
}
|
||||
|
||||
auto vamp_zero::load_performance_library(std::string const& assets_path) -> std::expected<vamp_zero::library_state, modulus::error> {
|
||||
auto documents = load_score_documents(std::filesystem::path{assets_path} / "songs", true);
|
||||
if (!documents) {
|
||||
return std::unexpected(documents.error());
|
||||
}
|
||||
auto parts = std::vector<vamp_zero::library_part>{};
|
||||
for (auto& document : *documents) {
|
||||
parts.push_back({
|
||||
.id = document.id,
|
||||
.title = document.title,
|
||||
.composer = document.composer,
|
||||
.key_signature = document.key_signature,
|
||||
.part = document.part,
|
||||
.source_path = document.source_path,
|
||||
.source_json = std::move(document.source_json),
|
||||
.measure_count = document.measures.size(),
|
||||
.total_pages = score_page_count(document.measures.size()),
|
||||
});
|
||||
}
|
||||
return vamp_zero::group_library_parts(std::move(parts));
|
||||
}
|
||||
|
||||
auto vamp_zero::load_practice_library(std::string const& assets_path) -> std::expected<vamp_zero::practice_library, modulus::error> {
|
||||
auto root = std::filesystem::path{assets_path} / "practice";
|
||||
auto error = std::error_code{};
|
||||
if (!std::filesystem::exists(root, error)) {
|
||||
return vamp_zero::practice_library{};
|
||||
}
|
||||
|
||||
auto paths = std::vector<std::filesystem::path>{};
|
||||
for (auto const& entry : std::filesystem::recursive_directory_iterator{root, error}) {
|
||||
if (error) {
|
||||
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::UNAVAILABLE, "failed to scan practice assets: " + error.message()));
|
||||
}
|
||||
if (!entry.is_regular_file() || entry.path().extension() != ".json") {
|
||||
continue;
|
||||
}
|
||||
paths.push_back(entry.path());
|
||||
}
|
||||
std::sort(paths.begin(), paths.end());
|
||||
|
||||
auto exercises = std::vector<vamp_zero::practice_exercise>{};
|
||||
for (auto const& path : paths) {
|
||||
auto relative = std::filesystem::relative(path, root, error);
|
||||
if (error) {
|
||||
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::UNAVAILABLE, "failed to inspect practice asset path: " + error.message()));
|
||||
}
|
||||
auto parts = path_parts(relative);
|
||||
if (parts.size() != 4U) {
|
||||
return std::unexpected(invalid("practice JSON must be stored as practice/<type>/<lesson_group>/<exercise>/<key>.json: " + path.string()));
|
||||
}
|
||||
|
||||
auto input = std::ifstream{path, std::ios::binary};
|
||||
if (!input) {
|
||||
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::NOT_FOUND, "failed to open practice JSON: " + path.string()));
|
||||
}
|
||||
auto text = std::string{std::istreambuf_iterator<std::char_t>{input}, std::istreambuf_iterator<std::char_t>{}};
|
||||
auto document = vamp_zero::parse_score_document(text, path.string());
|
||||
if (!document) {
|
||||
return std::unexpected(document.error());
|
||||
}
|
||||
auto type_id = std::move(parts[0]);
|
||||
auto lesson_id = std::move(parts[1]);
|
||||
auto exercise_slug = std::move(parts[2]);
|
||||
auto key = document->key_signature.empty() ? std::string{"C"} : document->key_signature;
|
||||
auto exercise_id = type_id + "-" + lesson_id + "-" + exercise_slug;
|
||||
auto variant_id = exercise_id + "-" + std::filesystem::path{parts[3]}.stem().string();
|
||||
auto variant = vamp_zero::practice_exercise::score_variant{
|
||||
.id = std::move(variant_id),
|
||||
.key_signature = key,
|
||||
.source_path = document->source_path,
|
||||
.source_json = document->source_json,
|
||||
.measure_count = document->measures.size(),
|
||||
.total_pages = score_page_count(document->measures.size()),
|
||||
};
|
||||
exercises.push_back({
|
||||
.id = std::move(exercise_id),
|
||||
.practice_type_id = type_id,
|
||||
.practice_type_title = practice_type_label(type_id),
|
||||
.lesson_group_id = lesson_id,
|
||||
.lesson_group_title = human_label(lesson_id),
|
||||
.title = document->title,
|
||||
.composer = document->composer,
|
||||
.key_signature = variant.key_signature,
|
||||
.source_path = variant.source_path,
|
||||
.source_json = variant.source_json,
|
||||
.measure_count = variant.measure_count,
|
||||
.total_pages = variant.total_pages,
|
||||
.variants = {std::move(variant)},
|
||||
});
|
||||
}
|
||||
return vamp_zero::group_practice_exercises(std::move(exercises));
|
||||
}
|
||||
Reference in New Issue
Block a user