#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { auto invalid(std::string message) -> modulus::error { return vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, std::move(message)); } auto human_label(std::string value) -> std::string { auto capitalize_next = std::bool_t{true}; for (auto &ch : value) { if (ch == '-' || ch == '_') { ch = ' '; capitalize_next = true; continue; } if (capitalize_next && ch >= 'a' && ch <= 'z') { ch = static_cast(ch - ('a' - 'A')); } capitalize_next = false; } return value.empty() ? "Score" : value; } auto key_rank(std::string const &key) -> std::size_t { auto const &keys = vamp_zero::practice_key_names(); auto tonic = key.substr(0, key.find(' ')); return static_cast(std::distance(keys.begin(), std::find(keys.begin(), keys.end(), tonic))); } } auto vamp_zero::parse_score_document(std::string_view json, std::string source_path) -> std::expected { return vamp_zero::parse_notation_document(json, std::move(source_path)); } auto vamp_zero::read_score_document(std::filesystem::path const &path) -> std::expected { 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{input}, std::istreambuf_iterator{}}; if (input.bad()) { return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::UNAVAILABLE, "failed to read score JSON: " + path.string())); } return vamp_zero::parse_score_document(text, path.string()); } auto vamp_zero::score_paths(std::string const &assets_path, vamp_zero::score_category category) -> std::expected, modulus::error> { auto practice = category == vamp_zero::score_category::PRACTICE; auto root = std::filesystem::path{assets_path} / (practice ? "practice" : "songs"); auto error = std::error_code{}; auto exists = std::filesystem::exists(root, error); if (error) { return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::UNAVAILABLE, root.string() + ": " + error.message())); } if (!exists) { if (practice) { return std::vector{}; } return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::NOT_FOUND, "score asset directory not found: " + root.string())); } auto paths = std::vector{}; auto entries = std::filesystem::recursive_directory_iterator{root, error}; auto end = std::filesystem::recursive_directory_iterator{}; while (!error && entries != end) { auto const &entry = *entries; if (entry.is_regular_file(error) && entry.path().extension() == ".json") { auto relative = entry.path().lexically_relative(root); if (std::distance(relative.begin(), relative.end()) != (practice ? 4 : 1)) { return std::unexpected(invalid("unexpected score asset path: " + entry.path().string())); } paths.push_back(entry.path()); } if (!error) { entries.increment(error); } } if (error) { return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::UNAVAILABLE, "failed to scan score assets: " + error.message())); } std::sort(paths.begin(), paths.end()); if (practice) { auto exercises = std::map{}; auto variants = std::map{}; for (auto const &path : paths) { auto relative = path.lexically_relative(root); auto exercise_path = relative.parent_path(); auto id = std::string{}; for (auto const &part : exercise_path) { id += (id.empty() ? "" : "-") + part.string(); } auto [exercise, added] = exercises.emplace(id, relative); if (!added && exercise->second.parent_path() != exercise_path) { return std::unexpected(invalid("practice exercise ID collision '" + id + "': " + (root / exercise->second).string() + " and " + path.string())); } auto variant_id = id + "-" + path.stem().string(); auto [variant, unique] = variants.emplace(variant_id, relative); if (!unique) { return std::unexpected(invalid("practice variant ID collision '" + variant_id + "': " + (root / variant->second).string() + " and " + path.string())); } } } return paths; } auto vamp_zero::performance_metadata(vamp_zero::score_document const &document) -> vamp_zero::library_part { return { .id = document.id, .title = document.title, .composer = document.composer, .key_signature = document.key_signature, .part = document.part, .source_path = document.source_path, .measure_count = document.measure_count, .total_pages = vamp_zero::score_page_count(document), }; } auto vamp_zero::practice_metadata(vamp_zero::score_document const &document, std::string const &assets_path) -> std::expected { auto relative = std::filesystem::path{document.source_path}.lexically_relative(std::filesystem::path{assets_path} / "practice"); auto parts = std::vector{}; for (auto const &part : relative) { parts.push_back(part.string()); } if (parts.size() != 4U) { return std::unexpected(invalid("practice JSON must be stored as practice////.json: " + document.source_path)); } auto id = parts[0] + "-" + parts[1] + "-" + parts[2]; return vamp_zero::practice_exercise{ .id = id, .practice_type_id = parts[0], .practice_type_title = human_label(parts[0]) + " Practice", .lesson_group_id = parts[1], .lesson_group_title = human_label(parts[1]), .title = document.title, .composer = document.composer, .authored_key_signature = document.authored_key_signature, .variants = {{ .id = id + "-" + std::filesystem::path{parts[3]}.stem().string(), .key_signature = document.key_signature, .source_path = document.source_path, .measure_count = document.measure_count, .total_pages = vamp_zero::score_page_count(document), }}, }; } auto vamp_zero::group_library_parts(std::vector parts) -> vamp_zero::library_state { std::sort(parts.begin(), parts.end(), [](auto const &left, auto const &right) { return std::tie(left.title, left.composer, left.part, left.id) < std::tie(right.title, right.composer, right.part, right.id); }); auto groups = std::vector{}; for (auto index = std::size_t{}; index < parts.size(); ++index) { auto const &part = parts[index]; 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(index); } return {.parts = std::move(parts), .groups = std::move(groups)}; } auto vamp_zero::group_practice_exercises(std::vector exercises) -> std::expected { std::sort(exercises.begin(), exercises.end(), [](auto const &left, auto const &right) { return std::tie(left.practice_type_id, left.lesson_group_id, left.id) < std::tie(right.practice_type_id, right.lesson_group_id, right.id); }); auto merged = std::vector{}; for (auto &exercise : exercises) { if (!merged.empty() && std::tie(merged.back().practice_type_id, merged.back().lesson_group_id, merged.back().id) == std::tie(exercise.practice_type_id, exercise.lesson_group_id, exercise.id)) { if (merged.back().authored_key_signature != exercise.authored_key_signature) { return std::unexpected(invalid("practice variants disagree on authored key: " + exercise.id)); } merged.back().variants.insert(merged.back().variants.end(), std::make_move_iterator(exercise.variants.begin()), std::make_move_iterator(exercise.variants.end())); } else { merged.push_back(std::move(exercise)); } } auto types = std::vector{}; for (auto index = std::size_t{}; index < merged.size(); ++index) { auto &exercise = merged[index]; std::sort(exercise.variants.begin(), exercise.variants.end(), [](auto const &left, auto const &right) { return std::tuple{key_rank(left.key_signature), left.id} < std::tuple{key_rank(right.key_signature), right.id}; }); if (types.empty() || types.back().id != exercise.practice_type_id) { types.push_back({.id = exercise.practice_type_id, .title = exercise.practice_type_title}); } auto &lessons = types.back().lesson_groups; if (lessons.empty() || lessons.back().id != exercise.lesson_group_id) { lessons.push_back({.id = exercise.lesson_group_id, .title = exercise.lesson_group_title}); } lessons.back().exercises.push_back(index); } 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 { auto paths = vamp_zero::score_paths(assets_path, vamp_zero::score_category::PERFORMANCE); if (!paths) { return std::unexpected(paths.error()); } auto parts = std::vector{}; for (auto const &path : *paths) { auto document = vamp_zero::read_score_document(path); if (!document) { return std::unexpected(document.error()); } parts.push_back(vamp_zero::performance_metadata(*document)); } return vamp_zero::group_library_parts(std::move(parts)); } auto vamp_zero::load_practice_library(std::string const &assets_path) -> std::expected { auto paths = vamp_zero::score_paths(assets_path, vamp_zero::score_category::PRACTICE); if (!paths) { return std::unexpected(paths.error()); } auto exercises = std::vector{}; for (auto const &path : *paths) { auto document = vamp_zero::read_score_document(path); if (!document) { return std::unexpected(document.error()); } auto exercise = vamp_zero::practice_metadata(*document, assets_path); if (!exercise) { return std::unexpected(exercise.error()); } exercises.push_back(std::move(*exercise)); } return vamp_zero::group_practice_exercises(std::move(exercises)); }