@@ -0,0 +1,15 @@
|
|||||||
|
set(CMAKE_HOST_SYSTEM "Linux-7.1.9-arch1-2")
|
||||||
|
set(CMAKE_HOST_SYSTEM_NAME "Linux")
|
||||||
|
set(CMAKE_HOST_SYSTEM_VERSION "7.1.9-arch1-2")
|
||||||
|
set(CMAKE_HOST_SYSTEM_PROCESSOR "x86_64")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
set(CMAKE_SYSTEM "Linux-7.1.9-arch1-2")
|
||||||
|
set(CMAKE_SYSTEM_NAME "Linux")
|
||||||
|
set(CMAKE_SYSTEM_VERSION "7.1.9-arch1-2")
|
||||||
|
set(CMAKE_SYSTEM_PROCESSOR "x86_64")
|
||||||
|
|
||||||
|
set(CMAKE_CROSSCOMPILING "FALSE")
|
||||||
|
|
||||||
|
set(CMAKE_SYSTEM_LOADED 1)
|
||||||
+4
-1
@@ -5,11 +5,12 @@ set(CMAKE_CXX_STANDARD 23)
|
|||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||||
|
|
||||||
find_package(modulus CONFIG REQUIRED)
|
find_package(modulus CONFIG REQUIRED COMPONENTS boost_json openssl)
|
||||||
find_package(Threads REQUIRED)
|
find_package(Threads REQUIRED)
|
||||||
find_package(ligo CONFIG REQUIRED)
|
find_package(ligo CONFIG REQUIRED)
|
||||||
find_package(noto CONFIG REQUIRED)
|
find_package(noto CONFIG REQUIRED)
|
||||||
find_package(luma CONFIG REQUIRED)
|
find_package(luma CONFIG REQUIRED)
|
||||||
|
find_package(sonus CONFIG REQUIRED)
|
||||||
find_package(Python3 COMPONENTS Interpreter REQUIRED)
|
find_package(Python3 COMPONENTS Interpreter REQUIRED)
|
||||||
|
|
||||||
execute_process(
|
execute_process(
|
||||||
@@ -52,6 +53,7 @@ target_link_libraries(vamp-protocol PUBLIC
|
|||||||
|
|
||||||
set(VAMP_CORE_SOURCES
|
set(VAMP_CORE_SOURCES
|
||||||
src/vamp_zero/app.cpp
|
src/vamp_zero/app.cpp
|
||||||
|
src/vamp_zero/audio.cpp
|
||||||
src/vamp_zero/color.cpp
|
src/vamp_zero/color.cpp
|
||||||
src/vamp_zero/config.cpp
|
src/vamp_zero/config.cpp
|
||||||
src/vamp_zero/database.cpp
|
src/vamp_zero/database.cpp
|
||||||
@@ -84,6 +86,7 @@ target_link_libraries(vamp-zero-core PUBLIC
|
|||||||
ligo::ligo
|
ligo::ligo
|
||||||
noto::noto
|
noto::noto
|
||||||
luma::luma
|
luma::luma
|
||||||
|
sonus::sonus
|
||||||
)
|
)
|
||||||
|
|
||||||
add_executable(vamp-zero src/vamp_zero/main.cpp)
|
add_executable(vamp-zero src/vamp_zero/main.cpp)
|
||||||
|
|||||||
@@ -57,11 +57,17 @@ class product_vamp_conan(conan.ConanFile):
|
|||||||
str(self.python_requires["modulus"].ref),
|
str(self.python_requires["modulus"].ref),
|
||||||
transitive_headers=True,
|
transitive_headers=True,
|
||||||
transitive_libs=True,
|
transitive_libs=True,
|
||||||
|
options={
|
||||||
|
"with_openssl": True,
|
||||||
|
"with_sqlite": True,
|
||||||
|
"with_raylib": True,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
for reference in (
|
for reference in (
|
||||||
"ligo/latest",
|
"ligo/latest",
|
||||||
"noto/latest",
|
"noto/latest",
|
||||||
"luma/latest",
|
"luma/latest",
|
||||||
|
"sonus/latest",
|
||||||
):
|
):
|
||||||
self.requires(
|
self.requires(
|
||||||
reference,
|
reference,
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <expected>
|
||||||
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
|
#include <modulus.hpp>
|
||||||
|
#include <sonus.hpp>
|
||||||
|
|
||||||
|
#include <vamp_zero/config.hpp>
|
||||||
|
#include <vamp_zero/types.hpp>
|
||||||
|
|
||||||
|
namespace vamp_zero {
|
||||||
|
|
||||||
|
class audio_controller {
|
||||||
|
public:
|
||||||
|
audio_controller(vamp_zero::audio_config config, sonus::engine engine);
|
||||||
|
audio_controller(vamp_zero::audio_controller const&) = delete;
|
||||||
|
auto operator=(vamp_zero::audio_controller const&) -> vamp_zero::audio_controller& = delete;
|
||||||
|
~audio_controller();
|
||||||
|
|
||||||
|
auto sync(vamp_zero::app_state const& state) -> void;
|
||||||
|
|
||||||
|
private:
|
||||||
|
auto make_metronome_config(vamp_zero::app_state const& state) const -> sonus::metronome_config;
|
||||||
|
|
||||||
|
vamp_zero::audio_config config_{};
|
||||||
|
sonus::engine engine_;
|
||||||
|
mutable std::mutex mutex_{};
|
||||||
|
std::bool_t metronome_running_{false};
|
||||||
|
std::uint32_t last_metronome_bpm_{};
|
||||||
|
};
|
||||||
|
|
||||||
|
auto create_audio_controller(vamp_zero::audio_config const& config) -> std::expected<std::unique_ptr<vamp_zero::audio_controller>, modulus::error>;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
|
#include <cstdint>
|
||||||
#include <expected>
|
#include <expected>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
@@ -44,11 +45,25 @@ struct control_config {
|
|||||||
std::string zero_cidr{"192.168.42.1/24"};
|
std::string zero_cidr{"192.168.42.1/24"};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct audio_config {
|
||||||
|
std::bool_t enabled{true};
|
||||||
|
std::bool_t track_enabled{false};
|
||||||
|
std::bool_t metronome_enabled{false};
|
||||||
|
std::uint32_t metronome_bpm{120};
|
||||||
|
std::uint32_t metronome_min_bpm{40};
|
||||||
|
std::uint32_t metronome_max_bpm{240};
|
||||||
|
std::uint32_t metronome_step{5};
|
||||||
|
std::uint32_t beats_per_bar{4};
|
||||||
|
std::float32_t master_volume{0.70F};
|
||||||
|
std::float32_t metronome_volume{0.45F};
|
||||||
|
};
|
||||||
|
|
||||||
struct config {
|
struct config {
|
||||||
modulus::config modulus{};
|
modulus::config modulus{};
|
||||||
vamp_zero::runtime_config runtime{};
|
vamp_zero::runtime_config runtime{};
|
||||||
vamp_zero::database_config database{};
|
vamp_zero::database_config database{};
|
||||||
vamp_zero::control_config control{};
|
vamp_zero::control_config control{};
|
||||||
|
vamp_zero::audio_config audio{};
|
||||||
luma::application_config display{.title = "Product Vamp"};
|
luma::application_config display{.title = "Product Vamp"};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -142,10 +142,21 @@ enum class app_mode {
|
|||||||
PRACTICE_SCORE,
|
PRACTICE_SCORE,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct audio_state {
|
||||||
|
std::bool_t track_enabled{false};
|
||||||
|
std::bool_t metronome_enabled{false};
|
||||||
|
std::uint32_t metronome_bpm{120};
|
||||||
|
std::uint32_t metronome_min_bpm{40};
|
||||||
|
std::uint32_t metronome_max_bpm{240};
|
||||||
|
std::uint32_t metronome_step{5};
|
||||||
|
std::bool_t tempo_editing{false};
|
||||||
|
};
|
||||||
|
|
||||||
struct app_state {
|
struct app_state {
|
||||||
vamp_zero::library_state performance{};
|
vamp_zero::library_state performance{};
|
||||||
vamp_zero::practice_library practice{};
|
vamp_zero::practice_library practice{};
|
||||||
vamp_zero::app_mode mode{vamp_zero::app_mode::MAIN_MENU};
|
vamp_zero::app_mode mode{vamp_zero::app_mode::MAIN_MENU};
|
||||||
|
vamp_zero::audio_state audio{};
|
||||||
std::size_t selected_main_menu_index{};
|
std::size_t selected_main_menu_index{};
|
||||||
std::size_t selected_song_index{};
|
std::size_t selected_song_index{};
|
||||||
std::size_t selected_part_index{};
|
std::size_t selected_part_index{};
|
||||||
|
|||||||
@@ -36,6 +36,18 @@
|
|||||||
"zero_ip": "192.168.42.1",
|
"zero_ip": "192.168.42.1",
|
||||||
"zero_cidr": "192.168.42.1/24"
|
"zero_cidr": "192.168.42.1/24"
|
||||||
},
|
},
|
||||||
|
"audio": {
|
||||||
|
"enabled": true,
|
||||||
|
"track_enabled": false,
|
||||||
|
"metronome_enabled": false,
|
||||||
|
"metronome_bpm": 120,
|
||||||
|
"metronome_min_bpm": 40,
|
||||||
|
"metronome_max_bpm": 240,
|
||||||
|
"metronome_step": 5,
|
||||||
|
"beats_per_bar": 4,
|
||||||
|
"master_volume": 0.7,
|
||||||
|
"metronome_volume": 0.45
|
||||||
|
},
|
||||||
"display": {
|
"display": {
|
||||||
"title": "Product Vamp",
|
"title": "Product Vamp",
|
||||||
"display": {
|
"display": {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ ENV DEBIAN_FRONTEND=noninteractive
|
|||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y --no-install-recommends \
|
&& apt-get install -y --no-install-recommends \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
|
libasound2t64 \
|
||||||
libdrm2 \
|
libdrm2 \
|
||||||
libegl1 \
|
libegl1 \
|
||||||
libgbm1 \
|
libgbm1 \
|
||||||
|
|||||||
+35
-6
@@ -10,6 +10,7 @@
|
|||||||
#include <future>
|
#include <future>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <iterator>
|
#include <iterator>
|
||||||
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
@@ -27,6 +28,7 @@
|
|||||||
#include <modulus.hpp>
|
#include <modulus.hpp>
|
||||||
|
|
||||||
#include <vamp_zero/config.hpp>
|
#include <vamp_zero/config.hpp>
|
||||||
|
#include <vamp_zero/audio.hpp>
|
||||||
#include <vamp_zero/database.hpp>
|
#include <vamp_zero/database.hpp>
|
||||||
#include <vamp_zero/errors.hpp>
|
#include <vamp_zero/errors.hpp>
|
||||||
#include <vamp_zero/library.hpp>
|
#include <vamp_zero/library.hpp>
|
||||||
@@ -129,6 +131,14 @@ auto load_initial_state(vamp_zero::config const& config) -> std::expected<vamp_z
|
|||||||
auto state = vamp_zero::app_state{
|
auto state = vamp_zero::app_state{
|
||||||
.performance = std::move(*performance),
|
.performance = std::move(*performance),
|
||||||
.practice = std::move(*practice),
|
.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,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
if (auto valid = vamp_zero::validate_app_state(state); !valid) {
|
if (auto valid = vamp_zero::validate_app_state(state); !valid) {
|
||||||
return std::unexpected(valid.error());
|
return std::unexpected(valid.error());
|
||||||
@@ -136,7 +146,7 @@ auto load_initial_state(vamp_zero::config const& config) -> std::expected<vamp_z
|
|||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto run_headless(vamp_zero::config const& config, vamp_zero::control_state& state) -> 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 io = boost::asio::io_context{};
|
||||||
auto endpoint = make_server_endpoint(config);
|
auto endpoint = make_server_endpoint(config);
|
||||||
if (!endpoint) {
|
if (!endpoint) {
|
||||||
@@ -147,6 +157,12 @@ auto run_headless(vamp_zero::config const& config, vamp_zero::control_state& sta
|
|||||||
if (!started) {
|
if (!started) {
|
||||||
return std::unexpected(started.error());
|
return std::unexpected(started.error());
|
||||||
}
|
}
|
||||||
|
if (audio != nullptr) {
|
||||||
|
state.set_observer([audio](vamp_zero::app_state app) {
|
||||||
|
audio->sync(app);
|
||||||
|
});
|
||||||
|
audio->sync(state.snapshot());
|
||||||
|
}
|
||||||
auto heartbeat = write_heartbeat(config);
|
auto heartbeat = write_heartbeat(config);
|
||||||
if (!heartbeat) {
|
if (!heartbeat) {
|
||||||
server.stop();
|
server.stop();
|
||||||
@@ -162,7 +178,7 @@ auto run_headless(vamp_zero::config const& config, vamp_zero::control_state& sta
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
auto run_display(vamp_zero::config const& config, vamp_zero::control_state& state) -> 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 io = boost::asio::io_context{};
|
||||||
auto endpoint = make_server_endpoint(config);
|
auto endpoint = make_server_endpoint(config);
|
||||||
if (!endpoint) {
|
if (!endpoint) {
|
||||||
@@ -171,12 +187,18 @@ auto run_display(vamp_zero::config const& config, vamp_zero::control_state& stat
|
|||||||
auto ui = vamp_zero::zero_ui{io.get_executor(), config.display, [&state](vamp_protocol::page_command command) {
|
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](vamp_zero::app_state app) {
|
state.set_observer([&ui, &config, audio](vamp_zero::app_state app) {
|
||||||
|
if (audio != nullptr) {
|
||||||
|
audio->sync(app);
|
||||||
|
}
|
||||||
auto status = ui.publish(vamp_zero::make_ui_state(config, std::move(app)));
|
auto status = ui.publish(vamp_zero::make_ui_state(config, std::move(app)));
|
||||||
if (!status) {
|
if (!status) {
|
||||||
modulus::log_error("failed to publish UI state: " + status.error().describe());
|
modulus::log_error("failed to publish UI state: " + status.error().describe());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
if (audio != nullptr) {
|
||||||
|
audio->sync(state.snapshot());
|
||||||
|
}
|
||||||
auto published = ui.publish(vamp_zero::make_ui_state(config, state.snapshot()));
|
auto published = ui.publish(vamp_zero::make_ui_state(config, state.snapshot()));
|
||||||
if (!published) {
|
if (!published) {
|
||||||
return std::unexpected(published.error());
|
return std::unexpected(published.error());
|
||||||
@@ -316,8 +338,15 @@ auto vamp_zero::run(vamp_zero::command_line command) -> std::expected<void, modu
|
|||||||
return std::unexpected(initial.error());
|
return std::unexpected(initial.error());
|
||||||
}
|
}
|
||||||
auto state = vamp_zero::control_state{*config, std::move(*initial)};
|
auto state = vamp_zero::control_state{*config, std::move(*initial)};
|
||||||
if (config->runtime.headless) {
|
auto audio_controller = std::unique_ptr<vamp_zero::audio_controller>{};
|
||||||
return run_headless(*config, state);
|
auto audio = vamp_zero::create_audio_controller(config->audio);
|
||||||
|
if (!audio) {
|
||||||
|
modulus::log_error("audio unavailable: " + audio.error().describe());
|
||||||
|
} else {
|
||||||
|
audio_controller = std::move(*audio);
|
||||||
}
|
}
|
||||||
return run_display(*config, state);
|
if (config->runtime.headless) {
|
||||||
|
return run_headless(*config, state, audio_controller.get());
|
||||||
|
}
|
||||||
|
return run_display(*config, state, audio_controller.get());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
#include <vamp_zero/audio.hpp>
|
||||||
|
|
||||||
|
#include <expected>
|
||||||
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include <modulus.hpp>
|
||||||
|
#include <sonus.hpp>
|
||||||
|
|
||||||
|
#include <vamp_zero/navigation.hpp>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
auto score_active(vamp_zero::app_state const& state) -> std::bool_t {
|
||||||
|
if (state.mode == vamp_zero::app_mode::PERFORMANCE_SCORE) {
|
||||||
|
return vamp_zero::active_part(state) != nullptr;
|
||||||
|
}
|
||||||
|
if (state.mode == vamp_zero::app_mode::PRACTICE_SCORE) {
|
||||||
|
auto const* exercise = vamp_zero::active_practice(state);
|
||||||
|
return exercise != nullptr && vamp_zero::selected_practice_variant(state, *exercise) != nullptr;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
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)} {}
|
||||||
|
|
||||||
|
vamp_zero::audio_controller::~audio_controller() {
|
||||||
|
auto lock = std::scoped_lock{mutex_};
|
||||||
|
engine_.stop_metronome();
|
||||||
|
engine_.stop_all();
|
||||||
|
engine_.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
result.volume = config_.metronome_volume;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
if (metronome_running_) {
|
||||||
|
engine_.stop_metronome();
|
||||||
|
metronome_running_ = false;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto metronome = make_metronome_config(state);
|
||||||
|
if (!metronome_running_) {
|
||||||
|
auto started = engine_.start_metronome(metronome);
|
||||||
|
if (!started) {
|
||||||
|
log_audio_error("failed to start metronome", started.error());
|
||||||
|
engine_.stop_metronome();
|
||||||
|
metronome_running_ = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
metronome_running_ = true;
|
||||||
|
last_metronome_bpm_ = metronome.bpm;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (last_metronome_bpm_ != metronome.bpm) {
|
||||||
|
auto updated = engine_.update_metronome(metronome);
|
||||||
|
if (!updated) {
|
||||||
|
log_audio_error("failed to update metronome", updated.error());
|
||||||
|
engine_.stop_metronome();
|
||||||
|
metronome_running_ = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
last_metronome_bpm_ = metronome.bpm;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
auto started = engine->start();
|
||||||
|
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/config.hpp>
|
#include <vamp_zero/config.hpp>
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <expected>
|
#include <expected>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
@@ -68,6 +69,50 @@ auto optional_bool(boost::json::object const& object, std::string_view field, st
|
|||||||
return found->value().as_bool();
|
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 found = object.find(field);
|
||||||
|
if (found == object.end()) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
auto value = std::uint64_t{};
|
||||||
|
if (found->value().is_uint64()) {
|
||||||
|
value = found->value().as_uint64();
|
||||||
|
} else if (found->value().is_int64()) {
|
||||||
|
auto signed_value = found->value().as_int64();
|
||||||
|
if (signed_value < 0) {
|
||||||
|
return std::unexpected(invalid(std::string{field} + " must not be negative"));
|
||||||
|
}
|
||||||
|
value = static_cast<std::uint64_t>(signed_value);
|
||||||
|
} else {
|
||||||
|
return std::unexpected(invalid(std::string{field} + " must be an integer"));
|
||||||
|
}
|
||||||
|
if (value < min_value || value > max_value) {
|
||||||
|
return std::unexpected(invalid(std::string{field} + " is out of range"));
|
||||||
|
}
|
||||||
|
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 found = object.find(field);
|
||||||
|
if (found == object.end()) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
auto value = std::float64_t{};
|
||||||
|
if (found->value().is_double()) {
|
||||||
|
value = found->value().as_double();
|
||||||
|
} else if (found->value().is_uint64()) {
|
||||||
|
value = static_cast<std::float64_t>(found->value().as_uint64());
|
||||||
|
} else if (found->value().is_int64()) {
|
||||||
|
value = static_cast<std::float64_t>(found->value().as_int64());
|
||||||
|
} else {
|
||||||
|
return std::unexpected(invalid(std::string{field} + " must be a number"));
|
||||||
|
}
|
||||||
|
if (!std::isfinite(value) || value < min_value || value > max_value) {
|
||||||
|
return std::unexpected(invalid(std::string{field} + " is out of range"));
|
||||||
|
}
|
||||||
|
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") {
|
if (value == "fake") {
|
||||||
return vamp_zero::hardware_mode::FAKE;
|
return vamp_zero::hardware_mode::FAKE;
|
||||||
@@ -162,6 +207,55 @@ auto parse_control(boost::json::object const& root, vamp_zero::control_config fa
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
if (*object == nullptr) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
auto enabled = optional_bool(**object, "enabled", fallback.enabled);
|
||||||
|
auto track_enabled = optional_bool(**object, "track_enabled", fallback.track_enabled);
|
||||||
|
auto metronome_enabled = optional_bool(**object, "metronome_enabled", fallback.metronome_enabled);
|
||||||
|
auto metronome_bpm = optional_uint32(**object, "metronome_bpm", fallback.metronome_bpm, 1U, 400U);
|
||||||
|
auto metronome_min_bpm = optional_uint32(**object, "metronome_min_bpm", fallback.metronome_min_bpm, 1U, 400U);
|
||||||
|
auto metronome_max_bpm = optional_uint32(**object, "metronome_max_bpm", fallback.metronome_max_bpm, 1U, 400U);
|
||||||
|
auto metronome_step = optional_uint32(**object, "metronome_step", fallback.metronome_step, 1U, 100U);
|
||||||
|
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 (*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"));
|
||||||
|
}
|
||||||
|
if (*metronome_bpm < *metronome_min_bpm || *metronome_bpm > *metronome_max_bpm) {
|
||||||
|
return std::unexpected(invalid("audio.metronome_bpm must be within the configured metronome range"));
|
||||||
|
}
|
||||||
|
return vamp_zero::audio_config{
|
||||||
|
.enabled = *enabled,
|
||||||
|
.track_enabled = *track_enabled,
|
||||||
|
.metronome_enabled = *metronome_enabled,
|
||||||
|
.metronome_bpm = *metronome_bpm,
|
||||||
|
.metronome_min_bpm = *metronome_min_bpm,
|
||||||
|
.metronome_max_bpm = *metronome_max_bpm,
|
||||||
|
.metronome_step = *metronome_step,
|
||||||
|
.beats_per_bar = *beats_per_bar,
|
||||||
|
.master_volume = *master_volume,
|
||||||
|
.metronome_volume = *metronome_volume,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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> {
|
||||||
@@ -180,12 +274,14 @@ auto vamp_zero::parse_config(boost::json::value const& value) -> std::expected<v
|
|||||||
auto runtime = parse_runtime(**root, result.runtime);
|
auto runtime = parse_runtime(**root, result.runtime);
|
||||||
auto database = parse_database(**root, result.database);
|
auto database = parse_database(**root, result.database);
|
||||||
auto control = parse_control(**root, result.control);
|
auto control = parse_control(**root, result.control);
|
||||||
if (!runtime || !database || !control) {
|
auto audio = parse_audio(**root, result.audio);
|
||||||
return std::unexpected(!runtime ? runtime.error() : (!database ? database.error() : control.error()));
|
if (!runtime || !database || !control || !audio) {
|
||||||
|
return std::unexpected(!runtime ? runtime.error() : (!database ? database.error() : (!control ? control.error() : audio.error())));
|
||||||
}
|
}
|
||||||
result.runtime = std::move(*runtime);
|
result.runtime = std::move(*runtime);
|
||||||
result.database = std::move(*database);
|
result.database = std::move(*database);
|
||||||
result.control = std::move(*control);
|
result.control = std::move(*control);
|
||||||
|
result.audio = std::move(*audio);
|
||||||
if (auto found = (*root)->find("display"); found != (*root)->end()) {
|
if (auto found = (*root)->find("display"); found != (*root)->end()) {
|
||||||
auto display = luma::parse_application_config(found->value());
|
auto display = luma::parse_application_config(found->value());
|
||||||
if (!display) {
|
if (!display) {
|
||||||
|
|||||||
@@ -18,7 +18,12 @@
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
constexpr auto main_menu_count = std::size_t{2};
|
constexpr auto main_menu_performance = std::size_t{0};
|
||||||
|
constexpr auto main_menu_practice = std::size_t{1};
|
||||||
|
constexpr auto main_menu_track = std::size_t{2};
|
||||||
|
constexpr auto main_menu_metronome = std::size_t{3};
|
||||||
|
constexpr auto main_menu_tempo = std::size_t{4};
|
||||||
|
constexpr auto main_menu_count = std::size_t{5};
|
||||||
|
|
||||||
auto wrap_next(std::size_t value, std::size_t count) -> std::size_t {
|
auto wrap_next(std::size_t value, std::size_t count) -> std::size_t {
|
||||||
if (count == 0U) {
|
if (count == 0U) {
|
||||||
@@ -38,7 +43,28 @@ auto menu_row_count(std::size_t item_count) -> std::size_t {
|
|||||||
return item_count + 1U;
|
return item_count + 1U;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto clamp_audio_state(vamp_zero::audio_state audio) -> vamp_zero::audio_state {
|
||||||
|
audio.metronome_min_bpm = std::clamp<std::uint32_t>(audio.metronome_min_bpm, 1U, 400U);
|
||||||
|
audio.metronome_max_bpm = std::clamp<std::uint32_t>(audio.metronome_max_bpm, audio.metronome_min_bpm, 400U);
|
||||||
|
audio.metronome_step = std::clamp<std::uint32_t>(audio.metronome_step, 1U, 100U);
|
||||||
|
audio.metronome_bpm = std::clamp(audio.metronome_bpm, audio.metronome_min_bpm, audio.metronome_max_bpm);
|
||||||
|
return audio;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto increase_bpm(vamp_zero::audio_state& audio) -> void {
|
||||||
|
audio.metronome_bpm = audio.metronome_bpm > audio.metronome_max_bpm - std::min(audio.metronome_step, audio.metronome_max_bpm)
|
||||||
|
? audio.metronome_max_bpm
|
||||||
|
: audio.metronome_bpm + audio.metronome_step;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto decrease_bpm(vamp_zero::audio_state& audio) -> void {
|
||||||
|
audio.metronome_bpm = audio.metronome_bpm < audio.metronome_min_bpm + audio.metronome_step
|
||||||
|
? audio.metronome_min_bpm
|
||||||
|
: audio.metronome_bpm - audio.metronome_step;
|
||||||
|
}
|
||||||
|
|
||||||
auto clamp_state(vamp_zero::app_state state) -> vamp_zero::app_state {
|
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_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);
|
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) {
|
||||||
@@ -96,6 +122,10 @@ auto clamp_state(vamp_zero::app_state state) -> vamp_zero::app_state {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (state.mode != vamp_zero::app_mode::MAIN_MENU) {
|
||||||
|
state.audio.tempo_editing = false;
|
||||||
|
}
|
||||||
|
|
||||||
if (auto pages = active_pages(state); pages != 0U) {
|
if (auto pages = active_pages(state); pages != 0U) {
|
||||||
state.page_index = std::min(state.page_index, pages - 1U);
|
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) {
|
||||||
@@ -114,6 +144,18 @@ auto command_json(vamp_zero::app_state const& state) -> boost::json::object {
|
|||||||
return commands;
|
return commands;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
audio["metronome_bpm"] = static_cast<std::uint64_t>(state.audio.metronome_bpm);
|
||||||
|
audio["metronome_min_bpm"] = static_cast<std::uint64_t>(state.audio.metronome_min_bpm);
|
||||||
|
audio["metronome_max_bpm"] = static_cast<std::uint64_t>(state.audio.metronome_max_bpm);
|
||||||
|
audio["metronome_step"] = static_cast<std::uint64_t>(state.audio.metronome_step);
|
||||||
|
audio["tempo_editing"] = state.audio.tempo_editing;
|
||||||
|
return audio;
|
||||||
|
}
|
||||||
|
|
||||||
auto part_json(vamp_zero::library_part const& part) -> boost::json::object {
|
auto part_json(vamp_zero::library_part const& part) -> boost::json::object {
|
||||||
auto result = boost::json::object{};
|
auto result = boost::json::object{};
|
||||||
result["id"] = part.id;
|
result["id"] = part.id;
|
||||||
@@ -291,17 +333,25 @@ auto vamp_zero::apply_app_command(vamp_zero::app_state state, vamp_protocol::pag
|
|||||||
|
|
||||||
switch (state.mode) {
|
switch (state.mode) {
|
||||||
case vamp_zero::app_mode::MAIN_MENU:
|
case vamp_zero::app_mode::MAIN_MENU:
|
||||||
if (command == vamp_protocol::page_command::NEXT) {
|
if (state.audio.tempo_editing) {
|
||||||
|
if (command == vamp_protocol::page_command::NEXT) {
|
||||||
|
increase_bpm(state.audio);
|
||||||
|
} else if (command == vamp_protocol::page_command::PREVIOUS) {
|
||||||
|
decrease_bpm(state.audio);
|
||||||
|
} else {
|
||||||
|
state.audio.tempo_editing = false;
|
||||||
|
}
|
||||||
|
} else if (command == vamp_protocol::page_command::NEXT) {
|
||||||
state.selected_main_menu_index = wrap_next(state.selected_main_menu_index, main_menu_count);
|
state.selected_main_menu_index = wrap_next(state.selected_main_menu_index, main_menu_count);
|
||||||
} else if (command == vamp_protocol::page_command::PREVIOUS) {
|
} else if (command == vamp_protocol::page_command::PREVIOUS) {
|
||||||
state.selected_main_menu_index = wrap_previous(state.selected_main_menu_index, main_menu_count);
|
state.selected_main_menu_index = wrap_previous(state.selected_main_menu_index, main_menu_count);
|
||||||
} else if (state.selected_main_menu_index == 0U) {
|
} else if (state.selected_main_menu_index == main_menu_performance) {
|
||||||
state.mode = vamp_zero::app_mode::PERFORMANCE_SONG_MENU;
|
state.mode = vamp_zero::app_mode::PERFORMANCE_SONG_MENU;
|
||||||
state.selected_song_index = 0;
|
state.selected_song_index = 0;
|
||||||
state.selected_part_index = 0;
|
state.selected_part_index = 0;
|
||||||
state.active_part_id.clear();
|
state.active_part_id.clear();
|
||||||
state.page_index = 0;
|
state.page_index = 0;
|
||||||
} else {
|
} else if (state.selected_main_menu_index == main_menu_practice) {
|
||||||
state.mode = vamp_zero::app_mode::PRACTICE_TYPE_MENU;
|
state.mode = vamp_zero::app_mode::PRACTICE_TYPE_MENU;
|
||||||
state.selected_practice_type_index = 0;
|
state.selected_practice_type_index = 0;
|
||||||
state.selected_practice_lesson_index = 0;
|
state.selected_practice_lesson_index = 0;
|
||||||
@@ -309,6 +359,12 @@ auto vamp_zero::apply_app_command(vamp_zero::app_state state, vamp_protocol::pag
|
|||||||
state.selected_practice_key_index = 0;
|
state.selected_practice_key_index = 0;
|
||||||
state.active_practice_id.clear();
|
state.active_practice_id.clear();
|
||||||
state.page_index = 0;
|
state.page_index = 0;
|
||||||
|
} else if (state.selected_main_menu_index == main_menu_track) {
|
||||||
|
state.audio.track_enabled = !state.audio.track_enabled;
|
||||||
|
} else if (state.selected_main_menu_index == main_menu_metronome) {
|
||||||
|
state.audio.metronome_enabled = !state.audio.metronome_enabled;
|
||||||
|
} else if (state.selected_main_menu_index == main_menu_tempo) {
|
||||||
|
state.audio.tempo_editing = true;
|
||||||
}
|
}
|
||||||
return clamp_state(std::move(state));
|
return clamp_state(std::move(state));
|
||||||
|
|
||||||
@@ -523,6 +579,7 @@ auto vamp_zero::app_status_json(vamp_zero::app_state const& state) -> boost::jso
|
|||||||
root["practice"] = practice_json(state);
|
root["practice"] = practice_json(state);
|
||||||
root["selection"] = std::move(selection);
|
root["selection"] = std::move(selection);
|
||||||
root["active"] = std::move(active);
|
root["active"] = std::move(active);
|
||||||
|
root["audio"] = audio_json(state);
|
||||||
root["commands"] = command_json(state);
|
root["commands"] = command_json(state);
|
||||||
root["remote"] = std::move(remote);
|
root["remote"] = std::move(remote);
|
||||||
return root;
|
return root;
|
||||||
@@ -567,6 +624,21 @@ auto vamp_zero::validate_app_state(vamp_zero::app_state const& state) -> std::ex
|
|||||||
if (state.selected_main_menu_index >= main_menu_count) {
|
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"));
|
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, "selected main menu index is out of range"));
|
||||||
}
|
}
|
||||||
|
if (state.audio.metronome_min_bpm == 0U || state.audio.metronome_min_bpm > 400U) {
|
||||||
|
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, "metronome minimum BPM is out of range"));
|
||||||
|
}
|
||||||
|
if (state.audio.metronome_max_bpm < state.audio.metronome_min_bpm || state.audio.metronome_max_bpm > 400U) {
|
||||||
|
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, "metronome maximum BPM is out of range"));
|
||||||
|
}
|
||||||
|
if (state.audio.metronome_step == 0U || state.audio.metronome_step > 100U) {
|
||||||
|
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, "metronome BPM step is out of range"));
|
||||||
|
}
|
||||||
|
if (state.audio.metronome_bpm < state.audio.metronome_min_bpm || state.audio.metronome_bpm > state.audio.metronome_max_bpm) {
|
||||||
|
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, "metronome BPM is out of range"));
|
||||||
|
}
|
||||||
|
if (state.audio.tempo_editing && state.mode != vamp_zero::app_mode::MAIN_MENU) {
|
||||||
|
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, "tempo edit mode requires the main menu"));
|
||||||
|
}
|
||||||
if (state.selected_song_index >= menu_row_count(state.performance.groups.size())) {
|
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"));
|
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, "selected performance song row is out of range"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -150,6 +150,13 @@ auto draw_main_menu(luma::draw_context& context, luma::rect bounds, vamp_zero::a
|
|||||||
auto row_width = bounds.width - 72.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, .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, .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);
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
|
|||||||
+184
@@ -22,12 +22,17 @@
|
|||||||
#include <boost/asio/co_spawn.hpp>
|
#include <boost/asio/co_spawn.hpp>
|
||||||
#include <boost/asio/io_context.hpp>
|
#include <boost/asio/io_context.hpp>
|
||||||
#include <boost/asio/use_future.hpp>
|
#include <boost/asio/use_future.hpp>
|
||||||
|
#include <boost/json/object.hpp>
|
||||||
|
#include <boost/json/parse.hpp>
|
||||||
|
|
||||||
#include <noto.hpp>
|
#include <noto.hpp>
|
||||||
|
|
||||||
|
#include <vamp_protocol/page_control.hpp>
|
||||||
|
#include <vamp_zero/config.hpp>
|
||||||
#include <vamp_pico/http_client.hpp>
|
#include <vamp_pico/http_client.hpp>
|
||||||
#include <vamp_zero/database.hpp>
|
#include <vamp_zero/database.hpp>
|
||||||
#include <vamp_zero/library.hpp>
|
#include <vamp_zero/library.hpp>
|
||||||
|
#include <vamp_zero/page_control.hpp>
|
||||||
#include <vamp_zero/score.hpp>
|
#include <vamp_zero/score.hpp>
|
||||||
#include <vamp_zero/types.hpp>
|
#include <vamp_zero/types.hpp>
|
||||||
|
|
||||||
@@ -311,6 +316,65 @@ auto score_asset_json(std::string title, std::string key = "C") -> std::string {
|
|||||||
)json";
|
)json";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto navigation_state() -> vamp_zero::app_state {
|
||||||
|
auto part = vamp_zero::library_part{
|
||||||
|
.id = "part-lead",
|
||||||
|
.title = "Navigation Tune",
|
||||||
|
.composer = "Composer",
|
||||||
|
.key_signature = "C",
|
||||||
|
.part = "Lead",
|
||||||
|
.source_path = "navigation.json",
|
||||||
|
.source_json = score_asset_json("Navigation Tune"),
|
||||||
|
.measure_count = 1,
|
||||||
|
.total_pages = 2,
|
||||||
|
};
|
||||||
|
auto variant = vamp_zero::practice_exercise::score_variant{
|
||||||
|
.id = "exercise-c",
|
||||||
|
.key_signature = "C",
|
||||||
|
.source_path = "exercise.json",
|
||||||
|
.source_json = score_asset_json("Exercise"),
|
||||||
|
.measure_count = 1,
|
||||||
|
.total_pages = 1,
|
||||||
|
};
|
||||||
|
auto exercise = vamp_zero::practice_exercise{
|
||||||
|
.id = "exercise-1",
|
||||||
|
.practice_type_id = "scale",
|
||||||
|
.practice_type_title = "Scales",
|
||||||
|
.lesson_group_id = "lesson-1",
|
||||||
|
.lesson_group_title = "Major",
|
||||||
|
.title = "C Major",
|
||||||
|
.composer = "Composer",
|
||||||
|
.key_signature = "C",
|
||||||
|
.source_path = "exercise.json",
|
||||||
|
.source_json = score_asset_json("Exercise"),
|
||||||
|
.measure_count = 1,
|
||||||
|
.total_pages = 1,
|
||||||
|
.variants = {variant},
|
||||||
|
};
|
||||||
|
return vamp_zero::app_state{
|
||||||
|
.performance = {
|
||||||
|
.parts = {part},
|
||||||
|
.groups = {{
|
||||||
|
.title = part.title,
|
||||||
|
.composer = part.composer,
|
||||||
|
.parts = {part},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
.practice = {
|
||||||
|
.exercises = {exercise},
|
||||||
|
.types = {{
|
||||||
|
.id = "scale",
|
||||||
|
.title = "Scales",
|
||||||
|
.lesson_groups = {{
|
||||||
|
.id = "lesson-1",
|
||||||
|
.title = "Major",
|
||||||
|
.exercises = {exercise},
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
void write_text(std::filesystem::path const& path, std::string const& text) {
|
void write_text(std::filesystem::path const& path, std::string const& text) {
|
||||||
std::filesystem::create_directories(path.parent_path());
|
std::filesystem::create_directories(path.parent_path());
|
||||||
auto output = std::ofstream{path, std::ios::binary | std::ios::trunc};
|
auto output = std::ofstream{path, std::ios::binary | std::ios::trunc};
|
||||||
@@ -319,6 +383,126 @@ void write_text(std::filesystem::path const& path, std::string const& text) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_CASE(product_vamp_audio_config_parses_defaults_and_values) {
|
||||||
|
auto defaults = vamp_zero::parse_config(boost::json::parse(R"json({})json"));
|
||||||
|
BOOST_REQUIRE(defaults);
|
||||||
|
BOOST_TEST(defaults->audio.enabled);
|
||||||
|
BOOST_TEST(!defaults->audio.track_enabled);
|
||||||
|
BOOST_TEST(!defaults->audio.metronome_enabled);
|
||||||
|
BOOST_TEST(defaults->audio.metronome_bpm == 120U);
|
||||||
|
BOOST_TEST(defaults->audio.metronome_min_bpm == 40U);
|
||||||
|
BOOST_TEST(defaults->audio.metronome_max_bpm == 240U);
|
||||||
|
BOOST_TEST(defaults->audio.metronome_step == 5U);
|
||||||
|
|
||||||
|
auto parsed = vamp_zero::parse_config(boost::json::parse(R"json({
|
||||||
|
"audio": {
|
||||||
|
"enabled": false,
|
||||||
|
"track_enabled": true,
|
||||||
|
"metronome_enabled": true,
|
||||||
|
"metronome_bpm": 90,
|
||||||
|
"metronome_min_bpm": 50,
|
||||||
|
"metronome_max_bpm": 180,
|
||||||
|
"metronome_step": 10,
|
||||||
|
"beats_per_bar": 3,
|
||||||
|
"master_volume": 0.5,
|
||||||
|
"metronome_volume": 0.25
|
||||||
|
}
|
||||||
|
})json"));
|
||||||
|
BOOST_REQUIRE(parsed);
|
||||||
|
BOOST_TEST(!parsed->audio.enabled);
|
||||||
|
BOOST_TEST(parsed->audio.track_enabled);
|
||||||
|
BOOST_TEST(parsed->audio.metronome_enabled);
|
||||||
|
BOOST_TEST(parsed->audio.metronome_bpm == 90U);
|
||||||
|
BOOST_TEST(parsed->audio.metronome_min_bpm == 50U);
|
||||||
|
BOOST_TEST(parsed->audio.metronome_max_bpm == 180U);
|
||||||
|
BOOST_TEST(parsed->audio.metronome_step == 10U);
|
||||||
|
BOOST_TEST(parsed->audio.beats_per_bar == 3U);
|
||||||
|
BOOST_TEST(parsed->audio.master_volume == 0.5F);
|
||||||
|
BOOST_TEST(parsed->audio.metronome_volume == 0.25F);
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_CASE(product_vamp_audio_config_rejects_invalid_tempo_range) {
|
||||||
|
auto parsed = vamp_zero::parse_config(boost::json::parse(R"json({
|
||||||
|
"audio": {
|
||||||
|
"metronome_bpm": 120,
|
||||||
|
"metronome_min_bpm": 160,
|
||||||
|
"metronome_max_bpm": 80
|
||||||
|
}
|
||||||
|
})json"));
|
||||||
|
BOOST_TEST(!parsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_CASE(main_menu_toggles_audio_settings_and_edits_tempo) {
|
||||||
|
auto state = navigation_state();
|
||||||
|
|
||||||
|
state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::NEXT);
|
||||||
|
state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::NEXT);
|
||||||
|
BOOST_TEST(state.selected_main_menu_index == 2U);
|
||||||
|
state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::SELECT);
|
||||||
|
BOOST_TEST(state.audio.track_enabled);
|
||||||
|
BOOST_TEST(static_cast<int>(state.mode) == static_cast<int>(vamp_zero::app_mode::MAIN_MENU));
|
||||||
|
|
||||||
|
state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::NEXT);
|
||||||
|
BOOST_TEST(state.selected_main_menu_index == 3U);
|
||||||
|
state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::SELECT);
|
||||||
|
BOOST_TEST(state.audio.metronome_enabled);
|
||||||
|
BOOST_TEST(static_cast<int>(state.mode) == static_cast<int>(vamp_zero::app_mode::MAIN_MENU));
|
||||||
|
|
||||||
|
state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::NEXT);
|
||||||
|
BOOST_TEST(state.selected_main_menu_index == 4U);
|
||||||
|
state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::SELECT);
|
||||||
|
BOOST_TEST(state.audio.tempo_editing);
|
||||||
|
state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::NEXT);
|
||||||
|
BOOST_TEST(state.audio.metronome_bpm == 125U);
|
||||||
|
state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::PREVIOUS);
|
||||||
|
BOOST_TEST(state.audio.metronome_bpm == 120U);
|
||||||
|
for (auto index = 0; index < 40; ++index) {
|
||||||
|
state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::PREVIOUS);
|
||||||
|
}
|
||||||
|
BOOST_TEST(state.audio.metronome_bpm == 40U);
|
||||||
|
for (auto index = 0; index < 80; ++index) {
|
||||||
|
state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::NEXT);
|
||||||
|
}
|
||||||
|
BOOST_TEST(state.audio.metronome_bpm == 240U);
|
||||||
|
state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::SELECT);
|
||||||
|
BOOST_TEST(!state.audio.tempo_editing);
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_CASE(audio_status_json_reports_menu_settings) {
|
||||||
|
auto state = navigation_state();
|
||||||
|
state.audio.track_enabled = true;
|
||||||
|
state.audio.metronome_enabled = true;
|
||||||
|
state.audio.metronome_bpm = 135;
|
||||||
|
state.audio.tempo_editing = true;
|
||||||
|
|
||||||
|
auto status = vamp_zero::app_status_json(state).as_object();
|
||||||
|
auto const& audio = status.at("audio").as_object();
|
||||||
|
BOOST_TEST(audio.at("track_enabled").as_bool());
|
||||||
|
BOOST_TEST(audio.at("metronome_enabled").as_bool());
|
||||||
|
BOOST_TEST(audio.at("metronome_bpm").as_uint64() == 135U);
|
||||||
|
BOOST_TEST(audio.at("metronome_min_bpm").as_uint64() == 40U);
|
||||||
|
BOOST_TEST(audio.at("metronome_max_bpm").as_uint64() == 240U);
|
||||||
|
BOOST_TEST(audio.at("metronome_step").as_uint64() == 5U);
|
||||||
|
BOOST_TEST(audio.at("tempo_editing").as_bool());
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_CASE(main_menu_metronome_setting_survives_score_selection) {
|
||||||
|
auto state = navigation_state();
|
||||||
|
state.audio.metronome_enabled = true;
|
||||||
|
|
||||||
|
state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::SELECT);
|
||||||
|
BOOST_TEST(static_cast<int>(state.mode) == static_cast<int>(vamp_zero::app_mode::PERFORMANCE_SONG_MENU));
|
||||||
|
state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::NEXT);
|
||||||
|
state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::SELECT);
|
||||||
|
BOOST_TEST(static_cast<int>(state.mode) == static_cast<int>(vamp_zero::app_mode::PERFORMANCE_PART_MENU));
|
||||||
|
state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::NEXT);
|
||||||
|
state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::SELECT);
|
||||||
|
|
||||||
|
BOOST_TEST(static_cast<int>(state.mode) == static_cast<int>(vamp_zero::app_mode::PERFORMANCE_SCORE));
|
||||||
|
BOOST_TEST(state.audio.metronome_enabled);
|
||||||
|
BOOST_TEST(!state.audio.tempo_editing);
|
||||||
|
}
|
||||||
|
|
||||||
BOOST_AUTO_TEST_CASE(score_parser_preserves_notation_preamble) {
|
BOOST_AUTO_TEST_CASE(score_parser_preserves_notation_preamble) {
|
||||||
auto parsed = vamp_zero::parse_score_document(R"json(
|
auto parsed = vamp_zero::parse_score_document(R"json(
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user