JSON Score
ci / checks (push) Successful in 39m41s

This commit is contained in:
khodowany
2026-09-07 21:12:22 -05:00
parent 9a3ec7caa5
commit 900604b9fb
15 changed files with 1065 additions and 5634 deletions
+6
View File
@@ -38,6 +38,10 @@ auto render(vamp_pico::display& display, link_status status, vamp_pico::remote_d
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 {
remote = vamp_pico::remote_display_state{};
}
auto connect_wifi(vamp_pico::display& display) -> std::bool_t {
auto empty = vamp_pico::remote_display_state{};
render(display, link_status::CONNECTING, empty);
@@ -58,6 +62,7 @@ auto connect_wifi(vamp_pico::display& display) -> std::bool_t {
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);
render(display, status, remote);
return;
}
@@ -136,6 +141,7 @@ auto main() -> int {
status = link_status::CONNECTED;
} else {
status = link_status::RETRYING;
clear_remote(remote);
}
render(display, status, remote);
}
+3 -1
View File
@@ -168,7 +168,9 @@ auto run_display(vamp_zero::config const& config, vamp_zero::control_state& stat
if (!endpoint) {
return std::unexpected(endpoint.error());
}
auto ui = vamp_zero::zero_ui{io.get_executor(), config.display};
auto ui = vamp_zero::zero_ui{io.get_executor(), config.display, [&state](vamp_protocol::page_command command) {
static_cast<void>(state.apply(command));
}};
state.set_observer([&ui, &config](vamp_zero::app_state app) {
auto status = ui.publish(vamp_zero::make_ui_state(config, std::move(app)));
if (!status) {
+247 -28
View File
@@ -86,10 +86,15 @@ constexpr auto tie_thickness_spaces = std::float32_t{0.13F};
constexpr auto normal_stem_length_spaces = std::float32_t{3.50F};
constexpr auto preamble_vertical_correction_spaces = std::float32_t{0.50F};
constexpr auto stem_head_horizontal_overlap_spaces = std::float32_t{0.06F};
constexpr auto stem_head_vertical_overlap_spaces = std::float32_t{0.20F};
constexpr auto measure_right_guard_spaces = std::float32_t{1.50F};
constexpr auto stem_head_vertical_overlap_spaces = std::float32_t{0.38F};
constexpr auto measure_right_guard_spaces = std::float32_t{2.05F};
constexpr auto chord_above_staff_spaces = std::float32_t{3.65F};
constexpr auto chord_clearance_spaces = std::float32_t{0.45F};
constexpr auto lyric_below_staff_spaces = std::float32_t{6.15F};
constexpr auto lyric_clearance_spaces = std::float32_t{0.75F};
constexpr auto tie_endpoint_gap_spaces = std::float32_t{0.28F};
constexpr auto tie_note_offset_spaces = std::float32_t{0.50F};
constexpr auto tie_arch_spaces = std::float32_t{0.72F};
auto to_raylib(luma::color color) -> Color {
return {
@@ -832,6 +837,26 @@ struct beam_group_layout {
std::float32_t beam_y{};
};
struct laid_out_tie_head {
vamp_zero::score_pitch pitch{};
luma::rect bounds{};
std::float32_t center_y{};
};
struct laid_out_note_ref {
std::size_t system_index{};
std::size_t measure_index{};
std::size_t note_index{};
luma::rect notation_bounds{};
std::bool_t stem_up{true};
std::vector<laid_out_tie_head> heads{};
};
struct laid_out_measure_ref {
std::size_t measure_index{};
luma::rect notation_bounds{};
};
auto layout_staff(
vamp_zero::score_page_layout& layout,
std::float32_t x,
@@ -1679,8 +1704,6 @@ auto layout_note_symbols(
note_layout const& note,
std::float32_t staff_top,
std::float32_t spacing,
std::float32_t next_x,
std::float32_t measure_right_x,
std::size_t system_index,
std::size_t measure_index
) -> void {
@@ -1714,25 +1737,6 @@ auto layout_note_symbols(
note.note_index);
}
}
if (note.source != nullptr && note.source->tie_to_next) {
auto tie_y = note.low_y + spacing * 0.62F;
auto tie_start_x = note.visual_right - spacing * 0.45F;
auto tie_end_x = std::min(next_x - spacing * 0.45F, measure_right_x - spacing * 0.30F);
if (tie_end_x <= tie_start_x + spacing * 0.50F) {
return;
}
add_curve(
layout,
vamp_zero::score_layout_item_role::TIE,
{.x = tie_start_x, .y = tie_y},
{.x = (tie_start_x + tie_end_x) * 0.5F, .y = tie_y + spacing * 0.78F},
{.x = tie_end_x, .y = tie_y},
at_least_one_pixel(scaled(tie_thickness_spaces, spacing)),
score_ink(),
system_index,
measure_index,
note.note_index);
}
}
auto note_x(std::float32_t start_x, std::float32_t width, std::int64_t total_ticks, std::int64_t elapsed_ticks, std::int64_t duration_ticks) -> std::float32_t {
@@ -1837,6 +1841,23 @@ auto measure_notation_top_y(
return top_y;
}
auto measure_notation_bottom_y(std::vector<note_layout> const& notes, std::vector<beam_group_layout> const& beam_groups, std::float32_t staff_top, std::float32_t spacing) -> std::float32_t {
auto bottom_y = staff_top + spacing * 4.0F;
for (auto const& note : notes) {
if (!note.hidden) {
bottom_y = std::max(bottom_y, note.visual_bottom);
}
}
for (auto const& group : beam_groups) {
bottom_y = std::max(bottom_y, group.beam_y + scaled(beam_thickness_spaces, spacing) * 0.5F);
}
return bottom_y;
}
auto lyric_y(std::float32_t staff_top, std::float32_t spacing, std::float32_t notation_bottom_y) -> std::float32_t {
return std::max(staff_top + scaled(lyric_below_staff_spaces, spacing), notation_bottom_y + scaled(lyric_clearance_spaces, spacing));
}
auto chord_symbol_y(std::float32_t staff_top, std::float32_t spacing, std::float32_t notation_top_y) -> std::float32_t {
auto y = staff_top - scaled(chord_above_staff_spaces, spacing);
auto const bottom_limit = notation_top_y - scaled(chord_clearance_spaces, spacing);
@@ -1846,6 +1867,189 @@ auto chord_symbol_y(std::float32_t staff_top, std::float32_t spacing, std::float
return y;
}
auto same_pitch(vamp_zero::score_pitch const& left, vamp_zero::score_pitch const& right) -> std::bool_t {
return normalized_letter(left.letter) == normalized_letter(right.letter)
&& left.octave == right.octave
&& pitch_alteration(left.accidental) == pitch_alteration(right.accidental);
}
auto make_laid_out_note_ref(note_layout const& note, luma::rect notation_bounds, std::size_t system_index, std::size_t measure_index) -> laid_out_note_ref {
auto ref = laid_out_note_ref{
.system_index = system_index,
.measure_index = measure_index,
.note_index = note.note_index,
.notation_bounds = notation_bounds,
.stem_up = note.stem_up,
};
if (note.source == nullptr || note.hidden || note.rest) {
return ref;
}
ref.heads.reserve(note.heads.size());
for (auto const& head : note.heads) {
if (head.source_pitch_index >= note.source->pitches.size()) {
continue;
}
ref.heads.push_back({
.pitch = note.source->pitches[head.source_pitch_index],
.bounds = head.bounds,
.center_y = head.center_y,
});
}
return ref;
}
auto find_note_ref(std::vector<laid_out_note_ref> const& refs, std::size_t measure_index, std::size_t note_index) -> laid_out_note_ref const* {
auto found = std::find_if(refs.begin(), refs.end(), [&](auto const& ref) {
return ref.measure_index == measure_index && ref.note_index == note_index;
});
return found == refs.end() ? nullptr : &*found;
}
auto find_measure_ref(std::vector<laid_out_measure_ref> const& refs, std::size_t measure_index) -> laid_out_measure_ref const* {
auto found = std::find_if(refs.begin(), refs.end(), [&](auto const& ref) {
return ref.measure_index == measure_index;
});
return found == refs.end() ? nullptr : &*found;
}
auto find_tie_head(laid_out_note_ref const& ref, vamp_zero::score_pitch const& pitch) -> laid_out_tie_head const* {
auto found = std::find_if(ref.heads.begin(), ref.heads.end(), [&](auto const& head) {
return same_pitch(head.pitch, pitch);
});
return found == ref.heads.end() ? nullptr : &*found;
}
struct tied_note_destination {
std::size_t measure_index{};
std::size_t note_index{};
};
auto find_tied_destination(vamp_zero::score_document const& score, std::size_t measure_index, std::size_t note_index, vamp_zero::score_pitch const& pitch) -> std::optional<tied_note_destination> {
for (auto current_measure = measure_index; current_measure < score.measures.size(); ++current_measure) {
auto const& measure = score.measures[current_measure];
auto first_note = current_measure == measure_index ? note_index + 1U : 0U;
for (auto current_note = first_note; current_note < measure.notes.size(); ++current_note) {
auto const& note = measure.notes[current_note];
if (note.hidden || note.kind == "rest") {
continue;
}
auto matching_pitch = std::find_if(note.pitches.begin(), note.pitches.end(), [&](auto const& candidate) {
return same_pitch(candidate, pitch);
});
if (matching_pitch == note.pitches.end()) {
return std::nullopt;
}
if (note.tie_from_previous) {
return tied_note_destination{.measure_index = current_measure, .note_index = current_note};
}
return std::nullopt;
}
}
return std::nullopt;
}
auto add_tie_curve(
vamp_zero::score_page_layout& layout,
luma::point from,
luma::point to,
std::bool_t below_note,
std::float32_t spacing,
std::size_t system_index,
std::size_t measure_index,
std::size_t note_index
) -> void {
if (to.x <= from.x + spacing * 0.40F) {
return;
}
auto direction = below_note ? std::float32_t{1.0F} : std::float32_t{-1.0F};
auto control = luma::point{
.x = (from.x + to.x) * 0.5F,
.y = (from.y + to.y) * 0.5F + direction * scaled(tie_arch_spaces, spacing),
};
add_curve(
layout,
vamp_zero::score_layout_item_role::TIE,
from,
control,
to,
at_least_one_pixel(scaled(tie_thickness_spaces, spacing)),
score_ink(),
system_index,
measure_index,
note_index);
}
auto layout_page_ties(
vamp_zero::score_page_layout& layout,
vamp_zero::score_document const& score,
std::vector<laid_out_note_ref> const& note_refs,
std::vector<laid_out_measure_ref> const& measure_refs,
std::float32_t spacing
) -> void {
for (auto measure_index = std::size_t{}; measure_index < score.measures.size(); ++measure_index) {
auto const& measure = score.measures[measure_index];
for (auto note_index = std::size_t{}; note_index < measure.notes.size(); ++note_index) {
auto const& note = measure.notes[note_index];
if (!note.tie_to_next || note.hidden || note.kind == "rest") {
continue;
}
auto const* start_ref = find_note_ref(note_refs, measure_index, note_index);
for (auto const& pitch : note.pitches) {
auto destination = find_tied_destination(score, measure_index, note_index, pitch);
auto const* destination_ref = destination ? find_note_ref(note_refs, destination->measure_index, destination->note_index) : nullptr;
auto const* start_head = start_ref == nullptr ? nullptr : find_tie_head(*start_ref, pitch);
auto const* destination_head = destination_ref == nullptr ? nullptr : find_tie_head(*destination_ref, pitch);
if (start_ref != nullptr && start_head != nullptr && destination_ref != nullptr && destination_head != nullptr && start_ref->system_index == destination_ref->system_index) {
auto below = start_ref->stem_up;
auto y_offset = scaled(tie_note_offset_spaces, spacing) * (below ? 1.0F : -1.0F);
add_tie_curve(
layout,
{.x = start_head->bounds.x + start_head->bounds.width + scaled(tie_endpoint_gap_spaces, spacing), .y = start_head->center_y + y_offset},
{.x = destination_head->bounds.x - scaled(tie_endpoint_gap_spaces, spacing), .y = destination_head->center_y + y_offset},
below,
spacing,
start_ref->system_index,
start_ref->measure_index,
start_ref->note_index);
continue;
}
if (start_ref != nullptr && start_head != nullptr) {
auto below = start_ref->stem_up;
auto y_offset = scaled(tie_note_offset_spaces, spacing) * (below ? 1.0F : -1.0F);
add_tie_curve(
layout,
{.x = start_head->bounds.x + start_head->bounds.width + scaled(tie_endpoint_gap_spaces, spacing), .y = start_head->center_y + y_offset},
{.x = start_ref->notation_bounds.x + start_ref->notation_bounds.width, .y = start_head->center_y + y_offset},
below,
spacing,
start_ref->system_index,
start_ref->measure_index,
start_ref->note_index);
}
if (destination && destination_ref != nullptr && destination_head != nullptr) {
auto const* measure_ref = find_measure_ref(measure_refs, destination->measure_index);
if (measure_ref == nullptr) {
continue;
}
auto below = destination_ref->stem_up;
auto y_offset = scaled(tie_note_offset_spaces, spacing) * (below ? 1.0F : -1.0F);
add_tie_curve(
layout,
{.x = measure_ref->notation_bounds.x, .y = destination_head->center_y + y_offset},
{.x = destination_head->bounds.x - scaled(tie_endpoint_gap_spaces, spacing), .y = destination_head->center_y + y_offset},
below,
spacing,
destination_ref->system_index,
destination_ref->measure_index,
destination_ref->note_index);
}
}
}
}
}
auto layout_measure(
vamp_zero::score_page_layout& layout,
vamp_zero::score_document const& score,
@@ -1855,7 +2059,9 @@ auto layout_measure(
std::size_t system_index,
std::size_t measure_index,
std::float32_t staff_top,
std::float32_t spacing
std::float32_t spacing,
std::vector<laid_out_note_ref>& note_refs,
std::vector<laid_out_measure_ref>& measure_refs
) -> void {
add_text(layout, vamp_zero::score_layout_item_role::MEASURE_NUMBER, bounds.x + spacing * 0.20F, bounds.y + spacing * 0.05F, std::to_string(measure_number), 1, score_muted_ink(), spacing * 4.0F, system_index, measure_index);
@@ -1863,6 +2069,10 @@ auto layout_measure(
auto notation_right_x = bounds.x + bounds.width - scaled(measure_right_guard_spaces, spacing);
auto notation_width = std::max<std::float32_t>(spacing * 3.0F, notation_right_x - notation_x);
auto notation_bounds = luma::rect{.x = notation_x, .y = bounds.y, .width = notation_width, .height = bounds.height};
measure_refs.push_back({
.measure_index = measure_index,
.notation_bounds = notation_bounds,
});
auto total_ticks = measure_ticks(measure);
auto elapsed = std::int64_t{};
auto accidentals = make_accidental_state(effective_key_signature(score, measure));
@@ -1889,7 +2099,12 @@ auto layout_measure(
}
auto const notation_top_y = measure_notation_top_y(measure, notes, beam_groups, staff_top, spacing);
auto const notation_bottom_y = measure_notation_bottom_y(notes, beam_groups, staff_top, spacing);
auto const chord_y = chord_symbol_y(staff_top, spacing, notation_top_y);
auto const lyrics_y = lyric_y(staff_top, spacing, notation_bottom_y);
for (auto const& note : notes) {
note_refs.push_back(make_laid_out_note_ref(note, notation_bounds, system_index, measure_index));
}
if (!measure.section_label.empty()) {
auto section_y = std::min(staff_top - spacing * 4.70F, chord_y - spacing * 0.65F);
add_text(layout, vamp_zero::score_layout_item_role::SECTION_LABEL, bounds.x + spacing * 2.10F, section_y, measure.section_label, 2, score_accent_ink(), bounds.width - spacing * 2.6F, system_index, measure_index);
@@ -1948,14 +2163,13 @@ auto layout_measure(
add_flag(layout, note, spacing, system_index, measure_index);
}
for (auto index = std::size_t{}; index < notes.size(); ++index) {
auto next_x = index + 1U < notes.size() ? notes[index + 1U].x : notes[index].x + spacing * 3.0F;
layout_note_symbols(layout, notes[index], staff_top, spacing, next_x, notation_bounds.x + notation_bounds.width, system_index, measure_index);
layout_note_symbols(layout, notes[index], staff_top, spacing, system_index, measure_index);
if (notes[index].source != nullptr && !notes[index].hidden && !notes[index].source->lyric.empty()) {
add_text(
layout,
vamp_zero::score_layout_item_role::LYRIC,
notes[index].x - spacing,
staff_top + spacing * 5.35F,
lyrics_y,
notes[index].source->lyric,
1,
score_muted_ink(),
@@ -2027,6 +2241,8 @@ auto vamp_zero::layout_score_page(luma::rect bounds, vamp_zero::score_document c
auto const content_width = std::max<std::float32_t>(spacing * 18.0F, bounds.width - margin * 2.0F);
auto const system_height = (bounds.height - 108.0F) / static_cast<std::float32_t>(vamp_zero::score_systems_per_page);
auto const first_measure = page_index * vamp_zero::score_measures_per_page;
auto note_refs = std::vector<laid_out_note_ref>{};
auto measure_refs = std::vector<laid_out_measure_ref>{};
for (auto system = std::size_t{}; system < vamp_zero::score_systems_per_page; ++system) {
auto const system_y = content_y + static_cast<std::float32_t>(system) * system_height;
@@ -2052,7 +2268,9 @@ auto vamp_zero::layout_score_page(luma::rect bounds, vamp_zero::score_document c
system,
measure_index,
staff_top,
spacing);
spacing,
note_refs,
measure_refs);
}
auto const bar_x = x + measure_width;
@@ -2069,6 +2287,7 @@ auto vamp_zero::layout_score_page(luma::rect bounds, vamp_zero::score_document c
}
}
layout_page_ties(layout, score, note_refs, measure_refs, spacing);
add_stroke_rect(layout, vamp_zero::score_layout_item_role::BORDER, bounds, 1.1F, score_border());
return layout;
}
+116 -3
View File
@@ -11,6 +11,7 @@
#include <utility>
#include <boost/asio/any_io_executor.hpp>
#include <boost/asio/post.hpp>
#include <luma.hpp>
@@ -22,6 +23,11 @@
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 colon = bind.rfind(':');
@@ -404,7 +410,7 @@ 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) : state_{std::move(state)} {}
explicit vamp_root_control(vamp_zero::ui_state state, touch_handler on_touch) : state_{std::move(state)}, 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);
}
@@ -438,13 +444,117 @@ public:
draw_main_menu(context, content, state_.app);
}
}
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_{};
touch_handler on_touch_{};
};
}
vamp_zero::zero_ui::zero_ui(boost::asio::any_io_executor executor, luma::application_config config) : executor_{std::move(executor)}, application_{std::move(config)} {}
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)} {}
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);
});
}
auto vamp_zero::zero_ui::cancel_pending_tap_locked() -> void {
pending_tap_ = false;
}
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) {
if (error) {
return;
}
auto send_next = false;
{
auto lock = std::scoped_lock{touch_mutex_};
send_next = pending_tap_;
pending_tap_ = false;
}
if (send_next) {
send_touch_command(vamp_protocol::page_command::NEXT);
}
});
}
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 = false;
auto commands = std::vector<vamp_protocol::page_command>{};
auto arm_tap = false;
{
auto lock = std::scoped_lock{touch_mutex_};
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;
hold_sent_ = false;
tap_timer_.cancel();
consumed = true;
continue;
}
if (!active_touch_id_ || point.id != *active_touch_id_) {
continue;
}
consumed = true;
if (point.phase == luma::touch_phase::CANCEL) {
active_touch_id_.reset();
hold_sent_ = false;
cancel_pending_tap_locked();
tap_timer_.cancel();
continue;
}
if (point.phase == luma::touch_phase::MOVE) {
if (!hold_sent_ && event.time - touch_down_time_ >= touch_hold_duration) {
hold_sent_ = true;
cancel_pending_tap_locked();
tap_timer_.cancel();
commands.push_back(vamp_protocol::page_command::SELECT);
}
continue;
}
if (point.phase == luma::touch_phase::UP) {
active_touch_id_.reset();
if (!hold_sent_ && event.time - touch_down_time_ >= touch_hold_duration) {
commands.push_back(vamp_protocol::page_command::SELECT);
} else if (!hold_sent_) {
if (pending_tap_ && event.time - last_tap_time_ <= touch_double_tap_duration) {
cancel_pending_tap_locked();
tap_timer_.cancel();
commands.push_back(vamp_protocol::page_command::PREVIOUS);
} else {
pending_tap_ = true;
last_tap_time_ = event.time;
arm_tap = true;
}
}
hold_sent_ = false;
}
}
}
for (auto command : commands) {
send_touch_command(command);
}
if (arm_tap) {
arm_pending_tap();
}
return consumed;
}
auto vamp_zero::zero_ui::run() -> std::expected<void, modulus::error> {
auto state = snapshot();
@@ -459,6 +569,7 @@ auto vamp_zero::zero_ui::run() -> std::expected<void, modulus::error> {
}
auto vamp_zero::zero_ui::stop() -> void {
tap_timer_.cancel();
application_.request_stop();
}
@@ -482,7 +593,9 @@ auto vamp_zero::zero_ui::snapshot() const -> vamp_zero::ui_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);
return std::make_unique<vamp_root_control>(state, [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 {