Files
product-vamp/src/vamp_zero/page_control.cpp
T
khodowany 142d843fac
ci / checks (push) Successful in 46m42s
Improve Catalog
2026-09-27 11:03:43 -05:00

369 lines
16 KiB
C++

#include <vamp_zero/page_control.hpp>
#include <algorithm>
#include <cstdint>
#include <expected>
#include <optional>
#include <sstream>
#include <string>
#include <string_view>
#include <utility>
#include <boost/json/array.hpp>
#include <boost/json/object.hpp>
#include <boost/json/value.hpp>
#include <vamp_zero/errors.hpp>
#include <vamp_zero/navigation.hpp>
namespace {
constexpr auto main_menu_performance = std::size_t{0};
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) {
return 0U;
}
return (value + 1U) % count;
}
auto wrap_previous(std::size_t value, std::size_t count) -> std::size_t {
if (count == 0U) {
return 0U;
}
return value == 0U ? count - 1U : value - 1U;
}
auto menu_row_count(std::size_t item_count) -> std::size_t {
return item_count + 1U;
}
auto clamp_audio_state(vamp_zero::audio_state audio) -> vamp_zero::audio_state {
audio.metronome_min_bpm = std::clamp<std::uint32_t>(audio.metronome_min_bpm, 1U, 400U);
audio.metronome_max_bpm = std::clamp<std::uint32_t>(audio.metronome_max_bpm, audio.metronome_min_bpm, 400U);
audio.metronome_step = std::clamp<std::uint32_t>(audio.metronome_step, 1U, 100U);
audio.metronome_bpm = std::clamp(audio.metronome_bpm, audio.metronome_min_bpm, audio.metronome_max_bpm);
return audio;
}
auto increase_bpm(vamp_zero::audio_state &audio) -> void {
audio.metronome_bpm =
audio.metronome_bpm > audio.metronome_max_bpm - std::min(audio.metronome_step, audio.metronome_max_bpm)
? audio.metronome_max_bpm
: audio.metronome_bpm + audio.metronome_step;
}
auto decrease_bpm(vamp_zero::audio_state &audio) -> void {
audio.metronome_bpm = audio.metronome_bpm < audio.metronome_min_bpm + audio.metronome_step
? audio.metronome_min_bpm
: audio.metronome_bpm - audio.metronome_step;
}
auto clamp_state(vamp_zero::app_state state) -> vamp_zero::app_state {
if (!state.catalog) {
state.catalog = vamp_zero::empty_catalog();
}
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->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;
}
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::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;
}
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.page_index = 0;
}
return state;
}
auto command_json(vamp_zero::app_state const &state) -> boost::json::object {
auto commands = boost::json::object{};
commands["last"] = state.last_command;
commands["next_count"] = state.next_count;
commands["previous_count"] = state.previous_count;
commands["select_count"] = state.select_count;
commands["heartbeat_count"] = state.heartbeat_count;
return commands;
}
auto audio_json(vamp_zero::app_state const &state) -> boost::json::object {
auto audio = boost::json::object{};
audio["playback_enabled"] = state.audio.playback_enabled;
audio["playback_error"] = state.audio.playback_error;
audio["metronome_enabled"] = state.audio.metronome_enabled;
audio["metronome_bpm"] = static_cast<std::uint64_t>(state.audio.metronome_bpm);
audio["metronome_min_bpm"] = static_cast<std::uint64_t>(state.audio.metronome_min_bpm);
audio["metronome_max_bpm"] = static_cast<std::uint64_t>(state.audio.metronome_max_bpm);
audio["metronome_step"] = static_cast<std::uint64_t>(state.audio.metronome_step);
audio["tempo_editing"] = state.audio.tempo_editing;
return audio;
}
auto part_json(vamp_zero::library_part const &part) -> boost::json::object {
auto result = boost::json::object{};
result["id"] = part.id;
result["title"] = part.title;
result["composer"] = part.composer;
result["key_signature"] = part.key_signature;
result["part"] = part.part;
result["source_path"] = part.source_path;
result["measures"] = static_cast<std::uint64_t>(part.measure_count);
result["pages"] = static_cast<std::uint64_t>(part.total_pages);
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->groups) {
auto parts = boost::json::array{};
for (auto const &part : group.parts) {
parts.push_back(part_json(state.catalog->parts[part]));
}
auto item = boost::json::object{};
item["title"] = group.title;
item["composer"] = group.composer;
item["parts"] = std::move(parts);
groups.push_back(std::move(item));
}
auto library = boost::json::object{};
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 sanitize_line_value(std::string value) -> std::string {
for (auto &ch : value) {
if (ch == '\r' || ch == '\n') {
ch = ' ';
}
}
return value;
}
}
auto vamp_zero::apply_app_command(vamp_zero::app_state state, vamp_protocol::page_command command) -> vamp_zero::app_state {
state = clamp_state(std::move(state));
if (command == vamp_protocol::page_command::NEXT) {
++state.next_count;
} else if (command == vamp_protocol::page_command::PREVIOUS) {
++state.previous_count;
} else {
++state.select_count;
}
state.last_command = vamp_protocol::page_command_name(command);
switch (state.mode) {
case vamp_zero::app_mode::MAIN_MENU:
if (state.audio.tempo_editing) {
if (command == vamp_protocol::page_command::NEXT) {
increase_bpm(state.audio);
} else if (command == vamp_protocol::page_command::PREVIOUS) {
decrease_bpm(state.audio);
} else {
state.audio.tempo_editing = false;
}
} else if (command == vamp_protocol::page_command::NEXT) {
state.selected_main_menu_index = wrap_next(state.selected_main_menu_index, main_menu_count);
} else if (command == vamp_protocol::page_command::PREVIOUS) {
state.selected_main_menu_index = wrap_previous(state.selected_main_menu_index, main_menu_count);
} else if (state.selected_main_menu_index == main_menu_performance) {
state.mode = vamp_zero::app_mode::PERFORMANCE_SONG_MENU;
state.selected_song_index = 0;
state.selected_part_index = 0;
state.active_part_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) {
state.audio.metronome_enabled = !state.audio.metronome_enabled;
} else if (state.selected_main_menu_index == main_menu_tempo) {
state.audio.tempo_editing = true;
}
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->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->groups.size()));
state.selected_part_index = 0;
} else if (state.selected_song_index == 0U) {
state.mode = vamp_zero::app_mode::MAIN_MENU;
} else {
state.mode = vamp_zero::app_mode::PERFORMANCE_PART_MENU;
state.selected_part_index = 0;
}
return clamp_state(std::move(state));
case vamp_zero::app_mode::PERFORMANCE_PART_MENU:
if (auto const *group = selected_group(state); group != nullptr) {
if (command == vamp_protocol::page_command::NEXT) {
state.selected_part_index = wrap_next(state.selected_part_index, menu_row_count(group->parts.size()));
} else if (command == vamp_protocol::page_command::PREVIOUS) {
state.selected_part_index = wrap_previous(state.selected_part_index, menu_row_count(group->parts.size()));
} else if (state.selected_part_index == 0U) {
state.mode = vamp_zero::app_mode::PERFORMANCE_SONG_MENU;
state.active_part_id.clear();
state.page_index = 0;
} else if (auto const *part = selected_part(state); part != nullptr) {
state.active_part_id = part->id;
state.page_index = 0;
state.mode = vamp_zero::app_mode::PERFORMANCE_SCORE;
++state.score_session;
state.audio.playback_error.clear();
}
}
return clamp_state(std::move(state));
case vamp_zero::app_mode::PERFORMANCE_SCORE:
if (auto const *part = active_part(state); part != nullptr) {
if (command == vamp_protocol::page_command::NEXT) {
state.page_index = std::min(state.page_index + 1U, part->total_pages == 0U ? 0U : part->total_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::PERFORMANCE_PART_MENU;
state.active_part_id.clear();
state.page_index = 0;
}
}
return clamp_state(std::move(state));
}
return clamp_state(std::move(state));
}
auto vamp_zero::app_status_json(vamp_zero::app_state const &state) -> boost::json::value {
auto selection = boost::json::object{};
selection["main_menu_index"] = static_cast<std::uint64_t>(state.selected_main_menu_index);
selection["song_row_index"] = static_cast<std::uint64_t>(state.selected_song_index);
selection["part_row_index"] = static_cast<std::uint64_t>(state.selected_part_index);
if (auto const *group = selected_group(state); group != nullptr) {
selection["title"] = group->title;
selection["composer"] = group->composer;
}
if (auto const *part = selected_part(state); part != nullptr) {
selection["part"] = part_json(*part);
}
auto active = boost::json::object{};
if (state.mode == vamp_zero::app_mode::PERFORMANCE_SCORE) {
if (auto const *part = active_part(state); part != nullptr) {
active["selected"] = true;
active["kind"] = "performance";
active["part"] = part_json(*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>(part->total_pages);
}
}
if (active.empty()) {
active["selected"] = false;
active["page_index"] = 0;
active["page_number"] = 0;
active["page_total"] = 0;
}
auto remote = boost::json::object{};
remote["pico_connected"] = state.pico_connected;
remote["heartbeat_count"] = state.heartbeat_count;
auto performance = performance_json(state);
auto root = boost::json::object{};
root["mode"] = vamp_zero::to_string(state.mode);
root["library"] = performance;
root["performance"] = std::move(performance);
root["selection"] = std::move(selection);
root["active"] = std::move(active);
root["audio"] = audio_json(state);
root["commands"] = command_json(state);
root["remote"] = std::move(remote);
return root;
}
auto vamp_zero::remote_display_text(vamp_zero::app_state const &state) -> std::string {
auto title = std::string{};
auto title_image = std::string{};
auto part = std::string{};
auto page = std::size_t{};
auto pages = std::size_t{};
auto active = std::bool_t{false};
if (state.mode == vamp_zero::app_mode::PERFORMANCE_SCORE) {
if (auto const *selected = active_part(state); selected != nullptr) {
active = true;
title = selected->title;
title_image = selected->title_image;
part = selected->part;
page = state.page_index + 1U;
pages = selected->total_pages;
}
}
auto output = std::ostringstream{};
output << "mode=" << vamp_zero::to_string(state.mode) << "\n";
output << "active=" << (active ? "1" : "0") << "\n";
output << "title=" << sanitize_line_value(title) << "\n";
output << "title_image=" << title_image << "\n";
output << "part=" << sanitize_line_value(part) << "\n";
output << "page=" << page << "\n";
output << "pages=" << pages << "\n";
return output.str();
}
auto vamp_zero::validate_app_state(vamp_zero::app_state const &state) -> std::expected<void, modulus::error> {
if (!state.catalog) {
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, "catalog is missing"));
}
if (state.selected_main_menu_index >= main_menu_count) {
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, "selected main menu index is out of range"));
}
if (state.audio.metronome_min_bpm == 0U || state.audio.metronome_min_bpm > 400U) {
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, "metronome minimum BPM is out of range"));
}
if (state.audio.metronome_max_bpm < state.audio.metronome_min_bpm || state.audio.metronome_max_bpm > 400U) {
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, "metronome maximum BPM is out of range"));
}
if (state.audio.metronome_step == 0U || state.audio.metronome_step > 100U) {
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, "metronome BPM step is out of range"));
}
if (state.audio.metronome_bpm < state.audio.metronome_min_bpm ||
state.audio.metronome_bpm > state.audio.metronome_max_bpm) {
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, "metronome BPM is out of range"));
}
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->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.mode == vamp_zero::app_mode::PERFORMANCE_SCORE) {
auto const *part = active_part(state);
if (part == nullptr) {
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, "performance score mode requires an active part"));
}
if (part->total_pages == 0U || state.page_index >= part->total_pages) {
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, "performance score page index is out of range"));
}
}
return {};
}