Rendering
ci / checks (push) Successful in 38m59s

This commit is contained in:
khodowany
2026-09-05 13:31:18 -05:00
parent 4f9a269f85
commit 6da21a2deb
12 changed files with 716 additions and 52 deletions
+2
View File
@@ -99,6 +99,8 @@ jobs:
run: python3 modulus.py configure --target x86_64_gcc12_release run: python3 modulus.py configure --target x86_64_gcc12_release
- name: Build native release - name: Build native release
run: python3 modulus.py build --target x86_64_gcc12_release run: python3 modulus.py build --target x86_64_gcc12_release
- name: Test native release
run: python3 modulus.py test --target x86_64_gcc12_release
- name: Fetch Pico SDK - name: Fetch Pico SDK
if: ${{ gitea.event_name == 'workflow_dispatch' || (gitea.event_name == 'push' && gitea.ref == 'refs/heads/master') }} if: ${{ gitea.event_name == 'workflow_dispatch' || (gitea.event_name == 'push' && gitea.ref == 'refs/heads/master') }}
env: env:
+2 -2
View File
@@ -30,8 +30,8 @@ The AP setup pins NetworkManager to 2.4 GHz channel 6 with WPA2/RSN and CCMP cip
On Raspberry Pi OS Lite, the installed display service owns `tty1`, conflicts with `getty@tty1.service`, and starts the Zero container through `product-vamp-start-zero`. The wrapper switches to the display VT when `chvt` is available and runs `vamp-zero` inside the container through `setsid --wait --ctty` with `/dev/tty1` as stdin, giving raylib's DRM backend a real virtual terminal while keeping Docker attached to the long-running display process. SSH remains the normal admin path and `tty2` is enabled as the fallback local console. On Raspberry Pi OS Lite, the installed display service owns `tty1`, conflicts with `getty@tty1.service`, and starts the Zero container through `product-vamp-start-zero`. The wrapper switches to the display VT when `chvt` is available and runs `vamp-zero` inside the container through `setsid --wait --ctty` with `/dev/tty1` as stdin, giving raylib's DRM backend a real virtual terminal while keeping Docker attached to the long-running display process. SSH remains the normal admin path and `tty2` is enabled as the fallback local console.
At startup the Zero scans `share/product-vamp/songs/*.json` and `share/product-vamp/practice/<type>/<lesson_group>/<exercise>/<key>.json`, parses generated score JSON, imports both catalogs into SQLite with Noto, then reloads runtime menu state from the database. Runtime navigation and rendering use that loaded Noto-backed state instead of rescanning asset folders. The UI supports a top-level mode menu, Performance song/part/score menus, and Practice type/lesson/exercise/key/score menus. Melody practice offers key selection by choosing a pre-generated score variant; chord practice skips key selection and renders the authored score key directly. The score renderer is intentionally local and compact for now: it draws title/composer/part metadata, systems, measures, enlarged chord symbols and section labels, Petaluma SMuFL noteheads/rests/flags/dots/accidentals, simple vector stems/ties, and lyrics. Slash notation is represented explicitly with `notehead: "slash"` in generated JSON. At startup the Zero scans `share/product-vamp/songs/*.json` and `share/product-vamp/practice/<type>/<lesson_group>/<exercise>/<key>.json`, parses generated score JSON, imports both catalogs into SQLite with Noto, then reloads runtime menu state from the database. Runtime navigation and rendering use that loaded Noto-backed state instead of rescanning asset folders. The UI supports a top-level mode menu, Performance song/part/score menus, and Practice type/lesson/exercise/key/score menus. Melody practice offers key selection by choosing a pre-generated score variant; chord practice skips key selection and renders the authored score key directly. The score renderer draws title/composer/part metadata, systems, measures, clef/key/time preambles, enlarged chord symbols and section labels, Petaluma SMuFL noteheads/rests/flags/dots/accidentals, simple vector stems/ties, and lyrics. Slash notation is represented explicitly with `notehead: "slash"` in generated JSON.
Petaluma music symbols are placed from SMuFL bounding boxes and stem/flag anchors instead of rough font-size offsets, which keeps noteheads, rests, dots, accidentals, and eighth-note flags aligned to staff-space coordinates. Petaluma music symbols are placed from SMuFL bounding boxes and stem/flag anchors instead of rough font-size offsets, which keeps clefs, noteheads, rests, dots, accidentals, and eighth-note flags aligned to staff-space coordinates. Printed accidentals are derived from the active key signature and per-measure accidental state, so natural signs are shown only when they are musically required.
## Score Generation ## Score Generation
+1 -1
View File
@@ -1,7 +1,7 @@
option(VAMP_PICO_FIRMWARE_PROJECT "Configure this CMake invocation as the Pi Pico 2 W firmware project" OFF) option(VAMP_PICO_FIRMWARE_PROJECT "Configure this CMake invocation as the Pi Pico 2 W firmware project" OFF)
option(VAMP_BUILD_PICO_FIRMWARE "Build the Pi Pico 2 W UF2 firmware artifact" OFF) option(VAMP_BUILD_PICO_FIRMWARE "Build the Pi Pico 2 W UF2 firmware artifact" OFF)
option(VAMP_BUILD_ZERO_IMAGE "Build the Pi Zero 2 W Docker image archive artifact" OFF) option(VAMP_BUILD_ZERO_IMAGE "Build the Pi Zero 2 W Docker image archive artifact" OFF)
option(BUILD_TESTING "Compatibility option accepted by Modulus; Product Vamp does not currently define tests" OFF) option(BUILD_TESTING "Build Product Vamp tests" OFF)
set(VAMP_PICO_SDK_VERSION "2.3.0" CACHE STRING "Raspberry Pi Pico SDK git tag used when fetching the SDK") set(VAMP_PICO_SDK_VERSION "2.3.0" CACHE STRING "Raspberry Pi Pico SDK git tag used when fetching the SDK")
set(VAMP_PICO_WIFI_SSID "Vamp Control" CACHE STRING "Default Pico Wi-Fi SSID") set(VAMP_PICO_WIFI_SSID "Vamp Control" CACHE STRING "Default Pico Wi-Fi SSID")
+5
View File
@@ -1,4 +1,5 @@
include(GNUInstallDirs) include(GNUInstallDirs)
include(CTest)
set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
@@ -82,6 +83,10 @@ add_executable(vamp-zero src/vamp_zero/main.cpp)
target_compile_features(vamp-zero PRIVATE cxx_std_23) target_compile_features(vamp-zero PRIVATE cxx_std_23)
target_link_libraries(vamp-zero PRIVATE vamp-zero-core) target_link_libraries(vamp-zero PRIVATE vamp-zero-core)
if(BUILD_TESTING)
add_subdirectory(tests)
endif()
set(VAMP_BUNDLE_DIR ${CMAKE_CURRENT_BINARY_DIR}/bundle) set(VAMP_BUNDLE_DIR ${CMAKE_CURRENT_BINARY_DIR}/bundle)
set(VAMP_MANIFEST ${VAMP_BUNDLE_DIR}/manifest.json) set(VAMP_MANIFEST ${VAMP_BUNDLE_DIR}/manifest.json)
configure_file(package/manifest.json.in ${VAMP_MANIFEST} @ONLY) configure_file(package/manifest.json.in ${VAMP_MANIFEST} @ONLY)
+7
View File
@@ -1,4 +1,5 @@
import conan import conan
import conan.tools.build
import conan.tools.cmake import conan.tools.cmake
class product_vamp_conan(conan.ConanFile): class product_vamp_conan(conan.ConanFile):
@@ -47,6 +48,9 @@ class product_vamp_conan(conan.ConanFile):
"src/vamp_pico/*.cpp", "src/vamp_pico/*.cpp",
"src/vamp_protocol/*.cpp", "src/vamp_protocol/*.cpp",
"src/vamp_zero/*.cpp", "src/vamp_zero/*.cpp",
"tests/CMakeLists.txt",
"tests/*.cpp",
"tests/*.hpp",
) )
def requirements(self): def requirements(self):
@@ -72,12 +76,15 @@ class product_vamp_conan(conan.ConanFile):
toolchain = conan.tools.cmake.CMakeToolchain(self) toolchain = conan.tools.cmake.CMakeToolchain(self)
if hasattr(toolchain, "user_presets_path"): if hasattr(toolchain, "user_presets_path"):
toolchain.user_presets_path = False toolchain.user_presets_path = False
toolchain.variables["BUILD_TESTING"] = bool(self.options.with_tests)
toolchain.generate() toolchain.generate()
def build(self): def build(self):
cmake = conan.tools.cmake.CMake(self) cmake = conan.tools.cmake.CMake(self)
cmake.configure() cmake.configure()
cmake.build() cmake.build()
if self.options.with_tests and conan.tools.build.can_run(self):
cmake.test(target="check")
def package_id(self): def package_id(self):
self.info.options.rm_safe("with_tests") self.info.options.rm_safe("with_tests")
+10
View File
@@ -1,6 +1,8 @@
#pragma once #pragma once
#include <cstddef> #include <cstddef>
#include <string_view>
#include <vector>
#include <luma.hpp> #include <luma.hpp>
@@ -8,10 +10,18 @@
namespace vamp_zero { namespace vamp_zero {
enum class score_accidental_mark {
NONE,
FLAT,
NATURAL,
SHARP,
};
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; 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 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;
} }
+2
View File
@@ -48,6 +48,8 @@ struct score_document {
std::string title{}; std::string title{};
std::string composer{}; std::string composer{};
std::string key_signature{}; std::string key_signature{};
std::string time_signature{};
std::string clef{};
std::string part{}; std::string part{};
std::string arrangement_part_type{}; std::string arrangement_part_type{};
std::string source_path{}; std::string source_path{};
+21 -2
View File
@@ -359,8 +359,9 @@ auto vamp_zero::parse_score_document(std::string_view json, std::string source_p
auto title = required_string(**metadata, "title"); auto title = required_string(**metadata, "title");
auto composer = optional_string_value(**metadata, "composer", {}); auto composer = optional_string_value(**metadata, "composer", {});
auto key = optional_string_value(**metadata, "key_signature", {}); auto key = optional_string_value(**metadata, "key_signature", {});
if (!title || !composer || !key) { auto time = optional_string_value(**metadata, "time_signature", {});
return std::unexpected(!title ? title.error() : (!composer ? composer.error() : key.error())); if (!title || !composer || !key || !time) {
return std::unexpected(!title ? title.error() : (!composer ? composer.error() : (!key ? key.error() : time.error())));
} }
auto staves = require_array(**root, "staves"); auto staves = require_array(**root, "staves");
@@ -381,6 +382,10 @@ auto vamp_zero::parse_score_document(std::string_view json, std::string source_p
if (!part) { if (!part) {
return std::unexpected(part.error()); return std::unexpected(part.error());
} }
auto clef = optional_string_value(staff, "clef", "treble");
if (!clef) {
return std::unexpected(clef.error());
}
auto measures = require_array(staff, "measures"); auto measures = require_array(staff, "measures");
if (!measures) { if (!measures) {
@@ -391,6 +396,8 @@ auto vamp_zero::parse_score_document(std::string_view json, std::string source_p
.title = std::move(*title), .title = std::move(*title),
.composer = std::move(*composer), .composer = std::move(*composer),
.key_signature = std::move(*key), .key_signature = std::move(*key),
.time_signature = std::move(*time),
.clef = clef->empty() ? std::string{"treble"} : std::move(*clef),
.part = human_label(*part), .part = human_label(*part),
.arrangement_part_type = std::move(*part), .arrangement_part_type = std::move(*part),
.source_path = std::move(source_path), .source_path = std::move(source_path),
@@ -401,8 +408,20 @@ auto vamp_zero::parse_score_document(std::string_view json, std::string source_p
if (!measure) { if (!measure) {
return std::unexpected(measure.error()); return std::unexpected(measure.error());
} }
if (document.key_signature.empty() && !measure->key_signature.empty()) {
document.key_signature = measure->key_signature;
}
if (document.time_signature.empty() && !measure->time_signature.empty()) {
document.time_signature = measure->time_signature;
}
document.measures.push_back(std::move(*measure)); document.measures.push_back(std::move(*measure));
} }
if (document.key_signature.empty()) {
document.key_signature = "C";
}
if (document.time_signature.empty()) {
document.time_signature = "4/4";
}
return document; return document;
} }
+424 -47
View File
@@ -1,13 +1,16 @@
#include <vamp_zero/score.hpp> #include <vamp_zero/score.hpp>
#include <algorithm> #include <algorithm>
#include <array>
#include <cmath> #include <cmath>
#include <cstddef> #include <cstddef>
#include <cstdint> #include <cstdint>
#include <map>
#include <optional> #include <optional>
#include <string> #include <string>
#include <string_view> #include <string_view>
#include <unordered_map> #include <unordered_map>
#include <utility>
#include <vector> #include <vector>
#include <raylib.h> #include <raylib.h>
@@ -52,6 +55,13 @@ struct notehead_instance {
luma::point origin{}; luma::point origin{};
}; };
struct key_signature_position {
std::char_t letter{};
std::int32_t octave{};
};
constexpr auto glyph_g_clef = std::int32_t{0xe050};
constexpr auto glyph_f_clef = std::int32_t{0xe062};
constexpr auto glyph_notehead_whole = std::int32_t{0xe0a2}; constexpr auto glyph_notehead_whole = std::int32_t{0xe0a2};
constexpr auto glyph_notehead_half = std::int32_t{0xe0a3}; constexpr auto glyph_notehead_half = std::int32_t{0xe0a3};
constexpr auto glyph_notehead_black = std::int32_t{0xe0a4}; constexpr auto glyph_notehead_black = std::int32_t{0xe0a4};
@@ -66,6 +76,7 @@ constexpr auto glyph_augmentation_dot = std::int32_t{0xe1e7};
constexpr auto glyph_accidental_flat = std::int32_t{0xe260}; constexpr auto glyph_accidental_flat = std::int32_t{0xe260};
constexpr auto glyph_accidental_natural = std::int32_t{0xe261}; constexpr auto glyph_accidental_natural = std::int32_t{0xe261};
constexpr auto glyph_accidental_sharp = std::int32_t{0xe262}; constexpr auto glyph_accidental_sharp = std::int32_t{0xe262};
constexpr auto glyph_time_sig_0 = std::int32_t{0xe080};
auto to_raylib(luma::color color) -> Color { auto to_raylib(luma::color color) -> Color {
return { return {
@@ -104,6 +115,8 @@ auto music_font(std::float32_t size) -> Font {
} }
auto codepoints = std::vector<std::int32_t>{ auto codepoints = std::vector<std::int32_t>{
glyph_g_clef,
glyph_f_clef,
glyph_notehead_whole, glyph_notehead_whole,
glyph_notehead_half, glyph_notehead_half,
glyph_notehead_black, glyph_notehead_black,
@@ -118,6 +131,16 @@ auto music_font(std::float32_t size) -> Font {
glyph_accidental_flat, glyph_accidental_flat,
glyph_accidental_natural, glyph_accidental_natural,
glyph_accidental_sharp, glyph_accidental_sharp,
glyph_time_sig_0,
glyph_time_sig_0 + 1,
glyph_time_sig_0 + 2,
glyph_time_sig_0 + 3,
glyph_time_sig_0 + 4,
glyph_time_sig_0 + 5,
glyph_time_sig_0 + 6,
glyph_time_sig_0 + 7,
glyph_time_sig_0 + 8,
glyph_time_sig_0 + 9,
}; };
auto path = std::string{vamp_style::score_music_font_path}; auto path = std::string{vamp_style::score_music_font_path};
auto loaded = LoadFontEx(path.c_str(), key, codepoints.data(), static_cast<std::int32_t>(codepoints.size())); auto loaded = LoadFontEx(path.c_str(), key, codepoints.data(), static_cast<std::int32_t>(codepoints.size()));
@@ -130,6 +153,10 @@ auto music_font(std::float32_t size) -> Font {
auto glyph_box(std::int32_t codepoint) -> smufl_box { auto glyph_box(std::int32_t codepoint) -> smufl_box {
switch (codepoint) { switch (codepoint) {
case glyph_g_clef:
return {.west = 0.0F, .south = -2.236F, .east = 2.656F, .north = 4.036F};
case glyph_f_clef:
return {.west = 0.0F, .south = -1.984F, .east = 3.104F, .north = 0.864F};
case glyph_notehead_whole: case glyph_notehead_whole:
return {.west = 0.0F, .south = -0.744684F, .east = 1.520666F, .north = 0.66F}; return {.west = 0.0F, .south = -0.744684F, .east = 1.520666F, .north = 0.66F};
case glyph_notehead_half: case glyph_notehead_half:
@@ -158,6 +185,26 @@ auto glyph_box(std::int32_t codepoint) -> smufl_box {
return {.west = 0.0F, .south = -1.824F, .east = 0.854F, .north = 1.848F}; return {.west = 0.0F, .south = -1.824F, .east = 0.854F, .north = 1.848F};
case glyph_accidental_sharp: case glyph_accidental_sharp:
return {.west = -0.088F, .south = -1.536F, .east = 1.56F, .north = 1.532F}; return {.west = -0.088F, .south = -1.536F, .east = 1.56F, .north = 1.532F};
case glyph_time_sig_0:
return {.west = 0.08F, .south = -1.432638F, .east = 2.052F, .north = 1.436F};
case glyph_time_sig_0 + 1:
return {.west = 0.08F, .south = -1.456F, .east = 1.132F, .north = 1.464F};
case glyph_time_sig_0 + 2:
return {.west = 0.08F, .south = -1.514881F, .east = 2.641855F, .north = 1.528F};
case glyph_time_sig_0 + 3:
return {.west = 0.079297F, .south = -1.568F, .east = 2.16F, .north = 1.568F};
case glyph_time_sig_0 + 4:
return {.west = 0.08F, .south = -1.958364F, .east = 2.532314F, .north = 1.964742F};
case glyph_time_sig_0 + 5:
return {.west = 0.078655F, .south = -1.556F, .east = 2.332F, .north = 1.556F};
case glyph_time_sig_0 + 6:
return {.west = 0.08F, .south = -1.452F, .east = 2.26F, .north = 1.452F};
case glyph_time_sig_0 + 7:
return {.west = 0.081447F, .south = -1.38F, .east = 2.368F, .north = 1.38F};
case glyph_time_sig_0 + 8:
return {.west = 0.08F, .south = -1.712F, .east = 2.016604F, .north = 1.712F};
case glyph_time_sig_0 + 9:
return {.west = 0.08F, .south = -1.684F, .east = 1.976F, .north = 1.684F};
default: default:
return {}; return {};
} }
@@ -192,26 +239,22 @@ auto draw_music_origin(std::int32_t codepoint, luma::point origin, std::float32_
DrawTextEx(font, text.c_str(), {.x = draw_x, .y = draw_y}, font_size, 0.0F, to_raylib(color)); DrawTextEx(font, text.c_str(), {.x = draw_x, .y = draw_y}, font_size, 0.0F, to_raylib(color));
} }
auto origin_for_center(std::int32_t codepoint, std::float32_t center_x, std::float32_t center_y, std::float32_t staff_spacing) -> luma::point { auto origin_for_centered_baseline(std::int32_t codepoint, std::float32_t center_x, std::float32_t baseline_y, std::float32_t staff_spacing) -> luma::point {
return { return {
.x = center_x - glyph_center_x(codepoint) * staff_spacing, .x = center_x - glyph_center_x(codepoint) * staff_spacing,
.y = center_y + glyph_center_y(codepoint) * staff_spacing, .y = baseline_y,
}; };
} }
auto draw_music_centered(std::int32_t codepoint, std::float32_t center_x, std::float32_t center_y, std::float32_t staff_spacing, luma::color color) -> void { auto draw_music_centered(std::int32_t codepoint, std::float32_t center_x, std::float32_t baseline_y, std::float32_t staff_spacing, luma::color color) -> void {
draw_music_origin(codepoint, origin_for_center(codepoint, center_x, center_y, staff_spacing), staff_spacing, color); draw_music_origin(codepoint, origin_for_centered_baseline(codepoint, center_x, baseline_y, staff_spacing), staff_spacing, color);
} }
auto draw_music_centered_at_origin_y(std::int32_t codepoint, std::float32_t center_x, std::float32_t origin_y, std::float32_t staff_spacing, luma::color color) -> void { auto draw_music_left_of(std::int32_t codepoint, std::float32_t right_x, std::float32_t baseline_y, std::float32_t staff_spacing, luma::color color) -> void {
draw_music_origin(codepoint, {.x = center_x - glyph_center_x(codepoint) * staff_spacing, .y = origin_y}, staff_spacing, color);
}
auto draw_music_left_of(std::int32_t codepoint, std::float32_t right_x, std::float32_t center_y, std::float32_t staff_spacing, luma::color color) -> void {
auto box = glyph_box(codepoint); auto box = glyph_box(codepoint);
auto origin = luma::point{ auto origin = luma::point{
.x = right_x - box.east * staff_spacing, .x = right_x - box.east * staff_spacing,
.y = center_y + glyph_center_y(codepoint) * staff_spacing, .y = baseline_y,
}; };
draw_music_origin(codepoint, origin, staff_spacing, color); draw_music_origin(codepoint, origin, staff_spacing, color);
} }
@@ -359,19 +402,185 @@ auto pitch_y(vamp_zero::score_pitch const& pitch, std::float32_t staff_top, std:
return bottom_line - static_cast<std::float32_t>(pitch_index - e4_index) * (staff_spacing * 0.5F); return bottom_line - static_cast<std::float32_t>(pitch_index - e4_index) * (staff_spacing * 0.5F);
} }
auto accidental_glyph(std::string const& accidental) -> std::optional<std::int32_t> { auto accidental_glyph(vamp_zero::score_accidental_mark mark) -> std::optional<std::int32_t> {
if (accidental == "#" || accidental == "sharp") { switch (mark) {
return glyph_accidental_sharp; case vamp_zero::score_accidental_mark::FLAT:
}
if (accidental == "b" || accidental == "flat") {
return glyph_accidental_flat; return glyph_accidental_flat;
} case vamp_zero::score_accidental_mark::NATURAL:
if (accidental == "natural" || accidental == "n") {
return glyph_accidental_natural; return glyph_accidental_natural;
case vamp_zero::score_accidental_mark::SHARP:
return glyph_accidental_sharp;
case vamp_zero::score_accidental_mark::NONE:
return std::nullopt;
} }
return std::nullopt; return std::nullopt;
} }
auto normalized_letter(std::char_t letter) -> std::char_t {
if (letter >= 'A' && letter <= 'Z') {
return static_cast<std::char_t>(letter + ('a' - 'A'));
}
return letter;
}
auto pitch_alteration(std::string_view accidental) -> std::int32_t {
if (accidental == "#" || accidental == "sharp") {
return 1;
}
if (accidental == "b" || accidental == "flat") {
return -1;
}
return 0;
}
auto alteration_mark(std::int32_t alteration) -> vamp_zero::score_accidental_mark {
if (alteration < 0) {
return vamp_zero::score_accidental_mark::FLAT;
}
if (alteration > 0) {
return vamp_zero::score_accidental_mark::SHARP;
}
return vamp_zero::score_accidental_mark::NATURAL;
}
auto normalized_key(std::string_view key_signature) -> std::string {
auto key = std::string{key_signature};
key.erase(std::remove(key.begin(), key.end(), ' '), key.end());
if (key.empty()) {
return "C";
}
key.front() = static_cast<std::char_t>(key.front() >= 'a' && key.front() <= 'z' ? key.front() - ('a' - 'A') : key.front());
for (auto index = std::size_t{1}; index < key.size(); ++index) {
if (key[index] >= 'A' && key[index] <= 'Z') {
key[index] = static_cast<std::char_t>(key[index] + ('a' - 'A'));
}
}
return key;
}
auto key_signature_count(std::string_view key_signature) -> std::int32_t {
auto key = normalized_key(key_signature);
if (key == "C") {
return 0;
}
if (key == "G") {
return 1;
}
if (key == "D") {
return 2;
}
if (key == "A") {
return 3;
}
if (key == "E") {
return 4;
}
if (key == "B") {
return 5;
}
if (key == "F#" || key == "Gb") {
return key == "F#" ? 6 : -6;
}
if (key == "C#") {
return 7;
}
if (key == "F") {
return -1;
}
if (key == "Bb") {
return -2;
}
if (key == "Eb") {
return -3;
}
if (key == "Ab") {
return -4;
}
if (key == "Db") {
return -5;
}
if (key == "Cb") {
return -7;
}
return 0;
}
auto key_signature_alterations(std::string_view key_signature) -> std::array<std::int32_t, 7> {
auto alterations = std::array<std::int32_t, 7>{};
constexpr auto sharp_order = std::array<std::char_t, 7>{'f', 'c', 'g', 'd', 'a', 'e', 'b'};
constexpr auto flat_order = std::array<std::char_t, 7>{'b', 'e', 'a', 'd', 'g', 'c', 'f'};
auto count = key_signature_count(key_signature);
if (count > 0) {
for (auto index = std::int32_t{}; index < count; ++index) {
alterations[static_cast<std::size_t>(letter_index(sharp_order[static_cast<std::size_t>(index)]))] = 1;
}
} else if (count < 0) {
for (auto index = std::int32_t{}; index < -count; ++index) {
alterations[static_cast<std::size_t>(letter_index(flat_order[static_cast<std::size_t>(index)]))] = -1;
}
}
return alterations;
}
struct accidental_state {
std::array<std::int32_t, 7> key_alterations{};
std::map<std::pair<std::int32_t, std::int32_t>, std::int32_t> measure_alterations{};
};
auto make_accidental_state(std::string_view key_signature) -> accidental_state {
return {
.key_alterations = key_signature_alterations(key_signature),
};
}
auto notated_accidental_for_pitch(accidental_state& state, vamp_zero::score_pitch const& pitch, std::bool_t tied_from_previous) -> vamp_zero::score_accidental_mark {
if (tied_from_previous) {
return vamp_zero::score_accidental_mark::NONE;
}
auto const letter = letter_index(normalized_letter(pitch.letter));
auto const desired = pitch_alteration(pitch.accidental);
auto const key = std::pair{letter, pitch.octave};
auto const found = state.measure_alterations.find(key);
auto const current = found == state.measure_alterations.end() ? state.key_alterations[static_cast<std::size_t>(letter)] : found->second;
if (desired == current) {
return vamp_zero::score_accidental_mark::NONE;
}
state.measure_alterations[key] = desired;
return alteration_mark(desired);
}
auto notated_accidentals_for_note(accidental_state& state, vamp_zero::score_note const& note, std::bool_t slash) -> std::vector<vamp_zero::score_accidental_mark> {
auto marks = std::vector<vamp_zero::score_accidental_mark>{};
if (note.hidden || note.kind == "rest" || slash) {
return marks;
}
marks.reserve(note.pitches.size());
for (auto const& pitch : note.pitches) {
marks.push_back(notated_accidental_for_pitch(state, pitch, note.tie_from_previous));
}
return marks;
}
auto effective_key_signature(vamp_zero::score_document const& score, vamp_zero::score_measure const& measure) -> std::string_view {
if (!measure.key_signature.empty()) {
return measure.key_signature;
}
if (!score.key_signature.empty()) {
return score.key_signature;
}
return "C";
}
auto effective_time_signature(vamp_zero::score_document const& score, vamp_zero::score_measure const* measure) -> std::string_view {
if (measure != nullptr && !measure->time_signature.empty()) {
return measure->time_signature;
}
if (!score.time_signature.empty()) {
return score.time_signature;
}
return "4/4";
}
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;
@@ -395,6 +604,142 @@ auto draw_staff(luma::draw_context& context, std::float32_t x, std::float32_t y,
} }
} }
auto normalized_clef(std::string_view clef) -> std::string {
auto value = std::string{clef};
for (auto& ch : value) {
if (ch >= 'A' && ch <= 'Z') {
ch = static_cast<std::char_t>(ch + ('a' - 'A'));
}
}
return value;
}
auto draw_clef(luma::draw_context&, std::string_view clef, std::float32_t x, std::float32_t staff_top, std::float32_t spacing, luma::color ink) -> std::float32_t {
auto name = normalized_clef(clef);
auto codepoint = name == "bass" || name == "f" ? glyph_f_clef : glyph_g_clef;
auto baseline_y = codepoint == glyph_f_clef ? staff_top + spacing : staff_top + spacing * 3.0F;
draw_music_origin(codepoint, {.x = x, .y = baseline_y}, spacing, ink);
return glyph_width(codepoint, spacing) + spacing * 0.95F;
}
auto key_signature_pitch(std::int32_t count, std::size_t index) -> vamp_zero::score_pitch {
constexpr auto sharp_positions = std::array<key_signature_position, 7>{{
{.letter = 'f', .octave = 5},
{.letter = 'c', .octave = 5},
{.letter = 'g', .octave = 5},
{.letter = 'd', .octave = 5},
{.letter = 'a', .octave = 4},
{.letter = 'e', .octave = 5},
{.letter = 'b', .octave = 4},
}};
constexpr auto flat_positions = std::array<key_signature_position, 7>{{
{.letter = 'b', .octave = 4},
{.letter = 'e', .octave = 5},
{.letter = 'a', .octave = 4},
{.letter = 'd', .octave = 5},
{.letter = 'g', .octave = 4},
{.letter = 'c', .octave = 5},
{.letter = 'f', .octave = 4},
}};
auto const position = count > 0 ? sharp_positions[index] : flat_positions[index];
return {
.letter = position.letter,
.octave = position.octave,
};
}
auto draw_key_signature(luma::draw_context&, std::string_view key_signature, std::float32_t x, std::float32_t staff_top, std::float32_t spacing, luma::color ink) -> std::float32_t {
auto count = key_signature_count(key_signature);
if (count == 0) {
return 0.0F;
}
auto const glyph = count > 0 ? glyph_accidental_sharp : glyph_accidental_flat;
auto const step = glyph_width(glyph, spacing) * (count > 0 ? 0.72F : 0.86F);
auto current_x = x;
auto const total = static_cast<std::size_t>(std::abs(count));
for (auto index = std::size_t{}; index < total; ++index) {
auto pitch = key_signature_pitch(count, index);
draw_music_origin(glyph, {.x = current_x, .y = pitch_y(pitch, staff_top, spacing)}, spacing, ink);
current_x += step;
}
return current_x - x + spacing * 0.45F;
}
auto time_signature_digit(std::char_t digit) -> std::optional<std::int32_t> {
if (digit < '0' || digit > '9') {
return std::nullopt;
}
return glyph_time_sig_0 + static_cast<std::int32_t>(digit - '0');
}
auto time_signature_run_width(std::string_view text, std::float32_t spacing) -> std::float32_t {
auto width = std::float32_t{};
for (auto digit : text) {
auto codepoint = time_signature_digit(digit);
if (!codepoint) {
continue;
}
if (width > 0.0F) {
width += spacing * 0.16F;
}
width += glyph_width(*codepoint, spacing);
}
return width;
}
auto draw_time_signature_run(std::string_view text, std::float32_t x, std::float32_t baseline_y, std::float32_t spacing, luma::color ink) -> void {
auto current_x = x;
for (auto digit : text) {
auto codepoint = time_signature_digit(digit);
if (!codepoint) {
continue;
}
draw_music_origin(*codepoint, {.x = current_x, .y = baseline_y}, spacing, ink);
current_x += glyph_width(*codepoint, spacing) + spacing * 0.16F;
}
}
auto draw_time_signature(luma::draw_context&, std::string_view time_signature, std::float32_t x, std::float32_t staff_top, std::float32_t spacing, luma::color ink) -> std::float32_t {
auto slash = time_signature.find('/');
if (slash == std::string_view::npos) {
return 0.0F;
}
auto numerator = time_signature.substr(0, slash);
auto denominator = time_signature.substr(slash + 1U);
auto numerator_width = time_signature_run_width(numerator, spacing);
auto denominator_width = time_signature_run_width(denominator, spacing);
auto width = std::max(numerator_width, denominator_width);
if (width <= 0.0F) {
return 0.0F;
}
draw_time_signature_run(numerator, x + (width - numerator_width) * 0.5F, staff_top + spacing, spacing, ink);
draw_time_signature_run(denominator, x + (width - denominator_width) * 0.5F, staff_top + spacing * 3.0F, spacing, ink);
return width + spacing * 0.95F;
}
auto draw_system_preamble(
luma::draw_context& context,
vamp_zero::score_document const& score,
vamp_zero::score_measure const* first_measure,
std::float32_t x,
std::float32_t staff_top,
std::float32_t spacing,
std::bool_t show_time_signature
) -> std::float32_t {
auto ink = vamp_zero::to_luma(vamp_style::paper_ink);
auto current_x = x + spacing * 0.35F;
current_x += draw_clef(context, score.clef, current_x, staff_top, spacing, ink);
auto key_width = draw_key_signature(context, first_measure == nullptr ? score.key_signature : effective_key_signature(score, *first_measure), current_x, staff_top, spacing, ink);
current_x += key_width;
if (key_width == 0.0F) {
current_x += spacing * 0.35F;
}
if (show_time_signature) {
current_x += draw_time_signature(context, effective_time_signature(score, first_measure), current_x, staff_top, spacing, ink);
}
return current_x + spacing * 0.25F;
}
auto draw_duration_dot(luma::draw_context& context, std::float32_t x, std::float32_t y, std::float32_t spacing, luma::color ink) -> void { auto draw_duration_dot(luma::draw_context& context, std::float32_t x, std::float32_t y, std::float32_t spacing, luma::color ink) -> void {
static_cast<void>(context); static_cast<void>(context);
draw_music_centered(glyph_augmentation_dot, x, y, spacing, ink); draw_music_centered(glyph_augmentation_dot, x, y, spacing, ink);
@@ -450,7 +795,7 @@ auto draw_rest(luma::draw_context& context, duration_shape duration, std::float3
if (centered) { if (centered) {
draw_music_centered(codepoint, x, center, spacing, ink); draw_music_centered(codepoint, x, center, spacing, ink);
} else { } else {
draw_music_centered_at_origin_y(codepoint, x, origin_y, spacing, ink); draw_music_centered(codepoint, x, origin_y, spacing, ink);
} }
if (duration.dots != 0) { if (duration.dots != 0) {
draw_duration_dot(context, x + glyph_width(codepoint, spacing) * 0.5F + spacing, center, spacing, ink); draw_duration_dot(context, x + glyph_width(codepoint, spacing) * 0.5F + spacing, center, spacing, ink);
@@ -475,12 +820,21 @@ auto notehead_codepoint(duration_shape duration, std::bool_t slash) -> std::int3
auto draw_notehead(duration_shape duration, std::bool_t slash, std::float32_t x, std::float32_t y, std::float32_t spacing, luma::color ink) -> notehead_instance { auto draw_notehead(duration_shape duration, std::bool_t slash, std::float32_t x, std::float32_t y, std::float32_t spacing, luma::color ink) -> notehead_instance {
auto codepoint = notehead_codepoint(duration, slash); auto codepoint = notehead_codepoint(duration, slash);
auto origin = origin_for_center(codepoint, x, y, spacing); auto origin = origin_for_centered_baseline(codepoint, x, y, spacing);
draw_music_origin(codepoint, origin, spacing, ink); draw_music_origin(codepoint, origin, spacing, ink);
return {.center_y = y, .origin = origin}; return {.center_y = y, .origin = origin};
} }
auto draw_note(luma::draw_context& context, vamp_zero::score_note const& note, std::float32_t x, std::float32_t staff_top, std::float32_t spacing, std::float32_t next_x, std::bool_t slash) -> void { auto draw_note(
luma::draw_context& context,
vamp_zero::score_note const& note,
std::float32_t x,
std::float32_t staff_top,
std::float32_t spacing,
std::float32_t next_x,
std::bool_t slash,
std::vector<vamp_zero::score_accidental_mark> const& accidentals
) -> void {
if (note.hidden) { if (note.hidden) {
return; return;
} }
@@ -508,11 +862,13 @@ auto draw_note(luma::draw_context& context, vamp_zero::score_note const& note, s
low_y = std::max(low_y, yy); low_y = std::max(low_y, yy);
high_y = std::min(high_y, yy); high_y = std::min(high_y, yy);
} }
for (auto const& pitch : note.pitches) { for (auto pitch_index = std::size_t{}; pitch_index < note.pitches.size(); ++pitch_index) {
auto const& pitch = note.pitches[pitch_index];
auto yy = std::clamp(pitch_y(pitch, staff_top, spacing), staff_top - spacing * 2.0F, bottom + spacing * 2.0F); auto yy = std::clamp(pitch_y(pitch, staff_top, spacing), staff_top - spacing * 2.0F, bottom + spacing * 2.0F);
draw_ledger_lines(context, x, yy, staff_top, spacing, ink); draw_ledger_lines(context, x, yy, staff_top, spacing, ink);
if (auto accidental = accidental_glyph(pitch.accidental)) { auto mark = pitch_index < accidentals.size() ? accidentals[pitch_index] : vamp_zero::score_accidental_mark::NONE;
auto head_origin = origin_for_center(notehead_codepoint(duration, false), x, yy, spacing); if (auto accidental = accidental_glyph(mark)) {
auto head_origin = origin_for_centered_baseline(notehead_codepoint(duration, false), x, yy, spacing);
auto head_left = head_origin.x + glyph_box(notehead_codepoint(duration, false)).west * spacing; auto head_left = head_origin.x + glyph_box(notehead_codepoint(duration, false)).west * spacing;
draw_music_left_of(*accidental, head_left - spacing * 0.65F, yy, spacing, ink); draw_music_left_of(*accidental, head_left - spacing * 0.65F, yy, spacing, ink);
} }
@@ -524,7 +880,7 @@ auto draw_note(luma::draw_context& context, vamp_zero::score_note const& note, s
if (has_stem(duration) && !heads.empty()) { if (has_stem(duration) && !heads.empty()) {
auto stem_up = (low_y + high_y) * 0.5F >= center; auto stem_up = (low_y + high_y) * 0.5F >= center;
if (stem_up) { if (stem_up) {
auto base_origin = origin_for_center(notehead, x, low_y, spacing); auto base_origin = origin_for_centered_baseline(notehead, x, low_y, spacing);
auto base = anchor_point(base_origin, stem_up_anchor(notehead), spacing); auto base = anchor_point(base_origin, stem_up_anchor(notehead), spacing);
auto stem_top = std::min(high_y, center) - spacing * 3.6F; auto stem_top = std::min(high_y, center) - spacing * 3.6F;
context.stroke_line(base, {.x = base.x, .y = stem_top}, 1.6F, ink); context.stroke_line(base, {.x = base.x, .y = stem_top}, 1.6F, ink);
@@ -532,7 +888,7 @@ auto draw_note(luma::draw_context& context, vamp_zero::score_note const& note, s
draw_eighth_flag(true, base.x, stem_top, spacing, ink); draw_eighth_flag(true, base.x, stem_top, spacing, ink);
} }
} else { } else {
auto base_origin = origin_for_center(notehead, x, high_y, spacing); auto base_origin = origin_for_centered_baseline(notehead, x, high_y, spacing);
auto base = anchor_point(base_origin, stem_down_anchor(notehead), spacing); auto base = anchor_point(base_origin, stem_down_anchor(notehead), spacing);
auto stem_bottom = std::max(low_y, center) + spacing * 3.6F; auto stem_bottom = std::max(low_y, center) + spacing * 3.6F;
context.stroke_line(base, {.x = base.x, .y = stem_bottom}, 1.6F, ink); context.stroke_line(base, {.x = base.x, .y = stem_bottom}, 1.6F, ink);
@@ -543,7 +899,7 @@ auto draw_note(luma::draw_context& context, vamp_zero::score_note const& note, s
} }
if (duration.dots != 0) { if (duration.dots != 0) {
auto dot_origin = origin_for_center(notehead, x, low_y, spacing); auto dot_origin = origin_for_centered_baseline(notehead, x, low_y, spacing);
auto dot_x = dot_origin.x + glyph_box(notehead).east * spacing + spacing * 0.85F; auto dot_x = dot_origin.x + glyph_box(notehead).east * spacing + spacing * 0.85F;
draw_duration_dot(context, dot_x, dot_y_for(low_y, staff_top, spacing), spacing, ink); draw_duration_dot(context, dot_x, dot_y_for(low_y, staff_top, spacing), spacing, ink);
} }
@@ -558,22 +914,26 @@ auto note_x(std::float32_t start_x, std::float32_t width, std::int64_t total_tic
return start_x + width * ratio; return start_x + width * ratio;
} }
auto draw_measure(luma::draw_context& context, vamp_zero::score_measure const& measure, luma::rect bounds, std::size_t measure_number) -> void { auto draw_measure(
constexpr auto spacing = std::float32_t{8.0F}; luma::draw_context& context,
auto staff_top = bounds.y + 52.0F; vamp_zero::score_document const& score,
draw_text(context, bounds.x + 5.0F, bounds.y + 1.0F, std::to_string(measure_number), 1, vamp_style::muted, 42.0F); vamp_zero::score_measure const& measure,
luma::rect bounds,
std::size_t measure_number,
std::float32_t staff_top,
std::float32_t spacing
) -> void {
draw_text(context, bounds.x + spacing * 0.2F, bounds.y + spacing * 0.05F, std::to_string(measure_number), 1, vamp_style::muted, spacing * 4.0F);
if (!measure.section_label.empty()) { if (!measure.section_label.empty()) {
draw_text(context, bounds.x + 28.0F, bounds.y - 2.0F, measure.section_label, 2, vamp_style::selection, bounds.width - 32.0F); draw_text(context, bounds.x + spacing * 2.3F, bounds.y - spacing * 0.25F, measure.section_label, 2, vamp_style::selection, bounds.width - spacing * 2.7F);
}
if (!measure.time_signature.empty() && measure_number == 1U) {
draw_text(context, bounds.x + 8.0F, staff_top + 2.0F, measure.time_signature, 1, vamp_style::paper_ink, 42.0F);
} }
auto notation_x = bounds.x + (measure_number == 1U ? 43.0F : 24.0F); auto notation_x = bounds.x + spacing * 1.4F;
auto notation_width = std::max(24.0F, bounds.width - (notation_x - bounds.x) - 16.0F); auto notation_width = std::max(spacing * 3.0F, bounds.width - (notation_x - bounds.x) - spacing);
auto total_ticks = measure_ticks(measure); auto total_ticks = measure_ticks(measure);
auto elapsed = std::int64_t{}; auto elapsed = std::int64_t{};
auto previous_chord = std::string{}; auto previous_chord = std::string{};
auto accidentals = make_accidental_state(effective_key_signature(score, measure));
for (auto index = std::size_t{}; index < measure.notes.size(); ++index) { for (auto index = std::size_t{}; index < measure.notes.size(); ++index) {
auto const& note = measure.notes[index]; auto const& note = measure.notes[index];
auto duration_ticks = spacing_ticks(note); auto duration_ticks = spacing_ticks(note);
@@ -581,12 +941,14 @@ auto draw_measure(luma::draw_context& context, vamp_zero::score_measure const& m
auto next_duration = index + 1U < measure.notes.size() ? spacing_ticks(measure.notes[index + 1U]) : duration_ticks; auto next_duration = index + 1U < measure.notes.size() ? spacing_ticks(measure.notes[index + 1U]) : duration_ticks;
auto next_x = note_x(notation_x, notation_width, total_ticks, elapsed + duration_ticks, next_duration); auto next_x = note_x(notation_x, notation_width, total_ticks, elapsed + duration_ticks, next_duration);
if (!note.hidden && !note.chord_symbol.empty() && note.chord_symbol != previous_chord) { if (!note.hidden && !note.chord_symbol.empty() && note.chord_symbol != previous_chord) {
draw_text(context, x - 18.0F, bounds.y + 24.0F, note.chord_symbol, 2, vamp_style::paper_ink, std::min(92.0F, bounds.width - (x - bounds.x))); draw_text(context, x - spacing * 1.5F, bounds.y + spacing * 2.0F, note.chord_symbol, 2, vamp_style::paper_ink, std::min(spacing * 10.0F, bounds.width - (x - bounds.x)));
previous_chord = note.chord_symbol; previous_chord = note.chord_symbol;
} }
draw_note(context, note, x, staff_top, spacing, next_x, note.notehead == "slash"); auto const slash = note.notehead == "slash";
auto accidental_marks = notated_accidentals_for_note(accidentals, note, slash);
draw_note(context, note, x, staff_top, spacing, next_x, slash, accidental_marks);
if (!note.hidden && !note.lyric.empty()) { if (!note.hidden && !note.lyric.empty()) {
draw_text(context, x - 12.0F, staff_top + 46.0F, note.lyric, 1, vamp_style::muted, 66.0F); draw_text(context, x - spacing, staff_top + spacing * 5.4F, note.lyric, 1, vamp_style::muted, spacing * 8.0F);
} }
elapsed += duration_ticks; elapsed += duration_ticks;
} }
@@ -594,11 +956,22 @@ auto draw_measure(luma::draw_context& context, vamp_zero::score_measure const& m
} }
auto vamp_zero::notated_accidentals_for_measure(vamp_zero::score_measure const& measure, std::string_view inherited_key_signature) -> std::vector<vamp_zero::score_accidental_mark> {
auto state = make_accidental_state(measure.key_signature.empty() ? inherited_key_signature : measure.key_signature);
auto accidentals = std::vector<vamp_zero::score_accidental_mark>{};
for (auto const& note : measure.notes) {
auto marks = notated_accidentals_for_note(state, note, note.notehead == "slash");
accidentals.insert(accidentals.end(), marks.begin(), marks.end());
}
return accidentals;
}
auto vamp_zero::draw_score_page(luma::draw_context& context, luma::rect bounds, vamp_zero::score_document const& score, std::size_t page_index) -> void { auto vamp_zero::draw_score_page(luma::draw_context& context, luma::rect bounds, vamp_zero::score_document const& score, std::size_t page_index) -> void {
context.fill_rect(bounds, vamp_zero::to_luma(vamp_style::paper)); context.fill_rect(bounds, vamp_zero::to_luma(vamp_style::paper));
context.stroke_rect(bounds, 2.0F, vamp_zero::to_luma(vamp_style::rule)); context.stroke_rect(bounds, 2.0F, vamp_zero::to_luma(vamp_style::rule));
context.push_clip(bounds); context.push_clip(bounds);
constexpr auto spacing = std::float32_t{12.0F};
auto margin = std::float32_t{28.0F}; auto margin = std::float32_t{28.0F};
auto header_y = bounds.y + 16.0F; auto header_y = bounds.y + 16.0F;
draw_text(context, bounds.x + margin, header_y, score.title, 3, vamp_style::paper_ink, bounds.width * 0.55F); draw_text(context, bounds.x + margin, header_y, score.title, 3, vamp_style::paper_ink, bounds.width * 0.55F);
@@ -610,22 +983,26 @@ auto vamp_zero::draw_score_page(luma::draw_context& context, luma::rect bounds,
auto content_y = bounds.y + 84.0F; auto content_y = bounds.y + 84.0F;
auto content_width = bounds.width - margin * 2.0F; auto content_width = bounds.width - margin * 2.0F;
auto system_height = (bounds.height - 108.0F) / static_cast<std::float32_t>(vamp_zero::score_systems_per_page); auto system_height = (bounds.height - 108.0F) / static_cast<std::float32_t>(vamp_zero::score_systems_per_page);
auto measure_width = content_width / static_cast<std::float32_t>(vamp_zero::score_measures_per_system);
auto first_measure = page_index * vamp_zero::score_measures_per_page; auto first_measure = page_index * vamp_zero::score_measures_per_page;
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 system_y = content_y + static_cast<std::float32_t>(system) * system_height; auto system_y = content_y + static_cast<std::float32_t>(system) * system_height;
auto staff_top = system_y + 52.0F; auto staff_top = system_y + spacing * 4.25F;
draw_staff(context, content_x, staff_top, content_width, 8.0F); auto system_first_measure = first_measure + system * vamp_zero::score_measures_per_system;
auto const* first_system_measure = system_first_measure < score.measures.size() ? &score.measures[system_first_measure] : nullptr;
draw_staff(context, content_x, staff_top, content_width, spacing);
auto measure_start_x = draw_system_preamble(context, score, first_system_measure, content_x, staff_top, spacing, page_index == 0U && system == 0U);
auto measure_area_width = std::max(spacing * 12.0F, content_x + content_width - measure_start_x);
auto measure_width = measure_area_width / static_cast<std::float32_t>(vamp_zero::score_measures_per_system);
for (auto measure = std::size_t{}; measure < vamp_zero::score_measures_per_system; ++measure) { for (auto measure = std::size_t{}; measure < vamp_zero::score_measures_per_system; ++measure) {
auto measure_index = first_measure + system * vamp_zero::score_measures_per_system + measure; auto measure_index = system_first_measure + measure;
auto x = content_x + static_cast<std::float32_t>(measure) * measure_width; auto x = measure_start_x + static_cast<std::float32_t>(measure) * measure_width;
context.stroke_line({.x = x, .y = staff_top}, {.x = x, .y = staff_top + 32.0F}, 1.2F, vamp_zero::to_luma(vamp_style::paper_ink)); context.stroke_line({.x = x, .y = staff_top}, {.x = x, .y = staff_top + spacing * 4.0F}, 1.2F, vamp_zero::to_luma(vamp_style::paper_ink));
if (measure_index < score.measures.size()) { if (measure_index < score.measures.size()) {
draw_measure(context, score.measures[measure_index], {.x = x, .y = system_y, .width = measure_width, .height = system_height}, measure_index + 1U); draw_measure(context, score, score.measures[measure_index], {.x = x, .y = system_y, .width = measure_width, .height = system_height}, measure_index + 1U, staff_top, spacing);
} }
} }
context.stroke_line({.x = content_x + content_width, .y = staff_top}, {.x = content_x + content_width, .y = staff_top + 32.0F}, 2.0F, vamp_zero::to_luma(vamp_style::paper_ink)); context.stroke_line({.x = content_x + content_width, .y = staff_top}, {.x = content_x + content_width, .y = staff_top + spacing * 4.0F}, 2.0F, vamp_zero::to_luma(vamp_style::paper_ink));
} }
context.pop_clip(); context.pop_clip();
+20
View File
@@ -0,0 +1,20 @@
add_executable(product-vamp-test
test.cpp
)
target_include_directories(product-vamp-test PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
)
target_link_libraries(product-vamp-test PRIVATE
vamp-zero-core
)
target_compile_features(product-vamp-test PRIVATE cxx_std_23)
add_test(NAME product-vamp COMMAND product-vamp-test)
set_tests_properties(product-vamp PROPERTIES TIMEOUT 60)
add_custom_target(check
COMMAND ${CMAKE_CTEST_COMMAND}
--test-dir ${CMAKE_BINARY_DIR}
-R ^product-vamp$
--output-on-failure
--timeout 60
USES_TERMINAL
)
+218
View File
@@ -0,0 +1,218 @@
#include <test.hpp>
#include <initializer_list>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include <vamp_zero/library.hpp>
#include <vamp_zero/score.hpp>
#include <vamp_zero/types.hpp>
namespace {
auto pitch(std::char_t letter, std::string accidental, std::int32_t octave = 4) -> vamp_zero::score_pitch {
return {
.letter = letter,
.octave = octave,
.accidental = std::move(accidental),
};
}
auto note(
std::initializer_list<vamp_zero::score_pitch> pitches,
std::bool_t tie_from_previous = false,
std::string notehead = {},
std::bool_t hidden = false
) -> vamp_zero::score_note {
return {
.kind = "note",
.hidden = hidden,
.duration_ticks = 4096,
.display_ticks = 4096,
.notehead = std::move(notehead),
.tie_from_previous = tie_from_previous,
.pitches = std::vector<vamp_zero::score_pitch>{pitches},
};
}
auto rest(std::initializer_list<vamp_zero::score_pitch> pitches) -> vamp_zero::score_note {
return {
.kind = "rest",
.duration_ticks = 4096,
.display_ticks = 4096,
.pitches = std::vector<vamp_zero::score_pitch>{pitches},
};
}
auto mark_name(vamp_zero::score_accidental_mark mark) -> std::string {
switch (mark) {
case vamp_zero::score_accidental_mark::NONE:
return "none";
case vamp_zero::score_accidental_mark::FLAT:
return "flat";
case vamp_zero::score_accidental_mark::NATURAL:
return "natural";
case vamp_zero::score_accidental_mark::SHARP:
return "sharp";
}
return {};
}
auto mark_names(std::vector<vamp_zero::score_accidental_mark> const& marks) -> std::vector<std::string> {
auto names = std::vector<std::string>{};
names.reserve(marks.size());
for (auto mark : marks) {
names.push_back(mark_name(mark));
}
return names;
}
void expect_marks(std::vector<vamp_zero::score_accidental_mark> const& marks, std::initializer_list<std::string_view> expected) {
auto actual_names = mark_names(marks);
BOOST_REQUIRE_EQUAL(actual_names.size(), expected.size());
auto index = std::size_t{};
for (auto name : expected) {
BOOST_TEST(actual_names[index] == std::string{name});
++index;
}
}
}
BOOST_AUTO_TEST_CASE(score_parser_preserves_notation_preamble) {
auto parsed = vamp_zero::parse_score_document(R"json(
{
"version": 1,
"metadata": {
"title": "Standard Tune",
"composer": "Composer",
"key_signature": "Eb",
"time_signature": "3/4"
},
"staves": [
{
"id": "staff-lead",
"name": "Lead",
"clef": "treble",
"role": "lead",
"measures": [
{
"id": "measure-1",
"time_signature": "3/4",
"key_signature": "Eb",
"voices": [
{
"id": "voice-1",
"notes": [
{
"id": "note-1",
"kind": "note",
"duration_ticks": 4096,
"pitches": [
{
"letter": "b",
"octave": 4,
"accidental": "b"
}
]
}
]
}
]
}
]
}
]
}
)json", "standard-tune.json");
BOOST_REQUIRE(static_cast<std::bool_t>(parsed));
BOOST_TEST(parsed->title == "Standard Tune");
BOOST_TEST(parsed->composer == "Composer");
BOOST_TEST(parsed->key_signature == "Eb");
BOOST_TEST(parsed->time_signature == "3/4");
BOOST_TEST(parsed->clef == "treble");
BOOST_TEST(parsed->part == "Lead");
BOOST_REQUIRE_EQUAL(parsed->measures.size(), 1U);
BOOST_TEST(parsed->measures.front().key_signature == "Eb");
BOOST_TEST(parsed->measures.front().time_signature == "3/4");
}
BOOST_AUTO_TEST_CASE(accidentals_are_suppressed_when_the_key_signature_already_covers_the_pitch) {
auto c_major = vamp_zero::score_measure{
.key_signature = "C",
.notes = {
note({pitch('c', "n")}),
note({pitch('d', "n")}),
note({pitch('e', "n")}),
},
};
expect_marks(vamp_zero::notated_accidentals_for_measure(c_major, "C"), {"none", "none", "none"});
auto f_major = vamp_zero::score_measure{
.key_signature = "F",
.notes = {
note({pitch('b', "b")}),
note({pitch('e', "n")}),
},
};
expect_marks(vamp_zero::notated_accidentals_for_measure(f_major, "C"), {"none", "none"});
auto d_major = vamp_zero::score_measure{
.key_signature = "D",
.notes = {
note({pitch('f', "#")}),
note({pitch('c', "#")}),
},
};
expect_marks(vamp_zero::notated_accidentals_for_measure(d_major, "C"), {"none", "none"});
}
BOOST_AUTO_TEST_CASE(accidentals_follow_measure_state_after_the_first_printed_change) {
auto measure = vamp_zero::score_measure{
.key_signature = "F",
.notes = {
note({pitch('b', "b")}),
note({pitch('b', "n")}),
note({pitch('b', "n")}),
note({pitch('b', "b")}),
note({pitch('b', "b")}),
},
};
expect_marks(vamp_zero::notated_accidentals_for_measure(measure, "C"), {"none", "natural", "none", "flat", "none"});
}
BOOST_AUTO_TEST_CASE(measure_state_resets_for_each_measure) {
auto first = vamp_zero::score_measure{
.key_signature = "C",
.notes = {
note({pitch('f', "#")}),
note({pitch('f', "#")}),
},
};
expect_marks(vamp_zero::notated_accidentals_for_measure(first, "C"), {"sharp", "none"});
auto second = vamp_zero::score_measure{
.key_signature = "C",
.notes = {
note({pitch('f', "#")}),
},
};
expect_marks(vamp_zero::notated_accidentals_for_measure(second, "C"), {"sharp"});
}
BOOST_AUTO_TEST_CASE(tied_rests_hidden_and_slash_notes_do_not_emit_accidentals) {
auto measure = vamp_zero::score_measure{
.key_signature = "C",
.notes = {
note({pitch('f', "#")}, true),
rest({pitch('b', "b")}),
note({pitch('c', "#")}, false, "slash"),
note({pitch('d', "#")}, false, {}, true),
note({pitch('e', "b")}),
},
};
expect_marks(vamp_zero::notated_accidentals_for_measure(measure, "C"), {"none", "flat"});
}
+4
View File
@@ -0,0 +1,4 @@
#pragma once
#define BOOST_TEST_MODULE product_vamp
#include <boost/test/included/unit_test.hpp>