Improve Catalog
ci / checks (push) Successful in 46m42s

This commit is contained in:
khodowany
2026-09-27 11:03:43 -05:00
parent a416a80dd9
commit 142d843fac
418 changed files with 440 additions and 324488 deletions
+4 -25
View File
@@ -153,22 +153,14 @@ auto make_server_endpoint(vamp_zero::config const &config) -> std::expected<ligo
auto load_initial_state(vamp_zero::config const &config) -> std::expected<vamp_zero::app_state, modulus::error> {
auto io = boost::asio::io_context{};
auto performance_future = boost::asio::co_spawn(io, vamp_zero::async_load_performance_library(io.get_executor(), config.database, config.runtime.assets_path), boost::asio::use_future);
auto practice_future = boost::asio::co_spawn(io, vamp_zero::async_load_practice_library(io.get_executor(), config.database, config.runtime.assets_path), boost::asio::use_future);
auto performance_future = boost::asio::co_spawn(io, vamp_zero::async_load_catalog(io.get_executor(), config.database, config.runtime.assets_path), boost::asio::use_future);
io.run();
auto performance = performance_future.get();
if (!performance) {
return std::unexpected(performance.error());
}
auto practice = practice_future.get();
if (!practice) {
return std::unexpected(practice.error());
}
auto state = vamp_zero::app_state{
.catalog = std::make_shared<vamp_zero::catalog const>(vamp_zero::catalog{
.performance = std::move(*performance),
.practice = std::move(*practice),
}),
.catalog = std::make_shared<vamp_zero::catalog const>(std::move(*performance)),
.audio =
{
.playback_enabled = config.audio.playback_enabled,
@@ -204,30 +196,17 @@ auto validate_score_pages(boost::asio::any_io_executor executor, vamp_zero::conf
}
auto validate_catalog(boost::asio::any_io_executor executor, vamp_zero::config const &config, vamp_zero::catalog const &catalog) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
for (auto const &part : catalog.performance.parts) {
for (auto const &part : catalog.parts) {
auto title = vamp_zero::validated_title_path(config.runtime.assets_path, part.title_image);
if (!title) {
co_return std::unexpected(title.error());
}
auto reference = vamp_zero::score_reference{.category = vamp_zero::score_category::PERFORMANCE, .id = part.id};
auto reference = vamp_zero::score_reference{.id = part.id};
auto valid = co_await validate_score_pages(executor, config, std::move(reference));
if (!valid) {
co_return valid;
}
}
for (auto const &exercise : catalog.practice.exercises) {
for (auto const &practice_part : exercise.parts) {
auto title = vamp_zero::validated_title_path(config.runtime.assets_path, practice_part.title_image);
if (!title) {
co_return std::unexpected(title.error());
}
auto reference = vamp_zero::score_reference{.category = vamp_zero::score_category::PRACTICE, .id = practice_part.id};
auto valid = co_await validate_score_pages(executor, config, std::move(reference));
if (!valid) {
co_return valid;
}
}
}
co_return std::expected<void, modulus::error>{};
}
-4
View File
@@ -20,10 +20,6 @@ auto score_active(vamp_zero::app_state const &state) -> std::bool_t {
if (state.mode == vamp_zero::app_mode::PERFORMANCE_SCORE) {
return vamp_zero::active_part(state) != nullptr;
}
if (state.mode == vamp_zero::app_mode::PRACTICE_SCORE) {
auto const *exercise = vamp_zero::active_practice(state);
return exercise != nullptr && vamp_zero::selected_practice_part(state, *exercise) != nullptr;
}
return false;
}
+12 -81
View File
@@ -93,13 +93,9 @@ auto valid_title_image(noto::sqlite_value const &value) -> std::bool_t {
return text.size() == 64U && std::all_of(text.begin(), text.end(), [](std::char_t c) { return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'); });
}
auto compatible_table(noto::connection &connection, std::string const &table) -> boost::asio::awaitable<std::bool_t> {
auto columns = std::vector<std::string_view>{"id", "title", "composer", "key_signature", "source_path", "source_json", "measure_count", "total_pages", "title_image"};
if (table == "practice_exercises") {
columns.push_back("exercise_id");
}
columns.push_back("part");
auto parameters = std::vector<noto::sqlite_value>{noto::sqlite_value{table}};
auto compatible_catalog(noto::connection &connection) -> boost::asio::awaitable<std::bool_t> {
auto columns = std::vector<std::string_view>{"id", "title", "composer", "key_signature", "source_path", "source_json", "measure_count", "total_pages", "title_image", "part"};
auto parameters = std::vector<noto::sqlite_value>{noto::sqlite_value{"songs"}};
auto selected = co_await connection.async_execute("SELECT name, type, \"notnull\", pk FROM pragma_table_info(?1) "
"WHERE EXISTS (SELECT 1 FROM pragma_table_list WHERE schema = 'main' AND name = ?1 AND type = 'table' AND strict = 1) ORDER BY cid",
std::move(parameters));
@@ -138,14 +134,12 @@ auto vamp_zero::async_validate_database(boost::asio::any_io_executor executor, v
if (!application || *application != application_id) {
status = std::unexpected(vamp_zero::make_error(vamp_zero::error_code::PERSISTENCE, "invalid or incompatible catalog: " + config.path + "; rebuild or reinstall Product Vamp"));
}
for (auto table : {"songs", "practice_exercises"}) {
if (status && !(co_await compatible_table(connection, table))) {
status = std::unexpected(vamp_zero::make_error(vamp_zero::error_code::PERSISTENCE, "incompatible catalog table " + std::string{table} + ": " + config.path + "; rebuild or reinstall Product Vamp"));
}
if (status && !(co_await compatible_catalog(connection))) {
status = std::unexpected(vamp_zero::make_error(vamp_zero::error_code::PERSISTENCE, "incompatible songs catalog: " + config.path + "; rebuild or reinstall Product Vamp"));
}
if (status && integrity) {
auto checked = co_await connection.async_execute("PRAGMA quick_check(1)");
if (!checked || checked->rows.size() != 1U || checked->rows[0].size() != 1U || text_value(checked->rows[0][0]) != "ok") {
auto checked = co_await connection.async_quick_check();
if (!checked || *checked != std::vector<std::string>{"ok"}) {
status = std::unexpected(vamp_zero::make_error(vamp_zero::error_code::PERSISTENCE, "corrupt catalog: " + config.path + "; rebuild or reinstall Product Vamp"));
}
}
@@ -156,8 +150,8 @@ auto vamp_zero::async_validate_database(boost::asio::any_io_executor executor, v
co_return closed;
}
auto vamp_zero::async_load_performance_library(boost::asio::any_io_executor executor, vamp_zero::database_config config, std::string assets_path)
-> boost::asio::awaitable<std::expected<vamp_zero::library_state, modulus::error>> {
auto vamp_zero::async_load_catalog(boost::asio::any_io_executor executor, vamp_zero::database_config config, std::string assets_path)
-> boost::asio::awaitable<std::expected<vamp_zero::catalog, modulus::error>> {
auto connection = noto::connection{executor, connection_config(config)};
auto connected = co_await connection.async_connect();
if (!connected) {
@@ -165,7 +159,7 @@ auto vamp_zero::async_load_performance_library(boost::asio::any_io_executor exec
}
auto selected =
co_await connection.async_prepare("SELECT id, title, composer, part, source_path, measure_count, key_signature, total_pages, title_image FROM "
"songs ORDER BY title COLLATE NOCASE, part COLLATE NOCASE");
"songs ORDER BY title, composer, part, id");
if (!selected) {
auto close = co_await connection.async_close();
if (!close) {
@@ -210,69 +204,7 @@ auto vamp_zero::async_load_performance_library(boost::asio::any_io_executor exec
if (!closed) {
co_return std::unexpected(closed.error());
}
co_return vamp_zero::group_library_parts(std::move(parts));
}
auto vamp_zero::async_load_practice_library(boost::asio::any_io_executor executor, vamp_zero::database_config config, std::string assets_path)
-> boost::asio::awaitable<std::expected<vamp_zero::practice_library, modulus::error>> {
auto connection = noto::connection{executor, connection_config(config)};
auto connected = co_await connection.async_connect();
if (!connected) {
co_return std::unexpected(connected.error());
}
auto selected = co_await connection.async_prepare("SELECT id, title, composer, key_signature, source_path, measure_count, total_pages, part, exercise_id, title_image "
"FROM practice_exercises ORDER BY exercise_id, id");
if (!selected) {
auto close = co_await connection.async_close();
if (!close) {
modulus::log_error("database close failed after practice load error: " + close.error().describe());
}
co_return std::unexpected(selected.error());
}
auto exercises = std::vector<vamp_zero::practice_exercise>{};
auto status = std::expected<void, modulus::error>{};
for (;;) {
auto next = co_await selected->async_step();
if (!next) {
status = std::unexpected(next.error());
break;
}
if (!*next) {
break;
}
auto const &row = **next;
if (row.size() != 10U || count_value(row[5]) == 0U || count_value(row[6]) == 0U || text_value(row[8]).empty() || text_value(row[3]).empty() || !valid_title_image(row[9])) {
status = std::unexpected(vamp_zero::make_error(vamp_zero::error_code::PERSISTENCE, "invalid practice metadata"));
break;
}
auto source = resolve_source_path(assets_path, text_value(row[4]));
if (!source) {
status = std::unexpected(source.error());
break;
}
auto practice_part = vamp_zero::library_part{
.id = text_value(row[0]),
.title = text_value(row[1]),
.composer = text_value(row[2]),
.key_signature = text_value(row[3]),
.part = text_value(row[7]),
.source_path = std::move(*source),
.measure_count = count_value(row[5]),
.total_pages = count_value(row[6]),
.title_image = text_value(row[9]),
};
exercises.push_back({
.id = text_value(row[8]),
.title = text_value(row[1]),
.composer = text_value(row[2]),
.parts = {std::move(practice_part)},
});
}
auto closed = co_await close_catalog_query(connection, *selected, std::move(status));
if (!closed) {
co_return std::unexpected(closed.error());
}
co_return vamp_zero::group_practice_exercises(std::move(exercises));
co_return vamp_zero::group_ordered_parts(std::move(parts));
}
auto vamp_zero::async_load_score(boost::asio::any_io_executor executor, vamp_zero::database_config config, vamp_zero::score_reference reference, std::string assets_path) -> boost::asio::awaitable<std::expected<vamp_zero::score_document, modulus::error>> {
@@ -283,8 +215,7 @@ auto vamp_zero::async_load_score(boost::asio::any_io_executor executor, vamp_zer
if (!connected) {
co_return std::unexpected(connected.error());
}
auto table = reference.category == vamp_zero::score_category::PERFORMANCE ? "songs" : "practice_exercises";
auto selected = co_await connection.async_prepare("SELECT source_json, source_path FROM " + std::string{table} + " WHERE id = ?1");
auto selected = co_await connection.async_prepare("SELECT source_json, source_path FROM songs WHERE id = ?1");
if (!selected) {
static_cast<void>(co_await connection.async_close());
co_return std::unexpected(selected.error());
+1 -56
View File
@@ -1,39 +1,18 @@
#include <algorithm>
#include <expected>
#include <iterator>
#include <string>
#include <string_view>
#include <tuple>
#include <utility>
#include <vector>
#include <vamp_zero/errors.hpp>
#include <vamp_zero/score.hpp>
#include <vamp_zero/library.hpp>
#include <vamp_zero/notation.hpp>
namespace {
auto invalid(std::string message) -> modulus::error {
return vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, std::move(message));
}
auto part_rank(std::string const &part) -> std::size_t {
auto const &parts = vamp_zero::practice_part_names();
return static_cast<std::size_t>(std::distance(parts.begin(), std::find(parts.begin(), parts.end(), part)));
}
}
auto vamp_zero::parse_score_document(std::string_view json, std::string source_path) -> std::expected<vamp_zero::score_document, modulus::error> {
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 {
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 vamp_zero::group_ordered_parts(std::vector<vamp_zero::library_part> parts) -> vamp_zero::catalog {
auto groups = std::vector<vamp_zero::song_group>{};
for (auto index = std::size_t{}; index < parts.size(); ++index) {
auto const &part = parts[index];
@@ -44,37 +23,3 @@ auto vamp_zero::group_library_parts(std::vector<vamp_zero::library_part> parts)
}
return {.parts = std::move(parts), .groups = std::move(groups)};
}
auto vamp_zero::group_practice_exercises(std::vector<vamp_zero::practice_exercise> exercises) -> std::expected<vamp_zero::practice_library, modulus::error> {
std::sort(exercises.begin(), exercises.end(), [](auto const &left, auto const &right) {
return left.id < right.id;
});
auto merged = std::vector<vamp_zero::practice_exercise>{};
for (auto &exercise : exercises) {
if (!merged.empty() && merged.back().id == exercise.id) {
if (merged.back().title != exercise.title || merged.back().composer != exercise.composer) {
return std::unexpected(invalid("practice parts disagree on exercise identity: " + exercise.id));
}
merged.back().parts.insert(merged.back().parts.end(), std::make_move_iterator(exercise.parts.begin()), std::make_move_iterator(exercise.parts.end()));
} else {
merged.push_back(std::move(exercise));
}
}
for (auto &exercise : merged) {
std::sort(exercise.parts.begin(), exercise.parts.end(), [](auto const &left, auto const &right) {
return std::tuple{part_rank(left.part), left.id} < std::tuple{part_rank(right.part), right.id};
});
if (exercise.parts.size() != vamp_zero::practice_part_names().size()) {
return std::unexpected(invalid("practice exercise requires acoustic, electric, and vocal parts: " + exercise.id));
}
for (auto index = std::size_t{}; index < exercise.parts.size(); ++index) {
if (exercise.parts[index].key_signature != exercise.parts.front().key_signature) {
return std::unexpected(invalid("practice parts disagree on key: " + exercise.id));
}
if (part_rank(exercise.parts[index].part) != index) {
return std::unexpected(invalid("practice exercise has missing or duplicate parts: " + exercise.id));
}
}
}
return vamp_zero::practice_library{.exercises = std::move(merged)};
}
+6 -62
View File
@@ -3,23 +3,15 @@
#include <vamp_zero/navigation.hpp>
namespace {
auto menu_row_count(std::size_t item_count) -> std::size_t {
return item_count + 1U;
}
}
auto vamp_zero::selected_group(vamp_zero::app_state const &state) -> vamp_zero::song_group const * {
if (state.selected_song_index == 0U || state.catalog->performance.groups.empty()) {
if (state.selected_song_index == 0U || state.catalog->groups.empty()) {
return nullptr;
}
auto index = state.selected_song_index - 1U;
if (index >= state.catalog->performance.groups.size()) {
if (index >= state.catalog->groups.size()) {
return nullptr;
}
return &state.catalog->performance.groups[index];
return &state.catalog->groups[index];
}
auto vamp_zero::selected_part(vamp_zero::app_state const &state) -> vamp_zero::library_part const * {
@@ -32,60 +24,17 @@ auto vamp_zero::selected_part(vamp_zero::app_state const &state) -> vamp_zero::l
return nullptr;
}
auto part_index = group->parts[index];
return part_index < state.catalog->performance.parts.size() ? &state.catalog->performance.parts[part_index] : nullptr;
return part_index < state.catalog->parts.size() ? &state.catalog->parts[part_index] : nullptr;
}
auto vamp_zero::active_part(vamp_zero::app_state const &state) -> vamp_zero::library_part const * {
if (state.active_part_id.empty()) {
return nullptr;
}
auto found = std::find_if(state.catalog->performance.parts.begin(), state.catalog->performance.parts.end(), [&](auto const &part) {
auto found = std::find_if(state.catalog->parts.begin(), state.catalog->parts.end(), [&](auto const &part) {
return part.id == state.active_part_id;
});
return found == state.catalog->performance.parts.end() ? nullptr : &*found;
}
auto vamp_zero::selected_practice_exercise(vamp_zero::app_state const &state) -> vamp_zero::practice_exercise const * {
if (state.selected_practice_exercise_index == 0U) {
return nullptr;
}
auto index = state.selected_practice_exercise_index - 1U;
if (index >= state.catalog->practice.exercises.size()) {
return nullptr;
}
return &state.catalog->practice.exercises[index];
}
auto vamp_zero::active_practice(vamp_zero::app_state const &state) -> vamp_zero::practice_exercise const * {
if (state.active_practice_id.empty()) {
return nullptr;
}
auto found = std::find_if(state.catalog->practice.exercises.begin(),
state.catalog->practice.exercises.end(),
[&](auto const &exercise) { return exercise.id == state.active_practice_id; });
return found == state.catalog->practice.exercises.end() ? nullptr : &*found;
}
auto vamp_zero::selected_practice_part(vamp_zero::app_state const &state, vamp_zero::practice_exercise const &exercise) -> vamp_zero::library_part const * {
if (exercise.parts.empty()) {
return nullptr;
}
if (state.selected_practice_part_index == 0U) {
return nullptr;
}
auto index = state.selected_practice_part_index - 1U;
if (index >= exercise.parts.size()) {
return nullptr;
}
return &exercise.parts[index];
}
auto vamp_zero::practice_part_row_count(vamp_zero::app_state const &state) -> std::size_t {
auto const *exercise = vamp_zero::selected_practice_exercise(state);
if (exercise == nullptr) {
return 1U;
}
return menu_row_count(exercise->parts.size());
return found == state.catalog->parts.end() ? nullptr : &*found;
}
auto vamp_zero::active_pages(vamp_zero::app_state const &state) -> std::size_t {
@@ -93,10 +42,5 @@ auto vamp_zero::active_pages(vamp_zero::app_state const &state) -> std::size_t {
auto const *part = vamp_zero::active_part(state);
return part == nullptr ? 0U : part->total_pages;
}
if (state.mode == vamp_zero::app_mode::PRACTICE_SCORE) {
auto const *exercise = vamp_zero::active_practice(state);
auto const *practice_part = exercise == nullptr ? nullptr : vamp_zero::selected_practice_part(state, *exercise);
return practice_part == nullptr ? 0U : practice_part->total_pages;
}
return 0U;
}
+13 -184
View File
@@ -20,11 +20,10 @@
namespace {
constexpr auto main_menu_performance = std::size_t{0};
constexpr auto main_menu_practice = std::size_t{1};
constexpr auto main_menu_playback = std::size_t{2};
constexpr auto main_menu_metronome = std::size_t{3};
constexpr auto main_menu_tempo = std::size_t{4};
constexpr auto main_menu_count = std::size_t{5};
constexpr auto main_menu_playback = std::size_t{1};
constexpr auto main_menu_metronome = std::size_t{2};
constexpr auto main_menu_tempo = std::size_t{3};
constexpr auto main_menu_count = std::size_t{4};
auto wrap_next(std::size_t value, std::size_t count) -> std::size_t {
if (count == 0U) {
@@ -71,48 +70,27 @@ auto clamp_state(vamp_zero::app_state state) -> vamp_zero::app_state {
}
state.audio = clamp_audio_state(state.audio);
state.selected_main_menu_index = std::min(state.selected_main_menu_index, main_menu_count - 1U);
state.selected_song_index = std::min(state.selected_song_index, menu_row_count(state.catalog->performance.groups.size()) - 1U);
state.selected_song_index = std::min(state.selected_song_index, menu_row_count(state.catalog->groups.size()) - 1U);
if (auto const *group = selected_group(state); group != nullptr) {
state.selected_part_index = std::min(state.selected_part_index, menu_row_count(group->parts.size()) - 1U);
} else {
state.selected_part_index = 0U;
}
state.selected_practice_exercise_index = std::min(state.selected_practice_exercise_index, state.catalog->practice.exercises.size());
state.selected_practice_part_index = std::min(state.selected_practice_part_index, practice_part_row_count(state) - 1U);
if (state.mode == vamp_zero::app_mode::PERFORMANCE_PART_MENU && selected_group(state) == nullptr) {
state.mode = vamp_zero::app_mode::PERFORMANCE_SONG_MENU;
}
if (state.mode == vamp_zero::app_mode::PRACTICE_PART_MENU) {
auto const *exercise = selected_practice_exercise(state);
if (exercise == nullptr || exercise->parts.empty()) {
state.mode = vamp_zero::app_mode::PRACTICE_EXERCISE_MENU;
state.selected_practice_part_index = 0U;
}
}
if (state.mode == vamp_zero::app_mode::PERFORMANCE_SCORE && active_part(state) == nullptr) {
state.mode = selected_group(state) == nullptr ? vamp_zero::app_mode::PERFORMANCE_SONG_MENU
: vamp_zero::app_mode::PERFORMANCE_PART_MENU;
state.active_part_id.clear();
state.page_index = 0;
} else if (state.mode == vamp_zero::app_mode::PRACTICE_SCORE) {
auto const *exercise = active_practice(state);
if (exercise == nullptr) {
state.mode = vamp_zero::app_mode::PRACTICE_EXERCISE_MENU;
state.active_practice_id.clear();
state.page_index = 0;
} else if (selected_practice_part(state, *exercise) == nullptr) {
state.mode = vamp_zero::app_mode::PRACTICE_PART_MENU;
state.active_practice_id.clear();
state.page_index = 0;
}
}
if (state.mode != vamp_zero::app_mode::MAIN_MENU) {
state.audio.tempo_editing = false;
}
if (auto pages = active_pages(state); pages != 0U) {
state.page_index = std::min(state.page_index, pages - 1U);
} else if (state.mode != vamp_zero::app_mode::PERFORMANCE_SCORE &&
state.mode != vamp_zero::app_mode::PRACTICE_SCORE) {
} else if (state.mode != vamp_zero::app_mode::PERFORMANCE_SCORE) {
state.page_index = 0;
}
return state;
@@ -154,26 +132,12 @@ auto part_json(vamp_zero::library_part const &part) -> boost::json::object {
return result;
}
auto exercise_json(vamp_zero::practice_exercise const &exercise) -> boost::json::object {
auto parts = boost::json::array{};
for (auto const &part : exercise.parts) {
parts.push_back(part_json(part));
}
auto result = boost::json::object{};
result["id"] = exercise.id;
result["title"] = exercise.title;
result["composer"] = exercise.composer;
result["part_count"] = static_cast<std::uint64_t>(exercise.parts.size());
result["parts"] = std::move(parts);
return result;
}
auto performance_json(vamp_zero::app_state const &state) -> boost::json::object {
auto groups = boost::json::array{};
for (auto const &group : state.catalog->performance.groups) {
for (auto const &group : state.catalog->groups) {
auto parts = boost::json::array{};
for (auto const &part : group.parts) {
parts.push_back(part_json(state.catalog->performance.parts[part]));
parts.push_back(part_json(state.catalog->parts[part]));
}
auto item = boost::json::object{};
item["title"] = group.title;
@@ -182,34 +146,12 @@ auto performance_json(vamp_zero::app_state const &state) -> boost::json::object
groups.push_back(std::move(item));
}
auto library = boost::json::object{};
library["song_count"] = static_cast<std::uint64_t>(state.catalog->performance.groups.size());
library["part_count"] = static_cast<std::uint64_t>(state.catalog->performance.parts.size());
library["song_count"] = static_cast<std::uint64_t>(state.catalog->groups.size());
library["part_count"] = static_cast<std::uint64_t>(state.catalog->parts.size());
library["groups"] = std::move(groups);
return library;
}
auto practice_json(vamp_zero::app_state const &state) -> boost::json::object {
auto exercises = boost::json::array{};
for (auto const &exercise : state.catalog->practice.exercises) {
exercises.push_back(exercise_json(exercise));
}
auto selection = boost::json::object{};
selection["exercise_row_index"] = static_cast<std::uint64_t>(state.selected_practice_exercise_index);
selection["part_row_index"] = static_cast<std::uint64_t>(state.selected_practice_part_index);
if (auto const *exercise = selected_practice_exercise(state); exercise != nullptr) {
selection["exercise"] = exercise_json(*exercise);
auto practice_part = selected_practice_part(state, *exercise);
if (practice_part != nullptr) {
selection["part"] = part_json(*practice_part);
}
}
auto practice = boost::json::object{};
practice["exercise_count"] = static_cast<std::uint64_t>(state.catalog->practice.exercises.size());
practice["exercises"] = std::move(exercises);
practice["selection"] = std::move(selection);
return practice;
}
auto sanitize_line_value(std::string value) -> std::string {
for (auto &ch : value) {
if (ch == '\r' || ch == '\n') {
@@ -251,12 +193,6 @@ auto vamp_zero::apply_app_command(vamp_zero::app_state state, vamp_protocol::pag
state.selected_part_index = 0;
state.active_part_id.clear();
state.page_index = 0;
} else if (state.selected_main_menu_index == main_menu_practice) {
state.mode = vamp_zero::app_mode::PRACTICE_EXERCISE_MENU;
state.selected_practice_exercise_index = 0;
state.selected_practice_part_index = 0;
state.active_practice_id.clear();
state.page_index = 0;
} else if (state.selected_main_menu_index == main_menu_playback) {
state.audio.playback_enabled = !state.audio.playback_enabled;
} else if (state.selected_main_menu_index == main_menu_metronome) {
@@ -267,11 +203,11 @@ auto vamp_zero::apply_app_command(vamp_zero::app_state state, vamp_protocol::pag
return clamp_state(std::move(state));
case vamp_zero::app_mode::PERFORMANCE_SONG_MENU:
if (command == vamp_protocol::page_command::NEXT) {
state.selected_song_index = wrap_next(state.selected_song_index, menu_row_count(state.catalog->performance.groups.size()));
state.selected_song_index = wrap_next(state.selected_song_index, menu_row_count(state.catalog->groups.size()));
state.selected_part_index = 0;
} else if (command == vamp_protocol::page_command::PREVIOUS) {
state.selected_song_index =
wrap_previous(state.selected_song_index, menu_row_count(state.catalog->performance.groups.size()));
wrap_previous(state.selected_song_index, menu_row_count(state.catalog->groups.size()));
state.selected_part_index = 0;
} else if (state.selected_song_index == 0U) {
state.mode = vamp_zero::app_mode::MAIN_MENU;
@@ -292,7 +228,6 @@ auto vamp_zero::apply_app_command(vamp_zero::app_state state, vamp_protocol::pag
state.page_index = 0;
} else if (auto const *part = selected_part(state); part != nullptr) {
state.active_part_id = part->id;
state.active_practice_id.clear();
state.page_index = 0;
state.mode = vamp_zero::app_mode::PERFORMANCE_SCORE;
++state.score_session;
@@ -313,63 +248,6 @@ auto vamp_zero::apply_app_command(vamp_zero::app_state state, vamp_protocol::pag
}
}
return clamp_state(std::move(state));
case vamp_zero::app_mode::PRACTICE_EXERCISE_MENU:
if (command == vamp_protocol::page_command::NEXT) {
state.selected_practice_exercise_index = wrap_next(state.selected_practice_exercise_index, menu_row_count(state.catalog->practice.exercises.size()));
state.selected_practice_part_index = 0;
} else if (command == vamp_protocol::page_command::PREVIOUS) {
state.selected_practice_exercise_index = wrap_previous(state.selected_practice_exercise_index, menu_row_count(state.catalog->practice.exercises.size()));
state.selected_practice_part_index = 0;
} else if (state.selected_practice_exercise_index == 0U) {
state.mode = vamp_zero::app_mode::MAIN_MENU;
state.active_practice_id.clear();
state.page_index = 0;
} else if (selected_practice_exercise(state) != nullptr) {
state.active_part_id.clear();
state.active_practice_id.clear();
state.page_index = 0;
state.mode = vamp_zero::app_mode::PRACTICE_PART_MENU;
state.selected_practice_part_index = 0;
}
return clamp_state(std::move(state));
case vamp_zero::app_mode::PRACTICE_PART_MENU:
if (auto const *exercise = selected_practice_exercise(state);
exercise != nullptr) {
if (command == vamp_protocol::page_command::NEXT) {
state.selected_practice_part_index =
wrap_next(state.selected_practice_part_index, menu_row_count(exercise->parts.size()));
} else if (command == vamp_protocol::page_command::PREVIOUS) {
state.selected_practice_part_index =
wrap_previous(state.selected_practice_part_index, menu_row_count(exercise->parts.size()));
} else if (state.selected_practice_part_index == 0U) {
state.mode = vamp_zero::app_mode::PRACTICE_EXERCISE_MENU;
state.active_practice_id.clear();
state.page_index = 0;
} else if (selected_practice_part(state, *exercise) != nullptr) {
state.active_practice_id = exercise->id;
state.active_part_id.clear();
state.page_index = 0;
state.mode = vamp_zero::app_mode::PRACTICE_SCORE;
++state.score_session;
state.audio.playback_error.clear();
}
}
return clamp_state(std::move(state));
case vamp_zero::app_mode::PRACTICE_SCORE:
if (auto const *exercise = active_practice(state); exercise != nullptr) {
auto const *practice_part = selected_practice_part(state, *exercise);
if (command == vamp_protocol::page_command::NEXT) {
auto pages = practice_part == nullptr ? 0U : practice_part->total_pages;
state.page_index = std::min(state.page_index + 1U, pages == 0U ? 0U : pages - 1U);
} else if (command == vamp_protocol::page_command::PREVIOUS) {
state.page_index = state.page_index == 0U ? 0U : state.page_index - 1U;
} else {
state.mode = vamp_zero::app_mode::PRACTICE_PART_MENU;
state.active_practice_id.clear();
state.page_index = 0;
}
}
return clamp_state(std::move(state));
}
return clamp_state(std::move(state));
}
@@ -396,19 +274,6 @@ auto vamp_zero::app_status_json(vamp_zero::app_state const &state) -> boost::jso
active["page_number"] = static_cast<std::uint64_t>(state.page_index + 1U);
active["page_total"] = static_cast<std::uint64_t>(part->total_pages);
}
} else if (state.mode == vamp_zero::app_mode::PRACTICE_SCORE) {
if (auto const *exercise = active_practice(state); exercise != nullptr) {
auto const *practice_part = selected_practice_part(state, *exercise);
active["selected"] = true;
active["kind"] = "practice";
active["exercise"] = exercise_json(*exercise);
if (practice_part != nullptr) {
active["part"] = part_json(*practice_part);
}
active["page_index"] = static_cast<std::uint64_t>(state.page_index);
active["page_number"] = static_cast<std::uint64_t>(state.page_index + 1U);
active["page_total"] = static_cast<std::uint64_t>(practice_part == nullptr ? 0U : practice_part->total_pages);
}
}
if (active.empty()) {
active["selected"] = false;
@@ -424,7 +289,6 @@ auto vamp_zero::app_status_json(vamp_zero::app_state const &state) -> boost::jso
root["mode"] = vamp_zero::to_string(state.mode);
root["library"] = performance;
root["performance"] = std::move(performance);
root["practice"] = practice_json(state);
root["selection"] = std::move(selection);
root["active"] = std::move(active);
root["audio"] = audio_json(state);
@@ -449,16 +313,6 @@ auto vamp_zero::remote_display_text(vamp_zero::app_state const &state) -> std::s
page = state.page_index + 1U;
pages = selected->total_pages;
}
} else if (state.mode == vamp_zero::app_mode::PRACTICE_SCORE) {
if (auto const *selected = active_practice(state); selected != nullptr) {
auto const *practice_part = selected_practice_part(state, *selected);
active = true;
title = selected->title;
part = practice_part == nullptr ? std::string{} : practice_part->part;
title_image = practice_part == nullptr ? std::string{} : practice_part->title_image;
page = state.page_index + 1U;
pages = practice_part == nullptr ? 0U : practice_part->total_pages;
}
}
auto output = std::ostringstream{};
output << "mode=" << vamp_zero::to_string(state.mode) << "\n";
@@ -494,25 +348,13 @@ auto vamp_zero::validate_app_state(vamp_zero::app_state const &state) -> std::ex
if (state.audio.tempo_editing && state.mode != vamp_zero::app_mode::MAIN_MENU) {
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, "tempo edit mode requires the main menu"));
}
if (state.selected_song_index >= menu_row_count(state.catalog->performance.groups.size())) {
if (state.selected_song_index >= menu_row_count(state.catalog->groups.size())) {
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, "selected performance song row is out of range"));
}
if (auto const *group = selected_group(state);
group != nullptr && state.selected_part_index >= menu_row_count(group->parts.size())) {
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, "selected performance part row is out of range"));
}
if (state.selected_practice_exercise_index >= menu_row_count(state.catalog->practice.exercises.size())) {
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, "selected practice exercise row is out of range"));
}
if (state.selected_practice_part_index >= practice_part_row_count(state)) {
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, "selected practice part row is out of range"));
}
if (state.mode == vamp_zero::app_mode::PRACTICE_PART_MENU) {
auto const *exercise = selected_practice_exercise(state);
if (exercise == nullptr || exercise->parts.empty()) {
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, "practice part mode requires an exercise with parts"));
}
}
if (state.mode == vamp_zero::app_mode::PERFORMANCE_SCORE) {
auto const *part = active_part(state);
if (part == nullptr) {
@@ -522,18 +364,5 @@ auto vamp_zero::validate_app_state(vamp_zero::app_state const &state) -> std::ex
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, "performance score page index is out of range"));
}
}
if (state.mode == vamp_zero::app_mode::PRACTICE_SCORE) {
auto const *exercise = active_practice(state);
if (exercise == nullptr) {
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, "practice score mode requires an active exercise"));
}
auto const *practice_part = selected_practice_part(state, *exercise);
if (practice_part == nullptr) {
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, "practice score mode requires a selected generated part"));
}
if (practice_part->total_pages == 0U || state.page_index >= practice_part->total_pages) {
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, "practice score page index is out of range"));
}
}
return {};
}
+2 -7
View File
@@ -87,14 +87,9 @@ class title_assets {
public:
title_assets(std::filesystem::path root, vamp_zero::catalog const &catalog)
: root_{std::move(root)} {
for (auto const &part : catalog.performance.parts) {
for (auto const &part : catalog.parts) {
identifiers_.insert(part.title_image);
}
for (auto const &exercise : catalog.practice.exercises) {
for (auto const &part : exercise.parts) {
identifiers_.insert(part.title_image);
}
}
}
auto image(std::string const &identifier) const -> std::optional<title_file> {
return identifiers_.contains(identifier) ? read_title(root_, identifier) : std::nullopt;
@@ -224,7 +219,7 @@ auto vamp_zero::control_state::finish_playback(std::uint64_t session, std::strin
auto updated = vamp_zero::app_state{};
{
auto lock = std::scoped_lock{mutex_};
if (!state_.audio.playback_enabled || state_.score_session != session || (catalog && catalog != state_.catalog) || (state_.mode != vamp_zero::app_mode::PERFORMANCE_SCORE && state_.mode != vamp_zero::app_mode::PRACTICE_SCORE)) {
if (!state_.audio.playback_enabled || state_.score_session != session || (catalog && catalog != state_.catalog) || state_.mode != vamp_zero::app_mode::PERFORMANCE_SCORE) {
return;
}
if (error.empty()) {
+1 -7
View File
@@ -131,13 +131,7 @@ auto vamp_zero::selected_score_reference(vamp_zero::app_state const &state) -> s
}
if (state.mode == vamp_zero::app_mode::PERFORMANCE_SCORE) {
if (auto part = vamp_zero::active_part(state)) {
return vamp_zero::score_reference{.category = vamp_zero::score_category::PERFORMANCE, .id = part->id};
}
} else if (state.mode == vamp_zero::app_mode::PRACTICE_SCORE) {
if (auto exercise = vamp_zero::active_practice(state)) {
if (auto practice_part = vamp_zero::selected_practice_part(state, *exercise)) {
return vamp_zero::score_reference{.category = vamp_zero::score_category::PRACTICE, .id = practice_part->id};
}
return vamp_zero::score_reference{.id = part->id};
}
}
return std::nullopt;
-14
View File
@@ -1,8 +1,5 @@
#include <vamp_zero/types.hpp>
#include <string_view>
#include <vector>
auto vamp_zero::empty_catalog() -> std::shared_ptr<vamp_zero::catalog const> const & {
static auto const empty = std::make_shared<vamp_zero::catalog const>();
@@ -19,17 +16,6 @@ auto vamp_zero::to_string(vamp_zero::app_mode value) -> std::string {
return "performance_part_menu";
case vamp_zero::app_mode::PERFORMANCE_SCORE:
return "performance_score";
case vamp_zero::app_mode::PRACTICE_EXERCISE_MENU:
return "practice_exercise_menu";
case vamp_zero::app_mode::PRACTICE_PART_MENU:
return "practice_part_menu";
case vamp_zero::app_mode::PRACTICE_SCORE:
return "practice_score";
}
return "main_menu";
}
auto vamp_zero::practice_part_names() -> std::vector<std::string_view> const & {
static auto const parts = std::vector<std::string_view>{"Acoustic", "Electric", "Vocal"};
return parts;
}
+19 -73
View File
@@ -170,50 +170,45 @@ auto draw_menu_rows(luma::draw_context &context, luma::rect bounds, std::size_t
}
auto draw_main_menu(luma::draw_context &context, luma::rect bounds, vamp_zero::app_state const &state) -> void {
draw_text(context, bounds.x + 36.0F, bounds.y + 28.0F, "MODE", 4, vamp_style::ink, 260.0F);
draw_text(context, bounds.x + 38.0F, bounds.y + 66.0F, "SELECT WORKFLOW", 1, vamp_style::muted, 320.0F);
draw_text(context, bounds.x + 36.0F, bounds.y + 28.0F, "MENU", 4, vamp_style::ink, 260.0F);
draw_text(context, bounds.x + 38.0F, bounds.y + 66.0F, "PRODUCT VAMP", 1, vamp_style::muted, 320.0F);
auto list_y = bounds.y + 112.0F;
auto row_height = std::float32_t{54.0F};
auto row_width = bounds.width - 72.0F;
draw_row(context,
{.x = bounds.x + 36.0F, .y = list_y, .width = row_width, .height = row_height},
"Performance Mode",
std::to_string(state.catalog->performance.groups.size()) + " SONGS",
"Songs",
std::to_string(state.catalog->groups.size()) + " SONGS",
state.selected_main_menu_index == 0U);
draw_row(context,
{.x = bounds.x + 36.0F, .y = list_y + row_height, .width = row_width, .height = row_height},
"Practice Mode",
std::to_string(state.catalog->practice.exercises.size()) + " EXERCISES",
"Playback",
state.audio.playback_enabled ? "ON" : "OFF",
state.selected_main_menu_index == 1U);
draw_row(context,
{.x = bounds.x + 36.0F, .y = list_y + row_height * 2.0F, .width = row_width, .height = row_height},
"Playback",
state.audio.playback_enabled ? "ON" : "OFF",
state.selected_main_menu_index == 2U);
draw_row(context,
{.x = bounds.x + 36.0F, .y = list_y + row_height * 3.0F, .width = row_width, .height = row_height},
"Metronome",
state.audio.metronome_enabled ? "ON" : "OFF",
state.selected_main_menu_index == 3U);
state.selected_main_menu_index == 2U);
auto tempo = std::to_string(state.audio.metronome_bpm) + " BPM";
if (state.audio.tempo_editing) {
tempo = "EDIT " + tempo;
}
draw_row(context,
{.x = bounds.x + 36.0F, .y = list_y + row_height * 4.0F, .width = row_width, .height = row_height},
{.x = bounds.x + 36.0F, .y = list_y + row_height * 3.0F, .width = row_width, .height = row_height},
"Tempo",
tempo,
state.selected_main_menu_index == 4U);
state.selected_main_menu_index == 3U);
}
auto draw_performance_song_menu(luma::draw_context &context, luma::rect bounds, vamp_zero::app_state const &state) -> void {
draw_text(context, bounds.x + 36.0F, bounds.y + 28.0F, "PERFORMANCE", 4, vamp_style::ink, 360.0F);
draw_text(context, bounds.x + 38.0F, bounds.y + 66.0F, std::to_string(state.catalog->performance.groups.size()) + " SONGS " + std::to_string(state.catalog->performance.parts.size()) + " PARTS", 1, vamp_style::muted, 320.0F);
draw_menu_rows(context, bounds, state.catalog->performance.groups.size(), state.selected_song_index, [&](std::size_t index, luma::rect row, std::bool_t selected) {
auto const &group = state.catalog->performance.groups[index];
draw_text(context, bounds.x + 38.0F, bounds.y + 66.0F, std::to_string(state.catalog->groups.size()) + " SONGS " + std::to_string(state.catalog->parts.size()) + " PARTS", 1, vamp_style::muted, 320.0F);
draw_menu_rows(context, bounds, state.catalog->groups.size(), state.selected_song_index, [&](std::size_t index, luma::rect row, std::bool_t selected) {
auto const &group = state.catalog->groups[index];
draw_row(context, row, group.title, std::to_string(group.parts.size()) + " PARTS", selected);
});
if (state.catalog->performance.groups.empty()) {
if (state.catalog->groups.empty()) {
draw_text(context, bounds.x + 38.0F, bounds.y + 194.0F, "NO SONGS FOUND", 3, vamp_style::retrying, 420.0F);
}
}
@@ -234,62 +229,18 @@ auto draw_performance_part_menu(luma::draw_context &context, luma::rect bounds,
draw_text(context, bounds.x + 38.0F, bounds.y + 66.0F, group->composer, 1, vamp_style::muted, bounds.width - 72.0F);
draw_menu_rows(context, bounds, group->parts.size(), state.selected_part_index, [&](std::size_t index, luma::rect row, std::bool_t selected) {
auto part_index = group->parts[index];
if (part_index >= state.catalog->performance.parts.size()) {
if (part_index >= state.catalog->parts.size()) {
return;
}
auto const &part = state.catalog->performance.parts[part_index];
auto const &part = state.catalog->parts[part_index];
draw_row(context, row, part.part, std::to_string(part.total_pages) + " PAGES", selected);
});
}
auto draw_practice_exercise_menu(luma::draw_context &context, luma::rect bounds, vamp_zero::app_state const &state) -> void {
auto const &exercises = state.catalog->practice.exercises;
draw_text(context, bounds.x + 36.0F, bounds.y + 28.0F, "PRACTICE", 4, vamp_style::ink, bounds.width - 72.0F);
draw_text(context, bounds.x + 38.0F, bounds.y + 66.0F, std::to_string(exercises.size()) + " EXERCISES", 1, vamp_style::muted, 320.0F);
draw_menu_rows(context, bounds, exercises.size(), state.selected_practice_exercise_index, [&](std::size_t index, luma::rect row, std::bool_t selected) {
auto const &exercise = exercises[index];
auto title = exercise.title == exercise.id ? exercise.id : exercise.id + " - " + exercise.title;
draw_row(context, row, title, "", selected);
});
if (exercises.empty()) {
draw_text(context, bounds.x + 38.0F, bounds.y + 194.0F, "NO EXERCISES FOUND", 3, vamp_style::retrying, 440.0F);
}
}
auto draw_practice_part_menu(luma::draw_context &context, luma::rect bounds, vamp_zero::app_state const &state) -> void {
auto const *exercise = selected_practice_exercise(state);
draw_text(context,
bounds.x + 36.0F,
bounds.y + 28.0F,
exercise == nullptr ? std::string{"PART"} : exercise->title,
4,
vamp_style::ink,
bounds.width - 72.0F);
draw_text(context, bounds.x + 38.0F, bounds.y + 66.0F, "SELECT PRACTICE PART", 1, vamp_style::muted, 360.0F);
draw_menu_rows(context, bounds, exercise == nullptr ? 0U : exercise->parts.size(), state.selected_practice_part_index, [&](std::size_t index, luma::rect row, std::bool_t selected) {
auto const &part = exercise->parts[index];
draw_row(context, row, part.part, part.key_signature, selected);
});
}
auto draw_score(luma::draw_context &context, luma::rect bounds, vamp_zero::app_state const &state, vamp_zero::score_load_state const &loaded, score_draw_cache &score_cache) -> void {
if (state.mode == vamp_zero::app_mode::PERFORMANCE_SCORE) {
auto const *part = active_part(state);
if (part == nullptr) {
draw_performance_song_menu(context, bounds, state);
return;
}
} else {
auto const *exercise = active_practice(state);
if (exercise == nullptr) {
draw_practice_exercise_menu(context, bounds, state);
return;
}
auto const *practice_part = selected_practice_part(state, *exercise);
if (practice_part == nullptr) {
draw_practice_part_menu(context, bounds, state);
return;
}
if (active_part(state) == nullptr) {
draw_performance_song_menu(context, bounds, state);
return;
}
if (loaded.loading) {
draw_text(context, bounds.x + 36.0F, bounds.y + 38.0F, "LOADING SCORE", 4, vamp_style::ink, bounds.width - 72.0F);
@@ -348,12 +299,7 @@ public:
draw_performance_song_menu(context, content, state.app);
} else if (state.app.mode == vamp_zero::app_mode::PERFORMANCE_PART_MENU) {
draw_performance_part_menu(context, content, state.app);
} else if (state.app.mode == vamp_zero::app_mode::PRACTICE_EXERCISE_MENU) {
draw_practice_exercise_menu(context, content, state.app);
} else if (state.app.mode == vamp_zero::app_mode::PRACTICE_PART_MENU) {
draw_practice_part_menu(context, content, state.app);
} else if (state.app.mode == vamp_zero::app_mode::PERFORMANCE_SCORE ||
state.app.mode == vamp_zero::app_mode::PRACTICE_SCORE) {
} else if (state.app.mode == vamp_zero::app_mode::PERFORMANCE_SCORE) {
draw_score(context, content, state.app, loaded, score_cache_);
} else {
draw_main_menu(context, content, state.app);