291 lines
13 KiB
C++
291 lines
13 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 <vamp_zero/errors.hpp>
|
|
#include <vamp_zero/navigation.hpp>
|
|
#include <vamp_zero/menu.hpp>
|
|
|
|
|
|
namespace {
|
|
|
|
constexpr auto main_menu_performance = std::size_t{0};
|
|
constexpr auto settings_playback = std::size_t{1};
|
|
constexpr auto settings_metronome = std::size_t{2};
|
|
constexpr auto settings_tempo = std::size_t{3};
|
|
constexpr auto main_menu_count = std::size_t{2};
|
|
|
|
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_settings_index = std::min<std::size_t>(state.selected_settings_index, 3U);
|
|
state.remembered_settings_index = std::clamp<std::size_t>(state.remembered_settings_index, 1U, 3U);
|
|
if (state.mode == vamp_zero::app_mode::SETTINGS_MENU && state.selected_settings_index != 0U) {
|
|
state.remembered_settings_index = state.selected_settings_index;
|
|
}
|
|
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::SETTINGS_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 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));
|
|
++state.input_revision;
|
|
switch (state.mode) {
|
|
case vamp_zero::app_mode::MAIN_MENU:
|
|
if (command == vamp_protocol::page_command::NEXT || command == vamp_protocol::page_command::PREVIOUS) {
|
|
state.selected_main_menu_index = 1U - state.selected_main_menu_index;
|
|
} else if (state.selected_main_menu_index == main_menu_performance) {
|
|
state.mode = vamp_zero::app_mode::PERFORMANCE_SONG_MENU;
|
|
state.selected_song_index = 0U;
|
|
state.selected_part_index = 0U;
|
|
state.active_part_id.clear();
|
|
state.page_index = 0U;
|
|
} else {
|
|
state.mode = vamp_zero::app_mode::SETTINGS_MENU;
|
|
state.selected_settings_index = state.remembered_settings_index;
|
|
}
|
|
return clamp_state(std::move(state));
|
|
case vamp_zero::app_mode::SETTINGS_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_settings_index = wrap_next(state.selected_settings_index, 4U);
|
|
} else if (command == vamp_protocol::page_command::PREVIOUS) {
|
|
state.selected_settings_index = wrap_previous(state.selected_settings_index, 4U);
|
|
} else if (state.selected_settings_index == 0U) {
|
|
state.mode = vamp_zero::app_mode::MAIN_MENU;
|
|
} else if (state.selected_settings_index == settings_playback) {
|
|
state.audio.playback_enabled = !state.audio.playback_enabled;
|
|
} else if (state.selected_settings_index == settings_metronome) {
|
|
state.audio.metronome_enabled = !state.audio.metronome_enabled;
|
|
} else if (state.selected_settings_index == settings_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::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 focus = menu_row(state, menu_selection(state)).label;
|
|
if (!active && (state.mode == vamp_zero::app_mode::PERFORMANCE_SONG_MENU || state.mode == vamp_zero::app_mode::PERFORMANCE_PART_MENU)) {
|
|
if (auto group = selected_group(state); group != nullptr && !group->parts.empty()) {
|
|
title = group->title;
|
|
title_image = state.catalog->parts[group->parts.front()].title_image;
|
|
if (state.mode == vamp_zero::app_mode::PERFORMANCE_SONG_MENU) {
|
|
focus = "Performance";
|
|
}
|
|
}
|
|
}
|
|
auto output = std::ostringstream{};
|
|
output << "mode=" << vamp_zero::to_string(state.mode) << "\n";
|
|
output << "focus=" << sanitize_line_value(focus) << "\n";
|
|
output << "playback=" << (state.audio.playback_enabled ? "1" : "0") << "\n";
|
|
output << "metronome=" << (state.audio.metronome_enabled ? "1" : "0") << "\n";
|
|
output << "bpm=" << state.audio.metronome_bpm << "\n";
|
|
output << "editing=" << (state.audio.tempo_editing ? "1" : "0") << "\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.selected_settings_index >= 4U) {
|
|
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, "selected settings 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::SETTINGS_MENU) {
|
|
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, "tempo edit mode requires settings"));
|
|
}
|
|
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 {};
|
|
}
|