+20
-16
@@ -12,6 +12,7 @@
|
||||
#include <vamp_pico/display.hpp>
|
||||
#include <vamp_style/font.hpp>
|
||||
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto pin_lcd_cs = std::uint32_t{9};
|
||||
@@ -37,7 +38,7 @@ auto command(std::uchar_t value) -> void {
|
||||
deselect();
|
||||
}
|
||||
|
||||
auto data(std::uchar_t const* values, std::size_t count) -> void {
|
||||
auto data(std::uchar_t const *values, std::size_t count) -> void {
|
||||
gpio_put(pin_lcd_dc, 1);
|
||||
select();
|
||||
spi_write_blocking(spi1, values, count);
|
||||
@@ -77,14 +78,14 @@ auto same_color(vamp_style::color left, vamp_style::color right) -> std::bool_t
|
||||
}
|
||||
|
||||
template <std::size_t Count>
|
||||
auto same_text(std::array<std::char_t, Count> const& cached, std::char_t const* text) -> std::bool_t {
|
||||
auto const* value = text == nullptr ? "" : text;
|
||||
auto same_text(std::array<std::char_t, Count> const &cached, std::char_t const *text) -> std::bool_t {
|
||||
auto const *value = text == nullptr ? "" : text;
|
||||
return std::strncmp(cached.data(), value, Count) == 0;
|
||||
}
|
||||
|
||||
template <std::size_t Count>
|
||||
auto copy_text(std::array<std::char_t, Count>& cached, std::char_t const* text) -> void {
|
||||
auto const* value = text == nullptr ? "" : text;
|
||||
auto copy_text(std::array<std::char_t, Count> &cached, std::char_t const *text) -> void {
|
||||
auto const *value = text == nullptr ? "" : text;
|
||||
auto count = std::min(Count - 1U, std::strlen(value));
|
||||
std::memcpy(cached.data(), value, count);
|
||||
cached[count] = '\0';
|
||||
@@ -108,7 +109,7 @@ auto blend(vamp_style::color foreground, vamp_style::color background, std::uint
|
||||
};
|
||||
}
|
||||
|
||||
auto draw_centered_text(vamp_pico::display& display, std::int32_t y, std::char_t const* text, std::uint16_t color, std::int32_t preferred_scale, std::int32_t max_width) -> void {
|
||||
auto draw_centered_text(vamp_pico::display &display, std::int32_t y, std::char_t const *text, std::uint16_t color, std::int32_t preferred_scale, std::int32_t max_width) -> void {
|
||||
if (text == nullptr || text[0] == '\0') {
|
||||
return;
|
||||
}
|
||||
@@ -189,7 +190,7 @@ auto vamp_pico::display::fill_rect(std::int32_t x, std::int32_t y, std::int32_t
|
||||
deselect();
|
||||
}
|
||||
|
||||
auto vamp_pico::display::draw_text(std::int32_t x, std::int32_t y, std::char_t const* text, std::uint16_t color, std::int32_t scale) -> void {
|
||||
auto vamp_pico::display::draw_text(std::int32_t x, std::int32_t y, std::char_t const *text, std::uint16_t color, std::int32_t scale) -> void {
|
||||
if (text == nullptr || scale <= 0) {
|
||||
return;
|
||||
}
|
||||
@@ -210,14 +211,16 @@ auto vamp_pico::display::draw_text(std::int32_t x, std::int32_t y, std::char_t c
|
||||
auto col = std::int32_t{};
|
||||
while (col < static_cast<std::int32_t>(glyph.width)) {
|
||||
auto row_offset = static_cast<std::size_t>(row * vamp_style::ui_font_max_width);
|
||||
while (col < static_cast<std::int32_t>(glyph.width) && glyph.alpha[row_offset + static_cast<std::size_t>(col)] == 0U) {
|
||||
while (col < static_cast<std::int32_t>(glyph.width) &&
|
||||
glyph.alpha[row_offset + static_cast<std::size_t>(col)] == 0U) {
|
||||
++col;
|
||||
}
|
||||
auto start = col;
|
||||
auto alpha = start < static_cast<std::int32_t>(glyph.width)
|
||||
? glyph.alpha[row_offset + static_cast<std::size_t>(start)]
|
||||
: std::uint8_t{};
|
||||
while (col < static_cast<std::int32_t>(glyph.width) && glyph.alpha[row_offset + static_cast<std::size_t>(col)] == alpha) {
|
||||
? glyph.alpha[row_offset + static_cast<std::size_t>(start)]
|
||||
: std::uint8_t{};
|
||||
while (col < static_cast<std::int32_t>(glyph.width) &&
|
||||
glyph.alpha[row_offset + static_cast<std::size_t>(col)] == alpha) {
|
||||
++col;
|
||||
}
|
||||
if (col > start) {
|
||||
@@ -234,7 +237,7 @@ auto vamp_pico::display::draw_text(std::int32_t x, std::int32_t y, std::char_t c
|
||||
}
|
||||
}
|
||||
|
||||
auto vamp_pico::display::show_boot(std::char_t const* line1, std::char_t const* line2, std::uint16_t accent) -> void {
|
||||
auto vamp_pico::display::show_boot(std::char_t const *line1, std::char_t const *line2, std::uint16_t accent) -> void {
|
||||
remote_valid_ = false;
|
||||
fill(vamp_style::rgb565(vamp_style::background));
|
||||
fill_rect(0, 0, vamp_pico::display_width, 28, accent);
|
||||
@@ -247,11 +250,12 @@ auto vamp_pico::display::show_boot(std::char_t const* line1, std::char_t const*
|
||||
}
|
||||
}
|
||||
|
||||
auto vamp_pico::display::show_remote(vamp_style::color status, std::char_t const* title, std::char_t const* part, std::uint32_t page, std::uint32_t pages, std::bool_t active) -> void {
|
||||
auto const* title_text = title == nullptr ? "" : title;
|
||||
auto const* part_text = part == nullptr ? "" : part;
|
||||
auto vamp_pico::display::show_remote(vamp_style::color status, std::char_t const *title, std::char_t const *part, std::uint32_t page, std::uint32_t pages, std::bool_t active) -> void {
|
||||
auto const *title_text = title == nullptr ? "" : title;
|
||||
auto const *part_text = part == nullptr ? "" : part;
|
||||
auto status_changed = !remote_valid_ || !same_color(last_status_, status);
|
||||
auto content_changed = !remote_valid_ || last_active_ != active || !same_text(last_title_, title_text) || !same_text(last_part_, part_text) || last_page_ != page || last_pages_ != pages;
|
||||
auto content_changed = !remote_valid_ || last_active_ != active || !same_text(last_title_, title_text) ||
|
||||
!same_text(last_part_, part_text) || last_page_ != page || last_pages_ != pages;
|
||||
if (!remote_valid_) {
|
||||
fill(vamp_style::rgb565(vamp_style::background));
|
||||
}
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
#include <vamp_pico/http_client.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdio>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
#include "lwip/ip_addr.h"
|
||||
#include "lwip/pbuf.h"
|
||||
#include "lwip/tcp.h"
|
||||
#include "pico/cyw43_arch.h"
|
||||
#include "pico/stdlib.h"
|
||||
#include <lwip/ip_addr.h>
|
||||
#include <lwip/pbuf.h>
|
||||
#include <lwip/tcp.h>
|
||||
#include <pico/cyw43_arch.h>
|
||||
#include <pico/stdlib.h>
|
||||
|
||||
#include <vamp_pico/http_client.hpp>
|
||||
|
||||
|
||||
namespace {
|
||||
|
||||
struct request_state {
|
||||
tcp_pcb* pcb{};
|
||||
tcp_pcb *pcb{};
|
||||
std::bool_t done{};
|
||||
std::bool_t status_known{};
|
||||
std::bool_t success{};
|
||||
@@ -25,11 +26,11 @@ struct request_state {
|
||||
std::size_t response_size{};
|
||||
};
|
||||
|
||||
auto response_is_success(std::char_t const* data, std::size_t size) -> std::bool_t {
|
||||
auto response_is_success(std::char_t const *data, std::size_t size) -> std::bool_t {
|
||||
return size >= 10U && (std::memcmp(data, "HTTP/1.1 2", 10U) == 0 || std::memcmp(data, "HTTP/1.0 2", 10U) == 0);
|
||||
}
|
||||
|
||||
auto clear_callbacks(tcp_pcb* pcb) -> void {
|
||||
auto clear_callbacks(tcp_pcb *pcb) -> void {
|
||||
tcp_arg(pcb, nullptr);
|
||||
tcp_recv(pcb, nullptr);
|
||||
tcp_sent(pcb, nullptr);
|
||||
@@ -37,14 +38,13 @@ auto clear_callbacks(tcp_pcb* pcb) -> void {
|
||||
tcp_err(pcb, nullptr);
|
||||
}
|
||||
|
||||
auto close_request(request_state& state, std::bool_t success) -> err_t {
|
||||
auto close_request(request_state &state, std::bool_t success) -> err_t {
|
||||
state.success = success;
|
||||
state.done = true;
|
||||
if (state.pcb == nullptr) {
|
||||
return ERR_OK;
|
||||
}
|
||||
|
||||
auto* pcb = state.pcb;
|
||||
auto *pcb = state.pcb;
|
||||
state.pcb = nullptr;
|
||||
clear_callbacks(pcb);
|
||||
if (tcp_close(pcb) == ERR_OK) {
|
||||
@@ -54,7 +54,7 @@ auto close_request(request_state& state, std::bool_t success) -> err_t {
|
||||
return ERR_ABRT;
|
||||
}
|
||||
|
||||
auto append_response(request_state& state, pbuf* buffer) -> void {
|
||||
auto append_response(request_state &state, pbuf *buffer) -> void {
|
||||
auto remaining = sizeof(state.response) - state.response_size - 1U;
|
||||
if (remaining != 0U) {
|
||||
auto copied = pbuf_copy_partial(buffer, state.response + state.response_size, std::min<std::uint16_t>(buffer->tot_len, static_cast<std::uint16_t>(remaining)), 0);
|
||||
@@ -67,8 +67,8 @@ auto append_response(request_state& state, pbuf* buffer) -> void {
|
||||
}
|
||||
}
|
||||
|
||||
auto handle_receive(void* argument, tcp_pcb* pcb, pbuf* buffer, err_t error) -> err_t {
|
||||
auto* state = static_cast<request_state*>(argument);
|
||||
auto handle_receive(void *argument, tcp_pcb *pcb, pbuf *buffer, err_t error) -> err_t {
|
||||
auto *state = static_cast<request_state *>(argument);
|
||||
if (state == nullptr) {
|
||||
if (buffer != nullptr) {
|
||||
pbuf_free(buffer);
|
||||
@@ -86,8 +86,8 @@ auto handle_receive(void* argument, tcp_pcb* pcb, pbuf* buffer, err_t error) ->
|
||||
return ERR_OK;
|
||||
}
|
||||
|
||||
auto handle_connected(void* argument, tcp_pcb* pcb, err_t error) -> err_t {
|
||||
auto* state = static_cast<request_state*>(argument);
|
||||
auto handle_connected(void *argument, tcp_pcb *pcb, err_t error) -> err_t {
|
||||
auto *state = static_cast<request_state *>(argument);
|
||||
if (state == nullptr) {
|
||||
return ERR_ARG;
|
||||
}
|
||||
@@ -95,7 +95,6 @@ auto handle_connected(void* argument, tcp_pcb* pcb, err_t error) -> err_t {
|
||||
auto closed = close_request(*state, false);
|
||||
return closed == ERR_ABRT ? closed : error;
|
||||
}
|
||||
|
||||
tcp_arg(pcb, state);
|
||||
tcp_recv(pcb, handle_receive);
|
||||
auto written = tcp_write(pcb, state->request, static_cast<std::uint16_t>(state->request_size), TCP_WRITE_FLAG_COPY);
|
||||
@@ -111,8 +110,8 @@ auto handle_connected(void* argument, tcp_pcb* pcb, err_t error) -> err_t {
|
||||
return ERR_OK;
|
||||
}
|
||||
|
||||
auto handle_error(void* argument, err_t) -> void {
|
||||
auto* state = static_cast<request_state*>(argument);
|
||||
auto handle_error(void *argument, err_t) -> void {
|
||||
auto *state = static_cast<request_state *>(argument);
|
||||
if (state == nullptr) {
|
||||
return;
|
||||
}
|
||||
@@ -121,43 +120,25 @@ auto handle_error(void* argument, err_t) -> void {
|
||||
state->done = true;
|
||||
}
|
||||
|
||||
auto run_request(std::char_t const* method, std::char_t const* path, std::char_t const* body, std::char_t* response, std::size_t response_capacity, std::uint32_t timeout_ms) -> std::bool_t {
|
||||
auto run_request(std::char_t const *method, std::char_t const *path, std::char_t const *body, std::char_t *response, std::size_t response_capacity, std::uint32_t timeout_ms) -> std::bool_t {
|
||||
auto state = request_state{};
|
||||
auto body_size = body == nullptr ? std::size_t{} : std::strlen(body);
|
||||
auto written = std::int32_t{};
|
||||
if (body_size == 0U) {
|
||||
written = std::snprintf(
|
||||
state.request,
|
||||
sizeof(state.request),
|
||||
"%s %s HTTP/1.1\r\nHost: %s:%d\r\nConnection: close\r\n\r\n",
|
||||
method,
|
||||
path,
|
||||
VAMP_ZERO_HOST,
|
||||
VAMP_ZERO_PORT
|
||||
);
|
||||
written = std::snprintf(state.request, sizeof(state.request), "%s %s HTTP/1.1\r\nHost: %s:%d\r\nConnection: close\r\n\r\n", method, path, VAMP_ZERO_HOST, VAMP_ZERO_PORT);
|
||||
} else {
|
||||
written = std::snprintf(
|
||||
state.request,
|
||||
sizeof(state.request),
|
||||
"%s %s HTTP/1.1\r\nHost: %s:%d\r\nContent-Length: %zu\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n%s",
|
||||
method,
|
||||
path,
|
||||
VAMP_ZERO_HOST,
|
||||
VAMP_ZERO_PORT,
|
||||
body_size,
|
||||
body
|
||||
);
|
||||
written = std::snprintf(state.request, sizeof(state.request), "%s %s HTTP/1.1\r\nHost: %s:%d\r\nContent-Length: %zu\r\nContent-Type: application/json\r\nConnection: "
|
||||
"close\r\n\r\n%s",
|
||||
method, path, VAMP_ZERO_HOST, VAMP_ZERO_PORT, body_size, body);
|
||||
}
|
||||
if (written <= 0 || static_cast<std::size_t>(written) >= sizeof(state.request)) {
|
||||
return false;
|
||||
}
|
||||
state.request_size = static_cast<std::size_t>(written);
|
||||
|
||||
auto address = ip_addr_t{};
|
||||
if (ipaddr_aton(VAMP_ZERO_HOST, &address) == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
cyw43_arch_lwip_begin();
|
||||
state.pcb = tcp_new_ip_type(IPADDR_TYPE_V4);
|
||||
if (state.pcb == nullptr) {
|
||||
@@ -174,7 +155,6 @@ auto run_request(std::char_t const* method, std::char_t const* path, std::char_t
|
||||
if (connected != ERR_OK) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto deadline = make_timeout_time_ms(timeout_ms);
|
||||
while (!state.done && !time_reached(deadline)) {
|
||||
sleep_ms(5);
|
||||
@@ -192,14 +172,14 @@ auto run_request(std::char_t const* method, std::char_t const* path, std::char_t
|
||||
return state.success;
|
||||
}
|
||||
|
||||
auto body_start(std::char_t const* response) -> std::char_t const* {
|
||||
auto* marker = std::strstr(response, "\r\n\r\n");
|
||||
auto body_start(std::char_t const *response) -> std::char_t const * {
|
||||
auto *marker = std::strstr(response, "\r\n\r\n");
|
||||
return marker == nullptr ? response : marker + 4;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
auto vamp_pico::post_remote_command(vamp_protocol::page_command command, vamp_pico::remote_display_state& state) -> std::bool_t {
|
||||
auto vamp_pico::post_remote_command(vamp_protocol::page_command command, vamp_pico::remote_display_state &state) -> std::bool_t {
|
||||
auto response = std::array<std::char_t, 1024>{};
|
||||
if (!run_request("POST", vamp_protocol::page_command_path(command), "{}", response.data(), response.size(), 700)) {
|
||||
return false;
|
||||
@@ -214,7 +194,7 @@ auto vamp_pico::heartbeat() -> std::bool_t {
|
||||
return run_request("POST", vamp_protocol::remote_heartbeat_path, "{}", nullptr, 0, 700);
|
||||
}
|
||||
|
||||
auto vamp_pico::fetch_remote_display(vamp_pico::remote_display_state& state) -> std::bool_t {
|
||||
auto vamp_pico::fetch_remote_display(vamp_pico::remote_display_state &state) -> std::bool_t {
|
||||
auto response = std::array<std::char_t, 1024>{};
|
||||
if (!run_request("GET", vamp_protocol::remote_display_path, nullptr, response.data(), response.size(), 1000)) {
|
||||
return false;
|
||||
|
||||
+8
-16
@@ -1,10 +1,11 @@
|
||||
#include <pico/cyw43_arch.h>
|
||||
#include <pico/stdlib.h>
|
||||
|
||||
#include <vamp_pico/display.hpp>
|
||||
#include <vamp_pico/http_client.hpp>
|
||||
#include <vamp_pico/touch.hpp>
|
||||
#include <vamp_protocol/page_control.hpp>
|
||||
|
||||
#include "pico/cyw43_arch.h"
|
||||
#include "pico/stdlib.h"
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -34,15 +35,15 @@ auto color_for(link_status status) -> vamp_style::color {
|
||||
return vamp_style::retrying;
|
||||
}
|
||||
|
||||
auto render(vamp_pico::display& display, link_status status, vamp_pico::remote_display_state const& remote) -> void {
|
||||
auto render(vamp_pico::display &display, link_status status, vamp_pico::remote_display_state const &remote) -> void {
|
||||
display.show_remote(color_for(status), remote.title, remote.part, remote.page, remote.pages, remote.active);
|
||||
}
|
||||
|
||||
auto clear_remote(vamp_pico::remote_display_state& remote) -> void {
|
||||
auto clear_remote(vamp_pico::remote_display_state &remote) -> void {
|
||||
remote = vamp_pico::remote_display_state{};
|
||||
}
|
||||
|
||||
auto connect_wifi(vamp_pico::display& display) -> std::bool_t {
|
||||
auto connect_wifi(vamp_pico::display &display) -> std::bool_t {
|
||||
auto empty = vamp_pico::remote_display_state{};
|
||||
render(display, link_status::CONNECTING, empty);
|
||||
if (cyw43_arch_init() != 0) {
|
||||
@@ -59,7 +60,7 @@ auto connect_wifi(vamp_pico::display& display) -> std::bool_t {
|
||||
return true;
|
||||
}
|
||||
|
||||
auto send_command(vamp_protocol::page_command command, link_status& status, vamp_pico::remote_display_state& remote, vamp_pico::display& display) -> void {
|
||||
auto send_command(vamp_protocol::page_command command, link_status &status, vamp_pico::remote_display_state &remote, vamp_pico::display &display) -> void {
|
||||
if (!vamp_pico::post_remote_command(command, remote)) {
|
||||
status = link_status::RETRYING;
|
||||
clear_remote(remote);
|
||||
@@ -70,23 +71,20 @@ auto send_command(vamp_protocol::page_command command, link_status& status, vamp
|
||||
render(display, status, remote);
|
||||
}
|
||||
|
||||
auto update_gesture(gesture_state& gesture, std::bool_t touching, link_status& status, vamp_pico::remote_display_state& remote, vamp_pico::display& display) -> void {
|
||||
auto update_gesture(gesture_state &gesture, std::bool_t touching, link_status &status, vamp_pico::remote_display_state &remote, vamp_pico::display &display) -> void {
|
||||
constexpr auto hold_us = std::int64_t{700000};
|
||||
constexpr auto double_tap_us = std::int64_t{220000};
|
||||
auto now = get_absolute_time();
|
||||
|
||||
if (touching && !gesture.touching) {
|
||||
gesture.touching = true;
|
||||
gesture.hold_sent = false;
|
||||
gesture.down_time = now;
|
||||
}
|
||||
|
||||
if (touching && !gesture.hold_sent && absolute_time_diff_us(gesture.down_time, now) >= hold_us) {
|
||||
gesture.hold_sent = true;
|
||||
gesture.pending_tap = false;
|
||||
send_command(vamp_protocol::page_command::SELECT, status, remote, display);
|
||||
}
|
||||
|
||||
if (!touching && gesture.touching) {
|
||||
gesture.touching = false;
|
||||
if (!gesture.hold_sent) {
|
||||
@@ -99,7 +97,6 @@ auto update_gesture(gesture_state& gesture, std::bool_t touching, link_status& s
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!touching && gesture.pending_tap && absolute_time_diff_us(gesture.last_up_time, now) > double_tap_us) {
|
||||
gesture.pending_tap = false;
|
||||
send_command(vamp_protocol::page_command::NEXT, status, remote, display);
|
||||
@@ -110,29 +107,24 @@ auto update_gesture(gesture_state& gesture, std::bool_t touching, link_status& s
|
||||
|
||||
auto main() -> int {
|
||||
stdio_init_all();
|
||||
|
||||
auto display = vamp_pico::display{};
|
||||
auto touch = vamp_pico::touch_controller{};
|
||||
display.init();
|
||||
touch.init();
|
||||
|
||||
if (!connect_wifi(display)) {
|
||||
while (true) {
|
||||
sleep_ms(1000);
|
||||
}
|
||||
}
|
||||
|
||||
auto remote = vamp_pico::remote_display_state{};
|
||||
auto status = link_status::RETRYING;
|
||||
auto gesture = gesture_state{};
|
||||
auto last_poll = get_absolute_time();
|
||||
auto poll_now = std::bool_t{true};
|
||||
render(display, status, remote);
|
||||
|
||||
while (true) {
|
||||
auto point = vamp_pico::touch_point{};
|
||||
update_gesture(gesture, touch.read(point), status, remote, display);
|
||||
|
||||
auto now = get_absolute_time();
|
||||
if (poll_now || absolute_time_diff_us(last_poll, now) >= 1000000) {
|
||||
poll_now = false;
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
#include <vamp_pico/http_client.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <charconv>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <system_error>
|
||||
|
||||
#include <vamp_pico/http_client.hpp>
|
||||
|
||||
|
||||
namespace {
|
||||
|
||||
auto copy_value(std::string_view value, std::char_t* output, std::size_t capacity) -> void {
|
||||
auto copy_value(std::string_view value, std::char_t *output, std::size_t capacity) -> void {
|
||||
if (capacity == 0U) {
|
||||
return;
|
||||
}
|
||||
@@ -17,13 +18,13 @@ auto copy_value(std::string_view value, std::char_t* output, std::size_t capacit
|
||||
output[count] = '\0';
|
||||
}
|
||||
|
||||
auto parse_uint(std::string_view value, std::uint32_t& output) -> std::bool_t {
|
||||
auto parse_uint(std::string_view value, std::uint32_t &output) -> std::bool_t {
|
||||
if (value.empty()) {
|
||||
return false;
|
||||
}
|
||||
auto parsed = std::uint64_t{};
|
||||
auto const* begin = value.data();
|
||||
auto const* end = value.data() + value.size();
|
||||
auto const *begin = value.data();
|
||||
auto const *end = value.data() + value.size();
|
||||
auto result = std::from_chars(begin, end, parsed, 10);
|
||||
if (result.ec != std::errc{} || result.ptr != end || parsed > std::numeric_limits<std::uint32_t>::max()) {
|
||||
return false;
|
||||
@@ -41,7 +42,7 @@ auto strip_cr(std::string_view line) -> std::string_view {
|
||||
|
||||
}
|
||||
|
||||
auto vamp_pico::parse_remote_display_body(std::string_view body, vamp_pico::remote_display_state& state) -> std::bool_t {
|
||||
auto vamp_pico::parse_remote_display_body(std::string_view body, vamp_pico::remote_display_state &state) -> std::bool_t {
|
||||
auto parsed = vamp_pico::remote_display_state{};
|
||||
auto cursor = std::size_t{};
|
||||
while (cursor <= body.size()) {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <vamp_pico/display.hpp>
|
||||
#include <vamp_pico/touch.hpp>
|
||||
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto pin_tp_rst = std::uint32_t{5};
|
||||
@@ -34,13 +35,14 @@ auto vamp_pico::touch_controller::init() -> void {
|
||||
gpio_pull_up(pin_tp_int);
|
||||
}
|
||||
|
||||
auto vamp_pico::touch_controller::read(vamp_pico::touch_point& point) -> std::bool_t {
|
||||
auto vamp_pico::touch_controller::read(vamp_pico::touch_point &point) -> std::bool_t {
|
||||
auto reg = std::uchar_t{0x02};
|
||||
auto data = std::array<std::uchar_t, 5>{};
|
||||
if (i2c_write_blocking(i2c1, cst816_address, ®, 1, true) != 1) {
|
||||
return false;
|
||||
}
|
||||
if (i2c_read_blocking(i2c1, cst816_address, data.data(), data.size(), false) != static_cast<std::int32_t>(data.size())) {
|
||||
if (i2c_read_blocking(i2c1, cst816_address, data.data(), data.size(), false) !=
|
||||
static_cast<std::int32_t>(data.size())) {
|
||||
return false;
|
||||
}
|
||||
auto fingers = data[0] & 0x0f;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include <vamp_protocol/page_control.hpp>
|
||||
|
||||
auto vamp_protocol::page_command_path(vamp_protocol::page_command command) -> std::char_t const* {
|
||||
|
||||
auto vamp_protocol::page_command_path(vamp_protocol::page_command command) -> std::char_t const * {
|
||||
switch (command) {
|
||||
case vamp_protocol::page_command::PREVIOUS:
|
||||
return vamp_protocol::remote_previous_path;
|
||||
@@ -12,7 +13,7 @@ auto vamp_protocol::page_command_path(vamp_protocol::page_command command) -> st
|
||||
return vamp_protocol::remote_next_path;
|
||||
}
|
||||
|
||||
auto vamp_protocol::page_command_legacy_path(vamp_protocol::page_command command) -> std::char_t const* {
|
||||
auto vamp_protocol::page_command_legacy_path(vamp_protocol::page_command command) -> std::char_t const * {
|
||||
switch (command) {
|
||||
case vamp_protocol::page_command::PREVIOUS:
|
||||
return vamp_protocol::previous_page_path;
|
||||
@@ -24,7 +25,7 @@ auto vamp_protocol::page_command_legacy_path(vamp_protocol::page_command command
|
||||
return vamp_protocol::next_page_path;
|
||||
}
|
||||
|
||||
auto vamp_protocol::page_command_name(vamp_protocol::page_command command) -> std::char_t const* {
|
||||
auto vamp_protocol::page_command_name(vamp_protocol::page_command command) -> std::char_t const * {
|
||||
switch (command) {
|
||||
case vamp_protocol::page_command::PREVIOUS:
|
||||
return "previous";
|
||||
@@ -36,7 +37,7 @@ auto vamp_protocol::page_command_name(vamp_protocol::page_command command) -> st
|
||||
return "next";
|
||||
}
|
||||
|
||||
auto vamp_protocol::page_command_display_label(vamp_protocol::page_command command) -> std::char_t const* {
|
||||
auto vamp_protocol::page_command_display_label(vamp_protocol::page_command command) -> std::char_t const * {
|
||||
switch (command) {
|
||||
case vamp_protocol::page_command::PREVIOUS:
|
||||
return "PREV";
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
|
||||
auto vamp_style::rgb565(vamp_style::color value) -> std::uint16_t {
|
||||
auto r = static_cast<std::uint16_t>((value.r >> 3U) & 0x1fU);
|
||||
auto g = static_cast<std::uint16_t>((value.g >> 2U) & 0x3fU);
|
||||
|
||||
+37
-37
@@ -1,5 +1,3 @@
|
||||
#include <vamp_zero/app.hpp>
|
||||
|
||||
#include <csignal>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
@@ -26,6 +24,7 @@
|
||||
#include <ligo.hpp>
|
||||
#include <modulus.hpp>
|
||||
|
||||
#include <vamp_zero/app.hpp>
|
||||
#include <vamp_zero/config.hpp>
|
||||
#include <vamp_zero/audio.hpp>
|
||||
#include <vamp_zero/database.hpp>
|
||||
@@ -35,6 +34,7 @@
|
||||
#include <vamp_zero/portal.hpp>
|
||||
#include <vamp_zero/ui.hpp>
|
||||
|
||||
|
||||
namespace {
|
||||
|
||||
auto invalid(std::string message) -> modulus::error {
|
||||
@@ -45,7 +45,7 @@ auto usage() -> std::string {
|
||||
return "usage: vamp-zero [--config PATH] [--check-config] [--health-check] [--version] [--once] [--ensure-database]";
|
||||
}
|
||||
|
||||
auto bind_port(vamp_zero::config const& config) -> std::uint16_t {
|
||||
auto bind_port(vamp_zero::config const &config) -> std::uint16_t {
|
||||
auto endpoint = ligo::http::server_endpoint_from_config(config.control.endpoint);
|
||||
if (!endpoint) {
|
||||
return 8080;
|
||||
@@ -53,7 +53,7 @@ auto bind_port(vamp_zero::config const& config) -> std::uint16_t {
|
||||
return endpoint->port == 0 ? 8080 : endpoint->port;
|
||||
}
|
||||
|
||||
auto write_text_atomic(std::filesystem::path const& destination, std::string const& text) -> std::expected<void, modulus::error> {
|
||||
auto write_text_atomic(std::filesystem::path const &destination, std::string const &text) -> std::expected<void, modulus::error> {
|
||||
auto error = std::error_code{};
|
||||
if (!destination.parent_path().empty()) {
|
||||
std::filesystem::create_directories(destination.parent_path(), error);
|
||||
@@ -80,7 +80,7 @@ auto write_text_atomic(std::filesystem::path const& destination, std::string con
|
||||
return {};
|
||||
}
|
||||
|
||||
auto write_heartbeat(vamp_zero::config const& config) -> std::expected<void, modulus::error> {
|
||||
auto write_heartbeat(vamp_zero::config const &config) -> std::expected<void, modulus::error> {
|
||||
auto heartbeat = boost::json::object{};
|
||||
heartbeat["version"] = VAMP_VERSION;
|
||||
heartbeat["git_sha"] = VAMP_GIT_SHA;
|
||||
@@ -89,14 +89,14 @@ auto write_heartbeat(vamp_zero::config const& config) -> std::expected<void, mod
|
||||
return write_text_atomic(config.runtime.heartbeat_path, boost::json::serialize(heartbeat));
|
||||
}
|
||||
|
||||
auto ensure_database(vamp_zero::config const& config) -> std::expected<void, modulus::error> {
|
||||
auto ensure_database(vamp_zero::config const &config) -> std::expected<void, modulus::error> {
|
||||
auto io = boost::asio::io_context{};
|
||||
auto future = boost::asio::co_spawn(io, vamp_zero::async_ensure_database(io.get_executor(), config.database), boost::asio::use_future);
|
||||
io.run();
|
||||
return future.get();
|
||||
}
|
||||
|
||||
auto make_server_endpoint(vamp_zero::config const& config) -> std::expected<ligo::http::endpoint, modulus::error> {
|
||||
auto make_server_endpoint(vamp_zero::config const &config) -> std::expected<ligo::http::endpoint, modulus::error> {
|
||||
auto endpoint = ligo::http::server_endpoint_from_config(config.control.endpoint);
|
||||
if (!endpoint) {
|
||||
return std::unexpected(endpoint.error());
|
||||
@@ -107,14 +107,14 @@ auto make_server_endpoint(vamp_zero::config const& config) -> std::expected<ligo
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
auto import_library(vamp_zero::config const& config) -> std::expected<void, modulus::error> {
|
||||
auto import_library(vamp_zero::config const &config) -> std::expected<void, modulus::error> {
|
||||
auto io = boost::asio::io_context{};
|
||||
auto future = boost::asio::co_spawn(io, vamp_zero::async_import_catalogs(io.get_executor(), config.database, config.runtime.assets_path), boost::asio::use_future);
|
||||
io.run();
|
||||
return future.get();
|
||||
}
|
||||
|
||||
auto load_initial_state(vamp_zero::config const& config) -> std::expected<vamp_zero::app_state, modulus::error> {
|
||||
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), boost::asio::use_future);
|
||||
auto practice_future = boost::asio::co_spawn(io, vamp_zero::async_load_practice_library(io.get_executor(), config.database), boost::asio::use_future);
|
||||
@@ -130,14 +130,15 @@ auto load_initial_state(vamp_zero::config const& config) -> std::expected<vamp_z
|
||||
auto state = vamp_zero::app_state{
|
||||
.performance = std::move(*performance),
|
||||
.practice = std::move(*practice),
|
||||
.audio = {
|
||||
.track_enabled = config.audio.track_enabled,
|
||||
.metronome_enabled = config.audio.metronome_enabled,
|
||||
.metronome_bpm = config.audio.metronome_bpm,
|
||||
.metronome_min_bpm = config.audio.metronome_min_bpm,
|
||||
.metronome_max_bpm = config.audio.metronome_max_bpm,
|
||||
.metronome_step = config.audio.metronome_step,
|
||||
},
|
||||
.audio =
|
||||
{
|
||||
.track_enabled = config.audio.track_enabled,
|
||||
.metronome_enabled = config.audio.metronome_enabled,
|
||||
.metronome_bpm = config.audio.metronome_bpm,
|
||||
.metronome_min_bpm = config.audio.metronome_min_bpm,
|
||||
.metronome_max_bpm = config.audio.metronome_max_bpm,
|
||||
.metronome_step = config.audio.metronome_step,
|
||||
},
|
||||
};
|
||||
if (auto valid = vamp_zero::validate_app_state(state); !valid) {
|
||||
return std::unexpected(valid.error());
|
||||
@@ -145,21 +146,20 @@ auto load_initial_state(vamp_zero::config const& config) -> std::expected<vamp_z
|
||||
return state;
|
||||
}
|
||||
|
||||
auto run_headless(vamp_zero::config const& config, vamp_zero::control_state& state, vamp_zero::audio_controller* audio) -> std::expected<void, modulus::error> {
|
||||
auto run_headless(vamp_zero::config const &config, vamp_zero::control_state &state, vamp_zero::audio_controller *audio) -> std::expected<void, modulus::error> {
|
||||
auto io = boost::asio::io_context{};
|
||||
auto endpoint = make_server_endpoint(config);
|
||||
if (!endpoint) {
|
||||
return std::unexpected(endpoint.error());
|
||||
}
|
||||
auto server = ligo::http::server{io, *endpoint, vamp_zero::make_router(state), ligo::http::server_options_from_config(config.control.endpoint)};
|
||||
auto server = ligo::http::server{
|
||||
io, *endpoint, vamp_zero::make_router(state), ligo::http::server_options_from_config(config.control.endpoint)};
|
||||
auto started = server.start();
|
||||
if (!started) {
|
||||
return std::unexpected(started.error());
|
||||
}
|
||||
if (audio != nullptr) {
|
||||
state.set_observer([audio](vamp_zero::app_state app) {
|
||||
audio->sync(app);
|
||||
});
|
||||
state.set_observer([audio](vamp_zero::app_state app) { audio->sync(app); });
|
||||
audio->sync(state.snapshot());
|
||||
}
|
||||
auto heartbeat = write_heartbeat(config);
|
||||
@@ -168,7 +168,7 @@ auto run_headless(vamp_zero::config const& config, vamp_zero::control_state& sta
|
||||
return std::unexpected(heartbeat.error());
|
||||
}
|
||||
auto signals = boost::asio::signal_set{io, SIGINT, SIGTERM};
|
||||
signals.async_wait([&](auto const&, std::int32_t) {
|
||||
signals.async_wait([&](auto const &, std::int32_t) {
|
||||
server.stop();
|
||||
io.stop();
|
||||
});
|
||||
@@ -177,15 +177,15 @@ auto run_headless(vamp_zero::config const& config, vamp_zero::control_state& sta
|
||||
return {};
|
||||
}
|
||||
|
||||
auto run_display(vamp_zero::config const& config, vamp_zero::control_state& state, vamp_zero::audio_controller* audio) -> std::expected<void, modulus::error> {
|
||||
auto run_display(vamp_zero::config const &config, vamp_zero::control_state &state, vamp_zero::audio_controller *audio) -> std::expected<void, modulus::error> {
|
||||
auto io = boost::asio::io_context{};
|
||||
auto endpoint = make_server_endpoint(config);
|
||||
if (!endpoint) {
|
||||
return std::unexpected(endpoint.error());
|
||||
}
|
||||
auto ui = vamp_zero::zero_ui{io.get_executor(), config.display, [&state](vamp_protocol::page_command command) {
|
||||
static_cast<void>(state.apply(command));
|
||||
}};
|
||||
static_cast<void>(state.apply(command));
|
||||
}};
|
||||
state.set_observer([&ui, &config, audio](vamp_zero::app_state app) {
|
||||
if (audio != nullptr) {
|
||||
audio->sync(app);
|
||||
@@ -202,7 +202,8 @@ auto run_display(vamp_zero::config const& config, vamp_zero::control_state& stat
|
||||
if (!published) {
|
||||
return std::unexpected(published.error());
|
||||
}
|
||||
auto server = ligo::http::server{io, *endpoint, vamp_zero::make_router(state), ligo::http::server_options_from_config(config.control.endpoint)};
|
||||
auto server = ligo::http::server{
|
||||
io, *endpoint, vamp_zero::make_router(state), ligo::http::server_options_from_config(config.control.endpoint)};
|
||||
auto started = server.start();
|
||||
if (!started) {
|
||||
return std::unexpected(started.error());
|
||||
@@ -216,7 +217,7 @@ auto run_display(vamp_zero::config const& config, vamp_zero::control_state& stat
|
||||
auto schedule_connection_refresh = std::function<void()>{};
|
||||
schedule_connection_refresh = [&] {
|
||||
connection_timer.expires_after(std::chrono::milliseconds{500});
|
||||
connection_timer.async_wait([&](auto const& error) {
|
||||
connection_timer.async_wait([&](auto const &error) {
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
@@ -226,7 +227,7 @@ auto run_display(vamp_zero::config const& config, vamp_zero::control_state& stat
|
||||
};
|
||||
schedule_connection_refresh();
|
||||
auto signals = boost::asio::signal_set{io, SIGINT, SIGTERM};
|
||||
signals.async_wait([&](auto const&, std::int32_t) {
|
||||
signals.async_wait([&](auto const &, std::int32_t) {
|
||||
connection_timer.cancel();
|
||||
server.stop();
|
||||
ui.stop();
|
||||
@@ -247,12 +248,12 @@ auto run_display(vamp_zero::config const& config, vamp_zero::control_state& stat
|
||||
|
||||
}
|
||||
|
||||
auto vamp_zero::parse_command_line(std::int32_t argc, std::char_t** argv) -> std::expected<vamp_zero::command_line, modulus::error> {
|
||||
auto vamp_zero::parse_command_line(std::int32_t argc, std::char_t **argv) -> std::expected<vamp_zero::command_line, modulus::error> {
|
||||
if (argc < 1 || argv == nullptr) {
|
||||
return std::unexpected(invalid("process arguments are invalid"));
|
||||
}
|
||||
auto result = vamp_zero::command_line{};
|
||||
auto require_value = [&](std::int32_t& index, std::string_view option) -> std::expected<std::string_view, modulus::error> {
|
||||
auto require_value = [&](std::int32_t &index, std::string_view option) -> std::expected<std::string_view, modulus::error> {
|
||||
++index;
|
||||
if (index >= argc) {
|
||||
return std::unexpected(invalid(std::string{option} + " requires a value"));
|
||||
@@ -287,11 +288,11 @@ auto vamp_zero::parse_command_line(std::int32_t argc, std::char_t** argv) -> std
|
||||
return std::unexpected(invalid("unknown argument: " + std::string{argument}));
|
||||
}
|
||||
}
|
||||
auto command_count = static_cast<std::uint32_t>(result.check_config ? 1 : 0)
|
||||
+ static_cast<std::uint32_t>(result.health_check ? 1 : 0)
|
||||
+ static_cast<std::uint32_t>(result.print_version ? 1 : 0)
|
||||
+ static_cast<std::uint32_t>(result.exit_after_startup ? 1 : 0)
|
||||
+ static_cast<std::uint32_t>(result.ensure_database ? 1 : 0);
|
||||
auto command_count = static_cast<std::uint32_t>(result.check_config ? 1 : 0) +
|
||||
static_cast<std::uint32_t>(result.health_check ? 1 : 0) +
|
||||
static_cast<std::uint32_t>(result.print_version ? 1 : 0) +
|
||||
static_cast<std::uint32_t>(result.exit_after_startup ? 1 : 0) +
|
||||
static_cast<std::uint32_t>(result.ensure_database ? 1 : 0);
|
||||
if (command_count > 1U) {
|
||||
return std::unexpected(invalid("only one command flag may be selected"));
|
||||
}
|
||||
@@ -323,7 +324,6 @@ auto vamp_zero::run(vamp_zero::command_line command) -> std::expected<void, modu
|
||||
std::cout << "ok\n";
|
||||
return {};
|
||||
}
|
||||
|
||||
auto ensured = ensure_database(*config);
|
||||
if (!ensured) {
|
||||
return ensured;
|
||||
|
||||
+10
-14
@@ -1,5 +1,3 @@
|
||||
#include <vamp_zero/audio.hpp>
|
||||
|
||||
#include <expected>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
@@ -9,29 +7,31 @@
|
||||
#include <modulus.hpp>
|
||||
#include <sonus.hpp>
|
||||
|
||||
#include <vamp_zero/audio.hpp>
|
||||
#include <vamp_zero/navigation.hpp>
|
||||
|
||||
|
||||
namespace {
|
||||
|
||||
auto score_active(vamp_zero::app_state const& state) -> std::bool_t {
|
||||
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);
|
||||
auto const *exercise = vamp_zero::active_practice(state);
|
||||
return exercise != nullptr && vamp_zero::selected_practice_variant(state, *exercise) != nullptr;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
auto log_audio_error(std::string const& action, modulus::error const& error) -> void {
|
||||
auto log_audio_error(std::string const &action, modulus::error const &error) -> void {
|
||||
modulus::log_error(action + ": " + error.describe());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
vamp_zero::audio_controller::audio_controller(vamp_zero::audio_config config, sonus::engine engine)
|
||||
: config_{config}, engine_{std::move(engine)} {}
|
||||
: config_{config}, engine_{std::move(engine)} {}
|
||||
|
||||
vamp_zero::audio_controller::~audio_controller() {
|
||||
auto lock = std::scoped_lock{mutex_};
|
||||
@@ -40,7 +40,7 @@ vamp_zero::audio_controller::~audio_controller() {
|
||||
engine_.stop();
|
||||
}
|
||||
|
||||
auto vamp_zero::audio_controller::make_metronome_config(vamp_zero::app_state const& state) const -> sonus::metronome_config {
|
||||
auto vamp_zero::audio_controller::make_metronome_config(vamp_zero::app_state const &state) const -> sonus::metronome_config {
|
||||
auto result = sonus::metronome_config{};
|
||||
result.bpm = state.audio.metronome_bpm;
|
||||
result.beats_per_bar = config_.beats_per_bar;
|
||||
@@ -48,7 +48,7 @@ auto vamp_zero::audio_controller::make_metronome_config(vamp_zero::app_state con
|
||||
return result;
|
||||
}
|
||||
|
||||
auto vamp_zero::audio_controller::sync(vamp_zero::app_state const& state) -> void {
|
||||
auto vamp_zero::audio_controller::sync(vamp_zero::app_state const &state) -> void {
|
||||
auto lock = std::scoped_lock{mutex_};
|
||||
auto const should_run_metronome = state.audio.metronome_enabled && score_active(state);
|
||||
if (!should_run_metronome) {
|
||||
@@ -58,7 +58,6 @@ auto vamp_zero::audio_controller::sync(vamp_zero::app_state const& state) -> voi
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
auto metronome = make_metronome_config(state);
|
||||
if (!metronome_running_) {
|
||||
auto started = engine_.start_metronome(metronome);
|
||||
@@ -72,7 +71,6 @@ auto vamp_zero::audio_controller::sync(vamp_zero::app_state const& state) -> voi
|
||||
last_metronome_bpm_ = metronome.bpm;
|
||||
return;
|
||||
}
|
||||
|
||||
if (last_metronome_bpm_ != metronome.bpm) {
|
||||
auto updated = engine_.update_metronome(metronome);
|
||||
if (!updated) {
|
||||
@@ -85,16 +83,15 @@ auto vamp_zero::audio_controller::sync(vamp_zero::app_state const& state) -> voi
|
||||
}
|
||||
}
|
||||
|
||||
auto vamp_zero::create_audio_controller(vamp_zero::audio_config const& config) -> std::expected<std::unique_ptr<vamp_zero::audio_controller>, modulus::error> {
|
||||
auto vamp_zero::create_audio_controller(vamp_zero::audio_config const &config)
|
||||
-> std::expected<std::unique_ptr<vamp_zero::audio_controller>, modulus::error> {
|
||||
if (!config.enabled) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto sonus_config = sonus::audio_config{};
|
||||
sonus_config.sample_rate = 48000;
|
||||
sonus_config.channels = 2;
|
||||
sonus_config.master_volume = config.master_volume;
|
||||
|
||||
auto engine = sonus::engine::create(sonus_config);
|
||||
if (!engine) {
|
||||
return std::unexpected(engine.error());
|
||||
@@ -103,6 +100,5 @@ auto vamp_zero::create_audio_controller(vamp_zero::audio_config const& config) -
|
||||
if (!started) {
|
||||
return std::unexpected(started.error());
|
||||
}
|
||||
|
||||
return std::make_unique<vamp_zero::audio_controller>(config, std::move(*engine));
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include <vamp_zero/color.hpp>
|
||||
|
||||
|
||||
auto vamp_zero::to_luma(vamp_style::color value) -> luma::color {
|
||||
return {
|
||||
.r = value.r,
|
||||
|
||||
+83
-33
@@ -1,10 +1,6 @@
|
||||
#include <vamp_zero/config.hpp>
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
@@ -14,8 +10,10 @@
|
||||
#include <boost/json/value.hpp>
|
||||
#include <boost/system/error_code.hpp>
|
||||
|
||||
#include <vamp_zero/config.hpp>
|
||||
#include <vamp_zero/errors.hpp>
|
||||
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto json_options = modulus::json_config::options{
|
||||
@@ -28,14 +26,14 @@ auto invalid(std::string message) -> modulus::error {
|
||||
return vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, std::move(message));
|
||||
}
|
||||
|
||||
auto require_object(boost::json::value const& value, std::string_view name) -> std::expected<boost::json::object const*, modulus::error> {
|
||||
auto require_object(boost::json::value const &value, std::string_view name) -> std::expected<boost::json::object const *, modulus::error> {
|
||||
if (!value.is_object()) {
|
||||
return std::unexpected(invalid(std::string{name} + " must be an object"));
|
||||
}
|
||||
return &value.as_object();
|
||||
}
|
||||
|
||||
auto optional_object(boost::json::object const& object, std::string_view field) -> std::expected<boost::json::object const*, modulus::error> {
|
||||
auto optional_object(boost::json::object const &object, std::string_view field) -> std::expected<boost::json::object const *, modulus::error> {
|
||||
auto found = object.find(field);
|
||||
if (found == object.end()) {
|
||||
return nullptr;
|
||||
@@ -43,7 +41,7 @@ auto optional_object(boost::json::object const& object, std::string_view field)
|
||||
return require_object(found->value(), field);
|
||||
}
|
||||
|
||||
auto optional_string(boost::json::object const& object, std::string_view field, std::string fallback) -> std::expected<std::string, modulus::error> {
|
||||
auto optional_string(boost::json::object const &object, std::string_view field, std::string fallback) -> std::expected<std::string, modulus::error> {
|
||||
auto found = object.find(field);
|
||||
if (found == object.end()) {
|
||||
return fallback;
|
||||
@@ -58,7 +56,7 @@ auto optional_string(boost::json::object const& object, std::string_view field,
|
||||
return value;
|
||||
}
|
||||
|
||||
auto optional_bool(boost::json::object const& object, std::string_view field, std::bool_t fallback) -> std::expected<std::bool_t, modulus::error> {
|
||||
auto optional_bool(boost::json::object const &object, std::string_view field, std::bool_t fallback) -> std::expected<std::bool_t, modulus::error> {
|
||||
auto found = object.find(field);
|
||||
if (found == object.end()) {
|
||||
return fallback;
|
||||
@@ -69,7 +67,7 @@ auto optional_bool(boost::json::object const& object, std::string_view field, st
|
||||
return found->value().as_bool();
|
||||
}
|
||||
|
||||
auto optional_uint32(boost::json::object const& object, std::string_view field, std::uint32_t fallback, std::uint32_t min_value, std::uint32_t max_value) -> std::expected<std::uint32_t, modulus::error> {
|
||||
auto optional_uint32(boost::json::object const &object, std::string_view field, std::uint32_t fallback, std::uint32_t min_value, std::uint32_t max_value) -> std::expected<std::uint32_t, modulus::error> {
|
||||
auto found = object.find(field);
|
||||
if (found == object.end()) {
|
||||
return fallback;
|
||||
@@ -92,7 +90,7 @@ auto optional_uint32(boost::json::object const& object, std::string_view field,
|
||||
return static_cast<std::uint32_t>(value);
|
||||
}
|
||||
|
||||
auto optional_float(boost::json::object const& object, std::string_view field, std::float32_t fallback, std::float32_t min_value, std::float32_t max_value) -> std::expected<std::float32_t, modulus::error> {
|
||||
auto optional_float(boost::json::object const &object, std::string_view field, std::float32_t fallback, std::float32_t min_value, std::float32_t max_value) -> std::expected<std::float32_t, modulus::error> {
|
||||
auto found = object.find(field);
|
||||
if (found == object.end()) {
|
||||
return fallback;
|
||||
@@ -113,7 +111,7 @@ auto optional_float(boost::json::object const& object, std::string_view field, s
|
||||
return static_cast<std::float32_t>(value);
|
||||
}
|
||||
|
||||
auto parse_hardware(std::string const& value) -> std::expected<vamp_zero::hardware_mode, modulus::error> {
|
||||
auto parse_hardware(std::string const &value) -> std::expected<vamp_zero::hardware_mode, modulus::error> {
|
||||
if (value == "fake") {
|
||||
return vamp_zero::hardware_mode::FAKE;
|
||||
}
|
||||
@@ -123,7 +121,7 @@ auto parse_hardware(std::string const& value) -> std::expected<vamp_zero::hardwa
|
||||
return std::unexpected(invalid("runtime.hardware must be fake or real"));
|
||||
}
|
||||
|
||||
auto parse_runtime(boost::json::object const& root, vamp_zero::runtime_config fallback) -> std::expected<vamp_zero::runtime_config, modulus::error> {
|
||||
auto parse_runtime(boost::json::object const &root, vamp_zero::runtime_config fallback) -> std::expected<vamp_zero::runtime_config, modulus::error> {
|
||||
auto object = optional_object(root, "runtime");
|
||||
if (!object) {
|
||||
return std::unexpected(object.error());
|
||||
@@ -136,8 +134,20 @@ auto parse_runtime(boost::json::object const& root, vamp_zero::runtime_config fa
|
||||
auto exit_after_startup = optional_bool(**object, "exit_after_startup", fallback.exit_after_startup);
|
||||
auto assets_path = optional_string(**object, "assets_path", fallback.assets_path);
|
||||
auto heartbeat_path = optional_string(**object, "heartbeat_path", fallback.heartbeat_path);
|
||||
if (!hardware_text || !headless || !exit_after_startup || !assets_path || !heartbeat_path) {
|
||||
return std::unexpected(!hardware_text ? hardware_text.error() : (!headless ? headless.error() : (!exit_after_startup ? exit_after_startup.error() : (!assets_path ? assets_path.error() : heartbeat_path.error()))));
|
||||
if (!hardware_text) {
|
||||
return std::unexpected(hardware_text.error());
|
||||
}
|
||||
if (!headless) {
|
||||
return std::unexpected(headless.error());
|
||||
}
|
||||
if (!exit_after_startup) {
|
||||
return std::unexpected(exit_after_startup.error());
|
||||
}
|
||||
if (!assets_path) {
|
||||
return std::unexpected(assets_path.error());
|
||||
}
|
||||
if (!heartbeat_path) {
|
||||
return std::unexpected(heartbeat_path.error());
|
||||
}
|
||||
auto hardware = parse_hardware(*hardware_text);
|
||||
if (!hardware) {
|
||||
@@ -152,7 +162,7 @@ auto parse_runtime(boost::json::object const& root, vamp_zero::runtime_config fa
|
||||
};
|
||||
}
|
||||
|
||||
auto parse_database(boost::json::object const& root, vamp_zero::database_config fallback) -> std::expected<vamp_zero::database_config, modulus::error> {
|
||||
auto parse_database(boost::json::object const &root, vamp_zero::database_config fallback) -> std::expected<vamp_zero::database_config, modulus::error> {
|
||||
auto object = optional_object(root, "database");
|
||||
if (!object) {
|
||||
return std::unexpected(object.error());
|
||||
@@ -169,7 +179,7 @@ auto parse_database(boost::json::object const& root, vamp_zero::database_config
|
||||
};
|
||||
}
|
||||
|
||||
auto parse_control(boost::json::object const& root, vamp_zero::control_config fallback) -> std::expected<vamp_zero::control_config, modulus::error> {
|
||||
auto parse_control(boost::json::object const &root, vamp_zero::control_config fallback) -> std::expected<vamp_zero::control_config, modulus::error> {
|
||||
auto object = optional_object(root, "control");
|
||||
if (!object) {
|
||||
return std::unexpected(object.error());
|
||||
@@ -193,8 +203,20 @@ auto parse_control(boost::json::object const& root, vamp_zero::control_config fa
|
||||
auto ap_interface = optional_string(**object, "ap_interface", result.ap_interface);
|
||||
auto zero_ip = optional_string(**object, "zero_ip", result.zero_ip);
|
||||
auto zero_cidr = optional_string(**object, "zero_cidr", result.zero_cidr);
|
||||
if (!ap_ssid || !ap_passphrase || !ap_interface || !zero_ip || !zero_cidr) {
|
||||
return std::unexpected(!ap_ssid ? ap_ssid.error() : (!ap_passphrase ? ap_passphrase.error() : (!ap_interface ? ap_interface.error() : (!zero_ip ? zero_ip.error() : zero_cidr.error()))));
|
||||
if (!ap_ssid) {
|
||||
return std::unexpected(ap_ssid.error());
|
||||
}
|
||||
if (!ap_passphrase) {
|
||||
return std::unexpected(ap_passphrase.error());
|
||||
}
|
||||
if (!ap_interface) {
|
||||
return std::unexpected(ap_interface.error());
|
||||
}
|
||||
if (!zero_ip) {
|
||||
return std::unexpected(zero_ip.error());
|
||||
}
|
||||
if (!zero_cidr) {
|
||||
return std::unexpected(zero_cidr.error());
|
||||
}
|
||||
if (ap_passphrase->size() < 8U) {
|
||||
return std::unexpected(invalid("control.ap_passphrase must be at least 8 characters"));
|
||||
@@ -207,7 +229,7 @@ auto parse_control(boost::json::object const& root, vamp_zero::control_config fa
|
||||
return result;
|
||||
}
|
||||
|
||||
auto parse_audio(boost::json::object const& root, vamp_zero::audio_config fallback) -> std::expected<vamp_zero::audio_config, modulus::error> {
|
||||
auto parse_audio(boost::json::object const &root, vamp_zero::audio_config fallback) -> std::expected<vamp_zero::audio_config, modulus::error> {
|
||||
auto object = optional_object(root, "audio");
|
||||
if (!object) {
|
||||
return std::unexpected(object.error());
|
||||
@@ -225,16 +247,35 @@ auto parse_audio(boost::json::object const& root, vamp_zero::audio_config fallba
|
||||
auto beats_per_bar = optional_uint32(**object, "beats_per_bar", fallback.beats_per_bar, 1U, 16U);
|
||||
auto master_volume = optional_float(**object, "master_volume", fallback.master_volume, 0.0F, 1.0F);
|
||||
auto metronome_volume = optional_float(**object, "metronome_volume", fallback.metronome_volume, 0.0F, 1.0F);
|
||||
if (!enabled || !track_enabled || !metronome_enabled || !metronome_bpm || !metronome_min_bpm || !metronome_max_bpm || !metronome_step || !beats_per_bar || !master_volume || !metronome_volume) {
|
||||
return std::unexpected(!enabled ? enabled.error()
|
||||
: (!track_enabled ? track_enabled.error()
|
||||
: (!metronome_enabled ? metronome_enabled.error()
|
||||
: (!metronome_bpm ? metronome_bpm.error()
|
||||
: (!metronome_min_bpm ? metronome_min_bpm.error()
|
||||
: (!metronome_max_bpm ? metronome_max_bpm.error()
|
||||
: (!metronome_step ? metronome_step.error()
|
||||
: (!beats_per_bar ? beats_per_bar.error()
|
||||
: (!master_volume ? master_volume.error() : metronome_volume.error())))))))));
|
||||
if (!enabled) {
|
||||
return std::unexpected(enabled.error());
|
||||
}
|
||||
if (!track_enabled) {
|
||||
return std::unexpected(track_enabled.error());
|
||||
}
|
||||
if (!metronome_enabled) {
|
||||
return std::unexpected(metronome_enabled.error());
|
||||
}
|
||||
if (!metronome_bpm) {
|
||||
return std::unexpected(metronome_bpm.error());
|
||||
}
|
||||
if (!metronome_min_bpm) {
|
||||
return std::unexpected(metronome_min_bpm.error());
|
||||
}
|
||||
if (!metronome_max_bpm) {
|
||||
return std::unexpected(metronome_max_bpm.error());
|
||||
}
|
||||
if (!metronome_step) {
|
||||
return std::unexpected(metronome_step.error());
|
||||
}
|
||||
if (!beats_per_bar) {
|
||||
return std::unexpected(beats_per_bar.error());
|
||||
}
|
||||
if (!master_volume) {
|
||||
return std::unexpected(master_volume.error());
|
||||
}
|
||||
if (!metronome_volume) {
|
||||
return std::unexpected(metronome_volume.error());
|
||||
}
|
||||
if (*metronome_min_bpm > *metronome_max_bpm) {
|
||||
return std::unexpected(invalid("audio.metronome_min_bpm must be less than or equal to audio.metronome_max_bpm"));
|
||||
@@ -258,7 +299,7 @@ auto parse_audio(boost::json::object const& root, vamp_zero::audio_config fallba
|
||||
|
||||
}
|
||||
|
||||
auto vamp_zero::parse_config(boost::json::value const& value) -> std::expected<vamp_zero::config, modulus::error> {
|
||||
auto vamp_zero::parse_config(boost::json::value const &value) -> std::expected<vamp_zero::config, modulus::error> {
|
||||
auto root = require_object(value, "Product Vamp config root");
|
||||
if (!root) {
|
||||
return std::unexpected(root.error());
|
||||
@@ -275,8 +316,17 @@ auto vamp_zero::parse_config(boost::json::value const& value) -> std::expected<v
|
||||
auto database = parse_database(**root, result.database);
|
||||
auto control = parse_control(**root, result.control);
|
||||
auto audio = parse_audio(**root, result.audio);
|
||||
if (!runtime || !database || !control || !audio) {
|
||||
return std::unexpected(!runtime ? runtime.error() : (!database ? database.error() : (!control ? control.error() : audio.error())));
|
||||
if (!runtime) {
|
||||
return std::unexpected(runtime.error());
|
||||
}
|
||||
if (!database) {
|
||||
return std::unexpected(database.error());
|
||||
}
|
||||
if (!control) {
|
||||
return std::unexpected(control.error());
|
||||
}
|
||||
if (!audio) {
|
||||
return std::unexpected(audio.error());
|
||||
}
|
||||
result.runtime = std::move(*runtime);
|
||||
result.database = std::move(*database);
|
||||
@@ -292,7 +342,7 @@ auto vamp_zero::parse_config(boost::json::value const& value) -> std::expected<v
|
||||
return result;
|
||||
}
|
||||
|
||||
auto vamp_zero::load_config(std::string const& path) -> std::expected<vamp_zero::config, modulus::error> {
|
||||
auto vamp_zero::load_config(std::string const &path) -> std::expected<vamp_zero::config, modulus::error> {
|
||||
auto value = modulus::json_config::load_value(path, json_options);
|
||||
if (!value) {
|
||||
return std::unexpected(value.error());
|
||||
|
||||
+48
-42
@@ -1,6 +1,3 @@
|
||||
#include <vamp_zero/database.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
@@ -16,16 +13,19 @@
|
||||
|
||||
#include <noto.hpp>
|
||||
|
||||
#include <vamp_zero/database.hpp>
|
||||
#include <vamp_zero/errors.hpp>
|
||||
#include <vamp_zero/library.hpp>
|
||||
#include <vamp_zero/score.hpp>
|
||||
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto application_id = std::int64_t{0x56414D50};
|
||||
|
||||
auto connection_config(vamp_zero::database_config const& config) -> noto::connection_config {
|
||||
auto result = config.path == ":memory:" ? noto::memory_connection_config("product-vamp") : noto::file_connection_config(config.path, "product-vamp");
|
||||
auto connection_config(vamp_zero::database_config const &config) -> noto::connection_config {
|
||||
auto result = config.path == ":memory:" ? noto::memory_connection_config("product-vamp")
|
||||
: noto::file_connection_config(config.path, "product-vamp");
|
||||
result.busy_timeout = std::chrono::seconds{5};
|
||||
result.foreign_keys = true;
|
||||
result.pragmas = {
|
||||
@@ -35,12 +35,12 @@ auto connection_config(vamp_zero::database_config const& config) -> noto::connec
|
||||
return result;
|
||||
}
|
||||
|
||||
auto text_value(noto::sqlite_value const& value) -> std::string {
|
||||
auto text_value(noto::sqlite_value const &value) -> std::string {
|
||||
auto text = std::get_if<std::string>(&value.value);
|
||||
return text == nullptr ? std::string{} : *text;
|
||||
}
|
||||
|
||||
auto execute_schema_statement(noto::connection& connection, std::string statement) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
|
||||
auto execute_schema_statement(noto::connection &connection, std::string statement) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
|
||||
auto result = co_await connection.async_execute(std::move(statement));
|
||||
if (!result) {
|
||||
co_return std::unexpected(result.error());
|
||||
@@ -48,12 +48,12 @@ auto execute_schema_statement(noto::connection& connection, std::string statemen
|
||||
co_return std::expected<void, modulus::error>{};
|
||||
}
|
||||
|
||||
auto column_exists(noto::connection& connection, std::string table, std::string column) -> boost::asio::awaitable<std::expected<std::bool_t, modulus::error>> {
|
||||
auto column_exists(noto::connection &connection, std::string table, std::string column) -> boost::asio::awaitable<std::expected<std::bool_t, modulus::error>> {
|
||||
auto selected = co_await connection.async_execute("PRAGMA table_info(" + table + ")");
|
||||
if (!selected) {
|
||||
co_return std::unexpected(selected.error());
|
||||
}
|
||||
for (auto const& row : selected->rows) {
|
||||
for (auto const &row : selected->rows) {
|
||||
if (row.size() >= 2U && text_value(row[1]) == column) {
|
||||
co_return true;
|
||||
}
|
||||
@@ -61,7 +61,7 @@ auto column_exists(noto::connection& connection, std::string table, std::string
|
||||
co_return false;
|
||||
}
|
||||
|
||||
auto ensure_column(noto::connection& connection, std::string table, std::string column, std::string alter_statement) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
|
||||
auto ensure_column(noto::connection &connection, std::string table, std::string column, std::string alter_statement) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
|
||||
auto exists = co_await column_exists(connection, std::move(table), std::move(column));
|
||||
if (!exists) {
|
||||
co_return std::unexpected(exists.error());
|
||||
@@ -72,12 +72,11 @@ auto ensure_column(noto::connection& connection, std::string table, std::string
|
||||
co_return co_await execute_schema_statement(connection, std::move(alter_statement));
|
||||
}
|
||||
|
||||
auto ensure_current_schema(noto::connection& connection) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
|
||||
auto ensure_current_schema(noto::connection &connection) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
|
||||
auto begin = co_await connection.async_execute("BEGIN IMMEDIATE");
|
||||
if (!begin) {
|
||||
co_return std::unexpected(begin.error());
|
||||
}
|
||||
|
||||
auto ensured = std::expected<void, modulus::error>{};
|
||||
auto schema_statements = std::vector<std::string>{
|
||||
R"sql(
|
||||
@@ -121,16 +120,15 @@ CREATE TABLE IF NOT EXISTS practice_exercises (
|
||||
)sql",
|
||||
"DROP TABLE IF EXISTS _noto_schema_migrations",
|
||||
};
|
||||
|
||||
for (auto& statement : schema_statements) {
|
||||
for (auto &statement : schema_statements) {
|
||||
if (!ensured) {
|
||||
break;
|
||||
}
|
||||
ensured = co_await execute_schema_statement(connection, std::move(statement));
|
||||
}
|
||||
|
||||
if (ensured) {
|
||||
ensured = co_await ensure_column(connection, "songs", "part", "ALTER TABLE songs ADD COLUMN part TEXT NOT NULL DEFAULT ''");
|
||||
ensured =
|
||||
co_await ensure_column(connection, "songs", "part", "ALTER TABLE songs ADD COLUMN part TEXT NOT NULL DEFAULT ''");
|
||||
}
|
||||
if (ensured) {
|
||||
ensured = co_await ensure_column(connection, "songs", "source_path", "ALTER TABLE songs ADD COLUMN source_path TEXT NOT NULL DEFAULT ''");
|
||||
@@ -142,39 +140,43 @@ CREATE TABLE IF NOT EXISTS practice_exercises (
|
||||
ensured = co_await ensure_column(connection, "songs", "catalog_managed", "ALTER TABLE songs ADD COLUMN catalog_managed INTEGER NOT NULL DEFAULT 1 CHECK(catalog_managed IN (0, 1))");
|
||||
}
|
||||
if (ensured) {
|
||||
ensured = co_await ensure_column(connection, "practice_exercises", "practice_type_id", "ALTER TABLE practice_exercises ADD COLUMN practice_type_id TEXT NOT NULL DEFAULT 'general' CHECK(length(trim(practice_type_id)) BETWEEN 1 AND 120)");
|
||||
ensured = co_await ensure_column(connection, "practice_exercises", "practice_type_id", "ALTER TABLE practice_exercises ADD COLUMN practice_type_id TEXT NOT NULL DEFAULT 'general' "
|
||||
"CHECK(length(trim(practice_type_id)) BETWEEN 1 AND 120)");
|
||||
}
|
||||
if (ensured) {
|
||||
ensured = co_await ensure_column(connection, "practice_exercises", "practice_type_title", "ALTER TABLE practice_exercises ADD COLUMN practice_type_title TEXT NOT NULL DEFAULT 'General Practice' CHECK(length(trim(practice_type_title)) BETWEEN 1 AND 200)");
|
||||
ensured = co_await ensure_column(connection, "practice_exercises", "practice_type_title", "ALTER TABLE practice_exercises ADD COLUMN practice_type_title TEXT NOT NULL DEFAULT 'General Practice' "
|
||||
"CHECK(length(trim(practice_type_title)) BETWEEN 1 AND 200)");
|
||||
}
|
||||
if (ensured) {
|
||||
ensured = co_await ensure_column(connection, "practice_exercises", "lesson_group_id", "ALTER TABLE practice_exercises ADD COLUMN lesson_group_id TEXT NOT NULL DEFAULT 'general' CHECK(length(trim(lesson_group_id)) BETWEEN 1 AND 120)");
|
||||
ensured = co_await ensure_column(connection, "practice_exercises", "lesson_group_id", "ALTER TABLE practice_exercises ADD COLUMN lesson_group_id TEXT NOT NULL DEFAULT 'general' "
|
||||
"CHECK(length(trim(lesson_group_id)) BETWEEN 1 AND 120)");
|
||||
}
|
||||
if (ensured) {
|
||||
ensured = co_await ensure_column(connection, "practice_exercises", "lesson_group_title", "ALTER TABLE practice_exercises ADD COLUMN lesson_group_title TEXT NOT NULL DEFAULT 'General' CHECK(length(trim(lesson_group_title)) BETWEEN 1 AND 200)");
|
||||
ensured = co_await ensure_column(connection, "practice_exercises", "lesson_group_title", "ALTER TABLE practice_exercises ADD COLUMN lesson_group_title TEXT NOT NULL DEFAULT 'General' "
|
||||
"CHECK(length(trim(lesson_group_title)) BETWEEN 1 AND 200)");
|
||||
}
|
||||
if (ensured) {
|
||||
ensured = co_await ensure_column(connection, "practice_exercises", "source_path", "ALTER TABLE practice_exercises ADD COLUMN source_path TEXT NOT NULL DEFAULT ''");
|
||||
}
|
||||
if (ensured) {
|
||||
ensured = co_await ensure_column(connection, "practice_exercises", "catalog_managed", "ALTER TABLE practice_exercises ADD COLUMN catalog_managed INTEGER NOT NULL DEFAULT 1 CHECK(catalog_managed IN (0, 1))");
|
||||
ensured = co_await ensure_column(connection, "practice_exercises", "catalog_managed", "ALTER TABLE practice_exercises ADD COLUMN catalog_managed INTEGER NOT NULL DEFAULT 1 CHECK(catalog_managed IN "
|
||||
"(0, 1))");
|
||||
}
|
||||
|
||||
auto index_statements = std::vector<std::string>{
|
||||
"CREATE INDEX IF NOT EXISTS idx_songs_title ON songs(title COLLATE NOCASE)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_songs_title_part ON songs(title COLLATE NOCASE, part COLLATE NOCASE)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_songs_catalog_managed ON songs(catalog_managed)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_practice_exercises_title ON practice_exercises(title COLLATE NOCASE)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_practice_exercises_hierarchy ON practice_exercises(practice_type_id COLLATE NOCASE, lesson_group_id COLLATE NOCASE, title COLLATE NOCASE)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_practice_exercises_hierarchy ON practice_exercises(practice_type_id COLLATE "
|
||||
"NOCASE, lesson_group_id COLLATE NOCASE, title COLLATE NOCASE)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_practice_exercises_catalog_managed ON practice_exercises(catalog_managed)",
|
||||
};
|
||||
for (auto& statement : index_statements) {
|
||||
for (auto &statement : index_statements) {
|
||||
if (!ensured) {
|
||||
break;
|
||||
}
|
||||
ensured = co_await execute_schema_statement(connection, std::move(statement));
|
||||
}
|
||||
|
||||
if (ensured) {
|
||||
auto committed = co_await connection.async_execute("COMMIT");
|
||||
if (!committed) {
|
||||
@@ -183,7 +185,6 @@ CREATE TABLE IF NOT EXISTS practice_exercises (
|
||||
} else {
|
||||
static_cast<void>(co_await connection.async_execute("ROLLBACK"));
|
||||
}
|
||||
|
||||
if (!ensured) {
|
||||
co_return std::unexpected(ensured.error());
|
||||
}
|
||||
@@ -194,15 +195,15 @@ CREATE TABLE IF NOT EXISTS practice_exercises (
|
||||
co_return std::expected<void, modulus::error>{};
|
||||
}
|
||||
|
||||
auto path_parts(std::filesystem::path const& path) -> std::vector<std::string> {
|
||||
auto path_parts(std::filesystem::path const &path) -> std::vector<std::string> {
|
||||
auto parts = std::vector<std::string>{};
|
||||
for (auto const& part : path) {
|
||||
for (auto const &part : path) {
|
||||
parts.push_back(part.string());
|
||||
}
|
||||
return parts;
|
||||
}
|
||||
|
||||
auto logical_practice_exercise_id(std::string const& type_id, std::string const& lesson_id, std::string const& source_path, std::string const& fallback) -> std::string {
|
||||
auto logical_practice_exercise_id(std::string const &type_id, std::string const &lesson_id, std::string const &source_path, std::string const &fallback) -> std::string {
|
||||
auto parts = path_parts(std::filesystem::path{source_path});
|
||||
for (auto index = std::size_t{}; index + 3U < parts.size(); ++index) {
|
||||
if (parts[index] == "practice" && parts[index + 1U] == type_id && parts[index + 2U] == lesson_id) {
|
||||
@@ -217,10 +218,11 @@ auto logical_practice_exercise_id(std::string const& type_id, std::string const&
|
||||
}
|
||||
|
||||
auto now_ms() -> std::int64_t {
|
||||
return std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
|
||||
return std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch())
|
||||
.count();
|
||||
}
|
||||
|
||||
auto execute_delete_stale_catalog_rows(noto::connection& connection, std::string_view table, std::vector<std::string> const& ids) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
|
||||
auto execute_delete_stale_catalog_rows(noto::connection &connection, std::string_view table, std::vector<std::string> const &ids) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
|
||||
auto sql = std::string{"DELETE FROM "};
|
||||
sql += table;
|
||||
sql += " WHERE catalog_managed = 1";
|
||||
@@ -261,7 +263,6 @@ auto vamp_zero::async_ensure_database(boost::asio::any_io_executor executor, vam
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto connection = noto::connection{executor, connection_config(config)};
|
||||
auto connected = co_await connection.async_connect();
|
||||
if (!connected) {
|
||||
@@ -290,7 +291,6 @@ auto vamp_zero::async_ensure_database(boost::asio::any_io_executor executor, vam
|
||||
co_return std::unexpected(set.error());
|
||||
}
|
||||
}
|
||||
|
||||
auto ensured = co_await ensure_current_schema(connection);
|
||||
if (!ensured) {
|
||||
auto close = co_await connection.async_close();
|
||||
@@ -324,7 +324,7 @@ auto vamp_zero::async_import_catalogs(boost::asio::any_io_executor executor, vam
|
||||
auto timestamp = now_ms();
|
||||
auto song_ids = std::vector<std::string>{};
|
||||
song_ids.reserve(performance->parts.size());
|
||||
for (auto const& part : performance->parts) {
|
||||
for (auto const &part : performance->parts) {
|
||||
if (!imported) {
|
||||
break;
|
||||
}
|
||||
@@ -370,11 +370,11 @@ WHERE songs.catalog_managed = 1
|
||||
}
|
||||
}
|
||||
auto practice_ids = std::vector<std::string>{};
|
||||
for (auto const& exercise : practice->exercises) {
|
||||
for (auto const &exercise : practice->exercises) {
|
||||
if (!imported) {
|
||||
break;
|
||||
}
|
||||
for (auto const& variant : exercise.variants) {
|
||||
for (auto const &variant : exercise.variants) {
|
||||
practice_ids.push_back(variant.id);
|
||||
auto params = std::vector<noto::sqlite_value>{
|
||||
noto::sqlite_value{variant.id},
|
||||
@@ -457,13 +457,16 @@ WHERE practice_exercises.catalog_managed = 1
|
||||
co_return co_await connection.async_close();
|
||||
}
|
||||
|
||||
auto vamp_zero::async_load_performance_library(boost::asio::any_io_executor executor, vamp_zero::database_config config) -> boost::asio::awaitable<std::expected<vamp_zero::library_state, modulus::error>> {
|
||||
auto vamp_zero::async_load_performance_library(boost::asio::any_io_executor executor, vamp_zero::database_config config)
|
||||
-> boost::asio::awaitable<std::expected<vamp_zero::library_state, 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_execute("SELECT id, title, composer, part, source_path, source_json, key_signature FROM songs ORDER BY title COLLATE NOCASE, part COLLATE NOCASE");
|
||||
auto selected =
|
||||
co_await connection.async_execute("SELECT id, title, composer, part, source_path, source_json, key_signature FROM "
|
||||
"songs ORDER BY title COLLATE NOCASE, part COLLATE NOCASE");
|
||||
if (!selected) {
|
||||
auto close = co_await connection.async_close();
|
||||
if (!close) {
|
||||
@@ -472,7 +475,7 @@ auto vamp_zero::async_load_performance_library(boost::asio::any_io_executor exec
|
||||
co_return std::unexpected(selected.error());
|
||||
}
|
||||
auto parts = std::vector<vamp_zero::library_part>{};
|
||||
for (auto const& row : selected->rows) {
|
||||
for (auto const &row : selected->rows) {
|
||||
if (row.size() < 7U) {
|
||||
continue;
|
||||
}
|
||||
@@ -504,13 +507,16 @@ auto vamp_zero::async_load_performance_library(boost::asio::any_io_executor exec
|
||||
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) -> boost::asio::awaitable<std::expected<vamp_zero::practice_library, modulus::error>> {
|
||||
auto vamp_zero::async_load_practice_library(boost::asio::any_io_executor executor, vamp_zero::database_config config)
|
||||
-> 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_execute("SELECT id, practice_type_id, practice_type_title, lesson_group_id, lesson_group_title, title, composer, key_signature, source_path, source_json FROM practice_exercises ORDER BY practice_type_id COLLATE NOCASE, lesson_group_id COLLATE NOCASE, source_path COLLATE NOCASE, title COLLATE NOCASE, id COLLATE NOCASE");
|
||||
auto selected = co_await connection.async_execute("SELECT id, practice_type_id, practice_type_title, lesson_group_id, lesson_group_title, title, composer, "
|
||||
"key_signature, source_path, source_json FROM practice_exercises ORDER BY practice_type_id COLLATE NOCASE, "
|
||||
"lesson_group_id COLLATE NOCASE, source_path COLLATE NOCASE, title COLLATE NOCASE, id COLLATE NOCASE");
|
||||
if (!selected) {
|
||||
auto close = co_await connection.async_close();
|
||||
if (!close) {
|
||||
@@ -519,7 +525,7 @@ auto vamp_zero::async_load_practice_library(boost::asio::any_io_executor executo
|
||||
co_return std::unexpected(selected.error());
|
||||
}
|
||||
auto exercises = std::vector<vamp_zero::practice_exercise>{};
|
||||
for (auto const& row : selected->rows) {
|
||||
for (auto const &row : selected->rows) {
|
||||
if (row.size() < 10U) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include <utility>
|
||||
|
||||
#include <vamp_zero/errors.hpp>
|
||||
|
||||
#include <utility>
|
||||
|
||||
auto vamp_zero::make_error(vamp_zero::error_code code, std::string message, modulus::source_location location) -> modulus::error {
|
||||
return modulus::make_error("product-vamp", static_cast<std::int32_t>(code), std::move(message), location);
|
||||
|
||||
+61
-64
@@ -1,27 +1,20 @@
|
||||
#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 <system_error>
|
||||
#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>
|
||||
#include <vamp_zero/score.hpp>
|
||||
#include <vamp_zero/library.hpp>
|
||||
#include <vamp_zero/notation.hpp>
|
||||
|
||||
|
||||
namespace {
|
||||
|
||||
auto invalid(std::string message) -> modulus::error {
|
||||
@@ -33,7 +26,7 @@ auto human_label(std::string value) -> std::string {
|
||||
return "Score";
|
||||
}
|
||||
auto capitalize_next = std::bool_t{true};
|
||||
for (auto& ch : value) {
|
||||
for (auto &ch : value) {
|
||||
if (ch == '-' || ch == '_') {
|
||||
ch = ' ';
|
||||
capitalize_next = true;
|
||||
@@ -47,33 +40,40 @@ auto human_label(std::string value) -> std::string {
|
||||
return value;
|
||||
}
|
||||
|
||||
auto practice_type_label(std::string const& value) -> std::string {
|
||||
auto practice_type_label(std::string const &value) -> std::string {
|
||||
return human_label(value) + " Practice";
|
||||
}
|
||||
|
||||
auto load_score_documents(std::filesystem::path const& directory, std::bool_t required) -> std::expected<std::vector<vamp_zero::score_document>, modulus::error> {
|
||||
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)) {
|
||||
auto exists = std::filesystem::exists(directory, error);
|
||||
if (error) {
|
||||
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::UNAVAILABLE, "failed to inspect score assets: " + error.message()));
|
||||
}
|
||||
if (!exists) {
|
||||
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()));
|
||||
auto entries = std::filesystem::directory_iterator{directory, error};
|
||||
auto end = std::filesystem::directory_iterator{};
|
||||
while (!error && entries != end) {
|
||||
auto const &entry = *entries;
|
||||
if (entry.is_regular_file(error) && entry.path().extension() == ".json") {
|
||||
paths.push_back(entry.path());
|
||||
}
|
||||
if (!entry.is_regular_file() || entry.path().extension() != ".json") {
|
||||
continue;
|
||||
if (!error) {
|
||||
entries.increment(error);
|
||||
}
|
||||
paths.push_back(entry.path());
|
||||
}
|
||||
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());
|
||||
|
||||
auto documents = std::vector<vamp_zero::score_document>{};
|
||||
for (auto const& path : paths) {
|
||||
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()));
|
||||
@@ -88,15 +88,15 @@ auto load_score_documents(std::filesystem::path const& directory, std::bool_t re
|
||||
return documents;
|
||||
}
|
||||
|
||||
auto path_parts(std::filesystem::path const& path) -> std::vector<std::string> {
|
||||
auto path_parts(std::filesystem::path const &path) -> std::vector<std::string> {
|
||||
auto parts = std::vector<std::string>{};
|
||||
for (auto const& part : path) {
|
||||
for (auto const &part : path) {
|
||||
parts.push_back(part.string());
|
||||
}
|
||||
return parts;
|
||||
}
|
||||
|
||||
auto key_rank(std::string const& key) -> std::size_t {
|
||||
auto key_rank(std::string const &key) -> std::size_t {
|
||||
static auto const keys = std::vector<std::string>{
|
||||
"C",
|
||||
"Db",
|
||||
@@ -116,8 +116,8 @@ auto key_rank(std::string const& key) -> std::size_t {
|
||||
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 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) {
|
||||
@@ -128,7 +128,7 @@ auto sync_primary_variant(vamp_zero::practice_exercise& exercise) -> void {
|
||||
if (exercise.variants.empty()) {
|
||||
return;
|
||||
}
|
||||
auto const& primary = exercise.variants.front();
|
||||
auto const &primary = exercise.variants.front();
|
||||
exercise.key_signature = primary.key_signature;
|
||||
exercise.source_path = primary.source_path;
|
||||
exercise.source_json = primary.source_json;
|
||||
@@ -136,10 +136,9 @@ auto sync_primary_variant(vamp_zero::practice_exercise& exercise) -> void {
|
||||
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 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -149,15 +148,14 @@ auto vamp_zero::parse_score_document(std::string_view json, std::string source_p
|
||||
}
|
||||
|
||||
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) {
|
||||
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) {
|
||||
for (auto const &part : parts) {
|
||||
if (groups.empty() || groups.back().title != part.title || groups.back().composer != part.composer) {
|
||||
groups.push_back({
|
||||
.title = part.title,
|
||||
@@ -173,7 +171,7 @@ auto vamp_zero::group_library_parts(std::vector<vamp_zero::library_part> parts)
|
||||
}
|
||||
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
@@ -188,27 +186,20 @@ auto vamp_zero::group_practice_exercises(std::vector<vamp_zero::practice_exercis
|
||||
}
|
||||
return left.source_path < right.source_path;
|
||||
});
|
||||
|
||||
auto merged = std::vector<vamp_zero::practice_exercise>{};
|
||||
for (auto& exercise : exercises) {
|
||||
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())
|
||||
);
|
||||
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;
|
||||
});
|
||||
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,
|
||||
@@ -216,7 +207,7 @@ auto vamp_zero::group_practice_exercises(std::vector<vamp_zero::practice_exercis
|
||||
});
|
||||
type = std::prev(types.end());
|
||||
}
|
||||
auto lesson = std::find_if(type->lesson_groups.begin(), type->lesson_groups.end(), [&](auto const& item) {
|
||||
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()) {
|
||||
@@ -234,13 +225,13 @@ auto vamp_zero::group_practice_exercises(std::vector<vamp_zero::practice_exercis
|
||||
};
|
||||
}
|
||||
|
||||
auto vamp_zero::load_performance_library(std::string const& assets_path) -> std::expected<vamp_zero::library_state, modulus::error> {
|
||||
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) {
|
||||
for (auto &document : *documents) {
|
||||
parts.push_back({
|
||||
.id = document.id,
|
||||
.title = document.title,
|
||||
@@ -256,27 +247,34 @@ auto vamp_zero::load_performance_library(std::string const& assets_path) -> std:
|
||||
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 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)) {
|
||||
auto exists = std::filesystem::exists(root, error);
|
||||
if (error) {
|
||||
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::UNAVAILABLE, "failed to inspect practice assets: " + error.message()));
|
||||
}
|
||||
if (!exists) {
|
||||
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()));
|
||||
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") {
|
||||
paths.push_back(entry.path());
|
||||
}
|
||||
if (!entry.is_regular_file() || entry.path().extension() != ".json") {
|
||||
continue;
|
||||
if (!error) {
|
||||
entries.increment(error);
|
||||
}
|
||||
paths.push_back(entry.path());
|
||||
}
|
||||
if (error) {
|
||||
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::UNAVAILABLE, "failed to scan practice assets: " + error.message()));
|
||||
}
|
||||
std::sort(paths.begin(), paths.end());
|
||||
|
||||
auto exercises = std::vector<vamp_zero::practice_exercise>{};
|
||||
for (auto const& path : paths) {
|
||||
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()));
|
||||
@@ -285,7 +283,6 @@ auto vamp_zero::load_practice_library(std::string const& assets_path) -> std::ex
|
||||
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()));
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
#include <vamp_zero/app.hpp>
|
||||
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <iostream>
|
||||
#include <utility>
|
||||
|
||||
auto main(std::int32_t argc, std::char_t** argv) -> std::int32_t {
|
||||
#include <vamp_zero/app.hpp>
|
||||
|
||||
|
||||
auto main(std::int32_t argc, std::char_t **argv) -> std::int32_t {
|
||||
try {
|
||||
auto command = vamp_zero::parse_command_line(argc, argv);
|
||||
if (!command) {
|
||||
@@ -18,7 +19,7 @@ auto main(std::int32_t argc, std::char_t** argv) -> std::int32_t {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
} catch (std::exception const& error) {
|
||||
} catch (std::exception const &error) {
|
||||
std::cerr << "vamp-zero: unhandled exception: " << error.what() << '\n';
|
||||
return 1;
|
||||
} catch (...) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include <algorithm>
|
||||
|
||||
#include <vamp_zero/navigation.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -10,7 +11,7 @@ auto menu_row_count(std::size_t item_count) -> std::size_t {
|
||||
|
||||
}
|
||||
|
||||
auto vamp_zero::selected_group(vamp_zero::app_state const& state) -> vamp_zero::song_group const* {
|
||||
auto vamp_zero::selected_group(vamp_zero::app_state const &state) -> vamp_zero::song_group const * {
|
||||
if (state.selected_song_index == 0U || state.performance.groups.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
@@ -21,8 +22,8 @@ auto vamp_zero::selected_group(vamp_zero::app_state const& state) -> vamp_zero::
|
||||
return &state.performance.groups[index];
|
||||
}
|
||||
|
||||
auto vamp_zero::selected_part(vamp_zero::app_state const& state) -> vamp_zero::library_part const* {
|
||||
auto const* group = vamp_zero::selected_group(state);
|
||||
auto vamp_zero::selected_part(vamp_zero::app_state const &state) -> vamp_zero::library_part const * {
|
||||
auto const *group = vamp_zero::selected_group(state);
|
||||
if (state.selected_part_index == 0U || group == nullptr || group->parts.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
@@ -33,17 +34,17 @@ auto vamp_zero::selected_part(vamp_zero::app_state const& state) -> vamp_zero::l
|
||||
return &group->parts[index];
|
||||
}
|
||||
|
||||
auto vamp_zero::active_part(vamp_zero::app_state const& state) -> vamp_zero::library_part const* {
|
||||
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.performance.parts.begin(), state.performance.parts.end(), [&](auto const& part) {
|
||||
auto found = std::find_if(state.performance.parts.begin(), state.performance.parts.end(), [&](auto const &part) {
|
||||
return part.id == state.active_part_id;
|
||||
});
|
||||
return found == state.performance.parts.end() ? nullptr : &*found;
|
||||
}
|
||||
|
||||
auto vamp_zero::selected_practice_type(vamp_zero::app_state const& state) -> vamp_zero::practice_type_group const* {
|
||||
auto vamp_zero::selected_practice_type(vamp_zero::app_state const &state) -> vamp_zero::practice_type_group const * {
|
||||
if (state.selected_practice_type_index == 0U || state.practice.types.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
@@ -54,8 +55,8 @@ auto vamp_zero::selected_practice_type(vamp_zero::app_state const& state) -> vam
|
||||
return &state.practice.types[index];
|
||||
}
|
||||
|
||||
auto vamp_zero::selected_practice_lesson(vamp_zero::app_state const& state) -> vamp_zero::practice_lesson_group const* {
|
||||
auto const* type = vamp_zero::selected_practice_type(state);
|
||||
auto vamp_zero::selected_practice_lesson(vamp_zero::app_state const &state) -> vamp_zero::practice_lesson_group const * {
|
||||
auto const *type = vamp_zero::selected_practice_type(state);
|
||||
if (state.selected_practice_lesson_index == 0U || type == nullptr || type->lesson_groups.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
@@ -66,8 +67,8 @@ auto vamp_zero::selected_practice_lesson(vamp_zero::app_state const& state) -> v
|
||||
return &type->lesson_groups[index];
|
||||
}
|
||||
|
||||
auto vamp_zero::selected_practice_exercise(vamp_zero::app_state const& state) -> vamp_zero::practice_exercise const* {
|
||||
auto const* lesson = vamp_zero::selected_practice_lesson(state);
|
||||
auto vamp_zero::selected_practice_exercise(vamp_zero::app_state const &state) -> vamp_zero::practice_exercise const * {
|
||||
auto const *lesson = vamp_zero::selected_practice_lesson(state);
|
||||
if (state.selected_practice_exercise_index == 0U || lesson == nullptr || lesson->exercises.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
@@ -78,21 +79,21 @@ auto vamp_zero::selected_practice_exercise(vamp_zero::app_state const& state) ->
|
||||
return &lesson->exercises[index];
|
||||
}
|
||||
|
||||
auto vamp_zero::active_practice(vamp_zero::app_state const& state) -> vamp_zero::practice_exercise const* {
|
||||
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.practice.exercises.begin(), state.practice.exercises.end(), [&](auto const& exercise) {
|
||||
return exercise.id == state.active_practice_id;
|
||||
});
|
||||
auto found = std::find_if(state.practice.exercises.begin(),
|
||||
state.practice.exercises.end(),
|
||||
[&](auto const &exercise) { return exercise.id == state.active_practice_id; });
|
||||
return found == state.practice.exercises.end() ? nullptr : &*found;
|
||||
}
|
||||
|
||||
auto vamp_zero::supports_key_selection(vamp_zero::practice_exercise const& exercise) -> std::bool_t {
|
||||
auto vamp_zero::supports_key_selection(vamp_zero::practice_exercise const &exercise) -> std::bool_t {
|
||||
return vamp_zero::practice_type_supports_key_selection(exercise.practice_type_id);
|
||||
}
|
||||
|
||||
auto vamp_zero::selected_practice_variant(vamp_zero::app_state const& state, vamp_zero::practice_exercise const& exercise) -> vamp_zero::practice_exercise::score_variant const* {
|
||||
auto vamp_zero::selected_practice_variant(vamp_zero::app_state const &state, vamp_zero::practice_exercise const &exercise) -> vamp_zero::practice_exercise::score_variant const * {
|
||||
if (exercise.variants.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
@@ -109,43 +110,44 @@ auto vamp_zero::selected_practice_variant(vamp_zero::app_state const& state, vam
|
||||
return &exercise.variants[index];
|
||||
}
|
||||
|
||||
auto vamp_zero::selected_practice_key(vamp_zero::app_state const& state, vamp_zero::practice_exercise const& exercise) -> std::optional<std::string_view> {
|
||||
auto const* variant = vamp_zero::selected_practice_variant(state, exercise);
|
||||
auto vamp_zero::selected_practice_key(vamp_zero::app_state const &state, vamp_zero::practice_exercise const &exercise) -> std::optional<std::string_view> {
|
||||
auto const *variant = vamp_zero::selected_practice_variant(state, exercise);
|
||||
if (variant == nullptr) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return variant->key_signature;
|
||||
}
|
||||
|
||||
auto vamp_zero::practice_key_row_count(vamp_zero::app_state const& state) -> std::size_t {
|
||||
auto const* exercise = vamp_zero::selected_practice_exercise(state);
|
||||
auto vamp_zero::practice_key_row_count(vamp_zero::app_state const &state) -> std::size_t {
|
||||
auto const *exercise = vamp_zero::selected_practice_exercise(state);
|
||||
if (exercise == nullptr || !vamp_zero::supports_key_selection(*exercise)) {
|
||||
return 1U;
|
||||
}
|
||||
return menu_row_count(exercise->variants.size());
|
||||
}
|
||||
|
||||
auto vamp_zero::effective_practice_key(vamp_zero::app_state const& state, vamp_zero::practice_exercise const& exercise) -> std::string {
|
||||
if (auto const* variant = vamp_zero::selected_practice_variant(state, exercise); variant != nullptr && !variant->key_signature.empty()) {
|
||||
auto vamp_zero::effective_practice_key(vamp_zero::app_state const &state, vamp_zero::practice_exercise const &exercise) -> std::string {
|
||||
if (auto const *variant = vamp_zero::selected_practice_variant(state, exercise);
|
||||
variant != nullptr && !variant->key_signature.empty()) {
|
||||
return variant->key_signature;
|
||||
}
|
||||
return exercise.key_signature.empty() ? std::string{"C"} : exercise.key_signature;
|
||||
}
|
||||
|
||||
auto vamp_zero::active_pages(vamp_zero::app_state const& state) -> std::size_t {
|
||||
auto vamp_zero::active_pages(vamp_zero::app_state const &state) -> std::size_t {
|
||||
if (state.mode == vamp_zero::app_mode::PERFORMANCE_SCORE) {
|
||||
auto const* part = vamp_zero::active_part(state);
|
||||
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* variant = exercise == nullptr ? nullptr : vamp_zero::selected_practice_variant(state, *exercise);
|
||||
auto const *exercise = vamp_zero::active_practice(state);
|
||||
auto const *variant = exercise == nullptr ? nullptr : vamp_zero::selected_practice_variant(state, *exercise);
|
||||
return variant == nullptr ? 0U : variant->total_pages;
|
||||
}
|
||||
return 0U;
|
||||
}
|
||||
|
||||
auto vamp_zero::practice_fallback_menu(vamp_zero::app_state const& state) -> vamp_zero::app_mode {
|
||||
auto vamp_zero::practice_fallback_menu(vamp_zero::app_state const &state) -> vamp_zero::app_mode {
|
||||
if (vamp_zero::selected_practice_lesson(state) != nullptr) {
|
||||
return vamp_zero::app_mode::PRACTICE_EXERCISE_MENU;
|
||||
}
|
||||
|
||||
+878
-285
File diff suppressed because it is too large
Load Diff
+101
-99
@@ -16,6 +16,7 @@
|
||||
#include <vamp_zero/errors.hpp>
|
||||
#include <vamp_zero/navigation.hpp>
|
||||
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto main_menu_performance = std::size_t{0};
|
||||
@@ -51,41 +52,43 @@ auto clamp_audio_state(vamp_zero::audio_state audio) -> vamp_zero::audio_state {
|
||||
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 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 {
|
||||
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;
|
||||
? audio.metronome_min_bpm
|
||||
: audio.metronome_bpm - audio.metronome_step;
|
||||
}
|
||||
|
||||
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.performance.groups.size()) - 1U);
|
||||
if (auto const* group = selected_group(state); group != nullptr) {
|
||||
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_type_index = std::min(state.selected_practice_type_index, menu_row_count(state.practice.types.size()) - 1U);
|
||||
if (auto const* type = selected_practice_type(state); type != nullptr) {
|
||||
state.selected_practice_lesson_index = std::min(state.selected_practice_lesson_index, menu_row_count(type->lesson_groups.size()) - 1U);
|
||||
state.selected_practice_type_index =
|
||||
std::min(state.selected_practice_type_index, menu_row_count(state.practice.types.size()) - 1U);
|
||||
if (auto const *type = selected_practice_type(state); type != nullptr) {
|
||||
state.selected_practice_lesson_index =
|
||||
std::min(state.selected_practice_lesson_index, menu_row_count(type->lesson_groups.size()) - 1U);
|
||||
} else {
|
||||
state.selected_practice_lesson_index = 0U;
|
||||
}
|
||||
if (auto const* lesson = selected_practice_lesson(state); lesson != nullptr) {
|
||||
state.selected_practice_exercise_index = std::min(state.selected_practice_exercise_index, menu_row_count(lesson->exercises.size()) - 1U);
|
||||
if (auto const *lesson = selected_practice_lesson(state); lesson != nullptr) {
|
||||
state.selected_practice_exercise_index =
|
||||
std::min(state.selected_practice_exercise_index, menu_row_count(lesson->exercises.size()) - 1U);
|
||||
} else {
|
||||
state.selected_practice_exercise_index = 0U;
|
||||
}
|
||||
state.selected_practice_key_index = std::min(state.selected_practice_key_index, practice_key_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;
|
||||
}
|
||||
@@ -93,10 +96,11 @@ auto clamp_state(vamp_zero::app_state state) -> vamp_zero::app_state {
|
||||
state.mode = vamp_zero::app_mode::PRACTICE_TYPE_MENU;
|
||||
}
|
||||
if (state.mode == vamp_zero::app_mode::PRACTICE_EXERCISE_MENU && selected_practice_lesson(state) == nullptr) {
|
||||
state.mode = selected_practice_type(state) == nullptr ? vamp_zero::app_mode::PRACTICE_TYPE_MENU : vamp_zero::app_mode::PRACTICE_LESSON_MENU;
|
||||
state.mode = selected_practice_type(state) == nullptr ? vamp_zero::app_mode::PRACTICE_TYPE_MENU
|
||||
: vamp_zero::app_mode::PRACTICE_LESSON_MENU;
|
||||
}
|
||||
if (state.mode == vamp_zero::app_mode::PRACTICE_KEY_MENU) {
|
||||
auto const* exercise = selected_practice_exercise(state);
|
||||
auto const *exercise = selected_practice_exercise(state);
|
||||
if (exercise == nullptr) {
|
||||
state.mode = practice_fallback_menu(state);
|
||||
} else if (!supports_key_selection(*exercise) || exercise->variants.empty()) {
|
||||
@@ -104,13 +108,13 @@ auto clamp_state(vamp_zero::app_state state) -> vamp_zero::app_state {
|
||||
state.selected_practice_key_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.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);
|
||||
auto const *exercise = active_practice(state);
|
||||
if (exercise == nullptr) {
|
||||
state.mode = practice_fallback_menu(state);
|
||||
state.active_practice_id.clear();
|
||||
@@ -121,20 +125,19 @@ auto clamp_state(vamp_zero::app_state state) -> vamp_zero::app_state {
|
||||
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.mode != vamp_zero::app_mode::PRACTICE_SCORE) {
|
||||
state.page_index = 0;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
auto command_json(vamp_zero::app_state const& state) -> boost::json::object {
|
||||
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;
|
||||
@@ -144,7 +147,7 @@ auto command_json(vamp_zero::app_state const& state) -> boost::json::object {
|
||||
return commands;
|
||||
}
|
||||
|
||||
auto audio_json(vamp_zero::app_state const& state) -> boost::json::object {
|
||||
auto audio_json(vamp_zero::app_state const &state) -> boost::json::object {
|
||||
auto audio = boost::json::object{};
|
||||
audio["track_enabled"] = state.audio.track_enabled;
|
||||
audio["metronome_enabled"] = state.audio.metronome_enabled;
|
||||
@@ -156,7 +159,7 @@ auto audio_json(vamp_zero::app_state const& state) -> boost::json::object {
|
||||
return audio;
|
||||
}
|
||||
|
||||
auto part_json(vamp_zero::library_part const& part) -> boost::json::object {
|
||||
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;
|
||||
@@ -169,9 +172,9 @@ 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 exercise_json(vamp_zero::practice_exercise const &exercise) -> boost::json::object {
|
||||
auto variants = boost::json::array{};
|
||||
for (auto const& variant : exercise.variants) {
|
||||
for (auto const &variant : exercise.variants) {
|
||||
auto item = boost::json::object{};
|
||||
item["id"] = variant.id;
|
||||
item["key_signature"] = variant.key_signature;
|
||||
@@ -198,9 +201,9 @@ auto exercise_json(vamp_zero::practice_exercise const& exercise) -> boost::json:
|
||||
return result;
|
||||
}
|
||||
|
||||
auto lesson_json(vamp_zero::practice_lesson_group const& lesson) -> boost::json::object {
|
||||
auto lesson_json(vamp_zero::practice_lesson_group const &lesson) -> boost::json::object {
|
||||
auto exercises = boost::json::array{};
|
||||
for (auto const& exercise : lesson.exercises) {
|
||||
for (auto const &exercise : lesson.exercises) {
|
||||
exercises.push_back(exercise_json(exercise));
|
||||
}
|
||||
auto result = boost::json::object{};
|
||||
@@ -211,17 +214,17 @@ auto lesson_json(vamp_zero::practice_lesson_group const& lesson) -> boost::json:
|
||||
return result;
|
||||
}
|
||||
|
||||
auto practice_type_exercise_count(vamp_zero::practice_type_group const& type) -> std::size_t {
|
||||
auto practice_type_exercise_count(vamp_zero::practice_type_group const &type) -> std::size_t {
|
||||
auto count = std::size_t{};
|
||||
for (auto const& lesson : type.lesson_groups) {
|
||||
for (auto const &lesson : type.lesson_groups) {
|
||||
count += lesson.exercises.size();
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
auto type_json(vamp_zero::practice_type_group const& type) -> boost::json::object {
|
||||
auto type_json(vamp_zero::practice_type_group const &type) -> boost::json::object {
|
||||
auto lessons = boost::json::array{};
|
||||
for (auto const& lesson : type.lesson_groups) {
|
||||
for (auto const &lesson : type.lesson_groups) {
|
||||
lessons.push_back(lesson_json(lesson));
|
||||
}
|
||||
auto result = boost::json::object{};
|
||||
@@ -233,11 +236,11 @@ auto type_json(vamp_zero::practice_type_group const& type) -> boost::json::objec
|
||||
return result;
|
||||
}
|
||||
|
||||
auto performance_json(vamp_zero::app_state const& state) -> boost::json::object {
|
||||
auto performance_json(vamp_zero::app_state const &state) -> boost::json::object {
|
||||
auto groups = boost::json::array{};
|
||||
for (auto const& group : state.performance.groups) {
|
||||
for (auto const &group : state.performance.groups) {
|
||||
auto parts = boost::json::array{};
|
||||
for (auto const& part : group.parts) {
|
||||
for (auto const &part : group.parts) {
|
||||
parts.push_back(part_json(part));
|
||||
}
|
||||
auto item = boost::json::object{};
|
||||
@@ -246,7 +249,6 @@ auto performance_json(vamp_zero::app_state const& state) -> boost::json::object
|
||||
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.performance.groups.size());
|
||||
library["part_count"] = static_cast<std::uint64_t>(state.performance.parts.size());
|
||||
@@ -254,32 +256,31 @@ auto performance_json(vamp_zero::app_state const& state) -> boost::json::object
|
||||
return library;
|
||||
}
|
||||
|
||||
auto practice_json(vamp_zero::app_state const& state) -> boost::json::object {
|
||||
auto practice_json(vamp_zero::app_state const &state) -> boost::json::object {
|
||||
auto types = boost::json::array{};
|
||||
for (auto const& type : state.practice.types) {
|
||||
for (auto const &type : state.practice.types) {
|
||||
types.push_back(type_json(type));
|
||||
}
|
||||
auto exercises = boost::json::array{};
|
||||
for (auto const& exercise : state.practice.exercises) {
|
||||
for (auto const &exercise : state.practice.exercises) {
|
||||
exercises.push_back(exercise_json(exercise));
|
||||
}
|
||||
auto keys = boost::json::array{};
|
||||
for (auto key : vamp_zero::practice_key_names()) {
|
||||
keys.push_back(boost::json::value{std::string{key}});
|
||||
}
|
||||
|
||||
auto selection = boost::json::object{};
|
||||
selection["type_row_index"] = static_cast<std::uint64_t>(state.selected_practice_type_index);
|
||||
selection["lesson_row_index"] = static_cast<std::uint64_t>(state.selected_practice_lesson_index);
|
||||
selection["exercise_row_index"] = static_cast<std::uint64_t>(state.selected_practice_exercise_index);
|
||||
selection["key_row_index"] = static_cast<std::uint64_t>(state.selected_practice_key_index);
|
||||
if (auto const* type = selected_practice_type(state); type != nullptr) {
|
||||
if (auto const *type = selected_practice_type(state); type != nullptr) {
|
||||
selection["type"] = type_json(*type);
|
||||
}
|
||||
if (auto const* lesson = selected_practice_lesson(state); lesson != nullptr) {
|
||||
if (auto const *lesson = selected_practice_lesson(state); lesson != nullptr) {
|
||||
selection["lesson_group"] = lesson_json(*lesson);
|
||||
}
|
||||
if (auto const* exercise = selected_practice_exercise(state); exercise != nullptr) {
|
||||
if (auto const *exercise = selected_practice_exercise(state); exercise != nullptr) {
|
||||
selection["exercise"] = exercise_json(*exercise);
|
||||
auto variant = selected_practice_variant(state, *exercise);
|
||||
if (variant != nullptr) {
|
||||
@@ -292,12 +293,11 @@ auto practice_json(vamp_zero::app_state const& state) -> boost::json::object {
|
||||
selection["variant"] = std::move(item);
|
||||
}
|
||||
}
|
||||
if (auto const* exercise = selected_practice_exercise(state); exercise != nullptr) {
|
||||
if (auto const *exercise = selected_practice_exercise(state); exercise != nullptr) {
|
||||
if (auto key = selected_practice_key(state, *exercise)) {
|
||||
selection["key"] = std::string{*key};
|
||||
}
|
||||
}
|
||||
|
||||
auto practice = boost::json::object{};
|
||||
practice["type_count"] = static_cast<std::uint64_t>(state.practice.types.size());
|
||||
practice["exercise_count"] = static_cast<std::uint64_t>(state.practice.exercises.size());
|
||||
@@ -310,7 +310,7 @@ auto practice_json(vamp_zero::app_state const& state) -> boost::json::object {
|
||||
}
|
||||
|
||||
auto sanitize_line_value(std::string value) -> std::string {
|
||||
for (auto& ch : value) {
|
||||
for (auto &ch : value) {
|
||||
if (ch == '\r' || ch == '\n') {
|
||||
ch = ' ';
|
||||
}
|
||||
@@ -330,7 +330,6 @@ auto vamp_zero::apply_app_command(vamp_zero::app_state state, vamp_protocol::pag
|
||||
++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) {
|
||||
@@ -367,13 +366,13 @@ auto vamp_zero::apply_app_command(vamp_zero::app_state state, vamp_protocol::pag
|
||||
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.performance.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.performance.groups.size()));
|
||||
state.selected_song_index =
|
||||
wrap_previous(state.selected_song_index, menu_row_count(state.performance.groups.size()));
|
||||
state.selected_part_index = 0;
|
||||
} else if (state.selected_song_index == 0U) {
|
||||
state.mode = vamp_zero::app_mode::MAIN_MENU;
|
||||
@@ -382,9 +381,8 @@ auto vamp_zero::apply_app_command(vamp_zero::app_state state, vamp_protocol::pag
|
||||
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 (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) {
|
||||
@@ -393,7 +391,7 @@ auto vamp_zero::apply_app_command(vamp_zero::app_state state, vamp_protocol::pag
|
||||
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) {
|
||||
} 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;
|
||||
@@ -401,9 +399,8 @@ 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_SCORE:
|
||||
if (auto const* part = active_part(state); part != nullptr) {
|
||||
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) {
|
||||
@@ -415,15 +412,16 @@ 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_TYPE_MENU:
|
||||
if (command == vamp_protocol::page_command::NEXT) {
|
||||
state.selected_practice_type_index = wrap_next(state.selected_practice_type_index, menu_row_count(state.practice.types.size()));
|
||||
state.selected_practice_type_index =
|
||||
wrap_next(state.selected_practice_type_index, menu_row_count(state.practice.types.size()));
|
||||
state.selected_practice_lesson_index = 0;
|
||||
state.selected_practice_exercise_index = 0;
|
||||
state.selected_practice_key_index = 0;
|
||||
} else if (command == vamp_protocol::page_command::PREVIOUS) {
|
||||
state.selected_practice_type_index = wrap_previous(state.selected_practice_type_index, menu_row_count(state.practice.types.size()));
|
||||
state.selected_practice_type_index =
|
||||
wrap_previous(state.selected_practice_type_index, menu_row_count(state.practice.types.size()));
|
||||
state.selected_practice_lesson_index = 0;
|
||||
state.selected_practice_exercise_index = 0;
|
||||
state.selected_practice_key_index = 0;
|
||||
@@ -436,15 +434,16 @@ auto vamp_zero::apply_app_command(vamp_zero::app_state state, vamp_protocol::pag
|
||||
state.selected_practice_key_index = 0;
|
||||
}
|
||||
return clamp_state(std::move(state));
|
||||
|
||||
case vamp_zero::app_mode::PRACTICE_LESSON_MENU:
|
||||
if (auto const* type = selected_practice_type(state); type != nullptr) {
|
||||
if (auto const *type = selected_practice_type(state); type != nullptr) {
|
||||
if (command == vamp_protocol::page_command::NEXT) {
|
||||
state.selected_practice_lesson_index = wrap_next(state.selected_practice_lesson_index, menu_row_count(type->lesson_groups.size()));
|
||||
state.selected_practice_lesson_index =
|
||||
wrap_next(state.selected_practice_lesson_index, menu_row_count(type->lesson_groups.size()));
|
||||
state.selected_practice_exercise_index = 0;
|
||||
state.selected_practice_key_index = 0;
|
||||
} else if (command == vamp_protocol::page_command::PREVIOUS) {
|
||||
state.selected_practice_lesson_index = wrap_previous(state.selected_practice_lesson_index, menu_row_count(type->lesson_groups.size()));
|
||||
state.selected_practice_lesson_index =
|
||||
wrap_previous(state.selected_practice_lesson_index, menu_row_count(type->lesson_groups.size()));
|
||||
state.selected_practice_exercise_index = 0;
|
||||
state.selected_practice_key_index = 0;
|
||||
} else if (state.selected_practice_lesson_index == 0U) {
|
||||
@@ -458,20 +457,21 @@ 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 (auto const* lesson = selected_practice_lesson(state); lesson != nullptr) {
|
||||
if (auto const *lesson = selected_practice_lesson(state); lesson != nullptr) {
|
||||
if (command == vamp_protocol::page_command::NEXT) {
|
||||
state.selected_practice_exercise_index = wrap_next(state.selected_practice_exercise_index, menu_row_count(lesson->exercises.size()));
|
||||
state.selected_practice_exercise_index =
|
||||
wrap_next(state.selected_practice_exercise_index, menu_row_count(lesson->exercises.size()));
|
||||
state.selected_practice_key_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(lesson->exercises.size()));
|
||||
state.selected_practice_exercise_index =
|
||||
wrap_previous(state.selected_practice_exercise_index, menu_row_count(lesson->exercises.size()));
|
||||
state.selected_practice_key_index = 0;
|
||||
} else if (state.selected_practice_exercise_index == 0U) {
|
||||
state.mode = vamp_zero::app_mode::PRACTICE_LESSON_MENU;
|
||||
state.active_practice_id.clear();
|
||||
state.page_index = 0;
|
||||
} else if (auto const* exercise = selected_practice_exercise(state); exercise != nullptr) {
|
||||
} else if (auto const *exercise = selected_practice_exercise(state); exercise != nullptr) {
|
||||
state.active_part_id.clear();
|
||||
state.active_practice_id.clear();
|
||||
state.page_index = 0;
|
||||
@@ -486,13 +486,15 @@ 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_KEY_MENU:
|
||||
if (auto const* exercise = selected_practice_exercise(state); exercise != nullptr && supports_key_selection(*exercise)) {
|
||||
if (auto const *exercise = selected_practice_exercise(state);
|
||||
exercise != nullptr && supports_key_selection(*exercise)) {
|
||||
if (command == vamp_protocol::page_command::NEXT) {
|
||||
state.selected_practice_key_index = wrap_next(state.selected_practice_key_index, menu_row_count(exercise->variants.size()));
|
||||
state.selected_practice_key_index =
|
||||
wrap_next(state.selected_practice_key_index, menu_row_count(exercise->variants.size()));
|
||||
} else if (command == vamp_protocol::page_command::PREVIOUS) {
|
||||
state.selected_practice_key_index = wrap_previous(state.selected_practice_key_index, menu_row_count(exercise->variants.size()));
|
||||
state.selected_practice_key_index =
|
||||
wrap_previous(state.selected_practice_key_index, menu_row_count(exercise->variants.size()));
|
||||
} else if (state.selected_practice_key_index == 0U) {
|
||||
state.mode = vamp_zero::app_mode::PRACTICE_EXERCISE_MENU;
|
||||
state.active_practice_id.clear();
|
||||
@@ -505,17 +507,17 @@ 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_SCORE:
|
||||
if (auto const* exercise = active_practice(state); exercise != nullptr) {
|
||||
auto const* variant = selected_practice_variant(state, *exercise);
|
||||
if (auto const *exercise = active_practice(state); exercise != nullptr) {
|
||||
auto const *variant = selected_practice_variant(state, *exercise);
|
||||
if (command == vamp_protocol::page_command::NEXT) {
|
||||
auto pages = variant == nullptr ? 0U : variant->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 = supports_key_selection(*exercise) ? vamp_zero::app_mode::PRACTICE_KEY_MENU : vamp_zero::app_mode::PRACTICE_EXERCISE_MENU;
|
||||
state.mode = supports_key_selection(*exercise) ? vamp_zero::app_mode::PRACTICE_KEY_MENU
|
||||
: vamp_zero::app_mode::PRACTICE_EXERCISE_MENU;
|
||||
state.active_practice_id.clear();
|
||||
state.page_index = 0;
|
||||
}
|
||||
@@ -525,22 +527,21 @@ auto vamp_zero::apply_app_command(vamp_zero::app_state state, vamp_protocol::pag
|
||||
return clamp_state(std::move(state));
|
||||
}
|
||||
|
||||
auto vamp_zero::app_status_json(vamp_zero::app_state const& state) -> boost::json::value {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
if (auto const *part = active_part(state); part != nullptr) {
|
||||
active["selected"] = true;
|
||||
active["kind"] = "performance";
|
||||
active["part"] = part_json(*part);
|
||||
@@ -549,8 +550,8 @@ auto vamp_zero::app_status_json(vamp_zero::app_state const& state) -> boost::jso
|
||||
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* variant = selected_practice_variant(state, *exercise);
|
||||
if (auto const *exercise = active_practice(state); exercise != nullptr) {
|
||||
auto const *variant = selected_practice_variant(state, *exercise);
|
||||
active["selected"] = true;
|
||||
active["kind"] = "practice";
|
||||
active["exercise"] = exercise_json(*exercise);
|
||||
@@ -566,11 +567,9 @@ auto vamp_zero::app_status_json(vamp_zero::app_state const& state) -> boost::jso
|
||||
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);
|
||||
@@ -585,14 +584,14 @@ auto vamp_zero::app_status_json(vamp_zero::app_state const& state) -> boost::jso
|
||||
return root;
|
||||
}
|
||||
|
||||
auto vamp_zero::remote_display_text(vamp_zero::app_state const& state) -> std::string {
|
||||
auto vamp_zero::remote_display_text(vamp_zero::app_state const &state) -> std::string {
|
||||
auto title = 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) {
|
||||
if (auto const *selected = active_part(state); selected != nullptr) {
|
||||
active = true;
|
||||
title = selected->title;
|
||||
part = selected->part;
|
||||
@@ -600,8 +599,8 @@ auto vamp_zero::remote_display_text(vamp_zero::app_state const& state) -> std::s
|
||||
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* variant = selected_practice_variant(state, *selected);
|
||||
if (auto const *selected = active_practice(state); selected != nullptr) {
|
||||
auto const *variant = selected_practice_variant(state, *selected);
|
||||
active = true;
|
||||
title = selected->title;
|
||||
part = "Key: " + effective_practice_key(state, *selected);
|
||||
@@ -609,7 +608,6 @@ auto vamp_zero::remote_display_text(vamp_zero::app_state const& state) -> std::s
|
||||
pages = variant == nullptr ? 0U : variant->total_pages;
|
||||
}
|
||||
}
|
||||
|
||||
auto output = std::ostringstream{};
|
||||
output << "mode=" << vamp_zero::to_string(state.mode) << "\n";
|
||||
output << "active=" << (active ? "1" : "0") << "\n";
|
||||
@@ -620,7 +618,7 @@ auto vamp_zero::remote_display_text(vamp_zero::app_state const& state) -> std::s
|
||||
return output.str();
|
||||
}
|
||||
|
||||
auto vamp_zero::validate_app_state(vamp_zero::app_state const& state) -> std::expected<void, modulus::error> {
|
||||
auto vamp_zero::validate_app_state(vamp_zero::app_state const &state) -> std::expected<void, modulus::error> {
|
||||
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"));
|
||||
}
|
||||
@@ -633,7 +631,8 @@ auto vamp_zero::validate_app_state(vamp_zero::app_state const& state) -> std::ex
|
||||
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) {
|
||||
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) {
|
||||
@@ -642,16 +641,19 @@ auto vamp_zero::validate_app_state(vamp_zero::app_state const& state) -> std::ex
|
||||
if (state.selected_song_index >= menu_row_count(state.performance.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())) {
|
||||
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_type_index >= menu_row_count(state.practice.types.size())) {
|
||||
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, "selected practice type row is out of range"));
|
||||
}
|
||||
if (auto const* type = selected_practice_type(state); type != nullptr && state.selected_practice_lesson_index >= menu_row_count(type->lesson_groups.size())) {
|
||||
if (auto const *type = selected_practice_type(state);
|
||||
type != nullptr && state.selected_practice_lesson_index >= menu_row_count(type->lesson_groups.size())) {
|
||||
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, "selected practice lesson row is out of range"));
|
||||
}
|
||||
if (auto const* lesson = selected_practice_lesson(state); lesson != nullptr && state.selected_practice_exercise_index >= menu_row_count(lesson->exercises.size())) {
|
||||
if (auto const *lesson = selected_practice_lesson(state);
|
||||
lesson != nullptr && state.selected_practice_exercise_index >= menu_row_count(lesson->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_key_index >= practice_key_row_count(state)) {
|
||||
@@ -664,13 +666,13 @@ 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, "practice exercise mode requires a selected lesson group"));
|
||||
}
|
||||
if (state.mode == vamp_zero::app_mode::PRACTICE_KEY_MENU) {
|
||||
auto const* exercise = selected_practice_exercise(state);
|
||||
auto const *exercise = selected_practice_exercise(state);
|
||||
if (exercise == nullptr || !supports_key_selection(*exercise)) {
|
||||
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, "practice key mode requires a melody exercise"));
|
||||
}
|
||||
}
|
||||
if (state.mode == vamp_zero::app_mode::PERFORMANCE_SCORE) {
|
||||
auto const* part = active_part(state);
|
||||
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"));
|
||||
}
|
||||
@@ -679,11 +681,11 @@ auto vamp_zero::validate_app_state(vamp_zero::app_state const& state) -> std::ex
|
||||
}
|
||||
}
|
||||
if (state.mode == vamp_zero::app_mode::PRACTICE_SCORE) {
|
||||
auto const* exercise = active_practice(state);
|
||||
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* variant = selected_practice_variant(state, *exercise);
|
||||
auto const *variant = selected_practice_variant(state, *exercise);
|
||||
if (variant == nullptr) {
|
||||
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, "practice score mode requires a selected generated variant"));
|
||||
}
|
||||
|
||||
+17
-17
@@ -1,7 +1,6 @@
|
||||
#include <vamp_zero/portal.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
@@ -15,19 +14,20 @@
|
||||
#include <vamp_protocol/page_control.hpp>
|
||||
#include <vamp_zero/page_control.hpp>
|
||||
|
||||
|
||||
namespace {
|
||||
|
||||
auto portal_html() -> std::string {
|
||||
return R"html(<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Product Vamp</title></head><body><main><h1>Product Vamp</h1><pre id="status">Loading...</pre><p><button id="prev">Previous</button> <button id="select">Select</button> <button id="next">Next</button></p><script>
|
||||
async function refresh(){const r=await fetch('/api/status');const j=await r.json();document.getElementById('status').textContent=JSON.stringify(j.payload||j,null,2);}
|
||||
document.getElementById('prev').onclick=async()=>{await fetch('/api/remote/previous',{method:'POST'});refresh();};
|
||||
document.getElementById('select').onclick=async()=>{await fetch('/api/remote/select',{method:'POST'});refresh();};
|
||||
document.getElementById('next').onclick=async()=>{await fetch('/api/remote/next',{method:'POST'});refresh();};
|
||||
refresh();
|
||||
</script></main></body></html>)html";
|
||||
async function refresh(){const r=await fetch('/api/status');const j=await r.json();document.getElementById('status').textContent=JSON.stringify(j.payload||j,null,2);}
|
||||
document.getElementById('prev').onclick=async()=>{await fetch('/api/remote/previous',{method:'POST'});refresh();};
|
||||
document.getElementById('select').onclick=async()=>{await fetch('/api/remote/select',{method:'POST'});refresh();};
|
||||
document.getElementById('next').onclick=async()=>{await fetch('/api/remote/next',{method:'POST'});refresh();};
|
||||
refresh();
|
||||
</script></main></body></html>)html";
|
||||
}
|
||||
|
||||
auto endpoint_port(std::string const& endpoint) -> std::string {
|
||||
auto endpoint_port(std::string const &endpoint) -> std::string {
|
||||
auto separator = endpoint.rfind(':');
|
||||
if (separator == std::string::npos || separator + 1U >= endpoint.size()) {
|
||||
return "8080";
|
||||
@@ -35,13 +35,14 @@ auto endpoint_port(std::string const& endpoint) -> std::string {
|
||||
return endpoint.substr(separator + 1U);
|
||||
}
|
||||
|
||||
auto remote_response(vamp_zero::app_state const& state) -> ligo::service_response {
|
||||
auto remote_response(vamp_zero::app_state const &state) -> ligo::service_response {
|
||||
return ligo::text_response("text/plain; charset=utf-8", vamp_zero::remote_display_text(state));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
vamp_zero::control_state::control_state(vamp_zero::config config, vamp_zero::app_state state) : config_{std::move(config)}, state_{std::move(state)} {}
|
||||
vamp_zero::control_state::control_state(vamp_zero::config config, vamp_zero::app_state state)
|
||||
: config_{std::move(config)}, state_{std::move(state)} {}
|
||||
|
||||
auto vamp_zero::control_state::config() const -> vamp_zero::config {
|
||||
auto lock = std::scoped_lock{mutex_};
|
||||
@@ -64,7 +65,7 @@ auto vamp_zero::control_state::snapshot() const -> vamp_zero::app_state {
|
||||
}
|
||||
|
||||
auto vamp_zero::control_state::apply(vamp_protocol::page_command command) -> vamp_zero::app_state {
|
||||
auto observer = vamp_zero::control_state::observer{};
|
||||
auto observer = std::function<void(vamp_zero::app_state)>{};
|
||||
auto updated = vamp_zero::app_state{};
|
||||
{
|
||||
auto lock = std::scoped_lock{mutex_};
|
||||
@@ -80,7 +81,7 @@ auto vamp_zero::control_state::apply(vamp_protocol::page_command command) -> vam
|
||||
}
|
||||
|
||||
auto vamp_zero::control_state::heartbeat() -> vamp_zero::app_state {
|
||||
auto observer = vamp_zero::control_state::observer{};
|
||||
auto observer = std::function<void(vamp_zero::app_state)>{};
|
||||
auto updated = vamp_zero::app_state{};
|
||||
{
|
||||
auto lock = std::scoped_lock{mutex_};
|
||||
@@ -100,7 +101,7 @@ auto vamp_zero::control_state::heartbeat() -> vamp_zero::app_state {
|
||||
}
|
||||
|
||||
auto vamp_zero::control_state::refresh_connection() -> std::optional<vamp_zero::app_state> {
|
||||
auto observer = vamp_zero::control_state::observer{};
|
||||
auto observer = std::function<void(vamp_zero::app_state)>{};
|
||||
auto updated = vamp_zero::app_state{};
|
||||
{
|
||||
auto lock = std::scoped_lock{mutex_};
|
||||
@@ -118,17 +119,16 @@ auto vamp_zero::control_state::refresh_connection() -> std::optional<vamp_zero::
|
||||
return updated;
|
||||
}
|
||||
|
||||
auto vamp_zero::control_state::set_observer(vamp_zero::control_state::observer observer) -> void {
|
||||
auto vamp_zero::control_state::set_observer(std::function<void(vamp_zero::app_state)> observer) -> void {
|
||||
auto lock = std::scoped_lock{mutex_};
|
||||
observer_ = std::move(observer);
|
||||
}
|
||||
|
||||
auto vamp_zero::portal_status_json(vamp_zero::config const& config, vamp_zero::app_state const& state) -> boost::json::value {
|
||||
auto vamp_zero::portal_status_json(vamp_zero::config const &config, vamp_zero::app_state const &state) -> boost::json::value {
|
||||
auto control = boost::json::object{};
|
||||
control["ap_ssid"] = config.control.ap_ssid;
|
||||
control["zero_ip"] = config.control.zero_ip;
|
||||
control["endpoint"] = "http://" + config.control.zero_ip + ":" + endpoint_port(config.control.endpoint.bind);
|
||||
|
||||
auto root = boost::json::object{};
|
||||
root["device"] = "zero";
|
||||
root["version"] = VAMP_VERSION;
|
||||
@@ -138,7 +138,7 @@ auto vamp_zero::portal_status_json(vamp_zero::config const& config, vamp_zero::a
|
||||
return root;
|
||||
}
|
||||
|
||||
auto vamp_zero::make_router(vamp_zero::control_state& state) -> ligo::service_router {
|
||||
auto vamp_zero::make_router(vamp_zero::control_state &state) -> ligo::service_router {
|
||||
auto router = ligo::service_router{};
|
||||
router.add_response_route("GET", vamp_protocol::root_path, [](ligo::request_context, boost::json::value) -> boost::asio::awaitable<std::expected<ligo::service_response, modulus::error>> {
|
||||
co_return ligo::text_response("text/html; charset=utf-8", portal_html());
|
||||
|
||||
+989
-895
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,7 @@
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
|
||||
auto vamp_zero::to_string(vamp_zero::hardware_mode value) -> std::string {
|
||||
switch (value) {
|
||||
case vamp_zero::hardware_mode::FAKE:
|
||||
@@ -37,7 +38,7 @@ auto vamp_zero::to_string(vamp_zero::app_mode value) -> std::string {
|
||||
return "main_menu";
|
||||
}
|
||||
|
||||
auto vamp_zero::practice_key_names() -> std::vector<std::string_view> const& {
|
||||
auto vamp_zero::practice_key_names() -> std::vector<std::string_view> const & {
|
||||
static auto const keys = std::vector<std::string_view>{
|
||||
"C",
|
||||
"Db",
|
||||
|
||||
+213
-102
@@ -22,14 +22,15 @@
|
||||
#include <vamp_zero/color.hpp>
|
||||
#include <vamp_zero/library.hpp>
|
||||
#include <vamp_zero/navigation.hpp>
|
||||
#include <vamp_zero/parse.hpp>
|
||||
#include <vamp_zero/score.hpp>
|
||||
|
||||
|
||||
struct vamp_zero::score_draw_cache {
|
||||
struct entry {
|
||||
std::string source_json{};
|
||||
std::shared_ptr<vamp_zero::score_document const> document{};
|
||||
};
|
||||
|
||||
auto get(std::string_view source_json, std::string source_path, std::string part_override) -> std::expected<std::shared_ptr<vamp_zero::score_document const>, modulus::error> {
|
||||
auto key = source_path;
|
||||
key.push_back('\n');
|
||||
@@ -38,12 +39,10 @@ struct vamp_zero::score_draw_cache {
|
||||
key += std::to_string(source_json.size());
|
||||
key.push_back('\n');
|
||||
key += std::to_string(std::hash<std::string_view>{}(source_json));
|
||||
|
||||
auto found = entries.find(key);
|
||||
if (found != entries.end() && found->second.source_json == source_json && found->second.document) {
|
||||
return found->second.document;
|
||||
}
|
||||
|
||||
auto parsed = vamp_zero::parse_score_document(source_json, source_path);
|
||||
if (!parsed) {
|
||||
return std::unexpected(parsed.error());
|
||||
@@ -58,15 +57,13 @@ struct vamp_zero::score_draw_cache {
|
||||
};
|
||||
return document;
|
||||
}
|
||||
|
||||
std::unordered_map<std::string, entry> entries{};
|
||||
std::shared_ptr<vamp_zero::score_document const> page_document{};
|
||||
std::size_t page_index{};
|
||||
std::optional<vamp_zero::score_page_layout> page{};
|
||||
|
||||
auto draw(luma::draw_context& context, luma::rect bounds, std::shared_ptr<vamp_zero::score_document const> document, std::size_t index) -> void {
|
||||
if (!page || page_document != document || index != page_index
|
||||
|| page->bounds.x != bounds.x || page->bounds.y != bounds.y || page->bounds.width != bounds.width || page->bounds.height != bounds.height) {
|
||||
auto draw(luma::draw_context &context, luma::rect bounds, std::shared_ptr<vamp_zero::score_document const> document, std::size_t index) -> void {
|
||||
if (!page || page_document != document || index != page_index || page->bounds.x != bounds.x ||
|
||||
page->bounds.y != bounds.y || page->bounds.width != bounds.width || page->bounds.height != bounds.height) {
|
||||
page = vamp_zero::layout_score_page(bounds, *document, index);
|
||||
page_document = std::move(document);
|
||||
page_index = index;
|
||||
@@ -80,22 +77,16 @@ namespace {
|
||||
constexpr auto touch_hold_duration = std::chrono::milliseconds{700};
|
||||
constexpr auto touch_double_tap_duration = std::chrono::milliseconds{220};
|
||||
|
||||
using touch_handler = std::function<std::bool_t(luma::touch_event const&)>;
|
||||
|
||||
auto bind_port(vamp_zero::config const& config) -> std::uint16_t {
|
||||
auto const& bind = config.control.endpoint.bind;
|
||||
auto bind_port(vamp_zero::config const &config) -> std::uint16_t {
|
||||
auto const &bind = config.control.endpoint.bind;
|
||||
auto colon = bind.rfind(':');
|
||||
if (colon == std::string::npos) {
|
||||
return 8080;
|
||||
}
|
||||
try {
|
||||
return static_cast<std::uint16_t>(std::stoul(bind.substr(colon + 1U)));
|
||||
} catch (...) {
|
||||
return 8080;
|
||||
}
|
||||
return vamp_zero::parse_integer<std::uint16_t>(std::string_view{bind}.substr(colon + 1U), "port").value_or(8080);
|
||||
}
|
||||
|
||||
auto status_color(vamp_zero::app_state const& state) -> vamp_style::color {
|
||||
auto status_color(vamp_zero::app_state const &state) -> vamp_style::color {
|
||||
return state.pico_connected ? vamp_style::connected : vamp_style::retrying;
|
||||
}
|
||||
|
||||
@@ -115,7 +106,7 @@ auto text_style(std::int32_t scale) -> luma::text_style {
|
||||
};
|
||||
}
|
||||
|
||||
auto draw_text(luma::draw_context& context, std::float32_t x, std::float32_t y, std::string const& text, std::int32_t scale, vamp_style::color color, std::float32_t max_width) -> void {
|
||||
auto draw_text(luma::draw_context &context, std::float32_t x, std::float32_t y, std::string const &text, std::int32_t scale, vamp_style::color color, std::float32_t max_width) -> void {
|
||||
if (text.empty() || max_width <= 0.0F) {
|
||||
return;
|
||||
}
|
||||
@@ -125,23 +116,27 @@ auto draw_text(luma::draw_context& context, std::float32_t x, std::float32_t y,
|
||||
context.pop_clip();
|
||||
}
|
||||
|
||||
auto draw_header(luma::draw_context& context, luma::rect bounds, vamp_zero::ui_state const& state) -> void {
|
||||
context.fill_rect({.x = bounds.x, .y = bounds.y, .width = bounds.width, .height = 80.0F}, vamp_zero::to_luma(vamp_style::panel));
|
||||
context.fill_rect({.x = bounds.x, .y = bounds.y + 78.0F, .width = bounds.width, .height = 2.0F}, vamp_zero::to_luma(vamp_style::accent));
|
||||
auto draw_header(luma::draw_context &context, luma::rect bounds, vamp_zero::ui_state const &state) -> void {
|
||||
context.fill_rect(
|
||||
{.x = bounds.x, .y = bounds.y, .width = bounds.width, .height = 80.0F}, vamp_zero::to_luma(vamp_style::panel));
|
||||
context.fill_rect({.x = bounds.x, .y = bounds.y + 78.0F, .width = bounds.width, .height = 2.0F},
|
||||
vamp_zero::to_luma(vamp_style::accent));
|
||||
draw_text(context, bounds.x + 28.0F, bounds.y + 18.0F, "PRODUCT VAMP", 4, vamp_style::ink, 360.0F);
|
||||
draw_text(context, bounds.x + 30.0F, bounds.y + 55.0F, "AP " + state.ap_ssid + " " + state.zero_ip + ":" + std::to_string(state.port), 1, vamp_style::muted, 520.0F);
|
||||
|
||||
auto indicator = status_color(state.app);
|
||||
auto x = bounds.x + bounds.width - 148.0F;
|
||||
context.fill_rect({.x = x, .y = bounds.y + 22.0F, .width = 24.0F, .height = 24.0F}, vamp_zero::to_luma(indicator));
|
||||
draw_text(context, x + 36.0F, bounds.y + 26.0F, "PICO", 2, vamp_style::ink, 88.0F);
|
||||
}
|
||||
|
||||
auto draw_row(luma::draw_context& context, luma::rect row, std::string const& primary, std::string const& secondary, std::bool_t selected) -> void {
|
||||
auto draw_row(luma::draw_context &context, luma::rect row, std::string const &primary, std::string const &secondary, std::bool_t selected) -> void {
|
||||
if (selected) {
|
||||
context.fill_rect(row, vamp_zero::to_luma(vamp_style::selection));
|
||||
}
|
||||
context.stroke_line({.x = row.x, .y = row.y + row.height}, {.x = row.x + row.width, .y = row.y + row.height}, 1.0F, vamp_zero::to_luma(vamp_style::rule));
|
||||
context.stroke_line({.x = row.x, .y = row.y + row.height},
|
||||
{.x = row.x + row.width, .y = row.y + row.height},
|
||||
1.0F,
|
||||
vamp_zero::to_luma(vamp_style::rule));
|
||||
draw_text(context, row.x + 16.0F, row.y + 10.0F, primary, 2, vamp_style::ink, row.width * 0.62F);
|
||||
if (!secondary.empty()) {
|
||||
draw_text(context, row.x + row.width - 250.0F, row.y + 17.0F, secondary, 1, selected ? vamp_style::ink : vamp_style::muted, 230.0F);
|
||||
@@ -155,27 +150,46 @@ auto first_visible_row(std::size_t selected, std::size_t total, std::size_t max_
|
||||
return selected - max_rows + 1U;
|
||||
}
|
||||
|
||||
auto draw_main_menu(luma::draw_context& context, luma::rect bounds, vamp_zero::app_state const& state) -> void {
|
||||
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);
|
||||
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.performance.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.practice.exercises.size()) + " EXERCISES", 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}, "Track", state.audio.track_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);
|
||||
draw_row(context,
|
||||
{.x = bounds.x + 36.0F, .y = list_y, .width = row_width, .height = row_height},
|
||||
"Performance Mode",
|
||||
std::to_string(state.performance.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.practice.exercises.size()) + " EXERCISES",
|
||||
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},
|
||||
"Track",
|
||||
state.audio.track_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);
|
||||
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}, "Tempo", tempo, state.selected_main_menu_index == 4U);
|
||||
draw_row(context,
|
||||
{.x = bounds.x + 36.0F, .y = list_y + row_height * 4.0F, .width = row_width, .height = row_height},
|
||||
"Tempo",
|
||||
tempo,
|
||||
state.selected_main_menu_index == 4U);
|
||||
}
|
||||
|
||||
auto draw_performance_song_menu(luma::draw_context& context, luma::rect bounds, vamp_zero::app_state const& state) -> void {
|
||||
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.performance.groups.size()) + " SONGS " + std::to_string(state.performance.parts.size()) + " PARTS", 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;
|
||||
@@ -185,26 +199,45 @@ auto draw_performance_song_menu(luma::draw_context& context, luma::rect bounds,
|
||||
for (auto row = std::size_t{}; row < max_rows; ++row) {
|
||||
auto index = first + row;
|
||||
if (index == 0U) {
|
||||
draw_row(context, {.x = bounds.x + 36.0F, .y = list_y + static_cast<std::float32_t>(row) * row_height, .width = row_width, .height = row_height}, "BACK", "", state.selected_song_index == 0U);
|
||||
draw_row(context,
|
||||
{.x = bounds.x + 36.0F,
|
||||
.y = list_y + static_cast<std::float32_t>(row) * row_height,
|
||||
.width = row_width,
|
||||
.height = row_height},
|
||||
"BACK",
|
||||
"",
|
||||
state.selected_song_index == 0U);
|
||||
continue;
|
||||
}
|
||||
auto const& group = state.performance.groups[index - 1U];
|
||||
draw_row(context, {.x = bounds.x + 36.0F, .y = list_y + static_cast<std::float32_t>(row) * row_height, .width = row_width, .height = row_height}, group.title, std::to_string(group.parts.size()) + " PARTS", index == state.selected_song_index);
|
||||
auto const &group = state.performance.groups[index - 1U];
|
||||
draw_row(context,
|
||||
{.x = bounds.x + 36.0F,
|
||||
.y = list_y + static_cast<std::float32_t>(row) * row_height,
|
||||
.width = row_width,
|
||||
.height = row_height},
|
||||
group.title,
|
||||
std::to_string(group.parts.size()) + " PARTS",
|
||||
index == state.selected_song_index);
|
||||
}
|
||||
if (state.performance.groups.empty()) {
|
||||
draw_text(context, bounds.x + 38.0F, list_y + row_height + 28.0F, "NO SONGS FOUND", 3, vamp_style::retrying, 420.0F);
|
||||
}
|
||||
}
|
||||
|
||||
auto draw_performance_part_menu(luma::draw_context& context, luma::rect bounds, vamp_zero::app_state const& state) -> void {
|
||||
auto const* group = selected_group(state);
|
||||
draw_text(context, bounds.x + 36.0F, bounds.y + 28.0F, group == nullptr ? std::string{"PARTS"} : group->title, 4, vamp_style::ink, bounds.width - 72.0F);
|
||||
auto draw_performance_part_menu(luma::draw_context &context, luma::rect bounds, vamp_zero::app_state const &state) -> void {
|
||||
auto const *group = selected_group(state);
|
||||
draw_text(context,
|
||||
bounds.x + 36.0F,
|
||||
bounds.y + 28.0F,
|
||||
group == nullptr ? std::string{"PARTS"} : group->title,
|
||||
4,
|
||||
vamp_style::ink,
|
||||
bounds.width - 72.0F);
|
||||
if (group == nullptr) {
|
||||
draw_text(context, bounds.x + 38.0F, bounds.y + 118.0F, "NO PARTS FOUND", 3, vamp_style::retrying, 420.0F);
|
||||
return;
|
||||
}
|
||||
draw_text(context, bounds.x + 38.0F, bounds.y + 66.0F, group->composer, 1, vamp_style::muted, bounds.width - 72.0F);
|
||||
|
||||
auto list_y = bounds.y + 112.0F;
|
||||
auto row_height = std::float32_t{54.0F};
|
||||
auto row_width = bounds.width - 72.0F;
|
||||
@@ -214,18 +247,31 @@ auto draw_performance_part_menu(luma::draw_context& context, luma::rect bounds,
|
||||
for (auto row = std::size_t{}; row < max_rows; ++row) {
|
||||
auto index = first + row;
|
||||
if (index == 0U) {
|
||||
draw_row(context, {.x = bounds.x + 36.0F, .y = list_y + static_cast<std::float32_t>(row) * row_height, .width = row_width, .height = row_height}, "BACK", "", state.selected_part_index == 0U);
|
||||
draw_row(context,
|
||||
{.x = bounds.x + 36.0F,
|
||||
.y = list_y + static_cast<std::float32_t>(row) * row_height,
|
||||
.width = row_width,
|
||||
.height = row_height},
|
||||
"BACK",
|
||||
"",
|
||||
state.selected_part_index == 0U);
|
||||
continue;
|
||||
}
|
||||
auto const& part = group->parts[index - 1U];
|
||||
draw_row(context, {.x = bounds.x + 36.0F, .y = list_y + static_cast<std::float32_t>(row) * row_height, .width = row_width, .height = row_height}, part.part, std::to_string(part.total_pages) + " PAGES", index == state.selected_part_index);
|
||||
auto const &part = group->parts[index - 1U];
|
||||
draw_row(context,
|
||||
{.x = bounds.x + 36.0F,
|
||||
.y = list_y + static_cast<std::float32_t>(row) * row_height,
|
||||
.width = row_width,
|
||||
.height = row_height},
|
||||
part.part,
|
||||
std::to_string(part.total_pages) + " PAGES",
|
||||
index == state.selected_part_index);
|
||||
}
|
||||
}
|
||||
|
||||
auto draw_practice_type_menu(luma::draw_context& context, luma::rect bounds, vamp_zero::app_state const& state) -> void {
|
||||
auto draw_practice_type_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, "PRACTICE", 4, vamp_style::ink, 300.0F);
|
||||
draw_text(context, bounds.x + 38.0F, bounds.y + 66.0F, std::to_string(state.practice.types.size()) + " TYPES " + std::to_string(state.practice.exercises.size()) + " EXERCISES", 1, vamp_style::muted, 420.0F);
|
||||
|
||||
auto list_y = bounds.y + 112.0F;
|
||||
auto row_height = std::float32_t{54.0F};
|
||||
auto row_width = bounds.width - 72.0F;
|
||||
@@ -235,30 +281,49 @@ auto draw_practice_type_menu(luma::draw_context& context, luma::rect bounds, vam
|
||||
for (auto row = std::size_t{}; row < max_rows; ++row) {
|
||||
auto index = first + row;
|
||||
if (index == 0U) {
|
||||
draw_row(context, {.x = bounds.x + 36.0F, .y = list_y + static_cast<std::float32_t>(row) * row_height, .width = row_width, .height = row_height}, "BACK", "", state.selected_practice_type_index == 0U);
|
||||
draw_row(context,
|
||||
{.x = bounds.x + 36.0F,
|
||||
.y = list_y + static_cast<std::float32_t>(row) * row_height,
|
||||
.width = row_width,
|
||||
.height = row_height},
|
||||
"BACK",
|
||||
"",
|
||||
state.selected_practice_type_index == 0U);
|
||||
continue;
|
||||
}
|
||||
auto const& type = state.practice.types[index - 1U];
|
||||
auto const &type = state.practice.types[index - 1U];
|
||||
auto exercise_count = std::size_t{};
|
||||
for (auto const& lesson : type.lesson_groups) {
|
||||
for (auto const &lesson : type.lesson_groups) {
|
||||
exercise_count += lesson.exercises.size();
|
||||
}
|
||||
draw_row(context, {.x = bounds.x + 36.0F, .y = list_y + static_cast<std::float32_t>(row) * row_height, .width = row_width, .height = row_height}, type.title, std::to_string(exercise_count) + " EXERCISES", index == state.selected_practice_type_index);
|
||||
draw_row(context,
|
||||
{.x = bounds.x + 36.0F,
|
||||
.y = list_y + static_cast<std::float32_t>(row) * row_height,
|
||||
.width = row_width,
|
||||
.height = row_height},
|
||||
type.title,
|
||||
std::to_string(exercise_count) + " EXERCISES",
|
||||
index == state.selected_practice_type_index);
|
||||
}
|
||||
if (state.practice.types.empty()) {
|
||||
draw_text(context, bounds.x + 38.0F, list_y + row_height + 28.0F, "NO PRACTICE EXERCISES FOUND", 3, vamp_style::retrying, 560.0F);
|
||||
}
|
||||
}
|
||||
|
||||
auto draw_practice_lesson_menu(luma::draw_context& context, luma::rect bounds, vamp_zero::app_state const& state) -> void {
|
||||
auto const* type = selected_practice_type(state);
|
||||
draw_text(context, bounds.x + 36.0F, bounds.y + 28.0F, type == nullptr ? std::string{"PRACTICE"} : type->title, 4, vamp_style::ink, bounds.width - 72.0F);
|
||||
auto draw_practice_lesson_menu(luma::draw_context &context, luma::rect bounds, vamp_zero::app_state const &state) -> void {
|
||||
auto const *type = selected_practice_type(state);
|
||||
draw_text(context,
|
||||
bounds.x + 36.0F,
|
||||
bounds.y + 28.0F,
|
||||
type == nullptr ? std::string{"PRACTICE"} : type->title,
|
||||
4,
|
||||
vamp_style::ink,
|
||||
bounds.width - 72.0F);
|
||||
draw_text(context, bounds.x + 38.0F, bounds.y + 66.0F, "SELECT LESSON GROUP", 1, vamp_style::muted, 360.0F);
|
||||
if (type == nullptr) {
|
||||
draw_text(context, bounds.x + 38.0F, bounds.y + 118.0F, "NO LESSON GROUPS FOUND", 3, vamp_style::retrying, 520.0F);
|
||||
return;
|
||||
}
|
||||
|
||||
auto list_y = bounds.y + 112.0F;
|
||||
auto row_height = std::float32_t{54.0F};
|
||||
auto row_width = bounds.width - 72.0F;
|
||||
@@ -268,23 +333,42 @@ auto draw_practice_lesson_menu(luma::draw_context& context, luma::rect bounds, v
|
||||
for (auto row = std::size_t{}; row < max_rows; ++row) {
|
||||
auto index = first + row;
|
||||
if (index == 0U) {
|
||||
draw_row(context, {.x = bounds.x + 36.0F, .y = list_y + static_cast<std::float32_t>(row) * row_height, .width = row_width, .height = row_height}, "BACK", "", state.selected_practice_lesson_index == 0U);
|
||||
draw_row(context,
|
||||
{.x = bounds.x + 36.0F,
|
||||
.y = list_y + static_cast<std::float32_t>(row) * row_height,
|
||||
.width = row_width,
|
||||
.height = row_height},
|
||||
"BACK",
|
||||
"",
|
||||
state.selected_practice_lesson_index == 0U);
|
||||
continue;
|
||||
}
|
||||
auto const& lesson = type->lesson_groups[index - 1U];
|
||||
draw_row(context, {.x = bounds.x + 36.0F, .y = list_y + static_cast<std::float32_t>(row) * row_height, .width = row_width, .height = row_height}, lesson.title, std::to_string(lesson.exercises.size()) + " EXERCISES", index == state.selected_practice_lesson_index);
|
||||
auto const &lesson = type->lesson_groups[index - 1U];
|
||||
draw_row(context,
|
||||
{.x = bounds.x + 36.0F,
|
||||
.y = list_y + static_cast<std::float32_t>(row) * row_height,
|
||||
.width = row_width,
|
||||
.height = row_height},
|
||||
lesson.title,
|
||||
std::to_string(lesson.exercises.size()) + " EXERCISES",
|
||||
index == state.selected_practice_lesson_index);
|
||||
}
|
||||
}
|
||||
|
||||
auto draw_practice_exercise_menu(luma::draw_context& context, luma::rect bounds, vamp_zero::app_state const& state) -> void {
|
||||
auto const* lesson = selected_practice_lesson(state);
|
||||
draw_text(context, bounds.x + 36.0F, bounds.y + 28.0F, lesson == nullptr ? std::string{"EXERCISES"} : lesson->title, 4, vamp_style::ink, bounds.width - 72.0F);
|
||||
auto draw_practice_exercise_menu(luma::draw_context &context, luma::rect bounds, vamp_zero::app_state const &state) -> void {
|
||||
auto const *lesson = selected_practice_lesson(state);
|
||||
draw_text(context,
|
||||
bounds.x + 36.0F,
|
||||
bounds.y + 28.0F,
|
||||
lesson == nullptr ? std::string{"EXERCISES"} : lesson->title,
|
||||
4,
|
||||
vamp_style::ink,
|
||||
bounds.width - 72.0F);
|
||||
draw_text(context, bounds.x + 38.0F, bounds.y + 66.0F, "SELECT EXERCISE", 1, vamp_style::muted, 320.0F);
|
||||
if (lesson == nullptr) {
|
||||
draw_text(context, bounds.x + 38.0F, bounds.y + 118.0F, "NO EXERCISES FOUND", 3, vamp_style::retrying, 440.0F);
|
||||
return;
|
||||
}
|
||||
|
||||
auto list_y = bounds.y + 112.0F;
|
||||
auto row_height = std::float32_t{54.0F};
|
||||
auto row_width = bounds.width - 72.0F;
|
||||
@@ -294,20 +378,40 @@ auto draw_practice_exercise_menu(luma::draw_context& context, luma::rect bounds,
|
||||
for (auto row = std::size_t{}; row < max_rows; ++row) {
|
||||
auto index = first + row;
|
||||
if (index == 0U) {
|
||||
draw_row(context, {.x = bounds.x + 36.0F, .y = list_y + static_cast<std::float32_t>(row) * row_height, .width = row_width, .height = row_height}, "BACK", "", state.selected_practice_exercise_index == 0U);
|
||||
draw_row(context,
|
||||
{.x = bounds.x + 36.0F,
|
||||
.y = list_y + static_cast<std::float32_t>(row) * row_height,
|
||||
.width = row_width,
|
||||
.height = row_height},
|
||||
"BACK",
|
||||
"",
|
||||
state.selected_practice_exercise_index == 0U);
|
||||
continue;
|
||||
}
|
||||
auto const& exercise = lesson->exercises[index - 1U];
|
||||
auto secondary = supports_key_selection(exercise) ? std::string{"KEY SELECT"} : "KEY " + effective_practice_key(state, exercise);
|
||||
draw_row(context, {.x = bounds.x + 36.0F, .y = list_y + static_cast<std::float32_t>(row) * row_height, .width = row_width, .height = row_height}, exercise.title, secondary, index == state.selected_practice_exercise_index);
|
||||
auto const &exercise = lesson->exercises[index - 1U];
|
||||
auto secondary =
|
||||
supports_key_selection(exercise) ? std::string{"KEY SELECT"} : "KEY " + effective_practice_key(state, exercise);
|
||||
draw_row(context,
|
||||
{.x = bounds.x + 36.0F,
|
||||
.y = list_y + static_cast<std::float32_t>(row) * row_height,
|
||||
.width = row_width,
|
||||
.height = row_height},
|
||||
exercise.title,
|
||||
secondary,
|
||||
index == state.selected_practice_exercise_index);
|
||||
}
|
||||
}
|
||||
|
||||
auto draw_practice_key_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{"KEY"} : exercise->title, 4, vamp_style::ink, bounds.width - 72.0F);
|
||||
auto draw_practice_key_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{"KEY"} : exercise->title,
|
||||
4,
|
||||
vamp_style::ink,
|
||||
bounds.width - 72.0F);
|
||||
draw_text(context, bounds.x + 38.0F, bounds.y + 66.0F, "SELECT PRACTICE KEY", 1, vamp_style::muted, 360.0F);
|
||||
|
||||
auto list_y = bounds.y + 112.0F;
|
||||
auto row_height = std::float32_t{54.0F};
|
||||
auto row_width = bounds.width - 72.0F;
|
||||
@@ -317,21 +421,36 @@ auto draw_practice_key_menu(luma::draw_context& context, luma::rect bounds, vamp
|
||||
for (auto row = std::size_t{}; row < max_rows; ++row) {
|
||||
auto index = first + row;
|
||||
if (index == 0U) {
|
||||
draw_row(context, {.x = bounds.x + 36.0F, .y = list_y + static_cast<std::float32_t>(row) * row_height, .width = row_width, .height = row_height}, "BACK", "", state.selected_practice_key_index == 0U);
|
||||
draw_row(context,
|
||||
{.x = bounds.x + 36.0F,
|
||||
.y = list_y + static_cast<std::float32_t>(row) * row_height,
|
||||
.width = row_width,
|
||||
.height = row_height},
|
||||
"BACK",
|
||||
"",
|
||||
state.selected_practice_key_index == 0U);
|
||||
continue;
|
||||
}
|
||||
auto key = exercise == nullptr ? std::string{"C"} : exercise->variants[index - 1U].key_signature;
|
||||
auto source = exercise == nullptr || exercise->key_signature.empty() ? std::string{"SOURCE C"} : "SOURCE " + exercise->key_signature;
|
||||
draw_row(context, {.x = bounds.x + 36.0F, .y = list_y + static_cast<std::float32_t>(row) * row_height, .width = row_width, .height = row_height}, key, source, index == state.selected_practice_key_index);
|
||||
auto source = exercise == nullptr || exercise->key_signature.empty() ? std::string{"SOURCE C"}
|
||||
: "SOURCE " + exercise->key_signature;
|
||||
draw_row(context,
|
||||
{.x = bounds.x + 36.0F,
|
||||
.y = list_y + static_cast<std::float32_t>(row) * row_height,
|
||||
.width = row_width,
|
||||
.height = row_height},
|
||||
key,
|
||||
source,
|
||||
index == state.selected_practice_key_index);
|
||||
}
|
||||
}
|
||||
|
||||
auto draw_score(luma::draw_context& context, luma::rect bounds, vamp_zero::app_state const& state, vamp_zero::score_draw_cache& score_cache) -> void {
|
||||
auto draw_score(luma::draw_context &context, luma::rect bounds, vamp_zero::app_state const &state, vamp_zero::score_draw_cache &score_cache) -> void {
|
||||
auto source_json = std::string_view{};
|
||||
auto source_path = std::string{};
|
||||
auto part_override = std::string{};
|
||||
if (state.mode == vamp_zero::app_mode::PERFORMANCE_SCORE) {
|
||||
auto const* part = active_part(state);
|
||||
auto const *part = active_part(state);
|
||||
if (part == nullptr) {
|
||||
draw_performance_song_menu(context, bounds, state);
|
||||
return;
|
||||
@@ -339,12 +458,12 @@ auto draw_score(luma::draw_context& context, luma::rect bounds, vamp_zero::app_s
|
||||
source_json = part->source_json;
|
||||
source_path = part->source_path;
|
||||
} else {
|
||||
auto const* exercise = active_practice(state);
|
||||
auto const *exercise = active_practice(state);
|
||||
if (exercise == nullptr) {
|
||||
draw_practice_type_menu(context, bounds, state);
|
||||
return;
|
||||
}
|
||||
auto const* variant = selected_practice_variant(state, *exercise);
|
||||
auto const *variant = selected_practice_variant(state, *exercise);
|
||||
if (variant == nullptr) {
|
||||
draw_practice_key_menu(context, bounds, state);
|
||||
return;
|
||||
@@ -353,7 +472,6 @@ auto draw_score(luma::draw_context& context, luma::rect bounds, vamp_zero::app_s
|
||||
source_path = variant->source_path;
|
||||
part_override = "Key: " + effective_practice_key(state, *exercise);
|
||||
}
|
||||
|
||||
auto parsed = score_cache.get(source_json, source_path, std::move(part_override));
|
||||
if (!parsed) {
|
||||
draw_text(context, bounds.x + 36.0F, bounds.y + 38.0F, "SCORE ERROR", 4, vamp_style::danger, 360.0F);
|
||||
@@ -371,12 +489,12 @@ auto draw_score(luma::draw_context& context, luma::rect bounds, vamp_zero::app_s
|
||||
|
||||
class vamp_root_control final : public luma::control {
|
||||
public:
|
||||
explicit vamp_root_control(vamp_zero::ui_state state, std::shared_ptr<vamp_zero::score_draw_cache> score_cache, touch_handler on_touch)
|
||||
: state_{std::move(state)}, score_cache_{std::move(score_cache)}, on_touch_{std::move(on_touch)} {}
|
||||
auto measure(luma::draw_context&, luma::theme const&, luma::constraints limits) -> luma::size override {
|
||||
explicit vamp_root_control(vamp_zero::ui_state state, std::shared_ptr<vamp_zero::score_draw_cache> score_cache, std::function<std::bool_t(luma::touch_event const &)> on_touch)
|
||||
: state_{std::move(state)}, score_cache_{std::move(score_cache)}, on_touch_{std::move(on_touch)} {}
|
||||
auto measure(luma::draw_context &, luma::theme const &, luma::constraints limits) -> luma::size override {
|
||||
return luma::clamp({.width = 1024.0F, .height = 600.0F}, limits);
|
||||
}
|
||||
auto draw(luma::draw_context& context, luma::theme const&) const -> void override {
|
||||
auto draw(luma::draw_context &context, luma::theme const &) const -> void override {
|
||||
auto area = bounds();
|
||||
context.fill_rect(area, vamp_zero::to_luma(vamp_style::background));
|
||||
draw_header(context, area, state_);
|
||||
@@ -400,7 +518,8 @@ public:
|
||||
draw_practice_exercise_menu(context, content, state_.app);
|
||||
} else if (state_.app.mode == vamp_zero::app_mode::PRACTICE_KEY_MENU) {
|
||||
draw_practice_key_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 ||
|
||||
state_.app.mode == vamp_zero::app_mode::PRACTICE_SCORE) {
|
||||
if (score_cache_) {
|
||||
draw_score(context, content, state_.app, *score_cache_);
|
||||
}
|
||||
@@ -408,31 +527,26 @@ public:
|
||||
draw_main_menu(context, content, state_.app);
|
||||
}
|
||||
}
|
||||
auto handle_touch(luma::touch_event const& event) -> std::bool_t override {
|
||||
auto handle_touch(luma::touch_event const &event) -> std::bool_t override {
|
||||
return on_touch_ ? on_touch_(event) : false;
|
||||
}
|
||||
private:
|
||||
vamp_zero::ui_state state_{};
|
||||
std::shared_ptr<vamp_zero::score_draw_cache> score_cache_{};
|
||||
touch_handler on_touch_{};
|
||||
std::function<std::bool_t(luma::touch_event const &)> on_touch_{};
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
vamp_zero::zero_ui::zero_ui(boost::asio::any_io_executor executor, luma::application_config config, vamp_zero::zero_ui::command_handler command_handler)
|
||||
: executor_{std::move(executor)},
|
||||
tap_timer_{executor_},
|
||||
application_{std::move(config)},
|
||||
command_handler_{std::move(command_handler)},
|
||||
score_cache_{std::make_shared<vamp_zero::score_draw_cache>()} {}
|
||||
vamp_zero::zero_ui::zero_ui(boost::asio::any_io_executor executor, luma::application_config config, std::function<void(vamp_protocol::page_command)> command_handler)
|
||||
: executor_{std::move(executor)}, tap_timer_{executor_}, application_{std::move(config)},
|
||||
command_handler_{std::move(command_handler)}, score_cache_{std::make_shared<vamp_zero::score_draw_cache>()} {}
|
||||
|
||||
auto vamp_zero::zero_ui::send_touch_command(vamp_protocol::page_command command) -> void {
|
||||
if (!command_handler_) {
|
||||
return;
|
||||
}
|
||||
boost::asio::post(executor_, [handler = command_handler_, command] {
|
||||
handler(command);
|
||||
});
|
||||
boost::asio::post(executor_, [handler = command_handler_, command] { handler(command); });
|
||||
}
|
||||
|
||||
auto vamp_zero::zero_ui::cancel_pending_tap_locked() -> void {
|
||||
@@ -441,7 +555,7 @@ auto vamp_zero::zero_ui::cancel_pending_tap_locked() -> void {
|
||||
|
||||
auto vamp_zero::zero_ui::arm_pending_tap() -> void {
|
||||
tap_timer_.expires_after(touch_double_tap_duration);
|
||||
tap_timer_.async_wait([this](boost::system::error_code const& error) {
|
||||
tap_timer_.async_wait([this](boost::system::error_code const &error) {
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
@@ -457,17 +571,16 @@ auto vamp_zero::zero_ui::arm_pending_tap() -> void {
|
||||
});
|
||||
}
|
||||
|
||||
auto vamp_zero::zero_ui::handle_touch(luma::touch_event const& event) -> std::bool_t {
|
||||
auto vamp_zero::zero_ui::handle_touch(luma::touch_event const &event) -> std::bool_t {
|
||||
if (!command_handler_ || event.points.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto consumed = std::bool_t{false};
|
||||
auto commands = std::vector<vamp_protocol::page_command>{};
|
||||
auto arm_tap = std::bool_t{false};
|
||||
{
|
||||
auto lock = std::scoped_lock{touch_mutex_};
|
||||
for (auto const& point : event.points) {
|
||||
for (auto const &point : event.points) {
|
||||
if (point.phase == luma::touch_phase::DOWN && !active_touch_id_) {
|
||||
active_touch_id_ = point.id;
|
||||
touch_down_time_ = event.time;
|
||||
@@ -515,7 +628,6 @@ auto vamp_zero::zero_ui::handle_touch(luma::touch_event const& event) -> std::bo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (auto command : commands) {
|
||||
send_touch_command(command);
|
||||
}
|
||||
@@ -561,13 +673,12 @@ auto vamp_zero::zero_ui::snapshot() const -> vamp_zero::ui_state {
|
||||
return state_;
|
||||
}
|
||||
|
||||
auto vamp_zero::zero_ui::make_root(vamp_zero::ui_state const& state) -> std::expected<std::unique_ptr<luma::control>, modulus::error> {
|
||||
return std::make_unique<vamp_root_control>(state, score_cache_, [this](luma::touch_event const& event) {
|
||||
return handle_touch(event);
|
||||
});
|
||||
auto vamp_zero::zero_ui::make_root(vamp_zero::ui_state const &state) -> std::expected<std::unique_ptr<luma::control>, modulus::error> {
|
||||
return std::make_unique<vamp_root_control>(
|
||||
state, score_cache_, [this](luma::touch_event const &event) { return handle_touch(event); });
|
||||
}
|
||||
|
||||
auto vamp_zero::make_ui_state(vamp_zero::config const& config, vamp_zero::app_state app) -> vamp_zero::ui_state {
|
||||
auto vamp_zero::make_ui_state(vamp_zero::config const &config, vamp_zero::app_state app) -> vamp_zero::ui_state {
|
||||
return vamp_zero::ui_state{
|
||||
.app = std::move(app),
|
||||
.ap_ssid = config.control.ap_ssid,
|
||||
|
||||
Reference in New Issue
Block a user