Cleanup
ci / checks (push) Successful in 38m56s

This commit is contained in:
khodowany
2026-09-08 08:53:41 -05:00
parent 62abb2d3f6
commit d7919f148d
21 changed files with 1322 additions and 479 deletions
+1
View File
@@ -79,6 +79,7 @@ add_executable(product-vamp-pico2w
src/vamp_pico/display.cpp src/vamp_pico/display.cpp
src/vamp_pico/http_client.cpp src/vamp_pico/http_client.cpp
src/vamp_pico/main.cpp src/vamp_pico/main.cpp
src/vamp_pico/remote_display.cpp
src/vamp_pico/touch.cpp src/vamp_pico/touch.cpp
src/vamp_protocol/page_control.cpp src/vamp_protocol/page_control.cpp
) )
+2
View File
@@ -57,6 +57,7 @@ set(VAMP_CORE_SOURCES
src/vamp_zero/database.cpp src/vamp_zero/database.cpp
src/vamp_zero/errors.cpp src/vamp_zero/errors.cpp
src/vamp_zero/library.cpp src/vamp_zero/library.cpp
src/vamp_zero/navigation.cpp
src/vamp_zero/page_control.cpp src/vamp_zero/page_control.cpp
src/vamp_zero/portal.cpp src/vamp_zero/portal.cpp
src/vamp_zero/score.cpp src/vamp_zero/score.cpp
@@ -175,6 +176,7 @@ if(VAMP_BUILD_ZERO_IMAGE)
package/migrations/002_song_import_metadata.sql package/migrations/002_song_import_metadata.sql
package/migrations/003_practice_catalog.sql package/migrations/003_practice_catalog.sql
package/migrations/004_practice_hierarchy.sql package/migrations/004_practice_hierarchy.sql
package/migrations/005_catalog_managed_rows.sql
${VAMP_GENERATED_ASSETS_STAMP} ${VAMP_GENERATED_ASSETS_STAMP}
VERBATIM VERBATIM
) )
File diff suppressed because it is too large Load Diff
+82 -15
View File
@@ -15,6 +15,12 @@ class TabPosition(NamedTuple):
midi: int midi: int
class ChordSymbol(NamedTuple):
root: str
quality: str
bass: str
ROOTS = { ROOTS = {
"C": 0, "C": 0,
"C#": 1, "C#": 1,
@@ -37,6 +43,7 @@ ROOTS = {
def parse_root(symbol: str) -> tuple[str, str] | None: def parse_root(symbol: str) -> tuple[str, str] | None:
symbol = symbol.strip()
if not symbol or symbol[0].upper() not in "ABCDEFG": if not symbol or symbol[0].upper() not in "ABCDEFG":
return None return None
length = 1 length = 1
@@ -45,36 +52,97 @@ def parse_root(symbol: str) -> tuple[str, str] | None:
return symbol[:length], symbol[length:] return symbol[:length], symbol[length:]
def parse_chord_symbol(symbol: str) -> ChordSymbol | None:
clean = symbol.strip().replace(" ", "")
base, _, slash = clean.partition("/")
parsed = parse_root(base)
if parsed is None:
return None
root, quality = parsed
bass = ""
if slash:
bass_parsed = parse_root(slash)
if bass_parsed is not None:
bass = bass_parsed[0]
return ChordSymbol(root, quality, bass)
def unique_intervals(values: tuple[int, ...]) -> tuple[int, ...]:
result: list[int] = []
seen: set[int] = set()
for value in values:
interval = mod12(value)
if interval in seen:
continue
seen.add(interval)
result.append(interval)
return tuple(result)
def color_intervals(value: str) -> tuple[int, ...]:
colors: list[int] = []
if "alt" in value:
colors.append(1)
if "b9" in value:
colors.append(1)
if "#9" in value:
colors.append(3)
if "9" in value and "b9" not in value and "#9" not in value:
colors.append(2)
if "#11" in value:
colors.append(6)
elif "11" in value:
colors.append(5)
if "b13" in value:
colors.append(8)
elif "13" in value:
colors.append(9)
return tuple(colors)
def chord_intervals(quality: str) -> tuple[int, ...]: def chord_intervals(quality: str) -> tuple[int, ...]:
value = quality.lower() value = quality.lower().replace(" ", "")
if "7b5" in value or "ø" in value or "half" in value: if "7b5" in value or "ø" in value or "half" in value:
return (0, 3, 6, 10) return (0, 3, 6, 10)
if "dim" in value or "o" in value: if "dim" in value or "o" in value:
return (0, 3, 6, 9) return (0, 3, 6, 9)
colors = color_intervals(value)
if "aug" in value or "+" in value: if "aug" in value or "+" in value:
return (0, 4, 8, 10) return unique_intervals((0, 4, 8, 10) + colors) if ("7" in value or colors) else (0, 4, 8)
if "sus" in value: if "sus" in value:
return (0, 5, 7, 10) return unique_intervals((0, 5, 10) + colors) if ("7" in value or colors) else (0, 5, 7)
if value.startswith("-") or (value.startswith("m") and not value.startswith("maj")): minor = value.startswith("-") or (value.startswith("m") and not value.startswith(("maj", "ma")))
return (0, 3, 7, 10 if "7" in value else 10) if minor:
if "maj7" in value or "ma7" in value or "Δ" in value: if "6" in value and "7" not in value:
return (0, 4, 7, 11) return unique_intervals((0, 3, 7, 9) + colors)
if "7" in value or colors:
return unique_intervals((0, 3, 10) + colors)
return (0, 3, 7)
if "maj" in value or value.startswith("ma") or "Δ" in value:
return unique_intervals((0, 4, 11) + colors)
if "6" in value and "7" not in value: if "6" in value and "7" not in value:
return (0, 4, 7, 9) return unique_intervals((0, 4, 7, 9) + colors)
if "7" in value: if "7" in value or colors:
return (0, 4, 7, 10) return unique_intervals((0, 4, 10) + colors)
return (0, 4, 7) return (0, 4, 7)
def pitch_class_for_root(root: str) -> int | None:
return ROOTS.get(root[0].upper() + root[1:], ROOTS.get(root.upper()))
def chord_pitch_classes(symbol: str) -> tuple[int, ...]: def chord_pitch_classes(symbol: str) -> tuple[int, ...]:
parsed = parse_root(symbol) parsed = parse_chord_symbol(symbol)
if parsed is None: if parsed is None:
return () return ()
root, quality = parsed root_pc = pitch_class_for_root(parsed.root)
root_pc = ROOTS.get(root[0].upper() + root[1:], ROOTS.get(root.upper()))
if root_pc is None: if root_pc is None:
return () return ()
return tuple(mod12(root_pc + interval) for interval in chord_intervals(quality)) pitch_classes = [mod12(root_pc + interval) for interval in chord_intervals(parsed.quality)]
bass_pc = pitch_class_for_root(parsed.bass) if parsed.bass else None
if bass_pc is not None and bass_pc not in pitch_classes:
pitch_classes.append(bass_pc)
return tuple(pitch_classes)
def positions_for_string(string_number: int, open_midi: int, pitch_classes: tuple[int, ...]) -> list[TabPosition]: def positions_for_string(string_number: int, open_midi: int, pitch_classes: tuple[int, ...]) -> list[TabPosition]:
@@ -148,4 +216,3 @@ def pitch_from_midi(midi: int) -> dict[str, int | str]:
def pitches_for_voicing(symbol: str) -> list[dict[str, int | str]]: def pitches_for_voicing(symbol: str) -> list[dict[str, int | str]]:
return [pitch_from_midi(position.midi + WRITTEN_GUITAR_OCTAVE_OFFSET) for position in find_playable_voicing(symbol)] return [pitch_from_midi(position.midi + WRITTEN_GUITAR_OCTAVE_OFFSET) for position in find_playable_voicing(symbol)]
+3 -3
View File
@@ -121,7 +121,7 @@ def accompaniment_notes(measure: dict, chord_symbol: str, sequence: int, *, incl
return apply_source_chord_markers(notes, measure, include_optional=include_optional), sequence return apply_source_chord_markers(notes, measure, include_optional=include_optional), sequence
def slash_notes(measure: dict, chord_symbol: str, sequence: int) -> tuple[list[dict], int]: def slash_notes(measure: dict, chord_symbol: str, sequence: int, *, include_optional: bool = True) -> tuple[list[dict], int]:
capacity = measure_capacity_ticks(str(measure.get("time_signature", "4/4"))) capacity = measure_capacity_ticks(str(measure.get("time_signature", "4/4")))
notes: list[dict] = [] notes: list[dict] = []
cursor = 0 cursor = 0
@@ -130,7 +130,7 @@ def slash_notes(measure: dict, chord_symbol: str, sequence: int) -> tuple[list[d
duration = min(QUARTER_TICKS, capacity - cursor) duration = min(QUARTER_TICKS, capacity - cursor)
notes.append(slash_note(f"arr-slash-{sequence}", duration)) notes.append(slash_note(f"arr-slash-{sequence}", duration))
cursor += duration cursor += duration
return notes, sequence return apply_source_chord_markers(notes, measure, include_optional=include_optional), sequence
def rest_notes(measure: dict, chord_symbol: str, sequence: int, *, include_optional: bool = True) -> tuple[list[dict], int]: def rest_notes(measure: dict, chord_symbol: str, sequence: int, *, include_optional: bool = True) -> tuple[list[dict], int]:
@@ -211,7 +211,7 @@ def voice_for_section(section: ArrangementSection, measure: dict, chord_symbol:
elif section.role == "accompaniment": elif section.role == "accompaniment":
notes, sequence = accompaniment_notes(measure, chord_symbol, sequence, include_optional=include_optional) notes, sequence = accompaniment_notes(measure, chord_symbol, sequence, include_optional=include_optional)
elif section.role == "slash": elif section.role == "slash":
notes, sequence = slash_notes(measure, chord_symbol, sequence) notes, sequence = slash_notes(measure, chord_symbol, sequence, include_optional=include_optional)
else: else:
notes, sequence = rest_notes(measure, chord_symbol, sequence, include_optional=include_optional) notes, sequence = rest_notes(measure, chord_symbol, sequence, include_optional=include_optional)
return {"id": "voice-arranged-guitar", "tuplets": [], "notes": notes}, sequence return {"id": "voice-arranged-guitar", "tuplets": [], "notes": notes}, sequence
+4
View File
@@ -1,5 +1,8 @@
#pragma once #pragma once
#include <cstdint>
#include <string_view>
#include <vamp_protocol/page_control.hpp> #include <vamp_protocol/page_control.hpp>
#include <modulus/types.hpp> #include <modulus/types.hpp>
@@ -14,6 +17,7 @@ struct remote_display_state {
std::uint32_t pages{}; std::uint32_t pages{};
}; };
auto parse_remote_display_body(std::string_view body, vamp_pico::remote_display_state& state) -> std::bool_t;
auto post_remote_command(vamp_protocol::page_command command, vamp_pico::remote_display_state& state) -> std::bool_t; auto post_remote_command(vamp_protocol::page_command command, vamp_pico::remote_display_state& state) -> std::bool_t;
auto heartbeat() -> std::bool_t; auto heartbeat() -> std::bool_t;
auto fetch_remote_display(vamp_pico::remote_display_state& state) -> std::bool_t; auto fetch_remote_display(vamp_pico::remote_display_state& state) -> std::bool_t;
+29
View File
@@ -0,0 +1,29 @@
#pragma once
#include <cstddef>
#include <optional>
#include <string>
#include <string_view>
#include <modulus/types.hpp>
#include <vamp_zero/types.hpp>
namespace vamp_zero {
auto selected_group(vamp_zero::app_state const& state) -> vamp_zero::song_group const*;
auto selected_part(vamp_zero::app_state const& state) -> vamp_zero::library_part const*;
auto active_part(vamp_zero::app_state const& state) -> vamp_zero::library_part const*;
auto selected_practice_type(vamp_zero::app_state const& state) -> vamp_zero::practice_type_group const*;
auto selected_practice_lesson(vamp_zero::app_state const& state) -> vamp_zero::practice_lesson_group const*;
auto selected_practice_exercise(vamp_zero::app_state const& state) -> vamp_zero::practice_exercise const*;
auto active_practice(vamp_zero::app_state const& state) -> vamp_zero::practice_exercise const*;
auto supports_key_selection(vamp_zero::practice_exercise const& exercise) -> std::bool_t;
auto selected_practice_variant(vamp_zero::app_state const& state, vamp_zero::practice_exercise const& exercise) -> vamp_zero::practice_exercise::score_variant const*;
auto selected_practice_key(vamp_zero::app_state const& state, vamp_zero::practice_exercise const& exercise) -> std::optional<std::string_view>;
auto practice_key_row_count(vamp_zero::app_state const& state) -> std::size_t;
auto effective_practice_key(vamp_zero::app_state const& state, vamp_zero::practice_exercise const& exercise) -> std::string;
auto active_pages(vamp_zero::app_state const& state) -> std::size_t;
auto practice_fallback_menu(vamp_zero::app_state const& state) -> vamp_zero::app_mode;
}
+7 -1
View File
@@ -83,11 +83,17 @@ struct score_page_layout {
std::vector<vamp_zero::score_layout_item> items{}; std::vector<vamp_zero::score_layout_item> items{};
}; };
struct score_system_range {
std::size_t first_measure{};
std::size_t measure_count{};
};
constexpr auto score_measures_per_system = std::size_t{4}; constexpr auto score_measures_per_system = std::size_t{4};
constexpr auto score_systems_per_page = std::size_t{3}; constexpr auto score_systems_per_page = std::size_t{3};
constexpr auto score_measures_per_page = score_measures_per_system * score_systems_per_page;
auto notated_accidentals_for_measure(vamp_zero::score_measure const& measure, std::string_view inherited_key_signature) -> std::vector<vamp_zero::score_accidental_mark>; auto notated_accidentals_for_measure(vamp_zero::score_measure const& measure, std::string_view inherited_key_signature) -> std::vector<vamp_zero::score_accidental_mark>;
auto score_system_ranges(vamp_zero::score_document const& score) -> std::vector<vamp_zero::score_system_range>;
auto score_page_count(vamp_zero::score_document const& score) -> std::size_t;
auto layout_score_page(luma::rect bounds, vamp_zero::score_document const& score, std::size_t page_index) -> vamp_zero::score_page_layout; auto layout_score_page(luma::rect bounds, vamp_zero::score_document const& score, std::size_t page_index) -> vamp_zero::score_page_layout;
auto draw_score_page(luma::draw_context& context, luma::rect bounds, vamp_zero::score_document const& score, std::size_t page_index) -> void; auto draw_score_page(luma::draw_context& context, luma::rect bounds, vamp_zero::score_document const& score, std::size_t page_index) -> void;
+3
View File
@@ -21,6 +21,8 @@
namespace vamp_zero { namespace vamp_zero {
struct score_draw_cache;
struct ui_state { struct ui_state {
vamp_zero::app_state app{}; vamp_zero::app_state app{};
std::string ap_ssid{}; std::string ap_ssid{};
@@ -47,6 +49,7 @@ private:
boost::asio::steady_timer tap_timer_; boost::asio::steady_timer tap_timer_;
luma::application application_; luma::application application_;
command_handler command_handler_{}; command_handler command_handler_{};
std::shared_ptr<vamp_zero::score_draw_cache> score_cache_{};
mutable std::mutex mutex_{}; mutable std::mutex mutex_{};
vamp_zero::ui_state state_{}; vamp_zero::ui_state state_{};
mutable std::mutex touch_mutex_{}; mutable std::mutex touch_mutex_{};
@@ -0,0 +1,7 @@
ALTER TABLE songs ADD COLUMN catalog_managed INTEGER NOT NULL DEFAULT 1 CHECK(catalog_managed IN (0, 1));
-- vamp-statement
ALTER TABLE practice_exercises ADD COLUMN catalog_managed INTEGER NOT NULL DEFAULT 1 CHECK(catalog_managed IN (0, 1));
-- vamp-statement
CREATE INDEX idx_songs_catalog_managed ON songs(catalog_managed);
-- vamp-statement
CREATE INDEX idx_practice_exercises_catalog_managed ON practice_exercises(catalog_managed);
+2 -57
View File
@@ -3,7 +3,6 @@
#include <algorithm> #include <algorithm>
#include <array> #include <array>
#include <cstdio> #include <cstdio>
#include <cstdlib>
#include <cstdint> #include <cstdint>
#include <cstring> #include <cstring>
@@ -198,60 +197,6 @@ auto body_start(std::char_t const* response) -> std::char_t const* {
return marker == nullptr ? response : marker + 4; return marker == nullptr ? response : marker + 4;
} }
auto copy_value(std::char_t const* begin, std::size_t size, std::char_t* output, std::size_t capacity) -> void {
if (capacity == 0U) {
return;
}
auto count = std::min(capacity - 1U, size);
std::memcpy(output, begin, count);
output[count] = '\0';
}
auto parse_uint(std::char_t const* begin, std::size_t size) -> std::uint32_t {
auto buffer = std::array<std::char_t, 16>{};
copy_value(begin, std::min<std::size_t>(size, buffer.size() - 1U), buffer.data(), buffer.size());
return static_cast<std::uint32_t>(std::strtoul(buffer.data(), nullptr, 10));
}
auto parse_display_body(std::char_t const* body, vamp_pico::remote_display_state& state) -> std::bool_t {
auto parsed = vamp_pico::remote_display_state{};
auto saw_version = std::bool_t{};
auto* cursor = body;
while (cursor != nullptr && *cursor != '\0') {
auto* line_end = std::strchr(cursor, '\n');
auto size = line_end == nullptr ? std::strlen(cursor) : static_cast<std::size_t>(line_end - cursor);
if (size != 0U && cursor[size - 1U] == '\r') {
--size;
}
auto* equals = static_cast<std::char_t const*>(std::memchr(cursor, '=', size));
if (equals != nullptr) {
auto key_size = static_cast<std::size_t>(equals - cursor);
auto* value = equals + 1;
auto value_size = size - key_size - 1U;
if (key_size == 7U && std::memcmp(cursor, "version", 7U) == 0) {
saw_version = true;
} else if (key_size == 6U && std::memcmp(cursor, "active", 6U) == 0) {
parsed.active = value_size != 0U && value[0] == '1';
} else if (key_size == 5U && std::memcmp(cursor, "title", 5U) == 0) {
copy_value(value, value_size, parsed.title, sizeof(parsed.title));
} else if (key_size == 4U && std::memcmp(cursor, "part", 4U) == 0) {
copy_value(value, value_size, parsed.part, sizeof(parsed.part));
} else if (key_size == 4U && std::memcmp(cursor, "mode", 4U) == 0) {
copy_value(value, value_size, parsed.mode, sizeof(parsed.mode));
} else if (key_size == 4U && std::memcmp(cursor, "page", 4U) == 0) {
parsed.page = parse_uint(value, value_size);
} else if (key_size == 5U && std::memcmp(cursor, "pages", 5U) == 0) {
parsed.pages = parse_uint(value, value_size);
}
}
cursor = line_end == nullptr ? nullptr : line_end + 1;
}
if (saw_version) {
state = parsed;
}
return saw_version;
}
} }
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 {
@@ -259,7 +204,7 @@ auto vamp_pico::post_remote_command(vamp_protocol::page_command command, vamp_pi
if (!run_request("POST", vamp_protocol::page_command_path(command), "{}", response.data(), response.size(), 700)) { if (!run_request("POST", vamp_protocol::page_command_path(command), "{}", response.data(), response.size(), 700)) {
return false; return false;
} }
if (parse_display_body(body_start(response.data()), state)) { if (vamp_pico::parse_remote_display_body(body_start(response.data()), state)) {
return true; return true;
} }
return vamp_pico::fetch_remote_display(state); return vamp_pico::fetch_remote_display(state);
@@ -274,5 +219,5 @@ auto vamp_pico::fetch_remote_display(vamp_pico::remote_display_state& state) ->
if (!run_request("GET", vamp_protocol::remote_display_path, nullptr, response.data(), response.size(), 1000)) { if (!run_request("GET", vamp_protocol::remote_display_path, nullptr, response.data(), response.size(), 1000)) {
return false; return false;
} }
return parse_display_body(body_start(response.data()), state); return vamp_pico::parse_remote_display_body(body_start(response.data()), state);
} }
+86
View File
@@ -0,0 +1,86 @@
#include <vamp_pico/http_client.hpp>
#include <algorithm>
#include <charconv>
#include <cstring>
#include <limits>
#include <system_error>
namespace {
auto copy_value(std::string_view value, std::char_t* output, std::size_t capacity) -> void {
if (capacity == 0U) {
return;
}
auto count = std::min(capacity - 1U, value.size());
std::memcpy(output, value.data(), count);
output[count] = '\0';
}
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 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;
}
output = static_cast<std::uint32_t>(parsed);
return true;
}
auto strip_cr(std::string_view line) -> std::string_view {
if (!line.empty() && line.back() == '\r') {
line.remove_suffix(1U);
}
return line;
}
}
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 saw_version = std::bool_t{};
auto cursor = std::size_t{};
while (cursor <= body.size()) {
auto line_end = body.find('\n', cursor);
auto line = line_end == std::string_view::npos ? body.substr(cursor) : body.substr(cursor, line_end - cursor);
line = strip_cr(line);
auto equals = line.find('=');
if (equals != std::string_view::npos) {
auto key = line.substr(0U, equals);
auto value = line.substr(equals + 1U);
if (key == "version") {
saw_version = true;
} else if (key == "active") {
parsed.active = value == "1";
} else if (key == "title") {
copy_value(value, parsed.title, sizeof(parsed.title));
} else if (key == "part") {
copy_value(value, parsed.part, sizeof(parsed.part));
} else if (key == "mode") {
copy_value(value, parsed.mode, sizeof(parsed.mode));
} else if (key == "page") {
if (!parse_uint(value, parsed.page)) {
return false;
}
} else if (key == "pages") {
if (!parse_uint(value, parsed.pages)) {
return false;
}
}
}
if (line_end == std::string_view::npos) {
break;
}
cursor = line_end + 1U;
}
if (!saw_version) {
return false;
}
state = parsed;
return true;
}
+112 -15
View File
@@ -21,6 +21,7 @@
#include <vamp_zero/errors.hpp> #include <vamp_zero/errors.hpp>
#include <vamp_zero/library.hpp> #include <vamp_zero/library.hpp>
#include <vamp_zero/score.hpp>
namespace { namespace {
@@ -108,6 +109,31 @@ 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 sql = std::string{"DELETE FROM "};
sql += table;
sql += " WHERE catalog_managed = 1";
auto params = std::vector<noto::sqlite_value>{};
params.reserve(ids.size());
if (!ids.empty()) {
sql += " AND id NOT IN (";
for (auto index = std::size_t{}; index < ids.size(); ++index) {
if (index != 0U) {
sql += ", ";
}
sql += "?";
sql += std::to_string(index + 1U);
params.emplace_back(ids[index]);
}
sql += ")";
}
auto deleted = co_await connection.async_execute(std::move(sql), std::move(params));
if (!deleted) {
co_return std::unexpected(deleted.error());
}
co_return std::expected<void, modulus::error>{};
}
} }
auto vamp_zero::split_migration_statements(std::string const& input) -> std::expected<std::vector<std::string>, modulus::error> { auto vamp_zero::split_migration_statements(std::string const& input) -> std::expected<std::vector<std::string>, modulus::error> {
@@ -203,6 +229,10 @@ auto vamp_zero::async_migrate_database(boost::asio::any_io_executor executor, va
if (!practice_hierarchy_statements) { if (!practice_hierarchy_statements) {
co_return std::unexpected(practice_hierarchy_statements.error()); co_return std::unexpected(practice_hierarchy_statements.error());
} }
auto catalog_managed_statements = read_migration(config, "005_catalog_managed_rows.sql");
if (!catalog_managed_statements) {
co_return std::unexpected(catalog_managed_statements.error());
}
auto migrator = noto::migrator{connection}; auto migrator = noto::migrator{connection};
auto migrations = std::vector<noto::migration>{}; auto migrations = std::vector<noto::migration>{};
auto foundation = noto::migration{}; auto foundation = noto::migration{};
@@ -237,12 +267,20 @@ auto vamp_zero::async_migrate_database(boost::asio::any_io_executor executor, va
co_return co_await execute_statements(connection, statements); co_return co_await execute_statements(connection, statements);
}; };
migrations.push_back(std::move(practice_hierarchy)); migrations.push_back(std::move(practice_hierarchy));
auto catalog_managed = noto::migration{};
catalog_managed.id = "005_catalog_managed_rows";
catalog_managed.description = "mark rows managed by bundled catalog import";
catalog_managed.checksum = "005_catalog_managed_rows_v1";
catalog_managed.up = [statements = std::move(*catalog_managed_statements)](noto::connection& connection) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
co_return co_await execute_statements(connection, statements);
};
migrations.push_back(std::move(catalog_managed));
auto migrated = co_await migrator.async_migrate(std::move(migrations)); auto migrated = co_await migrator.async_migrate(std::move(migrations));
if (!migrated) { if (!migrated) {
co_return std::unexpected(migrated.error()); co_return std::unexpected(migrated.error());
} }
auto version = co_await connection.async_set_user_version(4); auto version = co_await connection.async_set_user_version(5);
if (!version) { if (!version) {
co_return std::unexpected(version.error()); co_return std::unexpected(version.error());
} }
@@ -268,21 +306,14 @@ auto vamp_zero::async_import_catalogs(boost::asio::any_io_executor executor, vam
co_return std::unexpected(begin.error()); co_return std::unexpected(begin.error());
} }
auto imported = std::expected<void, modulus::error>{}; auto imported = std::expected<void, modulus::error>{};
auto cleared = co_await connection.async_execute("DELETE FROM songs");
if (!cleared) {
imported = std::unexpected(cleared.error());
}
if (imported) {
auto cleared_practice = co_await connection.async_execute("DELETE FROM practice_exercises");
if (!cleared_practice) {
imported = std::unexpected(cleared_practice.error());
}
}
auto timestamp = now_ms(); 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) { if (!imported) {
break; break;
} }
song_ids.push_back(part.id);
auto params = std::vector<noto::sqlite_value>{ auto params = std::vector<noto::sqlite_value>{
noto::sqlite_value{part.id}, noto::sqlite_value{part.id},
noto::sqlite_value{part.title}, noto::sqlite_value{part.title},
@@ -294,16 +325,41 @@ auto vamp_zero::async_import_catalogs(boost::asio::any_io_executor executor, vam
noto::sqlite_value{part.source_path}, noto::sqlite_value{part.source_path},
noto::sqlite_value{part.key_signature}, noto::sqlite_value{part.key_signature},
}; };
auto inserted = co_await connection.async_execute("INSERT INTO songs(id, title, composer, source_json, created_at_ms, updated_at_ms, part, source_path, key_signature) VALUES(?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)", std::move(params)); auto inserted = co_await connection.async_execute(
R"sql(
INSERT INTO songs(id, title, composer, source_json, created_at_ms, updated_at_ms, part, source_path, key_signature, catalog_managed)
VALUES(?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, 1)
ON CONFLICT(id) DO UPDATE SET
title = excluded.title,
composer = excluded.composer,
source_json = excluded.source_json,
part = excluded.part,
source_path = excluded.source_path,
key_signature = excluded.key_signature,
catalog_managed = 1,
updated_at_ms = CASE
WHEN songs.title != excluded.title
OR songs.composer != excluded.composer
OR songs.source_json != excluded.source_json
OR songs.part != excluded.part
OR songs.source_path != excluded.source_path
OR songs.key_signature != excluded.key_signature
THEN excluded.updated_at_ms
ELSE songs.updated_at_ms
END
)sql",
std::move(params));
if (!inserted) { if (!inserted) {
imported = std::unexpected(inserted.error()); imported = std::unexpected(inserted.error());
} }
} }
auto practice_ids = std::vector<std::string>{};
for (auto const& exercise : practice->exercises) { for (auto const& exercise : practice->exercises) {
if (!imported) { if (!imported) {
break; 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>{ auto params = std::vector<noto::sqlite_value>{
noto::sqlite_value{variant.id}, noto::sqlite_value{variant.id},
noto::sqlite_value{exercise.practice_type_id}, noto::sqlite_value{exercise.practice_type_id},
@@ -318,13 +374,54 @@ auto vamp_zero::async_import_catalogs(boost::asio::any_io_executor executor, vam
noto::sqlite_value{timestamp}, noto::sqlite_value{timestamp},
noto::sqlite_value{timestamp}, noto::sqlite_value{timestamp},
}; };
auto inserted = co_await connection.async_execute("INSERT INTO practice_exercises(id, practice_type_id, practice_type_title, lesson_group_id, lesson_group_title, title, composer, key_signature, source_json, source_path, created_at_ms, updated_at_ms) VALUES(?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)", std::move(params)); auto inserted = co_await connection.async_execute(
R"sql(
INSERT INTO practice_exercises(id, practice_type_id, practice_type_title, lesson_group_id, lesson_group_title, title, composer, key_signature, source_json, source_path, created_at_ms, updated_at_ms, catalog_managed)
VALUES(?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, 1)
ON CONFLICT(id) DO UPDATE SET
practice_type_id = excluded.practice_type_id,
practice_type_title = excluded.practice_type_title,
lesson_group_id = excluded.lesson_group_id,
lesson_group_title = excluded.lesson_group_title,
title = excluded.title,
composer = excluded.composer,
key_signature = excluded.key_signature,
source_json = excluded.source_json,
source_path = excluded.source_path,
catalog_managed = 1,
updated_at_ms = CASE
WHEN practice_exercises.practice_type_id != excluded.practice_type_id
OR practice_exercises.practice_type_title != excluded.practice_type_title
OR practice_exercises.lesson_group_id != excluded.lesson_group_id
OR practice_exercises.lesson_group_title != excluded.lesson_group_title
OR practice_exercises.title != excluded.title
OR practice_exercises.composer != excluded.composer
OR practice_exercises.key_signature != excluded.key_signature
OR practice_exercises.source_json != excluded.source_json
OR practice_exercises.source_path != excluded.source_path
THEN excluded.updated_at_ms
ELSE practice_exercises.updated_at_ms
END
)sql",
std::move(params));
if (!inserted) { if (!inserted) {
imported = std::unexpected(inserted.error()); imported = std::unexpected(inserted.error());
break; break;
} }
} }
} }
if (imported) {
auto deleted = co_await execute_delete_stale_catalog_rows(connection, "songs", song_ids);
if (!deleted) {
imported = std::unexpected(deleted.error());
}
}
if (imported) {
auto deleted = co_await execute_delete_stale_catalog_rows(connection, "practice_exercises", practice_ids);
if (!deleted) {
imported = std::unexpected(deleted.error());
}
}
if (imported) { if (imported) {
auto committed = co_await connection.async_execute("COMMIT"); auto committed = co_await connection.async_execute("COMMIT");
if (!committed) { if (!committed) {
@@ -380,7 +477,7 @@ auto vamp_zero::async_load_performance_library(boost::asio::any_io_executor exec
.source_path = text_value(row[4]), .source_path = text_value(row[4]),
.source_json = std::move(source_json), .source_json = std::move(source_json),
.measure_count = parsed->measures.size(), .measure_count = parsed->measures.size(),
.total_pages = parsed->measures.empty() ? 1U : (parsed->measures.size() + 11U) / 12U, .total_pages = vamp_zero::score_page_count(*parsed),
}); });
} }
auto closed = co_await connection.async_close(); auto closed = co_await connection.async_close();
@@ -428,7 +525,7 @@ auto vamp_zero::async_load_practice_library(boost::asio::any_io_executor executo
.source_path = source_path, .source_path = source_path,
.source_json = source_json, .source_json = source_json,
.measure_count = parsed->measures.size(), .measure_count = parsed->measures.size(),
.total_pages = parsed->measures.empty() ? 1U : (parsed->measures.size() + 11U) / 12U, .total_pages = vamp_zero::score_page_count(*parsed),
}; };
exercises.push_back({ exercises.push_back({
.id = logical_practice_exercise_id(type_id, lesson_id, source_path, text_value(row[5])), .id = logical_practice_exercise_id(type_id, lesson_id, source_path, text_value(row[5])),
+3 -7
View File
@@ -19,6 +19,7 @@
#include <boost/system/error_code.hpp> #include <boost/system/error_code.hpp>
#include <vamp_zero/errors.hpp> #include <vamp_zero/errors.hpp>
#include <vamp_zero/score.hpp>
namespace { namespace {
@@ -273,11 +274,6 @@ auto parse_measure(boost::json::value const& value) -> std::expected<vamp_zero::
return measure; return measure;
} }
auto score_page_count(std::size_t measure_count) -> std::size_t {
constexpr auto measures_per_page = std::size_t{12};
return measure_count == 0U ? 1U : (measure_count + measures_per_page - 1U) / measures_per_page;
}
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{}; auto error = std::error_code{};
if (!std::filesystem::exists(directory, error)) { if (!std::filesystem::exists(directory, error)) {
@@ -559,7 +555,7 @@ auto vamp_zero::load_performance_library(std::string const& assets_path) -> std:
.source_path = document.source_path, .source_path = document.source_path,
.source_json = std::move(document.source_json), .source_json = std::move(document.source_json),
.measure_count = document.measures.size(), .measure_count = document.measures.size(),
.total_pages = score_page_count(document.measures.size()), .total_pages = vamp_zero::score_page_count(document),
}); });
} }
return vamp_zero::group_library_parts(std::move(parts)); return vamp_zero::group_library_parts(std::move(parts));
@@ -616,7 +612,7 @@ auto vamp_zero::load_practice_library(std::string const& assets_path) -> std::ex
.source_path = document->source_path, .source_path = document->source_path,
.source_json = document->source_json, .source_json = document->source_json,
.measure_count = document->measures.size(), .measure_count = document->measures.size(),
.total_pages = score_page_count(document->measures.size()), .total_pages = vamp_zero::score_page_count(*document),
}; };
exercises.push_back({ exercises.push_back({
.id = std::move(exercise_id), .id = std::move(exercise_id),
+156
View File
@@ -0,0 +1,156 @@
#include <vamp_zero/navigation.hpp>
#include <algorithm>
namespace {
auto menu_row_count(std::size_t item_count) -> std::size_t {
return item_count + 1U;
}
}
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;
}
auto index = state.selected_song_index - 1U;
if (index >= state.performance.groups.size()) {
return nullptr;
}
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);
if (state.selected_part_index == 0U || group == nullptr || group->parts.empty()) {
return nullptr;
}
auto index = state.selected_part_index - 1U;
if (index >= group->parts.size()) {
return nullptr;
}
return &group->parts[index];
}
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) {
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* {
if (state.selected_practice_type_index == 0U || state.practice.types.empty()) {
return nullptr;
}
auto index = state.selected_practice_type_index - 1U;
if (index >= state.practice.types.size()) {
return nullptr;
}
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);
if (state.selected_practice_lesson_index == 0U || type == nullptr || type->lesson_groups.empty()) {
return nullptr;
}
auto index = state.selected_practice_lesson_index - 1U;
if (index >= type->lesson_groups.size()) {
return nullptr;
}
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);
if (state.selected_practice_exercise_index == 0U || lesson == nullptr || lesson->exercises.empty()) {
return nullptr;
}
auto index = state.selected_practice_exercise_index - 1U;
if (index >= lesson->exercises.size()) {
return nullptr;
}
return &lesson->exercises[index];
}
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;
});
return found == state.practice.exercises.end() ? nullptr : &*found;
}
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* {
if (exercise.variants.empty()) {
return nullptr;
}
if (!vamp_zero::supports_key_selection(exercise)) {
return &exercise.variants.front();
}
if (state.selected_practice_key_index == 0U) {
return nullptr;
}
auto index = state.selected_practice_key_index - 1U;
if (index >= exercise.variants.size()) {
return nullptr;
}
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);
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);
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()) {
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 {
if (state.mode == vamp_zero::app_mode::PERFORMANCE_SCORE) {
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);
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 {
if (vamp_zero::selected_practice_lesson(state) != nullptr) {
return vamp_zero::app_mode::PRACTICE_EXERCISE_MENU;
}
if (vamp_zero::selected_practice_type(state) != nullptr) {
return vamp_zero::app_mode::PRACTICE_LESSON_MENU;
}
return vamp_zero::app_mode::PRACTICE_TYPE_MENU;
}
+1 -145
View File
@@ -14,6 +14,7 @@
#include <boost/json/value.hpp> #include <boost/json/value.hpp>
#include <vamp_zero/errors.hpp> #include <vamp_zero/errors.hpp>
#include <vamp_zero/navigation.hpp>
namespace { namespace {
@@ -37,151 +38,6 @@ auto menu_row_count(std::size_t item_count) -> std::size_t {
return item_count + 1U; return item_count + 1U;
} }
auto 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;
}
auto index = state.selected_song_index - 1U;
if (index >= state.performance.groups.size()) {
return nullptr;
}
return &state.performance.groups[index];
}
auto selected_part(vamp_zero::app_state const& state) -> vamp_zero::library_part const* {
auto const* group = selected_group(state);
if (state.selected_part_index == 0U || group == nullptr || group->parts.empty()) {
return nullptr;
}
auto index = state.selected_part_index - 1U;
if (index >= group->parts.size()) {
return nullptr;
}
return &group->parts[index];
}
auto 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) {
return part.id == state.active_part_id;
});
return found == state.performance.parts.end() ? nullptr : &*found;
}
auto 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;
}
auto index = state.selected_practice_type_index - 1U;
if (index >= state.practice.types.size()) {
return nullptr;
}
return &state.practice.types[index];
}
auto selected_practice_lesson(vamp_zero::app_state const& state) -> vamp_zero::practice_lesson_group const* {
auto const* type = selected_practice_type(state);
if (state.selected_practice_lesson_index == 0U || type == nullptr || type->lesson_groups.empty()) {
return nullptr;
}
auto index = state.selected_practice_lesson_index - 1U;
if (index >= type->lesson_groups.size()) {
return nullptr;
}
return &type->lesson_groups[index];
}
auto selected_practice_exercise(vamp_zero::app_state const& state) -> vamp_zero::practice_exercise const* {
auto const* lesson = selected_practice_lesson(state);
if (state.selected_practice_exercise_index == 0U || lesson == nullptr || lesson->exercises.empty()) {
return nullptr;
}
auto index = state.selected_practice_exercise_index - 1U;
if (index >= lesson->exercises.size()) {
return nullptr;
}
return &lesson->exercises[index];
}
auto 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;
});
return found == state.practice.exercises.end() ? nullptr : &*found;
}
auto 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 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;
}
if (!supports_key_selection(exercise)) {
return &exercise.variants.front();
}
if (state.selected_practice_key_index == 0U) {
return nullptr;
}
auto index = state.selected_practice_key_index - 1U;
if (index >= exercise.variants.size()) {
return nullptr;
}
return &exercise.variants[index];
}
auto selected_practice_key(vamp_zero::app_state const& state, vamp_zero::practice_exercise const& exercise) -> std::optional<std::string_view> {
auto const* variant = selected_practice_variant(state, exercise);
if (variant == nullptr) {
return std::nullopt;
}
return variant->key_signature;
}
auto practice_key_row_count(vamp_zero::app_state const& state) -> std::size_t {
auto const* exercise = selected_practice_exercise(state);
if (exercise == nullptr || !supports_key_selection(*exercise)) {
return 1U;
}
return menu_row_count(exercise->variants.size());
}
auto effective_practice_key(vamp_zero::app_state const& state, vamp_zero::practice_exercise const& exercise) -> std::string {
if (auto const* variant = 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 active_pages(vamp_zero::app_state const& state) -> std::size_t {
if (state.mode == vamp_zero::app_mode::PERFORMANCE_SCORE) {
auto const* part = active_part(state);
return part == nullptr ? 0U : part->total_pages;
}
if (state.mode == vamp_zero::app_mode::PRACTICE_SCORE) {
auto const* exercise = active_practice(state);
auto const* variant = exercise == nullptr ? nullptr : selected_practice_variant(state, *exercise);
return variant == nullptr ? 0U : variant->total_pages;
}
return 0U;
}
auto practice_fallback_menu(vamp_zero::app_state const& state) -> vamp_zero::app_mode {
if (selected_practice_lesson(state) != nullptr) {
return vamp_zero::app_mode::PRACTICE_EXERCISE_MENU;
}
if (selected_practice_type(state) != nullptr) {
return vamp_zero::app_mode::PRACTICE_LESSON_MENU;
}
return vamp_zero::app_mode::PRACTICE_TYPE_MENU;
}
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.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);
+64 -8
View File
@@ -777,6 +777,45 @@ auto effective_time_signature(vamp_zero::score_document const& score, vamp_zero:
return "4/4"; return "4/4";
} }
auto notation_state_changes_before(vamp_zero::score_document const& score, std::size_t measure_index) -> std::bool_t {
if (measure_index == 0U || measure_index >= score.measures.size()) {
return false;
}
auto const& previous = score.measures[measure_index - 1U];
auto const& current = score.measures[measure_index];
return effective_key_signature(score, previous) != effective_key_signature(score, current)
|| effective_time_signature(score, &previous) != effective_time_signature(score, &current);
}
auto make_score_system_ranges(vamp_zero::score_document const& score) -> std::vector<vamp_zero::score_system_range> {
auto ranges = std::vector<vamp_zero::score_system_range>{};
if (score.measures.empty()) {
return ranges;
}
auto current = vamp_zero::score_system_range{.first_measure = 0U, .measure_count = 0U};
for (auto measure_index = std::size_t{}; measure_index < score.measures.size(); ++measure_index) {
if (current.measure_count != 0U
&& (current.measure_count >= vamp_zero::score_measures_per_system || notation_state_changes_before(score, measure_index))) {
ranges.push_back(current);
current = vamp_zero::score_system_range{.first_measure = measure_index, .measure_count = 0U};
}
++current.measure_count;
}
if (current.measure_count != 0U) {
ranges.push_back(current);
}
return ranges;
}
auto show_time_signature_for_system(vamp_zero::score_document const& score, vamp_zero::score_system_range const& range) -> std::bool_t {
if (range.first_measure == 0U) {
return true;
}
return notation_state_changes_before(score, range.first_measure)
&& effective_time_signature(score, &score.measures[range.first_measure - 1U]) != effective_time_signature(score, &score.measures[range.first_measure]);
}
auto draw_curve(luma::draw_context& context, luma::point start, luma::point control, luma::point end, std::float32_t thickness, luma::color color) -> void { auto draw_curve(luma::draw_context& context, luma::point start, luma::point control, luma::point end, std::float32_t thickness, luma::color color) -> void {
constexpr auto segments = std::int32_t{10}; constexpr auto segments = std::int32_t{10};
auto previous = start; auto previous = start;
@@ -2221,6 +2260,18 @@ auto vamp_zero::notated_accidentals_for_measure(vamp_zero::score_measure const&
return accidentals; return accidentals;
} }
auto vamp_zero::score_system_ranges(vamp_zero::score_document const& score) -> std::vector<vamp_zero::score_system_range> {
return make_score_system_ranges(score);
}
auto vamp_zero::score_page_count(vamp_zero::score_document const& score) -> std::size_t {
auto ranges = make_score_system_ranges(score);
if (ranges.empty()) {
return 1U;
}
return (ranges.size() + vamp_zero::score_systems_per_page - 1U) / vamp_zero::score_systems_per_page;
}
auto vamp_zero::layout_score_page(luma::rect bounds, vamp_zero::score_document const& score, std::size_t page_index) -> vamp_zero::score_page_layout { auto vamp_zero::layout_score_page(luma::rect bounds, vamp_zero::score_document const& score, std::size_t page_index) -> vamp_zero::score_page_layout {
auto layout = vamp_zero::score_page_layout{ auto layout = vamp_zero::score_page_layout{
.bounds = bounds, .bounds = bounds,
@@ -2240,23 +2291,28 @@ auto vamp_zero::layout_score_page(luma::rect bounds, vamp_zero::score_document c
auto const content_y = bounds.y + 84.0F; auto const content_y = bounds.y + 84.0F;
auto const content_width = std::max<std::float32_t>(spacing * 18.0F, bounds.width - margin * 2.0F); 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 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 const system_ranges = make_score_system_ranges(score);
auto const first_system = page_index * vamp_zero::score_systems_per_page;
auto note_refs = std::vector<laid_out_note_ref>{}; auto note_refs = std::vector<laid_out_note_ref>{};
auto measure_refs = std::vector<laid_out_measure_ref>{}; auto measure_refs = std::vector<laid_out_measure_ref>{};
for (auto system = std::size_t{}; system < vamp_zero::score_systems_per_page; ++system) { 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; auto const system_y = content_y + static_cast<std::float32_t>(system) * system_height;
auto const staff_top = system_y + spacing * 4.25F; auto const staff_top = system_y + spacing * 4.25F;
auto const system_first_measure = first_measure + system * vamp_zero::score_measures_per_system; auto const range_index = first_system + system;
auto const* first_system_measure = system_first_measure < score.measures.size() ? &score.measures[system_first_measure] : nullptr; auto const* range = range_index < system_ranges.size() ? &system_ranges[range_index] : nullptr;
auto const* first_system_measure = range != nullptr && range->first_measure < score.measures.size() ? &score.measures[range->first_measure] : nullptr;
layout_staff(layout, content_x, staff_top, content_width, spacing, system); layout_staff(layout, content_x, staff_top, content_width, spacing, system);
auto const show_time = page_index == 0U && system == 0U; auto const show_time = range == nullptr ? page_index == 0U && system == 0U : show_time_signature_for_system(score, *range);
auto const measure_start_x = layout_system_preamble(layout, score, first_system_measure, content_x, staff_top, spacing, show_time, system); auto const measure_start_x = layout_system_preamble(layout, score, first_system_measure, content_x, staff_top, spacing, show_time, system);
auto const measure_area_width = std::max<std::float32_t>(spacing * 12.0F, content_x + content_width - measure_start_x); auto const measure_area_width = std::max<std::float32_t>(spacing * 12.0F, content_x + content_width - measure_start_x);
auto const measure_width = measure_area_width / static_cast<std::float32_t>(vamp_zero::score_measures_per_system); if (range == nullptr || range->measure_count == 0U) {
continue;
}
auto const measure_width = measure_area_width / static_cast<std::float32_t>(range->measure_count);
for (auto measure = std::size_t{}; measure < vamp_zero::score_measures_per_system; ++measure) { for (auto measure = std::size_t{}; measure < range->measure_count; ++measure) {
auto const measure_index = system_first_measure + measure; auto const measure_index = range->first_measure + measure;
auto const x = measure_start_x + static_cast<std::float32_t>(measure) * measure_width; auto const x = measure_start_x + static_cast<std::float32_t>(measure) * measure_width;
if (measure_index < score.measures.size()) { if (measure_index < score.measures.size()) {
layout_measure( layout_measure(
@@ -2274,7 +2330,7 @@ auto vamp_zero::layout_score_page(luma::rect bounds, vamp_zero::score_document c
} }
auto const bar_x = x + measure_width; auto const bar_x = x + measure_width;
auto const final_bar = measure + 1U == vamp_zero::score_measures_per_system || measure_index + 1U >= score.measures.size(); auto const final_bar = measure + 1U == range->measure_count || measure_index + 1U >= score.measures.size();
add_stroke_line( add_stroke_line(
layout, layout,
final_bar ? vamp_zero::score_layout_item_role::FINAL_BARLINE : vamp_zero::score_layout_item_role::BARLINE, final_bar ? vamp_zero::score_layout_item_role::FINAL_BARLINE : vamp_zero::score_layout_item_role::BARLINE,
+59 -110
View File
@@ -3,11 +3,13 @@
#include <algorithm> #include <algorithm>
#include <cstdint> #include <cstdint>
#include <expected> #include <expected>
#include <functional>
#include <memory> #include <memory>
#include <mutex> #include <mutex>
#include <optional> #include <optional>
#include <string> #include <string>
#include <string_view> #include <string_view>
#include <unordered_map>
#include <utility> #include <utility>
#include <boost/asio/any_io_executor.hpp> #include <boost/asio/any_io_executor.hpp>
@@ -19,8 +21,47 @@
#include <vamp_style/font.hpp> #include <vamp_style/font.hpp>
#include <vamp_zero/color.hpp> #include <vamp_zero/color.hpp>
#include <vamp_zero/library.hpp> #include <vamp_zero/library.hpp>
#include <vamp_zero/navigation.hpp>
#include <vamp_zero/score.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');
key += part_override;
key.push_back('\n');
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());
}
if (!part_override.empty()) {
parsed->part = std::move(part_override);
}
auto document = std::make_shared<vamp_zero::score_document const>(std::move(*parsed));
entries[key] = {
.source_json = std::string{source_json},
.document = document,
};
return document;
}
std::unordered_map<std::string, entry> entries{};
};
namespace { namespace {
constexpr auto touch_hold_duration = std::chrono::milliseconds{700}; constexpr auto touch_hold_duration = std::chrono::milliseconds{700};
@@ -41,102 +82,6 @@ auto bind_port(vamp_zero::config const& config) -> std::uint16_t {
} }
} }
auto active_part(vamp_zero::app_state const& state) -> vamp_zero::library_part const* {
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 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;
}
auto index = state.selected_song_index - 1U;
if (index >= state.performance.groups.size()) {
return nullptr;
}
return &state.performance.groups[index];
}
auto active_practice(vamp_zero::app_state const& state) -> vamp_zero::practice_exercise const* {
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 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;
}
auto index = state.selected_practice_type_index - 1U;
if (index >= state.practice.types.size()) {
return nullptr;
}
return &state.practice.types[index];
}
auto selected_practice_lesson(vamp_zero::app_state const& state) -> vamp_zero::practice_lesson_group const* {
auto const* type = selected_practice_type(state);
if (state.selected_practice_lesson_index == 0U || type == nullptr || type->lesson_groups.empty()) {
return nullptr;
}
auto index = state.selected_practice_lesson_index - 1U;
if (index >= type->lesson_groups.size()) {
return nullptr;
}
return &type->lesson_groups[index];
}
auto selected_practice_exercise(vamp_zero::app_state const& state) -> vamp_zero::practice_exercise const* {
auto const* lesson = selected_practice_lesson(state);
if (state.selected_practice_exercise_index == 0U || lesson == nullptr || lesson->exercises.empty()) {
return nullptr;
}
auto index = state.selected_practice_exercise_index - 1U;
if (index >= lesson->exercises.size()) {
return nullptr;
}
return &lesson->exercises[index];
}
auto 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 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;
}
if (!supports_key_selection(exercise)) {
return &exercise.variants.front();
}
if (state.selected_practice_key_index == 0U) {
return nullptr;
}
auto index = state.selected_practice_key_index - 1U;
if (index >= exercise.variants.size()) {
return nullptr;
}
return &exercise.variants[index];
}
auto selected_practice_key(vamp_zero::app_state const& state, vamp_zero::practice_exercise const& exercise) -> std::optional<std::string_view> {
auto const* variant = selected_practice_variant(state, exercise);
if (variant == nullptr) {
return std::nullopt;
}
return variant->key_signature;
}
auto effective_practice_key(vamp_zero::app_state const& state, vamp_zero::practice_exercise const& exercise) -> std::string {
if (auto const* variant = 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 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; return state.pico_connected ? vamp_style::connected : vamp_style::retrying;
} }
@@ -361,9 +306,10 @@ auto draw_practice_key_menu(luma::draw_context& context, luma::rect bounds, vamp
} }
} }
auto draw_score(luma::draw_context& context, luma::rect bounds, vamp_zero::app_state const& state) -> 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{}; auto source_json = std::string_view{};
auto source_path = std::string{}; auto source_path = std::string{};
auto part_override = std::string{};
if (state.mode == vamp_zero::app_mode::PERFORMANCE_SCORE) { if (state.mode == vamp_zero::app_mode::PERFORMANCE_SCORE) {
auto const* part = active_part(state); auto const* part = active_part(state);
if (part == nullptr) { if (part == nullptr) {
@@ -385,32 +331,28 @@ auto draw_score(luma::draw_context& context, luma::rect bounds, vamp_zero::app_s
} }
source_json = variant->source_json; source_json = variant->source_json;
source_path = variant->source_path; source_path = variant->source_path;
part_override = "Key: " + effective_practice_key(state, *exercise);
} }
auto parsed = vamp_zero::parse_score_document(source_json, source_path); auto parsed = score_cache.get(source_json, source_path, std::move(part_override));
if (!parsed) { if (!parsed) {
draw_text(context, bounds.x + 36.0F, bounds.y + 38.0F, "SCORE ERROR", 4, vamp_style::danger, 360.0F); draw_text(context, bounds.x + 36.0F, bounds.y + 38.0F, "SCORE ERROR", 4, vamp_style::danger, 360.0F);
draw_text(context, bounds.x + 38.0F, bounds.y + 86.0F, parsed.error().describe(), 1, vamp_style::muted, bounds.width - 72.0F); draw_text(context, bounds.x + 38.0F, bounds.y + 86.0F, parsed.error().describe(), 1, vamp_style::muted, bounds.width - 72.0F);
return; return;
} }
if (state.mode == vamp_zero::app_mode::PRACTICE_SCORE) {
auto const* exercise = active_practice(state);
if (exercise != nullptr) {
parsed->part = "Key: " + effective_practice_key(state, *exercise);
}
}
auto paper_bounds = luma::rect{ auto paper_bounds = luma::rect{
.x = bounds.x + 24.0F, .x = bounds.x + 24.0F,
.y = bounds.y + 18.0F, .y = bounds.y + 18.0F,
.width = bounds.width - 48.0F, .width = bounds.width - 48.0F,
.height = bounds.height - 36.0F, .height = bounds.height - 36.0F,
}; };
vamp_zero::draw_score_page(context, paper_bounds, *parsed, state.page_index); vamp_zero::draw_score_page(context, paper_bounds, **parsed, state.page_index);
} }
class vamp_root_control final : public luma::control { class vamp_root_control final : public luma::control {
public: public:
explicit vamp_root_control(vamp_zero::ui_state state, touch_handler on_touch) : state_{std::move(state)}, on_touch_{std::move(on_touch)} {} 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 { auto measure(luma::draw_context&, luma::theme const&, luma::constraints limits) -> luma::size override {
return luma::clamp({.width = 1024.0F, .height = 600.0F}, limits); return luma::clamp({.width = 1024.0F, .height = 600.0F}, limits);
} }
@@ -439,7 +381,9 @@ public:
} else if (state_.app.mode == vamp_zero::app_mode::PRACTICE_KEY_MENU) { } else if (state_.app.mode == vamp_zero::app_mode::PRACTICE_KEY_MENU) {
draw_practice_key_menu(context, content, state_.app); 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) {
draw_score(context, content, state_.app); if (score_cache_) {
draw_score(context, content, state_.app, *score_cache_);
}
} else { } else {
draw_main_menu(context, content, state_.app); draw_main_menu(context, content, state_.app);
} }
@@ -449,13 +393,18 @@ public:
} }
private: private:
vamp_zero::ui_state state_{}; vamp_zero::ui_state state_{};
std::shared_ptr<vamp_zero::score_draw_cache> score_cache_{};
touch_handler on_touch_{}; touch_handler 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) 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)} {} : 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 { auto vamp_zero::zero_ui::send_touch_command(vamp_protocol::page_command command) -> void {
if (!command_handler_) { if (!command_handler_) {
@@ -593,7 +542,7 @@ 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> { 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, [this](luma::touch_event const& event) { return std::make_unique<vamp_root_control>(state, score_cache_, [this](luma::touch_event const& event) {
return handle_touch(event); return handle_touch(event);
}); });
} }
+4
View File
@@ -1,4 +1,5 @@
add_executable(product-vamp-test add_executable(product-vamp-test
../src/vamp_pico/remote_display.cpp
test.cpp test.cpp
) )
target_include_directories(product-vamp-test PRIVATE target_include_directories(product-vamp-test PRIVATE
@@ -8,6 +9,9 @@ target_link_libraries(product-vamp-test PRIVATE
vamp-zero-core vamp-zero-core
) )
target_compile_features(product-vamp-test PRIVATE cxx_std_23) target_compile_features(product-vamp-test PRIVATE cxx_std_23)
target_compile_definitions(product-vamp-test PRIVATE
VAMP_TEST_SOURCE_DIR="${CMAKE_SOURCE_DIR}"
)
add_test(NAME product-vamp COMMAND product-vamp-test) add_test(NAME product-vamp COMMAND product-vamp-test)
set_tests_properties(product-vamp PROPERTIES TIMEOUT 60) set_tests_properties(product-vamp PROPERTIES TIMEOUT 60)
add_test( add_test(
+286
View File
@@ -1,16 +1,32 @@
#include <test.hpp> #include <test.hpp>
#include <algorithm> #include <algorithm>
#include <chrono>
#include <cmath> #include <cmath>
#include <cstddef> #include <cstddef>
#include <cstdio>
#include <cstdint> #include <cstdint>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <future>
#include <initializer_list> #include <initializer_list>
#include <limits> #include <limits>
#include <string> #include <string>
#include <string_view> #include <string_view>
#include <thread>
#include <utility> #include <utility>
#include <variant>
#include <vector> #include <vector>
#include <boost/asio/co_spawn.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/asio/use_future.hpp>
#include <noto.hpp>
#include <vamp_pico/http_client.hpp>
#include <vamp_zero/database.hpp>
#include <vamp_zero/library.hpp> #include <vamp_zero/library.hpp>
#include <vamp_zero/score.hpp> #include <vamp_zero/score.hpp>
#include <vamp_zero/types.hpp> #include <vamp_zero/types.hpp>
@@ -166,6 +182,113 @@ auto near(std::float32_t left, std::float32_t right) -> std::bool_t {
return std::abs(left - right) < 0.02F; return std::abs(left - right) < 0.02F;
} }
auto test_database_config(std::filesystem::path const& path) -> vamp_zero::database_config {
return {
.path = path.string(),
.migrations_path = (std::filesystem::path{VAMP_TEST_SOURCE_DIR} / "package" / "migrations").string(),
};
}
auto run_migrate(vamp_zero::database_config config) -> std::expected<void, modulus::error> {
auto io = boost::asio::io_context{};
auto future = boost::asio::co_spawn(io, vamp_zero::async_migrate_database(io.get_executor(), std::move(config)), boost::asio::use_future);
io.run();
return future.get();
}
auto run_import(vamp_zero::database_config config, std::filesystem::path const& assets_path) -> 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(), std::move(config), assets_path.string()), boost::asio::use_future);
io.run();
return future.get();
}
auto query_database(std::filesystem::path const& path, std::string sql, std::vector<noto::sqlite_value> params = {}) -> std::expected<noto::query_result, modulus::error> {
auto io = boost::asio::io_context{};
auto future = boost::asio::co_spawn(
io,
[executor = io.get_executor(), path = path.string(), sql = std::move(sql), params = std::move(params)]() mutable -> boost::asio::awaitable<std::expected<noto::query_result, modulus::error>> {
auto config = noto::file_connection_config(path, "product-vamp-test");
config.foreign_keys = true;
auto connection = noto::connection{executor, std::move(config)};
auto connected = co_await connection.async_connect();
if (!connected) {
co_return std::unexpected(connected.error());
}
auto selected = co_await connection.async_execute(std::move(sql), std::move(params));
auto closed = co_await connection.async_close();
if (!selected) {
co_return std::unexpected(selected.error());
}
if (!closed) {
co_return std::unexpected(closed.error());
}
co_return selected;
},
boost::asio::use_future);
io.run();
return future.get();
}
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 integer_value(noto::sqlite_value const& value) -> std::int64_t {
auto integer = std::get_if<std::int64_t>(&value.value);
return integer == nullptr ? 0 : *integer;
}
auto score_asset_json(std::string title, std::string key = "C") -> std::string {
return std::string{R"json({
"version": 1,
"metadata": {
"title": ")json"} + title + R"json(",
"composer": "Composer",
"key_signature": ")json" + key + R"json(",
"time_signature": "4/4"
},
"staves": [
{
"id": "staff-lead",
"name": "Lead",
"clef": "treble",
"role": "lead",
"measures": [
{
"id": "measure-1",
"time_signature": "4/4",
"key_signature": ")json" + key + R"json(",
"voices": [
{
"id": "voice-1",
"notes": [
{
"id": "note-1",
"kind": "note",
"duration_ticks": 4096,
"pitches": [
{"letter": "c", "octave": 4, "accidental": "n"}
]
}
]
}
]
}
]
}
]
}
)json";
}
void write_text(std::filesystem::path const& path, std::string const& text) {
std::filesystem::create_directories(path.parent_path());
auto output = std::ofstream{path, std::ios::binary | std::ios::trunc};
output << text;
}
} }
BOOST_AUTO_TEST_CASE(score_parser_preserves_notation_preamble) { BOOST_AUTO_TEST_CASE(score_parser_preserves_notation_preamble) {
@@ -318,6 +441,93 @@ BOOST_AUTO_TEST_CASE(tied_rests_hidden_and_slash_notes_do_not_emit_accidentals)
expect_marks(vamp_zero::notated_accidentals_for_measure(measure, "C"), {"none", "flat"}); expect_marks(vamp_zero::notated_accidentals_for_measure(measure, "C"), {"none", "flat"});
} }
BOOST_AUTO_TEST_CASE(pico_remote_display_parser_accepts_crlf_payloads) {
auto state = vamp_pico::remote_display_state{};
BOOST_REQUIRE(vamp_pico::parse_remote_display_body("version=1\r\nactive=1\r\ntitle=Autumn Leaves\r\npart=Vocal\r\nmode=score\r\npage=2\r\npages=11\r\n", state));
BOOST_TEST(state.active);
BOOST_TEST(std::strcmp(state.title, "Autumn Leaves") == 0);
BOOST_TEST(std::strcmp(state.part, "Vocal") == 0);
BOOST_TEST(std::strcmp(state.mode, "score") == 0);
BOOST_TEST(state.page == 2U);
BOOST_TEST(state.pages == 11U);
}
BOOST_AUTO_TEST_CASE(pico_remote_display_parser_rejects_missing_version_and_bad_numbers) {
auto state = vamp_pico::remote_display_state{};
std::snprintf(state.title, sizeof(state.title), "Keep");
BOOST_TEST(!vamp_pico::parse_remote_display_body("title=Autumn Leaves\npage=1\npages=2\n", state));
BOOST_TEST(std::strcmp(state.title, "Keep") == 0);
BOOST_TEST(!vamp_pico::parse_remote_display_body("version=1\npage=1x\npages=2\n", state));
BOOST_TEST(std::strcmp(state.title, "Keep") == 0);
}
BOOST_AUTO_TEST_CASE(pico_remote_display_parser_truncates_long_fields_safely) {
auto state = vamp_pico::remote_display_state{};
auto body = std::string{"version=1\ntitle="} + std::string(140U, 'A') + "\npart=" + std::string(80U, 'B') + "\nmode=performance-score";
BOOST_REQUIRE(vamp_pico::parse_remote_display_body(body, state));
BOOST_TEST(std::strlen(state.title) == sizeof(state.title) - 1U);
BOOST_TEST(std::strlen(state.part) == sizeof(state.part) - 1U);
BOOST_TEST(std::strcmp(state.mode, "performance-score") == 0);
}
BOOST_AUTO_TEST_CASE(database_import_upserts_catalog_rows_and_keeps_unmanaged_rows) {
auto root = std::filesystem::temp_directory_path()
/ ("product-vamp-db-test-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()));
auto assets = root / "assets";
auto database_path = root / "vamp.db";
auto song_path = assets / "songs" / "tune.json";
std::filesystem::remove_all(root);
write_text(song_path, score_asset_json("Catalog Tune"));
auto config = test_database_config(database_path);
BOOST_REQUIRE(run_migrate(config));
BOOST_REQUIRE(run_import(config, assets));
auto first = query_database(database_path, "SELECT title, created_at_ms, updated_at_ms, catalog_managed FROM songs WHERE id = 'tune'");
BOOST_REQUIRE(first);
BOOST_REQUIRE_EQUAL(first->rows.size(), 1U);
auto created = integer_value(first->rows[0][1]);
auto updated = integer_value(first->rows[0][2]);
BOOST_TEST(text_value(first->rows[0][0]) == "Catalog Tune");
BOOST_TEST(integer_value(first->rows[0][3]) == 1);
BOOST_REQUIRE(run_import(config, assets));
auto unchanged = query_database(database_path, "SELECT created_at_ms, updated_at_ms FROM songs WHERE id = 'tune'");
BOOST_REQUIRE(unchanged);
BOOST_REQUIRE_EQUAL(unchanged->rows.size(), 1U);
BOOST_TEST(integer_value(unchanged->rows[0][0]) == created);
BOOST_TEST(integer_value(unchanged->rows[0][1]) == updated);
std::this_thread::sleep_for(std::chrono::milliseconds{2});
write_text(song_path, score_asset_json("Catalog Tune Updated"));
BOOST_REQUIRE(run_import(config, assets));
auto changed = query_database(database_path, "SELECT title, created_at_ms, updated_at_ms FROM songs WHERE id = 'tune'");
BOOST_REQUIRE(changed);
BOOST_REQUIRE_EQUAL(changed->rows.size(), 1U);
BOOST_TEST(text_value(changed->rows[0][0]) == "Catalog Tune Updated");
BOOST_TEST(integer_value(changed->rows[0][1]) == created);
BOOST_TEST(integer_value(changed->rows[0][2]) >= updated);
auto inserted_custom = query_database(
database_path,
"INSERT INTO songs(id, title, composer, source_json, created_at_ms, updated_at_ms, part, source_path, key_signature, catalog_managed) VALUES('custom', 'Custom Tune', 'Composer', ?1, 1, 1, 'Lead', 'custom.json', 'C', 0)",
{noto::sqlite_value{score_asset_json("Custom Tune")}});
BOOST_REQUIRE(inserted_custom);
std::filesystem::remove(song_path);
BOOST_REQUIRE(run_import(config, assets));
auto counts = query_database(database_path, "SELECT sum(CASE WHEN id = 'tune' THEN 1 ELSE 0 END), sum(CASE WHEN id = 'custom' THEN 1 ELSE 0 END) FROM songs");
BOOST_REQUIRE(counts);
BOOST_REQUIRE_EQUAL(counts->rows.size(), 1U);
BOOST_TEST(integer_value(counts->rows[0][0]) == 0);
BOOST_TEST(integer_value(counts->rows[0][1]) == 1);
std::filesystem::remove_all(root);
}
BOOST_AUTO_TEST_CASE(score_layout_orders_preamble_on_staff_baselines) { BOOST_AUTO_TEST_CASE(score_layout_orders_preamble_on_staff_baselines) {
auto score = score_with_notes({note({pitch('e', "n")})}, "F", "4/4", "treble"); auto score = score_with_notes({note({pitch('e', "n")})}, "F", "4/4", "treble");
auto layout = vamp_zero::layout_score_page({.x = 0.0F, .y = 0.0F, .width = 976.0F, .height = 484.0F}, score, 0U); auto layout = vamp_zero::layout_score_page({.x = 0.0F, .y = 0.0F, .width = 976.0F, .height = 484.0F}, score, 0U);
@@ -340,6 +550,82 @@ BOOST_AUTO_TEST_CASE(score_layout_orders_preamble_on_staff_baselines) {
BOOST_TEST(near(times[1]->origin.y, staff_lines.front()->from.y + layout.staff_spacing * 3.5F)); BOOST_TEST(near(times[1]->origin.y, staff_lines.front()->from.y + layout.staff_spacing * 3.5F));
} }
BOOST_AUTO_TEST_CASE(score_system_ranges_force_breaks_before_key_changes) {
auto score = vamp_zero::score_document{
.title = "Key Change",
.composer = "Composer",
.key_signature = "C",
.time_signature = "4/4",
.clef = "treble",
.part = "Lead",
.measures = {
{.time_signature = "4/4", .key_signature = "C", .notes = {note({pitch('c', "n")})}},
{.time_signature = "4/4", .key_signature = "C", .notes = {note({pitch('d', "n")})}},
{.time_signature = "4/4", .key_signature = "G", .notes = {note({pitch('f', "#")})}},
{.time_signature = "4/4", .key_signature = "G", .notes = {note({pitch('g', "n")})}},
},
};
auto ranges = vamp_zero::score_system_ranges(score);
BOOST_REQUIRE_EQUAL(ranges.size(), 2U);
BOOST_TEST(ranges[0].first_measure == 0U);
BOOST_TEST(ranges[0].measure_count == 2U);
BOOST_TEST(ranges[1].first_measure == 2U);
BOOST_TEST(ranges[1].measure_count == 2U);
auto layout = vamp_zero::layout_score_page({.x = 0.0F, .y = 0.0F, .width = 976.0F, .height = 484.0F}, score, 0U);
BOOST_REQUIRE(!layout_items(layout, vamp_zero::score_layout_item_role::NOTEHEAD, 1U, 2U, 0U).empty());
BOOST_TEST(!layout_items(layout, vamp_zero::score_layout_item_role::KEY_SIGNATURE, 1U).empty());
BOOST_TEST(layout_items(layout, vamp_zero::score_layout_item_role::TIME_SIGNATURE, 1U).empty());
}
BOOST_AUTO_TEST_CASE(score_system_ranges_force_breaks_before_time_changes) {
auto score = vamp_zero::score_document{
.title = "Time Change",
.composer = "Composer",
.key_signature = "C",
.time_signature = "4/4",
.clef = "treble",
.part = "Lead",
.measures = {
{.time_signature = "4/4", .key_signature = "C", .notes = {note({pitch('c', "n")})}},
{.time_signature = "4/4", .key_signature = "C", .notes = {note({pitch('d', "n")})}},
{.time_signature = "3/4", .key_signature = "C", .notes = {note({pitch('e', "n")})}},
},
};
auto ranges = vamp_zero::score_system_ranges(score);
BOOST_REQUIRE_EQUAL(ranges.size(), 2U);
BOOST_TEST(ranges[1].first_measure == 2U);
BOOST_TEST(ranges[1].measure_count == 1U);
auto layout = vamp_zero::layout_score_page({.x = 0.0F, .y = 0.0F, .width = 976.0F, .height = 484.0F}, score, 0U);
BOOST_REQUIRE(!layout_items(layout, vamp_zero::score_layout_item_role::NOTEHEAD, 1U, 2U, 0U).empty());
BOOST_TEST(!layout_items(layout, vamp_zero::score_layout_item_role::TIME_SIGNATURE, 1U).empty());
}
BOOST_AUTO_TEST_CASE(score_page_count_uses_forced_notation_breaks) {
auto measures = std::vector<vamp_zero::score_measure>{};
for (auto index = std::size_t{}; index < 10U; ++index) {
measures.push_back({
.time_signature = "4/4",
.key_signature = (index / 2U) % 2U == 0U ? "C" : "G",
.notes = {note({pitch('c', "n")})},
});
}
auto score = vamp_zero::score_document{
.title = "Many Breaks",
.composer = "Composer",
.key_signature = "C",
.time_signature = "4/4",
.clef = "treble",
.part = "Lead",
.measures = std::move(measures),
};
BOOST_TEST(vamp_zero::score_page_count(score) == 2U);
}
BOOST_AUTO_TEST_CASE(score_layout_attaches_stems_to_notehead_anchors) { BOOST_AUTO_TEST_CASE(score_layout_attaches_stems_to_notehead_anchors) {
auto score = score_with_notes({note({pitch('d', "n")})}); auto score = score_with_notes({note({pitch('d', "n")})});
auto layout = vamp_zero::layout_score_page({.x = 0.0F, .y = 0.0F, .width = 976.0F, .height = 484.0F}, score, 0U); auto layout = vamp_zero::layout_score_page({.x = 0.0F, .y = 0.0F, .width = 976.0F, .height = 484.0F}, score, 0U);
+39
View File
@@ -3,10 +3,22 @@ from __future__ import annotations
import unittest import unittest
from generator.arrange.generate import arrange_document from generator.arrange.generate import arrange_document
from generator.arrange.fretboard import chord_pitch_classes, pitches_for_voicing
from generator.transposition import mod12
from generator.score import normalize_score_document from generator.score import normalize_score_document
class CompactScoreJsonTests(unittest.TestCase): class CompactScoreJsonTests(unittest.TestCase):
def pitch_classes(self, pitches: list[dict]) -> set[int]:
natural = {"c": 0, "d": 2, "e": 4, "f": 5, "g": 7, "a": 9, "b": 11}
alterations = {"bb": -2, "b": -1, "n": 0, "": 0, "#": 1, "x": 2}
result: set[int] = set()
for pitch in pitches:
letter = str(pitch.get("letter", "c")).lower()
accidental = str(pitch.get("accidental", "n"))
result.add(mod12(natural.get(letter, 0) + alterations.get(accidental, 0)))
return result
def chord_symbols(self, measure: dict) -> list[str]: def chord_symbols(self, measure: dict) -> list[str]:
return [ return [
note["chord_symbol"] note["chord_symbol"]
@@ -87,6 +99,33 @@ class CompactScoreJsonTests(unittest.TestCase):
if part_type in {"acoustic", "electric"}: if part_type in {"acoustic", "electric"}:
self.assertTrue(all(note["kind"] == "rest" for note in self.measure_notes(final_turnaround))) self.assertTrue(all(note["kind"] == "rest" for note in self.measure_notes(final_turnaround)))
def test_slash_arrangements_preserve_source_chord_markers(self) -> None:
document = normalize_score_document({
"title": "Slash Tune",
"key": "C",
"time": "4/4",
"measures": [
"[Cmaj7] C4:1",
"[F7#11] F4:1",
"[B-7b5] B4:1",
"[E7b9] E4:1",
],
})
arranged = arrange_document(document, "acoustic")
slash_measure = arranged["staves"][0]["measures"][12]
self.assertEqual(self.chord_symbols(slash_measure), ["Cmaj7"])
self.assertTrue(all(note.get("notehead") == "slash" for note in self.measure_notes(slash_measure)))
def test_jazz_core_chords_include_color_tones(self) -> None:
self.assertEqual(set(chord_pitch_classes("G7b9")), {7, 11, 5, 8})
self.assertEqual(set(chord_pitch_classes("C13")), {0, 4, 10, 9})
self.assertEqual(set(chord_pitch_classes("Fmaj7#11")), {5, 9, 4, 11})
self.assertTrue({11, 5, 8}.issubset(self.pitch_classes(pitches_for_voicing("G7b9"))))
self.assertTrue({4, 10, 9}.issubset(self.pitch_classes(pitches_for_voicing("C13"))))
def test_compact_json_normalizes_pitch_groups_and_tuplets(self) -> None: def test_compact_json_normalizes_pitch_groups_and_tuplets(self) -> None:
document = normalize_score_document({ document = normalize_score_document({
"title": "Practice", "title": "Practice",