2376 lines
85 KiB
C++
2376 lines
85 KiB
C++
#include <vamp_zero/score.hpp>
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cmath>
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <limits>
|
|
#include <map>
|
|
#include <optional>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <unordered_map>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include <raylib.h>
|
|
|
|
#include <vamp_style/font.hpp>
|
|
#include <vamp_zero/color.hpp>
|
|
|
|
namespace {
|
|
|
|
constexpr auto whole_note_ticks = std::int64_t{16384};
|
|
constexpr auto half_note_ticks = std::int64_t{8192};
|
|
constexpr auto quarter_note_ticks = std::int64_t{4096};
|
|
constexpr auto eighth_note_ticks = std::int64_t{2048};
|
|
|
|
enum class duration_base {
|
|
WHOLE,
|
|
HALF,
|
|
QUARTER,
|
|
EIGHTH,
|
|
};
|
|
|
|
struct duration_shape {
|
|
duration_base base{duration_base::QUARTER};
|
|
std::int32_t dots{};
|
|
std::int64_t ticks{quarter_note_ticks};
|
|
};
|
|
|
|
struct smufl_box {
|
|
std::float32_t west{};
|
|
std::float32_t south{};
|
|
std::float32_t east{};
|
|
std::float32_t north{};
|
|
};
|
|
|
|
struct smufl_anchor {
|
|
std::float32_t x{};
|
|
std::float32_t y{};
|
|
};
|
|
|
|
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_half = std::int32_t{0xe0a3};
|
|
constexpr auto glyph_notehead_black = std::int32_t{0xe0a4};
|
|
constexpr auto glyph_notehead_slash = std::int32_t{0xe101};
|
|
constexpr auto glyph_rest_whole = std::int32_t{0xe4e3};
|
|
constexpr auto glyph_rest_half = std::int32_t{0xe4e4};
|
|
constexpr auto glyph_rest_quarter = std::int32_t{0xe4e5};
|
|
constexpr auto glyph_rest_eighth = std::int32_t{0xe4e6};
|
|
constexpr auto glyph_flag_eighth_up = std::int32_t{0xe240};
|
|
constexpr auto glyph_flag_eighth_down = std::int32_t{0xe241};
|
|
constexpr auto glyph_augmentation_dot = std::int32_t{0xe1e7};
|
|
constexpr auto glyph_accidental_flat = std::int32_t{0xe260};
|
|
constexpr auto glyph_accidental_natural = std::int32_t{0xe261};
|
|
constexpr auto glyph_accidental_sharp = std::int32_t{0xe262};
|
|
constexpr auto glyph_time_sig_0 = std::int32_t{0xe080};
|
|
|
|
constexpr auto invalid_layout_index = std::numeric_limits<std::size_t>::max();
|
|
constexpr auto staff_line_thickness_spaces = std::float32_t{0.13F};
|
|
constexpr auto stem_thickness_spaces = std::float32_t{0.12F};
|
|
constexpr auto beam_thickness_spaces = std::float32_t{0.50F};
|
|
constexpr auto ledger_line_thickness_spaces = std::float32_t{0.16F};
|
|
constexpr auto ledger_line_extension_spaces = std::float32_t{0.40F};
|
|
constexpr auto thin_barline_thickness_spaces = std::float32_t{0.16F};
|
|
constexpr auto thick_barline_thickness_spaces = std::float32_t{0.50F};
|
|
constexpr auto tie_thickness_spaces = std::float32_t{0.13F};
|
|
constexpr auto normal_stem_length_spaces = std::float32_t{3.50F};
|
|
constexpr auto clef_vertical_correction_spaces = std::float32_t{1.00F};
|
|
constexpr auto key_signature_vertical_correction_spaces = std::float32_t{0.50F};
|
|
constexpr auto time_signature_vertical_correction_spaces = std::float32_t{0.50F};
|
|
constexpr auto stem_head_horizontal_overlap_spaces = std::float32_t{0.20F};
|
|
constexpr auto stem_head_vertical_overlap_spaces = std::float32_t{0.45F};
|
|
constexpr auto measure_right_guard_spaces = std::float32_t{2.05F};
|
|
constexpr auto chord_above_staff_spaces = std::float32_t{3.65F};
|
|
constexpr auto chord_clearance_spaces = std::float32_t{0.45F};
|
|
constexpr auto lyric_below_staff_spaces = std::float32_t{6.15F};
|
|
constexpr auto lyric_clearance_spaces = std::float32_t{0.75F};
|
|
constexpr auto tie_endpoint_gap_spaces = std::float32_t{0.28F};
|
|
constexpr auto tie_note_offset_spaces = std::float32_t{0.50F};
|
|
constexpr auto tie_arch_spaces = std::float32_t{0.72F};
|
|
|
|
auto to_raylib(luma::color color) -> Color {
|
|
return {
|
|
.r = color.r,
|
|
.g = color.g,
|
|
.b = color.b,
|
|
.a = color.a,
|
|
};
|
|
}
|
|
|
|
auto encode_utf8(std::int32_t codepoint) -> std::string {
|
|
auto output = std::string{};
|
|
if (codepoint <= 0x7f) {
|
|
output.push_back(static_cast<std::char_t>(codepoint));
|
|
} else if (codepoint <= 0x7ff) {
|
|
output.push_back(static_cast<std::char_t>(0xc0 | ((codepoint >> 6) & 0x1f)));
|
|
output.push_back(static_cast<std::char_t>(0x80 | (codepoint & 0x3f)));
|
|
} else if (codepoint <= 0xffff) {
|
|
output.push_back(static_cast<std::char_t>(0xe0 | ((codepoint >> 12) & 0x0f)));
|
|
output.push_back(static_cast<std::char_t>(0x80 | ((codepoint >> 6) & 0x3f)));
|
|
output.push_back(static_cast<std::char_t>(0x80 | (codepoint & 0x3f)));
|
|
} else {
|
|
output.push_back(static_cast<std::char_t>(0xf0 | ((codepoint >> 18) & 0x07)));
|
|
output.push_back(static_cast<std::char_t>(0x80 | ((codepoint >> 12) & 0x3f)));
|
|
output.push_back(static_cast<std::char_t>(0x80 | ((codepoint >> 6) & 0x3f)));
|
|
output.push_back(static_cast<std::char_t>(0x80 | (codepoint & 0x3f)));
|
|
}
|
|
return output;
|
|
}
|
|
|
|
auto music_font(std::float32_t size) -> Font {
|
|
auto const key = std::max(std::int32_t{1}, static_cast<std::int32_t>(std::ceil(size)));
|
|
static auto fonts = std::unordered_map<std::int32_t, Font>{};
|
|
if (auto found = fonts.find(key); found != fonts.end()) {
|
|
return found->second;
|
|
}
|
|
|
|
auto codepoints = std::vector<std::int32_t>{
|
|
glyph_g_clef,
|
|
glyph_f_clef,
|
|
glyph_notehead_whole,
|
|
glyph_notehead_half,
|
|
glyph_notehead_black,
|
|
glyph_notehead_slash,
|
|
glyph_rest_whole,
|
|
glyph_rest_half,
|
|
glyph_rest_quarter,
|
|
glyph_rest_eighth,
|
|
glyph_flag_eighth_up,
|
|
glyph_flag_eighth_down,
|
|
glyph_augmentation_dot,
|
|
glyph_accidental_flat,
|
|
glyph_accidental_natural,
|
|
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 loaded = LoadFontEx(path.c_str(), key, codepoints.data(), static_cast<std::int32_t>(codepoints.size()));
|
|
if (loaded.texture.id == 0) {
|
|
return GetFontDefault();
|
|
}
|
|
auto inserted = fonts.emplace(key, loaded);
|
|
return inserted.first->second;
|
|
}
|
|
|
|
auto glyph_box(std::int32_t codepoint) -> smufl_box {
|
|
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:
|
|
return {.west = 0.0F, .south = -0.744684F, .east = 1.520666F, .north = 0.66F};
|
|
case glyph_notehead_half:
|
|
return {.west = 0.0F, .south = -0.696F, .east = 1.336F, .north = 0.7F};
|
|
case glyph_notehead_black:
|
|
return {.west = 0.0F, .south = -0.656F, .east = 1.336186F, .north = 0.656F};
|
|
case glyph_notehead_slash:
|
|
return {.west = 0.0F, .south = -1.016F, .east = 1.952F, .north = 1.016F};
|
|
case glyph_rest_whole:
|
|
return {.west = 0.003563F, .south = -0.48F, .east = 2.0F, .north = 0.056F};
|
|
case glyph_rest_half:
|
|
return {.west = 0.0F, .south = 0.0F, .east = 2.073599F, .north = 0.648F};
|
|
case glyph_rest_quarter:
|
|
return {.west = -0.001514F, .south = -1.66F, .east = 1.052F, .north = 1.66F};
|
|
case glyph_rest_eighth:
|
|
return {.west = 0.0F, .south = -1.04F, .east = 1.156F, .north = 1.044F};
|
|
case glyph_flag_eighth_up:
|
|
return {.west = 0.0F, .south = -3.276F, .east = 1.044F, .north = 0.0F};
|
|
case glyph_flag_eighth_down:
|
|
return {.west = 0.0F, .south = 0.0F, .east = 1.044F, .north = 3.276F};
|
|
case glyph_augmentation_dot:
|
|
return {.west = 0.0F, .south = -0.228F, .east = 0.384F, .north = 0.228F};
|
|
case glyph_accidental_flat:
|
|
return {.west = 0.004F, .south = -0.832F, .east = 0.836F, .north = 1.888F};
|
|
case glyph_accidental_natural:
|
|
return {.west = 0.0F, .south = -1.824F, .east = 0.854F, .north = 1.848F};
|
|
case glyph_accidental_sharp:
|
|
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:
|
|
return {};
|
|
}
|
|
}
|
|
|
|
auto glyph_center_x(std::int32_t codepoint) -> std::float32_t {
|
|
auto box = glyph_box(codepoint);
|
|
return (box.west + box.east) * 0.5F;
|
|
}
|
|
|
|
auto glyph_center_y(std::int32_t codepoint) -> std::float32_t {
|
|
auto box = glyph_box(codepoint);
|
|
return (box.south + box.north) * 0.5F;
|
|
}
|
|
|
|
auto glyph_width(std::int32_t codepoint, std::float32_t staff_spacing) -> std::float32_t {
|
|
auto box = glyph_box(codepoint);
|
|
return (box.east - box.west) * staff_spacing;
|
|
}
|
|
|
|
auto glyph_bounds(std::int32_t codepoint, luma::point origin, std::float32_t staff_spacing) -> luma::rect {
|
|
auto box = glyph_box(codepoint);
|
|
return {
|
|
.x = origin.x + box.west * staff_spacing,
|
|
.y = origin.y - box.north * staff_spacing,
|
|
.width = (box.east - box.west) * staff_spacing,
|
|
.height = (box.north - box.south) * staff_spacing,
|
|
};
|
|
}
|
|
|
|
auto scaled(std::float32_t staff_spaces, std::float32_t staff_spacing) -> std::float32_t {
|
|
return staff_spaces * staff_spacing;
|
|
}
|
|
|
|
auto at_least_one_pixel(std::float32_t value) -> std::float32_t {
|
|
return std::max(std::float32_t{1.0F}, value);
|
|
}
|
|
|
|
auto score_paper() -> luma::color {
|
|
return {.r = 7, .g = 9, .b = 9, .a = 255};
|
|
}
|
|
|
|
auto score_border() -> luma::color {
|
|
return {.r = 43, .g = 53, .b = 51, .a = 255};
|
|
}
|
|
|
|
auto score_ink() -> luma::color {
|
|
return {.r = 239, .g = 242, .b = 234, .a = 255};
|
|
}
|
|
|
|
auto score_staff_ink() -> luma::color {
|
|
return {.r = 110, .g = 122, .b = 117, .a = 255};
|
|
}
|
|
|
|
auto score_muted_ink() -> luma::color {
|
|
return {.r = 143, .g = 155, .b = 150, .a = 255};
|
|
}
|
|
|
|
auto score_accent_ink() -> luma::color {
|
|
return {.r = 77, .g = 148, .b = 158, .a = 255};
|
|
}
|
|
|
|
auto add_item(vamp_zero::score_page_layout& layout, vamp_zero::score_layout_item item) -> void {
|
|
layout.items.push_back(std::move(item));
|
|
}
|
|
|
|
auto add_fill_rect(vamp_zero::score_page_layout& layout, vamp_zero::score_layout_item_role role, luma::rect bounds, luma::color color) -> void {
|
|
add_item(layout, {
|
|
.kind = vamp_zero::score_layout_item_kind::FILL_RECT,
|
|
.role = role,
|
|
.bounds = bounds,
|
|
.color = color,
|
|
});
|
|
}
|
|
|
|
auto add_stroke_rect(vamp_zero::score_page_layout& layout, vamp_zero::score_layout_item_role role, luma::rect bounds, std::float32_t thickness, luma::color color) -> void {
|
|
add_item(layout, {
|
|
.kind = vamp_zero::score_layout_item_kind::STROKE_RECT,
|
|
.role = role,
|
|
.bounds = bounds,
|
|
.color = color,
|
|
.thickness = thickness,
|
|
});
|
|
}
|
|
|
|
auto add_stroke_line(
|
|
vamp_zero::score_page_layout& layout,
|
|
vamp_zero::score_layout_item_role role,
|
|
luma::point from,
|
|
luma::point to,
|
|
std::float32_t thickness,
|
|
luma::color color,
|
|
std::size_t system_index = invalid_layout_index,
|
|
std::size_t measure_index = invalid_layout_index,
|
|
std::size_t note_index = invalid_layout_index
|
|
) -> void {
|
|
add_item(layout, {
|
|
.kind = vamp_zero::score_layout_item_kind::STROKE_LINE,
|
|
.role = role,
|
|
.bounds = {
|
|
.x = std::min(from.x, to.x),
|
|
.y = std::min(from.y, to.y),
|
|
.width = std::abs(to.x - from.x),
|
|
.height = std::abs(to.y - from.y),
|
|
},
|
|
.from = from,
|
|
.to = to,
|
|
.color = color,
|
|
.thickness = thickness,
|
|
.system_index = system_index,
|
|
.measure_index = measure_index,
|
|
.note_index = note_index,
|
|
});
|
|
}
|
|
|
|
auto add_curve(
|
|
vamp_zero::score_page_layout& layout,
|
|
vamp_zero::score_layout_item_role role,
|
|
luma::point from,
|
|
luma::point control,
|
|
luma::point to,
|
|
std::float32_t thickness,
|
|
luma::color color,
|
|
std::size_t system_index,
|
|
std::size_t measure_index,
|
|
std::size_t note_index
|
|
) -> void {
|
|
add_item(layout, {
|
|
.kind = vamp_zero::score_layout_item_kind::CURVE,
|
|
.role = role,
|
|
.bounds = {
|
|
.x = std::min({from.x, control.x, to.x}),
|
|
.y = std::min({from.y, control.y, to.y}),
|
|
.width = std::max({from.x, control.x, to.x}) - std::min({from.x, control.x, to.x}),
|
|
.height = std::max({from.y, control.y, to.y}) - std::min({from.y, control.y, to.y}),
|
|
},
|
|
.from = from,
|
|
.control = control,
|
|
.to = to,
|
|
.color = color,
|
|
.thickness = thickness,
|
|
.system_index = system_index,
|
|
.measure_index = measure_index,
|
|
.note_index = note_index,
|
|
});
|
|
}
|
|
|
|
auto add_glyph(
|
|
vamp_zero::score_page_layout& layout,
|
|
vamp_zero::score_layout_item_role role,
|
|
std::int32_t codepoint,
|
|
luma::point origin,
|
|
std::float32_t staff_spacing,
|
|
luma::color color,
|
|
std::size_t system_index = invalid_layout_index,
|
|
std::size_t measure_index = invalid_layout_index,
|
|
std::size_t note_index = invalid_layout_index
|
|
) -> void {
|
|
add_item(layout, {
|
|
.kind = vamp_zero::score_layout_item_kind::GLYPH,
|
|
.role = role,
|
|
.bounds = glyph_bounds(codepoint, origin, staff_spacing),
|
|
.origin = origin,
|
|
.color = color,
|
|
.staff_spacing = staff_spacing,
|
|
.codepoint = codepoint,
|
|
.system_index = system_index,
|
|
.measure_index = measure_index,
|
|
.note_index = note_index,
|
|
});
|
|
}
|
|
|
|
auto origin_for_left_baseline(std::int32_t codepoint, std::float32_t left_x, std::float32_t baseline_y, std::float32_t staff_spacing) -> luma::point {
|
|
auto box = glyph_box(codepoint);
|
|
return {
|
|
.x = left_x - box.west * staff_spacing,
|
|
.y = baseline_y,
|
|
};
|
|
}
|
|
|
|
auto draw_music_origin(std::int32_t codepoint, luma::point origin, std::float32_t staff_spacing, luma::color color) -> void {
|
|
auto font_size = staff_spacing * 4.0F;
|
|
auto font = music_font(font_size);
|
|
auto glyph = GetGlyphInfo(font, codepoint);
|
|
auto scale = font_size / std::max(std::float32_t{1.0F}, static_cast<std::float32_t>(font.baseSize));
|
|
auto box = glyph_box(codepoint);
|
|
auto screen_left = origin.x + box.west * staff_spacing;
|
|
auto screen_top = origin.y - box.north * staff_spacing;
|
|
auto draw_x = screen_left - static_cast<std::float32_t>(glyph.offsetX) * scale + static_cast<std::float32_t>(font.glyphPadding) * scale;
|
|
auto draw_y = screen_top - static_cast<std::float32_t>(glyph.offsetY) * scale + static_cast<std::float32_t>(font.glyphPadding) * scale;
|
|
auto text = encode_utf8(codepoint);
|
|
DrawTextEx(font, text.c_str(), {static_cast<float>(draw_x), static_cast<float>(draw_y)}, static_cast<float>(font_size), 0.0F, to_raylib(color));
|
|
}
|
|
|
|
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 {
|
|
.x = center_x - glyph_center_x(codepoint) * staff_spacing,
|
|
.y = baseline_y,
|
|
};
|
|
}
|
|
|
|
auto anchor_point(luma::point origin, smufl_anchor anchor, std::float32_t staff_spacing) -> luma::point {
|
|
return {
|
|
.x = origin.x + anchor.x * staff_spacing,
|
|
.y = origin.y - anchor.y * staff_spacing,
|
|
};
|
|
}
|
|
|
|
auto stem_up_anchor(std::int32_t notehead) -> smufl_anchor {
|
|
switch (notehead) {
|
|
case glyph_notehead_half:
|
|
return {.x = 1.312F, .y = 0.284F};
|
|
case glyph_notehead_slash:
|
|
return {.x = 1.952F, .y = 0.884F};
|
|
case glyph_notehead_black:
|
|
default:
|
|
return {.x = 1.336F, .y = 0.288F};
|
|
}
|
|
}
|
|
|
|
auto stem_down_anchor(std::int32_t notehead) -> smufl_anchor {
|
|
switch (notehead) {
|
|
case glyph_notehead_half:
|
|
return {.x = 0.0F, .y = -0.252F};
|
|
case glyph_notehead_slash:
|
|
return {.x = 0.0F, .y = -0.928F};
|
|
case glyph_notehead_black:
|
|
default:
|
|
return {.x = 0.0F, .y = -0.236F};
|
|
}
|
|
}
|
|
|
|
auto text_style(std::int32_t scale) -> luma::text_style {
|
|
auto size = 14.0F;
|
|
if (scale == 2) {
|
|
size = 22.0F;
|
|
} else if (scale >= 3) {
|
|
size = 30.0F;
|
|
}
|
|
return {
|
|
.font = std::string{vamp_style::score_text_font_path},
|
|
.size = size,
|
|
.line_height = size + 5.0F,
|
|
};
|
|
}
|
|
|
|
auto text_line_height(std::int32_t scale) -> std::float32_t {
|
|
return text_style(scale).line_height;
|
|
}
|
|
|
|
auto add_text(
|
|
vamp_zero::score_page_layout& layout,
|
|
vamp_zero::score_layout_item_role role,
|
|
std::float32_t x,
|
|
std::float32_t y,
|
|
std::string text,
|
|
std::int32_t scale,
|
|
luma::color color,
|
|
std::float32_t max_width,
|
|
std::size_t system_index = invalid_layout_index,
|
|
std::size_t measure_index = invalid_layout_index,
|
|
std::size_t note_index = invalid_layout_index
|
|
) -> void {
|
|
if (text.empty() || max_width <= 0.0F) {
|
|
return;
|
|
}
|
|
add_item(layout, {
|
|
.kind = vamp_zero::score_layout_item_kind::TEXT,
|
|
.role = role,
|
|
.bounds = {.x = x, .y = y, .width = max_width, .height = text_line_height(scale)},
|
|
.origin = {.x = x, .y = y},
|
|
.color = color,
|
|
.text_scale = scale,
|
|
.system_index = system_index,
|
|
.measure_index = measure_index,
|
|
.note_index = note_index,
|
|
.text = std::move(text),
|
|
});
|
|
}
|
|
|
|
auto duration_for(std::int64_t ticks) -> duration_shape {
|
|
if (ticks >= whole_note_ticks) {
|
|
return {.base = duration_base::WHOLE, .dots = 0, .ticks = whole_note_ticks};
|
|
}
|
|
if (ticks >= half_note_ticks + quarter_note_ticks) {
|
|
return {.base = duration_base::HALF, .dots = 1, .ticks = half_note_ticks + quarter_note_ticks};
|
|
}
|
|
if (ticks >= half_note_ticks) {
|
|
return {.base = duration_base::HALF, .dots = 0, .ticks = half_note_ticks};
|
|
}
|
|
if (ticks >= quarter_note_ticks + eighth_note_ticks) {
|
|
return {.base = duration_base::QUARTER, .dots = 1, .ticks = quarter_note_ticks + eighth_note_ticks};
|
|
}
|
|
if (ticks >= quarter_note_ticks) {
|
|
return {.base = duration_base::QUARTER, .dots = 0, .ticks = quarter_note_ticks};
|
|
}
|
|
return {.base = duration_base::EIGHTH, .dots = 0, .ticks = eighth_note_ticks};
|
|
}
|
|
|
|
auto spacing_ticks(vamp_zero::score_note const& note) -> std::int64_t {
|
|
return std::max<std::int64_t>(1, note.duration_ticks);
|
|
}
|
|
|
|
auto display_ticks(vamp_zero::score_note const& note) -> std::int64_t {
|
|
return note.display_ticks <= 0 ? note.duration_ticks : note.display_ticks;
|
|
}
|
|
|
|
auto measure_ticks(vamp_zero::score_measure const& measure) -> std::int64_t {
|
|
auto total = std::int64_t{};
|
|
for (auto const& note : measure.notes) {
|
|
total += spacing_ticks(note);
|
|
}
|
|
return std::max(whole_note_ticks, total);
|
|
}
|
|
|
|
auto has_stem(duration_shape duration) -> std::bool_t {
|
|
return duration.base != duration_base::WHOLE;
|
|
}
|
|
|
|
auto has_flag(duration_shape duration) -> std::bool_t {
|
|
return duration.base == duration_base::EIGHTH;
|
|
}
|
|
|
|
auto letter_index(std::char_t letter) -> std::int32_t {
|
|
switch (letter) {
|
|
case 'c':
|
|
case 'C':
|
|
return 0;
|
|
case 'd':
|
|
case 'D':
|
|
return 1;
|
|
case 'e':
|
|
case 'E':
|
|
return 2;
|
|
case 'f':
|
|
case 'F':
|
|
return 3;
|
|
case 'g':
|
|
case 'G':
|
|
return 4;
|
|
case 'a':
|
|
case 'A':
|
|
return 5;
|
|
case 'b':
|
|
case 'B':
|
|
return 6;
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
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 clef_bottom_line_pitch_index(std::string_view clef) -> std::int32_t {
|
|
auto name = normalized_clef(clef);
|
|
if (name == "bass" || name == "f") {
|
|
return 2 * 7 + letter_index('g');
|
|
}
|
|
return 4 * 7 + letter_index('e');
|
|
}
|
|
|
|
auto pitch_y(vamp_zero::score_pitch const& pitch, std::float32_t staff_top, std::float32_t staff_spacing, std::string_view clef) -> std::float32_t {
|
|
auto pitch_index = pitch.octave * 7 + letter_index(pitch.letter);
|
|
auto bottom_line = staff_top + staff_spacing * 4.0F;
|
|
return bottom_line - static_cast<std::float32_t>(pitch_index - clef_bottom_line_pitch_index(clef)) * (staff_spacing * 0.5F);
|
|
}
|
|
|
|
auto accidental_glyph(vamp_zero::score_accidental_mark mark) -> std::optional<std::int32_t> {
|
|
switch (mark) {
|
|
case vamp_zero::score_accidental_mark::FLAT:
|
|
return glyph_accidental_flat;
|
|
case vamp_zero::score_accidental_mark::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;
|
|
}
|
|
|
|
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 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, ¤t);
|
|
}
|
|
|
|
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 {
|
|
constexpr auto segments = std::int32_t{10};
|
|
auto previous = start;
|
|
for (auto index = std::int32_t{1}; index <= segments; ++index) {
|
|
auto t = static_cast<std::float32_t>(index) / static_cast<std::float32_t>(segments);
|
|
auto omt = 1.0F - t;
|
|
auto current = luma::point{
|
|
.x = omt * omt * start.x + 2.0F * omt * t * control.x + t * t * end.x,
|
|
.y = omt * omt * start.y + 2.0F * omt * t * control.y + t * t * end.y,
|
|
};
|
|
context.stroke_line(previous, current, thickness, color);
|
|
previous = current;
|
|
}
|
|
}
|
|
|
|
struct notehead_layout {
|
|
std::size_t source_pitch_index{};
|
|
std::int32_t codepoint{};
|
|
std::float32_t center_x{};
|
|
std::float32_t center_y{};
|
|
luma::point origin{};
|
|
luma::rect bounds{};
|
|
vamp_zero::score_accidental_mark accidental{vamp_zero::score_accidental_mark::NONE};
|
|
std::bool_t displaced{};
|
|
};
|
|
|
|
struct note_layout {
|
|
vamp_zero::score_note const* source{};
|
|
std::size_t note_index{};
|
|
std::int64_t elapsed_ticks{};
|
|
std::int64_t duration_ticks{};
|
|
duration_shape duration{};
|
|
std::int32_t notehead{};
|
|
std::bool_t hidden{};
|
|
std::bool_t rest{};
|
|
std::bool_t slash{};
|
|
std::bool_t stem_up{true};
|
|
std::bool_t beamed{};
|
|
std::size_t beam_group_index{invalid_layout_index};
|
|
std::float32_t x{};
|
|
std::float32_t low_y{};
|
|
std::float32_t high_y{};
|
|
std::float32_t visual_left{};
|
|
std::float32_t visual_right{};
|
|
std::float32_t visual_top{};
|
|
std::float32_t visual_bottom{};
|
|
std::float32_t stem_x{};
|
|
std::float32_t stem_base_y{};
|
|
std::float32_t stem_tip_y{};
|
|
std::vector<notehead_layout> heads{};
|
|
};
|
|
|
|
struct beam_group_layout {
|
|
std::size_t index{};
|
|
std::size_t first_note{};
|
|
std::size_t last_note{};
|
|
std::bool_t stem_up{true};
|
|
std::float32_t beam_y{};
|
|
};
|
|
|
|
struct laid_out_tie_head {
|
|
vamp_zero::score_pitch pitch{};
|
|
luma::rect bounds{};
|
|
std::float32_t center_y{};
|
|
};
|
|
|
|
struct laid_out_note_ref {
|
|
std::size_t system_index{};
|
|
std::size_t measure_index{};
|
|
std::size_t note_index{};
|
|
luma::rect notation_bounds{};
|
|
std::bool_t stem_up{true};
|
|
std::vector<laid_out_tie_head> heads{};
|
|
};
|
|
|
|
struct laid_out_measure_ref {
|
|
std::size_t measure_index{};
|
|
luma::rect notation_bounds{};
|
|
};
|
|
|
|
auto layout_staff(
|
|
vamp_zero::score_page_layout& layout,
|
|
std::float32_t x,
|
|
std::float32_t y,
|
|
std::float32_t width,
|
|
std::float32_t spacing,
|
|
std::size_t system_index
|
|
) -> void {
|
|
for (auto line = std::int32_t{}; line < 5; ++line) {
|
|
auto yy = y + static_cast<std::float32_t>(line) * spacing;
|
|
add_stroke_line(
|
|
layout,
|
|
vamp_zero::score_layout_item_role::STAFF_LINE,
|
|
{.x = x, .y = yy},
|
|
{.x = x + width, .y = yy},
|
|
at_least_one_pixel(scaled(staff_line_thickness_spaces, spacing)),
|
|
score_staff_ink(),
|
|
system_index);
|
|
}
|
|
}
|
|
|
|
auto layout_clef(vamp_zero::score_page_layout& layout, std::string_view clef, std::float32_t x, std::float32_t staff_top, std::float32_t spacing, std::size_t system_index) -> std::float32_t {
|
|
auto name = normalized_clef(clef);
|
|
auto codepoint = name == "bass" || name == "f" ? glyph_f_clef : glyph_g_clef;
|
|
auto reference_pitch = codepoint == glyph_f_clef
|
|
? vamp_zero::score_pitch{.letter = 'f', .octave = 3}
|
|
: vamp_zero::score_pitch{.letter = 'g', .octave = 4};
|
|
auto baseline_y = pitch_y(reference_pitch, staff_top, spacing, clef) + scaled(clef_vertical_correction_spaces, spacing);
|
|
auto origin = origin_for_left_baseline(codepoint, x, baseline_y, spacing);
|
|
add_glyph(layout, vamp_zero::score_layout_item_role::CLEF, codepoint, origin, spacing, score_ink(), system_index);
|
|
return glyph_width(codepoint, spacing) + spacing * 0.90F;
|
|
}
|
|
|
|
auto key_signature_pitch(std::int32_t count, std::size_t index, std::string_view clef) -> 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},
|
|
}};
|
|
constexpr auto bass_sharp_positions = std::array<key_signature_position, 7>{{
|
|
{.letter = 'f', .octave = 3},
|
|
{.letter = 'c', .octave = 3},
|
|
{.letter = 'g', .octave = 3},
|
|
{.letter = 'd', .octave = 3},
|
|
{.letter = 'a', .octave = 2},
|
|
{.letter = 'e', .octave = 3},
|
|
{.letter = 'b', .octave = 2},
|
|
}};
|
|
constexpr auto bass_flat_positions = std::array<key_signature_position, 7>{{
|
|
{.letter = 'b', .octave = 2},
|
|
{.letter = 'e', .octave = 3},
|
|
{.letter = 'a', .octave = 2},
|
|
{.letter = 'd', .octave = 3},
|
|
{.letter = 'g', .octave = 2},
|
|
{.letter = 'c', .octave = 3},
|
|
{.letter = 'f', .octave = 2},
|
|
}};
|
|
auto name = normalized_clef(clef);
|
|
auto const& positions = name == "bass" || name == "f"
|
|
? (count > 0 ? bass_sharp_positions : bass_flat_positions)
|
|
: (count > 0 ? sharp_positions : flat_positions);
|
|
auto const position = positions[index];
|
|
return {
|
|
.letter = position.letter,
|
|
.octave = position.octave,
|
|
};
|
|
}
|
|
|
|
auto layout_key_signature(
|
|
vamp_zero::score_page_layout& layout,
|
|
std::string_view clef,
|
|
std::string_view key_signature,
|
|
std::float32_t x,
|
|
std::float32_t staff_top,
|
|
std::float32_t spacing,
|
|
std::size_t system_index
|
|
) -> 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, clef);
|
|
auto baseline_y = pitch_y(pitch, staff_top, spacing, clef) + scaled(key_signature_vertical_correction_spaces, spacing);
|
|
add_glyph(
|
|
layout,
|
|
vamp_zero::score_layout_item_role::KEY_SIGNATURE,
|
|
glyph,
|
|
origin_for_left_baseline(glyph, current_x, baseline_y, spacing),
|
|
spacing,
|
|
score_ink(),
|
|
system_index);
|
|
current_x += step;
|
|
}
|
|
return current_x - x + spacing * 0.55F;
|
|
}
|
|
|
|
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 layout_time_signature_run(
|
|
vamp_zero::score_page_layout& layout,
|
|
std::string_view text,
|
|
std::float32_t x,
|
|
std::float32_t baseline_y,
|
|
std::float32_t spacing,
|
|
std::size_t system_index
|
|
) -> void {
|
|
auto current_x = x;
|
|
for (auto digit : text) {
|
|
auto codepoint = time_signature_digit(digit);
|
|
if (!codepoint) {
|
|
continue;
|
|
}
|
|
add_glyph(
|
|
layout,
|
|
vamp_zero::score_layout_item_role::TIME_SIGNATURE,
|
|
*codepoint,
|
|
origin_for_left_baseline(*codepoint, current_x, baseline_y, spacing),
|
|
spacing,
|
|
score_ink(),
|
|
system_index);
|
|
current_x += glyph_width(*codepoint, spacing) + spacing * 0.16F;
|
|
}
|
|
}
|
|
|
|
auto layout_time_signature(
|
|
vamp_zero::score_page_layout& layout,
|
|
std::string_view time_signature,
|
|
std::float32_t x,
|
|
std::float32_t staff_top,
|
|
std::float32_t spacing,
|
|
std::size_t system_index
|
|
) -> 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;
|
|
}
|
|
auto const vertical_correction = scaled(time_signature_vertical_correction_spaces, spacing);
|
|
layout_time_signature_run(layout, numerator, x + (width - numerator_width) * 0.5F, staff_top + spacing * 1.5F + vertical_correction, spacing, system_index);
|
|
layout_time_signature_run(layout, denominator, x + (width - denominator_width) * 0.5F, staff_top + spacing * 3.5F + vertical_correction, spacing, system_index);
|
|
return width + spacing * 0.95F;
|
|
}
|
|
|
|
auto layout_system_preamble(
|
|
vamp_zero::score_page_layout& layout,
|
|
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::size_t system_index
|
|
) -> std::float32_t {
|
|
auto current_x = x + spacing * 0.35F;
|
|
current_x += layout_clef(layout, score.clef, current_x, staff_top, spacing, system_index);
|
|
auto key_width = layout_key_signature(
|
|
layout,
|
|
score.clef,
|
|
first_measure == nullptr ? score.key_signature : effective_key_signature(score, *first_measure),
|
|
current_x,
|
|
staff_top,
|
|
spacing,
|
|
system_index);
|
|
current_x += key_width;
|
|
if (key_width == 0.0F) {
|
|
current_x += spacing * 0.45F;
|
|
}
|
|
if (show_time_signature) {
|
|
current_x += layout_time_signature(layout, effective_time_signature(score, first_measure), current_x, staff_top, spacing, system_index);
|
|
}
|
|
return current_x + spacing * 0.35F;
|
|
}
|
|
|
|
auto dot_y_for(std::float32_t note_y, std::float32_t staff_top, std::float32_t spacing) -> std::float32_t {
|
|
auto half_step = (note_y - staff_top) / (spacing * 0.5F);
|
|
auto rounded = std::round(half_step);
|
|
if (std::abs(half_step - rounded) < 0.08F && static_cast<std::int32_t>(rounded) % 2 == 0) {
|
|
return note_y - spacing * 0.5F;
|
|
}
|
|
return note_y;
|
|
}
|
|
|
|
auto rest_codepoint(duration_shape duration) -> std::int32_t {
|
|
switch (duration.base) {
|
|
case duration_base::WHOLE:
|
|
return glyph_rest_whole;
|
|
case duration_base::HALF:
|
|
return glyph_rest_half;
|
|
case duration_base::QUARTER:
|
|
return glyph_rest_quarter;
|
|
case duration_base::EIGHTH:
|
|
return glyph_rest_eighth;
|
|
}
|
|
return glyph_rest_quarter;
|
|
}
|
|
|
|
auto rest_baseline_y(duration_shape duration, std::float32_t staff_top, std::float32_t spacing) -> std::float32_t {
|
|
switch (duration.base) {
|
|
case duration_base::WHOLE:
|
|
return staff_top + spacing;
|
|
case duration_base::HALF:
|
|
return staff_top + spacing * 2.0F;
|
|
case duration_base::QUARTER:
|
|
case duration_base::EIGHTH:
|
|
return staff_top + spacing * 2.0F;
|
|
}
|
|
return staff_top + spacing * 2.0F;
|
|
}
|
|
|
|
auto layout_ledger_lines(
|
|
vamp_zero::score_page_layout& layout,
|
|
note_layout const& note,
|
|
std::float32_t staff_top,
|
|
std::float32_t spacing,
|
|
std::size_t system_index,
|
|
std::size_t measure_index
|
|
) -> void {
|
|
auto bottom = staff_top + spacing * 4.0F;
|
|
for (auto const& head : note.heads) {
|
|
auto extension = scaled(ledger_line_extension_spaces, spacing);
|
|
auto left = head.bounds.x - extension;
|
|
auto right = head.bounds.x + head.bounds.width + extension;
|
|
if (head.center_y < staff_top) {
|
|
for (auto yy = staff_top - spacing; yy >= head.center_y - 0.1F; yy -= spacing) {
|
|
add_stroke_line(
|
|
layout,
|
|
vamp_zero::score_layout_item_role::STAFF_LINE,
|
|
{.x = left, .y = yy},
|
|
{.x = right, .y = yy},
|
|
at_least_one_pixel(scaled(ledger_line_thickness_spaces, spacing)),
|
|
score_staff_ink(),
|
|
system_index,
|
|
measure_index,
|
|
note.note_index);
|
|
}
|
|
} else if (head.center_y > bottom) {
|
|
for (auto yy = bottom + spacing; yy <= head.center_y + 0.1F; yy += spacing) {
|
|
add_stroke_line(
|
|
layout,
|
|
vamp_zero::score_layout_item_role::STAFF_LINE,
|
|
{.x = left, .y = yy},
|
|
{.x = right, .y = yy},
|
|
at_least_one_pixel(scaled(ledger_line_thickness_spaces, spacing)),
|
|
score_staff_ink(),
|
|
system_index,
|
|
measure_index,
|
|
note.note_index);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
auto notehead_codepoint(duration_shape duration, std::bool_t slash) -> std::int32_t {
|
|
if (slash) {
|
|
return glyph_notehead_slash;
|
|
}
|
|
switch (duration.base) {
|
|
case duration_base::WHOLE:
|
|
return glyph_notehead_whole;
|
|
case duration_base::HALF:
|
|
return glyph_notehead_half;
|
|
case duration_base::QUARTER:
|
|
case duration_base::EIGHTH:
|
|
return glyph_notehead_black;
|
|
}
|
|
return glyph_notehead_black;
|
|
}
|
|
|
|
auto update_note_visual_extents(note_layout& note, std::float32_t spacing) -> void {
|
|
if (note.hidden) {
|
|
note.visual_left = note.x;
|
|
note.visual_right = note.x;
|
|
note.visual_top = note.low_y;
|
|
note.visual_bottom = note.low_y;
|
|
return;
|
|
}
|
|
if (note.rest) {
|
|
auto codepoint = rest_codepoint(note.duration);
|
|
auto origin = origin_for_centered_baseline(codepoint, note.x, note.low_y, spacing);
|
|
auto bounds = glyph_bounds(codepoint, origin, spacing);
|
|
note.visual_left = bounds.x;
|
|
note.visual_right = bounds.x + bounds.width;
|
|
note.visual_top = bounds.y;
|
|
note.visual_bottom = bounds.y + bounds.height;
|
|
if (note.duration.dots != 0) {
|
|
note.visual_right += spacing * 1.35F;
|
|
}
|
|
return;
|
|
}
|
|
note.visual_left = note.heads.empty() ? note.x : note.heads.front().bounds.x;
|
|
note.visual_right = note.heads.empty() ? note.x : note.heads.front().bounds.x + note.heads.front().bounds.width;
|
|
note.visual_top = note.heads.empty() ? note.high_y : note.heads.front().bounds.y;
|
|
note.visual_bottom = note.heads.empty() ? note.low_y : note.heads.front().bounds.y + note.heads.front().bounds.height;
|
|
for (auto const& head : note.heads) {
|
|
note.visual_left = std::min(note.visual_left, head.bounds.x);
|
|
note.visual_right = std::max(note.visual_right, head.bounds.x + head.bounds.width);
|
|
note.visual_top = std::min(note.visual_top, head.bounds.y);
|
|
note.visual_bottom = std::max(note.visual_bottom, head.bounds.y + head.bounds.height);
|
|
}
|
|
if (note.duration.dots != 0) {
|
|
note.visual_right += spacing * 1.40F;
|
|
}
|
|
}
|
|
|
|
auto shift_note_x(note_layout& note, std::float32_t dx, std::float32_t spacing) -> void {
|
|
note.x += dx;
|
|
note.visual_left += dx;
|
|
note.visual_right += dx;
|
|
note.stem_x += dx;
|
|
for (auto& head : note.heads) {
|
|
head.center_x += dx;
|
|
head.origin.x += dx;
|
|
head.bounds = glyph_bounds(head.codepoint, head.origin, spacing);
|
|
}
|
|
}
|
|
|
|
auto estimate_accidental_columns(note_layout const& note, std::float32_t spacing) -> std::size_t {
|
|
struct column_range {
|
|
std::float32_t top{};
|
|
std::float32_t bottom{};
|
|
};
|
|
auto columns = std::vector<std::vector<column_range>>{};
|
|
auto candidates = std::vector<column_range>{};
|
|
for (auto const& head : note.heads) {
|
|
auto glyph = accidental_glyph(head.accidental);
|
|
if (!glyph) {
|
|
continue;
|
|
}
|
|
auto box = glyph_box(*glyph);
|
|
candidates.push_back({
|
|
.top = head.center_y - box.north * spacing,
|
|
.bottom = head.center_y - box.south * spacing,
|
|
});
|
|
}
|
|
std::sort(candidates.begin(), candidates.end(), [](auto const& left, auto const& right) {
|
|
return left.top < right.top;
|
|
});
|
|
for (auto const& candidate : candidates) {
|
|
auto placed = false;
|
|
for (auto& column : columns) {
|
|
auto fits = true;
|
|
for (auto const& range : column) {
|
|
if (candidate.top < range.bottom + spacing * 0.15F && candidate.bottom + spacing * 0.15F > range.top) {
|
|
fits = false;
|
|
break;
|
|
}
|
|
}
|
|
if (fits) {
|
|
column.push_back(candidate);
|
|
placed = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!placed) {
|
|
columns.push_back({candidate});
|
|
}
|
|
}
|
|
return columns.size();
|
|
}
|
|
|
|
auto note_accidental_extent(note_layout const& note, std::float32_t spacing) -> std::float32_t {
|
|
auto columns = estimate_accidental_columns(note, spacing);
|
|
if (columns == 0U) {
|
|
return 0.0F;
|
|
}
|
|
auto max_width = std::float32_t{};
|
|
for (auto const& head : note.heads) {
|
|
if (auto glyph = accidental_glyph(head.accidental)) {
|
|
max_width = std::max(max_width, glyph_width(*glyph, spacing));
|
|
}
|
|
}
|
|
return spacing * 0.45F + static_cast<std::float32_t>(columns) * (max_width + spacing * 0.30F);
|
|
}
|
|
|
|
auto outer_head_for_stem(note_layout const& note) -> notehead_layout const* {
|
|
if (note.heads.empty()) {
|
|
return nullptr;
|
|
}
|
|
auto const* selected = ¬e.heads.front();
|
|
for (auto const& head : note.heads) {
|
|
if (note.stem_up) {
|
|
if (head.center_y > selected->center_y) {
|
|
selected = &head;
|
|
}
|
|
} else if (head.center_y < selected->center_y) {
|
|
selected = &head;
|
|
}
|
|
}
|
|
return selected;
|
|
}
|
|
|
|
auto update_stem_geometry(note_layout& note, std::float32_t staff_top, std::float32_t spacing) -> void {
|
|
if (!has_stem(note.duration) || note.heads.empty()) {
|
|
return;
|
|
}
|
|
auto const* head = outer_head_for_stem(note);
|
|
if (head == nullptr) {
|
|
return;
|
|
}
|
|
auto anchor = note.stem_up ? stem_up_anchor(note.notehead) : stem_down_anchor(note.notehead);
|
|
auto base = anchor_point(head->origin, anchor, spacing);
|
|
auto center = staff_top + spacing * 2.0F;
|
|
note.stem_x = note.stem_up
|
|
? base.x - scaled(stem_head_horizontal_overlap_spaces, spacing)
|
|
: base.x + scaled(stem_head_horizontal_overlap_spaces, spacing);
|
|
note.stem_base_y = note.stem_up
|
|
? base.y + scaled(stem_head_vertical_overlap_spaces, spacing)
|
|
: base.y - scaled(stem_head_vertical_overlap_spaces, spacing);
|
|
note.stem_tip_y = note.stem_up
|
|
? std::min(note.high_y, center) - scaled(normal_stem_length_spaces, spacing)
|
|
: std::max(note.low_y, center) + scaled(normal_stem_length_spaces, spacing);
|
|
}
|
|
|
|
auto build_note_layout(
|
|
vamp_zero::score_document const& score,
|
|
vamp_zero::score_note const& note,
|
|
std::size_t note_index,
|
|
std::int64_t elapsed_ticks,
|
|
std::int64_t duration_ticks,
|
|
std::float32_t x,
|
|
std::float32_t staff_top,
|
|
std::float32_t spacing,
|
|
std::vector<vamp_zero::score_accidental_mark> const& accidentals
|
|
) -> note_layout {
|
|
auto duration = duration_for(display_ticks(note));
|
|
auto slash = note.notehead == "slash";
|
|
auto result = note_layout{
|
|
.source = ¬e,
|
|
.note_index = note_index,
|
|
.elapsed_ticks = elapsed_ticks,
|
|
.duration_ticks = duration_ticks,
|
|
.duration = duration,
|
|
.notehead = notehead_codepoint(duration, slash),
|
|
.hidden = note.hidden,
|
|
.rest = note.kind == "rest",
|
|
.slash = slash,
|
|
.x = x,
|
|
.low_y = staff_top + spacing * 2.0F,
|
|
.high_y = staff_top + spacing * 2.0F,
|
|
.visual_left = x,
|
|
.visual_right = x,
|
|
};
|
|
if (note.hidden) {
|
|
return result;
|
|
}
|
|
if (note.kind == "rest") {
|
|
update_note_visual_extents(result, spacing);
|
|
return result;
|
|
}
|
|
|
|
auto center = staff_top + spacing * 2.0F;
|
|
auto bottom = staff_top + spacing * 4.0F;
|
|
if (slash || note.pitches.empty()) {
|
|
auto origin = origin_for_centered_baseline(result.notehead, x, center, spacing);
|
|
result.heads.push_back({
|
|
.source_pitch_index = 0U,
|
|
.codepoint = result.notehead,
|
|
.center_x = x,
|
|
.center_y = center,
|
|
.origin = origin,
|
|
.bounds = glyph_bounds(result.notehead, origin, spacing),
|
|
});
|
|
result.low_y = center;
|
|
result.high_y = center;
|
|
}
|
|
if (!slash) {
|
|
auto raw_heads = std::vector<notehead_layout>{};
|
|
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<std::float32_t>(pitch_y(pitch, staff_top, spacing, score.clef), staff_top - spacing * 2.0F, bottom + spacing * 2.0F);
|
|
result.low_y = std::max(result.low_y, yy);
|
|
result.high_y = std::min(result.high_y, yy);
|
|
raw_heads.push_back({
|
|
.source_pitch_index = pitch_index,
|
|
.codepoint = result.notehead,
|
|
.center_x = x,
|
|
.center_y = yy,
|
|
.accidental = pitch_index < accidentals.size() ? accidentals[pitch_index] : vamp_zero::score_accidental_mark::NONE,
|
|
});
|
|
}
|
|
|
|
auto ordered = raw_heads;
|
|
std::sort(ordered.begin(), ordered.end(), [](auto const& left, auto const& right) {
|
|
return left.center_y > right.center_y;
|
|
});
|
|
auto center_by_pitch = std::vector<std::float32_t>(raw_heads.size(), x);
|
|
auto displaced_by_pitch = std::vector<std::bool_t>(raw_heads.size(), false);
|
|
auto right_column_x = x + glyph_width(result.notehead, spacing) * 0.72F;
|
|
auto previous_y = std::optional<std::float32_t>{};
|
|
auto use_right_column = false;
|
|
for (auto const& head : ordered) {
|
|
if (previous_y && std::abs(*previous_y - head.center_y) <= spacing * 0.55F) {
|
|
use_right_column = !use_right_column;
|
|
} else {
|
|
use_right_column = false;
|
|
}
|
|
center_by_pitch[head.source_pitch_index] = use_right_column ? right_column_x : x;
|
|
displaced_by_pitch[head.source_pitch_index] = use_right_column;
|
|
previous_y = head.center_y;
|
|
}
|
|
|
|
result.heads.reserve(raw_heads.size());
|
|
for (auto& head : raw_heads) {
|
|
head.center_x = center_by_pitch[head.source_pitch_index];
|
|
head.displaced = displaced_by_pitch[head.source_pitch_index];
|
|
head.origin = origin_for_centered_baseline(head.codepoint, head.center_x, head.center_y, spacing);
|
|
head.bounds = glyph_bounds(head.codepoint, head.origin, spacing);
|
|
result.heads.push_back(head);
|
|
}
|
|
}
|
|
|
|
result.stem_up = (result.low_y + result.high_y) * 0.5F >= center;
|
|
update_note_visual_extents(result, spacing);
|
|
auto accidental_extent = note_accidental_extent(result, spacing);
|
|
if (accidental_extent > 0.0F) {
|
|
result.visual_left -= accidental_extent;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
auto fit_note_spacing(std::vector<note_layout>& notes, luma::rect bounds, std::float32_t spacing) -> void {
|
|
auto previous_right = bounds.x + spacing * 0.20F;
|
|
for (auto& note : notes) {
|
|
if (note.hidden) {
|
|
continue;
|
|
}
|
|
auto const minimum_gap = spacing * 0.05F;
|
|
auto min_left = previous_right + minimum_gap;
|
|
if (note.visual_left < min_left) {
|
|
shift_note_x(note, min_left - note.visual_left, spacing);
|
|
}
|
|
previous_right = std::max(previous_right, note.visual_right);
|
|
}
|
|
if (notes.empty()) {
|
|
return;
|
|
}
|
|
auto last_right = std::float32_t{};
|
|
for (auto const& note : notes) {
|
|
if (!note.hidden) {
|
|
last_right = std::max(last_right, note.visual_right);
|
|
}
|
|
}
|
|
auto right_limit = bounds.x + bounds.width - spacing * 0.25F;
|
|
if (last_right <= right_limit) {
|
|
return;
|
|
}
|
|
auto first_left = right_limit;
|
|
for (auto const& note : notes) {
|
|
if (!note.hidden) {
|
|
first_left = std::min(first_left, note.visual_left);
|
|
}
|
|
}
|
|
auto shift_left = std::min(last_right - right_limit, std::max(std::float32_t{0.0F}, first_left - (bounds.x + spacing * 0.20F)));
|
|
if (shift_left <= 0.0F) {
|
|
return;
|
|
}
|
|
for (auto& note : notes) {
|
|
shift_note_x(note, -shift_left, spacing);
|
|
}
|
|
}
|
|
|
|
auto keep_notes_inside_right_guard(std::vector<note_layout>& notes, luma::rect bounds, std::float32_t spacing) -> void {
|
|
auto last_right = std::numeric_limits<std::float32_t>::lowest();
|
|
for (auto const& note : notes) {
|
|
if (!note.hidden) {
|
|
last_right = std::max(last_right, note.visual_right);
|
|
}
|
|
}
|
|
auto const right_limit = bounds.x + bounds.width - spacing * 0.25F;
|
|
if (last_right <= right_limit) {
|
|
return;
|
|
}
|
|
for (auto& note : notes) {
|
|
if (!note.hidden) {
|
|
shift_note_x(note, right_limit - last_right, spacing);
|
|
}
|
|
}
|
|
}
|
|
|
|
auto eligible_for_beam(note_layout const& note) -> std::bool_t {
|
|
return !note.hidden && !note.rest && has_stem(note.duration) && note.duration.base == duration_base::EIGHTH;
|
|
}
|
|
|
|
auto update_beam_group_geometry(std::vector<note_layout>& notes, beam_group_layout& group, std::float32_t staff_top, std::float32_t spacing) -> void {
|
|
auto center_sum = std::float32_t{};
|
|
auto count = std::size_t{};
|
|
for (auto index = group.first_note; index <= group.last_note; ++index) {
|
|
if (!eligible_for_beam(notes[index])) {
|
|
continue;
|
|
}
|
|
center_sum += (notes[index].low_y + notes[index].high_y) * 0.5F;
|
|
++count;
|
|
}
|
|
auto staff_center = staff_top + spacing * 2.0F;
|
|
group.stem_up = count == 0U || center_sum / static_cast<std::float32_t>(count) >= staff_center;
|
|
group.beam_y = group.stem_up ? std::numeric_limits<std::float32_t>::max() : std::numeric_limits<std::float32_t>::lowest();
|
|
for (auto index = group.first_note; index <= group.last_note; ++index) {
|
|
auto& note = notes[index];
|
|
note.stem_up = group.stem_up;
|
|
note.beamed = true;
|
|
note.beam_group_index = group.index;
|
|
update_stem_geometry(note, staff_top, spacing);
|
|
group.beam_y = group.stem_up ? std::min(group.beam_y, note.stem_tip_y) : std::max(group.beam_y, note.stem_tip_y);
|
|
}
|
|
auto beam_half = scaled(beam_thickness_spaces, spacing) * 0.5F;
|
|
for (auto index = group.first_note; index <= group.last_note; ++index) {
|
|
auto& note = notes[index];
|
|
note.stem_tip_y = group.stem_up ? group.beam_y + beam_half : group.beam_y - beam_half;
|
|
}
|
|
}
|
|
|
|
auto make_beam_groups(std::vector<note_layout>& notes, std::float32_t staff_top, std::float32_t spacing) -> std::vector<beam_group_layout> {
|
|
auto groups = std::vector<beam_group_layout>{};
|
|
auto index = std::size_t{};
|
|
while (index < notes.size()) {
|
|
if (!eligible_for_beam(notes[index])) {
|
|
++index;
|
|
continue;
|
|
}
|
|
|
|
auto start = index;
|
|
auto end = index;
|
|
auto const& tuplet_id = notes[index].source == nullptr ? std::string{} : notes[index].source->tuplet_id;
|
|
if (!tuplet_id.empty()) {
|
|
while (end + 1U < notes.size()
|
|
&& eligible_for_beam(notes[end + 1U])
|
|
&& notes[end + 1U].source != nullptr
|
|
&& notes[end + 1U].source->tuplet_id == tuplet_id) {
|
|
++end;
|
|
}
|
|
} else {
|
|
auto beat_end = ((notes[index].elapsed_ticks / quarter_note_ticks) + 1) * quarter_note_ticks;
|
|
while (end + 1U < notes.size()
|
|
&& eligible_for_beam(notes[end + 1U])
|
|
&& notes[end + 1U].source != nullptr
|
|
&& notes[end + 1U].source->tuplet_id.empty()
|
|
&& notes[end + 1U].elapsed_ticks < beat_end) {
|
|
++end;
|
|
}
|
|
}
|
|
|
|
if (end > start) {
|
|
auto group = beam_group_layout{
|
|
.index = groups.size(),
|
|
.first_note = start,
|
|
.last_note = end,
|
|
};
|
|
update_beam_group_geometry(notes, group, staff_top, spacing);
|
|
groups.push_back(group);
|
|
index = end + 1U;
|
|
continue;
|
|
}
|
|
++index;
|
|
}
|
|
for (auto& note : notes) {
|
|
if (!note.beamed) {
|
|
update_stem_geometry(note, staff_top, spacing);
|
|
}
|
|
}
|
|
return groups;
|
|
}
|
|
|
|
auto accidental_layouts(note_layout const& note, std::float32_t spacing) -> std::vector<notehead_layout> {
|
|
auto candidates = std::vector<notehead_layout>{};
|
|
for (auto const& head : note.heads) {
|
|
if (accidental_glyph(head.accidental)) {
|
|
candidates.push_back(head);
|
|
}
|
|
}
|
|
std::sort(candidates.begin(), candidates.end(), [](auto const& left, auto const& right) {
|
|
return left.bounds.y < right.bounds.y;
|
|
});
|
|
|
|
auto result = std::vector<notehead_layout>{};
|
|
auto column_ranges = std::vector<std::vector<luma::rect>>{};
|
|
auto chord_left = note.visual_right;
|
|
auto max_width = std::float32_t{};
|
|
for (auto const& head : note.heads) {
|
|
chord_left = std::min(chord_left, head.bounds.x);
|
|
if (auto glyph = accidental_glyph(head.accidental)) {
|
|
max_width = std::max(max_width, glyph_width(*glyph, spacing));
|
|
}
|
|
}
|
|
auto column_step = max_width + spacing * 0.30F;
|
|
for (auto head : candidates) {
|
|
auto glyph = *accidental_glyph(head.accidental);
|
|
auto raw_bounds = glyph_bounds(glyph, {.x = 0.0F, .y = head.center_y}, spacing);
|
|
auto column_index = std::size_t{};
|
|
for (; column_index < column_ranges.size(); ++column_index) {
|
|
auto fits = true;
|
|
for (auto const& range : column_ranges[column_index]) {
|
|
if (raw_bounds.y < range.y + range.height + spacing * 0.15F && raw_bounds.y + raw_bounds.height + spacing * 0.15F > range.y) {
|
|
fits = false;
|
|
break;
|
|
}
|
|
}
|
|
if (fits) {
|
|
break;
|
|
}
|
|
}
|
|
if (column_index == column_ranges.size()) {
|
|
column_ranges.push_back({});
|
|
}
|
|
auto right_x = chord_left - spacing * 0.45F - static_cast<std::float32_t>(column_index) * column_step;
|
|
head.codepoint = glyph;
|
|
head.origin = {.x = right_x - glyph_box(glyph).east * spacing, .y = head.center_y};
|
|
head.bounds = glyph_bounds(glyph, head.origin, spacing);
|
|
column_ranges[column_index].push_back(head.bounds);
|
|
result.push_back(head);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
auto add_flag(vamp_zero::score_page_layout& layout, note_layout const& note, std::float32_t spacing, std::size_t system_index, std::size_t measure_index) -> void {
|
|
auto codepoint = note.stem_up ? glyph_flag_eighth_up : glyph_flag_eighth_down;
|
|
auto anchor = note.stem_up ? smufl_anchor{.x = 0.0F, .y = -0.176F} : smufl_anchor{.x = 0.0F, .y = 0.176F};
|
|
auto origin = luma::point{
|
|
.x = note.stem_x - anchor.x * spacing,
|
|
.y = note.stem_tip_y + anchor.y * spacing,
|
|
};
|
|
add_glyph(layout, vamp_zero::score_layout_item_role::FLAG, codepoint, origin, spacing, score_ink(), system_index, measure_index, note.note_index);
|
|
}
|
|
|
|
auto include_visual_bounds(note_layout& note, luma::rect bounds) -> void {
|
|
note.visual_left = std::min(note.visual_left, bounds.x);
|
|
note.visual_right = std::max(note.visual_right, bounds.x + bounds.width);
|
|
note.visual_top = std::min(note.visual_top, bounds.y);
|
|
note.visual_bottom = std::max(note.visual_bottom, bounds.y + bounds.height);
|
|
}
|
|
|
|
auto flag_bounds(note_layout const& note, std::float32_t spacing) -> luma::rect {
|
|
auto codepoint = note.stem_up ? glyph_flag_eighth_up : glyph_flag_eighth_down;
|
|
auto anchor = note.stem_up ? smufl_anchor{.x = 0.0F, .y = -0.176F} : smufl_anchor{.x = 0.0F, .y = 0.176F};
|
|
auto origin = luma::point{
|
|
.x = note.stem_x - anchor.x * spacing,
|
|
.y = note.stem_tip_y + anchor.y * spacing,
|
|
};
|
|
return glyph_bounds(codepoint, origin, spacing);
|
|
}
|
|
|
|
auto refresh_note_visual_extents(note_layout& note, std::float32_t spacing) -> void {
|
|
update_note_visual_extents(note, spacing);
|
|
if (note.hidden || note.rest) {
|
|
return;
|
|
}
|
|
|
|
auto accidental_extent = note_accidental_extent(note, spacing);
|
|
if (accidental_extent > 0.0F) {
|
|
note.visual_left -= accidental_extent;
|
|
}
|
|
for (auto const& head : note.heads) {
|
|
if (auto glyph = accidental_glyph(head.accidental)) {
|
|
auto box = glyph_box(*glyph);
|
|
note.visual_top = std::min(note.visual_top, head.center_y - box.north * spacing);
|
|
note.visual_bottom = std::max(note.visual_bottom, head.center_y - box.south * spacing);
|
|
}
|
|
}
|
|
|
|
if (has_stem(note.duration) && note.stem_x != 0.0F) {
|
|
auto half_stem = at_least_one_pixel(scaled(stem_thickness_spaces, spacing)) * 0.5F;
|
|
include_visual_bounds(note, {
|
|
.x = note.stem_x - half_stem,
|
|
.y = std::min(note.stem_base_y, note.stem_tip_y),
|
|
.width = half_stem * 2.0F,
|
|
.height = std::abs(note.stem_tip_y - note.stem_base_y),
|
|
});
|
|
}
|
|
if (has_flag(note.duration) && !note.beamed && note.stem_x != 0.0F) {
|
|
include_visual_bounds(note, flag_bounds(note, spacing));
|
|
}
|
|
}
|
|
|
|
auto layout_rest(
|
|
vamp_zero::score_page_layout& layout,
|
|
note_layout const& note,
|
|
std::float32_t staff_top,
|
|
std::float32_t spacing,
|
|
std::size_t system_index,
|
|
std::size_t measure_index
|
|
) -> void {
|
|
auto codepoint = rest_codepoint(note.duration);
|
|
auto origin = origin_for_centered_baseline(codepoint, note.x, rest_baseline_y(note.duration, staff_top, spacing), spacing);
|
|
add_glyph(layout, vamp_zero::score_layout_item_role::REST, codepoint, origin, spacing, score_ink(), system_index, measure_index, note.note_index);
|
|
if (note.duration.dots != 0) {
|
|
auto bounds = glyph_bounds(codepoint, origin, spacing);
|
|
add_glyph(
|
|
layout,
|
|
vamp_zero::score_layout_item_role::DOT,
|
|
glyph_augmentation_dot,
|
|
origin_for_centered_baseline(glyph_augmentation_dot, bounds.x + bounds.width + spacing * 0.75F, staff_top + spacing * 2.0F, spacing),
|
|
spacing,
|
|
score_ink(),
|
|
system_index,
|
|
measure_index,
|
|
note.note_index);
|
|
}
|
|
}
|
|
|
|
auto layout_note_symbols(
|
|
vamp_zero::score_page_layout& layout,
|
|
note_layout const& note,
|
|
std::float32_t staff_top,
|
|
std::float32_t spacing,
|
|
std::size_t system_index,
|
|
std::size_t measure_index
|
|
) -> void {
|
|
if (note.hidden) {
|
|
return;
|
|
}
|
|
if (note.rest) {
|
|
layout_rest(layout, note, staff_top, spacing, system_index, measure_index);
|
|
return;
|
|
}
|
|
|
|
layout_ledger_lines(layout, note, staff_top, spacing, system_index, measure_index);
|
|
for (auto const& accidental : accidental_layouts(note, spacing)) {
|
|
add_glyph(layout, vamp_zero::score_layout_item_role::ACCIDENTAL, accidental.codepoint, accidental.origin, spacing, score_ink(), system_index, measure_index, note.note_index);
|
|
}
|
|
for (auto const& head : note.heads) {
|
|
add_glyph(layout, vamp_zero::score_layout_item_role::NOTEHEAD, head.codepoint, head.origin, spacing, score_ink(), system_index, measure_index, note.note_index);
|
|
}
|
|
if (note.duration.dots != 0) {
|
|
auto dot_x = note.visual_right - spacing * 0.65F;
|
|
for (auto const& head : note.heads) {
|
|
add_glyph(
|
|
layout,
|
|
vamp_zero::score_layout_item_role::DOT,
|
|
glyph_augmentation_dot,
|
|
origin_for_centered_baseline(glyph_augmentation_dot, dot_x, dot_y_for(head.center_y, staff_top, spacing), spacing),
|
|
spacing,
|
|
score_ink(),
|
|
system_index,
|
|
measure_index,
|
|
note.note_index);
|
|
}
|
|
}
|
|
}
|
|
|
|
auto note_x(std::float32_t start_x, std::float32_t width, std::int64_t total_ticks, std::int64_t elapsed_ticks, std::int64_t duration_ticks) -> std::float32_t {
|
|
auto center_ticks = static_cast<std::float32_t>(elapsed_ticks) + static_cast<std::float32_t>(duration_ticks) * 0.5F;
|
|
auto ratio = std::clamp<std::float32_t>(center_ticks / static_cast<std::float32_t>(total_ticks), std::float32_t{0.0F}, std::float32_t{1.0F});
|
|
return start_x + width * ratio;
|
|
}
|
|
|
|
auto layout_tuplets(
|
|
vamp_zero::score_page_layout& layout,
|
|
vamp_zero::score_measure const& measure,
|
|
std::vector<note_layout> const& notes,
|
|
std::float32_t staff_top,
|
|
std::float32_t spacing,
|
|
std::size_t system_index,
|
|
std::size_t measure_index
|
|
) -> void {
|
|
for (auto const& tuplet : measure.tuplets) {
|
|
if (tuplet.id.empty() || tuplet.note_count <= 0) {
|
|
continue;
|
|
}
|
|
auto first = invalid_layout_index;
|
|
auto last = invalid_layout_index;
|
|
for (auto index = std::size_t{}; index < notes.size(); ++index) {
|
|
if (notes[index].source != nullptr && notes[index].source->tuplet_id == tuplet.id) {
|
|
first = std::min(first, index);
|
|
last = index;
|
|
}
|
|
}
|
|
if (first == invalid_layout_index || last == invalid_layout_index || first > last) {
|
|
continue;
|
|
}
|
|
auto same_beam = notes[first].beamed;
|
|
auto beam_index = notes[first].beam_group_index;
|
|
auto top_y = staff_top;
|
|
for (auto index = first; index <= last; ++index) {
|
|
top_y = std::min(top_y, notes[index].beamed ? notes[index].stem_tip_y : notes[index].high_y);
|
|
same_beam = same_beam && notes[index].beamed && notes[index].beam_group_index == beam_index;
|
|
}
|
|
auto text = std::to_string(tuplet.note_count);
|
|
auto y = top_y - spacing * 1.55F;
|
|
auto x0 = notes[first].x;
|
|
auto x1 = notes[last].x;
|
|
add_text(
|
|
layout,
|
|
vamp_zero::score_layout_item_role::TUPLET,
|
|
(x0 + x1) * 0.5F - spacing * 0.35F,
|
|
y - spacing * 0.25F,
|
|
std::move(text),
|
|
1,
|
|
score_muted_ink(),
|
|
spacing * 1.6F,
|
|
system_index,
|
|
measure_index,
|
|
first);
|
|
if (!same_beam) {
|
|
auto bracket_y = y + spacing * 0.65F;
|
|
add_stroke_line(layout, vamp_zero::score_layout_item_role::TUPLET_BRACKET, {.x = x0, .y = bracket_y}, {.x = x1, .y = bracket_y}, 1.0F, score_muted_ink(), system_index, measure_index);
|
|
add_stroke_line(layout, vamp_zero::score_layout_item_role::TUPLET_BRACKET, {.x = x0, .y = bracket_y}, {.x = x0, .y = bracket_y + spacing * 0.35F}, 1.0F, score_muted_ink(), system_index, measure_index);
|
|
add_stroke_line(layout, vamp_zero::score_layout_item_role::TUPLET_BRACKET, {.x = x1, .y = bracket_y}, {.x = x1, .y = bracket_y + spacing * 0.35F}, 1.0F, score_muted_ink(), system_index, measure_index);
|
|
}
|
|
}
|
|
}
|
|
|
|
auto measure_notation_top_y(
|
|
vamp_zero::score_measure const& measure,
|
|
std::vector<note_layout> const& notes,
|
|
std::vector<beam_group_layout> const& beam_groups,
|
|
std::float32_t staff_top,
|
|
std::float32_t spacing
|
|
) -> std::float32_t {
|
|
auto top_y = staff_top;
|
|
for (auto const& note : notes) {
|
|
if (!note.hidden) {
|
|
top_y = std::min(top_y, note.visual_top);
|
|
}
|
|
}
|
|
for (auto const& group : beam_groups) {
|
|
top_y = std::min(top_y, group.beam_y - scaled(beam_thickness_spaces, spacing) * 0.5F);
|
|
}
|
|
for (auto const& tuplet : measure.tuplets) {
|
|
if (tuplet.id.empty() || tuplet.note_count <= 0) {
|
|
continue;
|
|
}
|
|
auto first = invalid_layout_index;
|
|
auto last = invalid_layout_index;
|
|
for (auto index = std::size_t{}; index < notes.size(); ++index) {
|
|
if (notes[index].source != nullptr && notes[index].source->tuplet_id == tuplet.id) {
|
|
first = std::min(first, index);
|
|
last = index;
|
|
}
|
|
}
|
|
if (first == invalid_layout_index || last == invalid_layout_index || first > last) {
|
|
continue;
|
|
}
|
|
auto tuplet_top_y = staff_top;
|
|
for (auto index = first; index <= last; ++index) {
|
|
tuplet_top_y = std::min(tuplet_top_y, notes[index].beamed ? notes[index].stem_tip_y : notes[index].visual_top);
|
|
}
|
|
top_y = std::min(top_y, tuplet_top_y - spacing * 1.80F);
|
|
}
|
|
return top_y;
|
|
}
|
|
|
|
auto measure_notation_bottom_y(std::vector<note_layout> const& notes, std::vector<beam_group_layout> const& beam_groups, std::float32_t staff_top, std::float32_t spacing) -> std::float32_t {
|
|
auto bottom_y = staff_top + spacing * 4.0F;
|
|
for (auto const& note : notes) {
|
|
if (!note.hidden) {
|
|
bottom_y = std::max(bottom_y, note.visual_bottom);
|
|
}
|
|
}
|
|
for (auto const& group : beam_groups) {
|
|
bottom_y = std::max(bottom_y, group.beam_y + scaled(beam_thickness_spaces, spacing) * 0.5F);
|
|
}
|
|
return bottom_y;
|
|
}
|
|
|
|
auto lyric_y(std::float32_t staff_top, std::float32_t spacing, std::float32_t notation_bottom_y) -> std::float32_t {
|
|
return std::max(staff_top + scaled(lyric_below_staff_spaces, spacing), notation_bottom_y + scaled(lyric_clearance_spaces, spacing));
|
|
}
|
|
|
|
auto chord_symbol_y(std::float32_t staff_top, std::float32_t spacing, std::float32_t notation_top_y) -> std::float32_t {
|
|
auto y = staff_top - scaled(chord_above_staff_spaces, spacing);
|
|
auto const bottom_limit = notation_top_y - scaled(chord_clearance_spaces, spacing);
|
|
if (y + text_line_height(2) > bottom_limit) {
|
|
y = bottom_limit - text_line_height(2);
|
|
}
|
|
return y;
|
|
}
|
|
|
|
auto same_pitch(vamp_zero::score_pitch const& left, vamp_zero::score_pitch const& right) -> std::bool_t {
|
|
return normalized_letter(left.letter) == normalized_letter(right.letter)
|
|
&& left.octave == right.octave
|
|
&& pitch_alteration(left.accidental) == pitch_alteration(right.accidental);
|
|
}
|
|
|
|
auto make_laid_out_note_ref(note_layout const& note, luma::rect notation_bounds, std::size_t system_index, std::size_t measure_index) -> laid_out_note_ref {
|
|
auto ref = laid_out_note_ref{
|
|
.system_index = system_index,
|
|
.measure_index = measure_index,
|
|
.note_index = note.note_index,
|
|
.notation_bounds = notation_bounds,
|
|
.stem_up = note.stem_up,
|
|
};
|
|
if (note.source == nullptr || note.hidden || note.rest) {
|
|
return ref;
|
|
}
|
|
ref.heads.reserve(note.heads.size());
|
|
for (auto const& head : note.heads) {
|
|
if (head.source_pitch_index >= note.source->pitches.size()) {
|
|
continue;
|
|
}
|
|
ref.heads.push_back({
|
|
.pitch = note.source->pitches[head.source_pitch_index],
|
|
.bounds = head.bounds,
|
|
.center_y = head.center_y,
|
|
});
|
|
}
|
|
return ref;
|
|
}
|
|
|
|
auto find_note_ref(std::vector<laid_out_note_ref> const& refs, std::size_t measure_index, std::size_t note_index) -> laid_out_note_ref const* {
|
|
auto found = std::find_if(refs.begin(), refs.end(), [&](auto const& ref) {
|
|
return ref.measure_index == measure_index && ref.note_index == note_index;
|
|
});
|
|
return found == refs.end() ? nullptr : &*found;
|
|
}
|
|
|
|
auto find_measure_ref(std::vector<laid_out_measure_ref> const& refs, std::size_t measure_index) -> laid_out_measure_ref const* {
|
|
auto found = std::find_if(refs.begin(), refs.end(), [&](auto const& ref) {
|
|
return ref.measure_index == measure_index;
|
|
});
|
|
return found == refs.end() ? nullptr : &*found;
|
|
}
|
|
|
|
auto find_tie_head(laid_out_note_ref const& ref, vamp_zero::score_pitch const& pitch) -> laid_out_tie_head const* {
|
|
auto found = std::find_if(ref.heads.begin(), ref.heads.end(), [&](auto const& head) {
|
|
return same_pitch(head.pitch, pitch);
|
|
});
|
|
return found == ref.heads.end() ? nullptr : &*found;
|
|
}
|
|
|
|
struct tied_note_destination {
|
|
std::size_t measure_index{};
|
|
std::size_t note_index{};
|
|
};
|
|
|
|
auto find_tied_destination(vamp_zero::score_document const& score, std::size_t measure_index, std::size_t note_index, vamp_zero::score_pitch const& pitch) -> std::optional<tied_note_destination> {
|
|
for (auto current_measure = measure_index; current_measure < score.measures.size(); ++current_measure) {
|
|
auto const& measure = score.measures[current_measure];
|
|
auto first_note = current_measure == measure_index ? note_index + 1U : 0U;
|
|
for (auto current_note = first_note; current_note < measure.notes.size(); ++current_note) {
|
|
auto const& note = measure.notes[current_note];
|
|
if (note.hidden || note.kind == "rest") {
|
|
continue;
|
|
}
|
|
auto matching_pitch = std::find_if(note.pitches.begin(), note.pitches.end(), [&](auto const& candidate) {
|
|
return same_pitch(candidate, pitch);
|
|
});
|
|
if (matching_pitch == note.pitches.end()) {
|
|
return std::nullopt;
|
|
}
|
|
if (note.tie_from_previous) {
|
|
return tied_note_destination{.measure_index = current_measure, .note_index = current_note};
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
auto add_tie_curve(
|
|
vamp_zero::score_page_layout& layout,
|
|
luma::point from,
|
|
luma::point to,
|
|
std::bool_t below_note,
|
|
std::float32_t spacing,
|
|
std::size_t system_index,
|
|
std::size_t measure_index,
|
|
std::size_t note_index
|
|
) -> void {
|
|
if (to.x <= from.x + spacing * 0.40F) {
|
|
return;
|
|
}
|
|
auto direction = below_note ? std::float32_t{1.0F} : std::float32_t{-1.0F};
|
|
auto control = luma::point{
|
|
.x = (from.x + to.x) * 0.5F,
|
|
.y = (from.y + to.y) * 0.5F + direction * scaled(tie_arch_spaces, spacing),
|
|
};
|
|
add_curve(
|
|
layout,
|
|
vamp_zero::score_layout_item_role::TIE,
|
|
from,
|
|
control,
|
|
to,
|
|
at_least_one_pixel(scaled(tie_thickness_spaces, spacing)),
|
|
score_ink(),
|
|
system_index,
|
|
measure_index,
|
|
note_index);
|
|
}
|
|
|
|
auto layout_page_ties(
|
|
vamp_zero::score_page_layout& layout,
|
|
vamp_zero::score_document const& score,
|
|
std::vector<laid_out_note_ref> const& note_refs,
|
|
std::vector<laid_out_measure_ref> const& measure_refs,
|
|
std::float32_t spacing
|
|
) -> void {
|
|
for (auto measure_index = std::size_t{}; measure_index < score.measures.size(); ++measure_index) {
|
|
auto const& measure = score.measures[measure_index];
|
|
for (auto note_index = std::size_t{}; note_index < measure.notes.size(); ++note_index) {
|
|
auto const& note = measure.notes[note_index];
|
|
if (!note.tie_to_next || note.hidden || note.kind == "rest") {
|
|
continue;
|
|
}
|
|
auto const* start_ref = find_note_ref(note_refs, measure_index, note_index);
|
|
for (auto const& pitch : note.pitches) {
|
|
auto destination = find_tied_destination(score, measure_index, note_index, pitch);
|
|
auto const* destination_ref = destination ? find_note_ref(note_refs, destination->measure_index, destination->note_index) : nullptr;
|
|
auto const* start_head = start_ref == nullptr ? nullptr : find_tie_head(*start_ref, pitch);
|
|
auto const* destination_head = destination_ref == nullptr ? nullptr : find_tie_head(*destination_ref, pitch);
|
|
|
|
if (start_ref != nullptr && start_head != nullptr && destination_ref != nullptr && destination_head != nullptr && start_ref->system_index == destination_ref->system_index) {
|
|
auto below = start_ref->stem_up;
|
|
auto y_offset = scaled(tie_note_offset_spaces, spacing) * (below ? 1.0F : -1.0F);
|
|
add_tie_curve(
|
|
layout,
|
|
{.x = start_head->bounds.x + start_head->bounds.width + scaled(tie_endpoint_gap_spaces, spacing), .y = start_head->center_y + y_offset},
|
|
{.x = destination_head->bounds.x - scaled(tie_endpoint_gap_spaces, spacing), .y = destination_head->center_y + y_offset},
|
|
below,
|
|
spacing,
|
|
start_ref->system_index,
|
|
start_ref->measure_index,
|
|
start_ref->note_index);
|
|
continue;
|
|
}
|
|
|
|
if (start_ref != nullptr && start_head != nullptr) {
|
|
auto below = start_ref->stem_up;
|
|
auto y_offset = scaled(tie_note_offset_spaces, spacing) * (below ? 1.0F : -1.0F);
|
|
add_tie_curve(
|
|
layout,
|
|
{.x = start_head->bounds.x + start_head->bounds.width + scaled(tie_endpoint_gap_spaces, spacing), .y = start_head->center_y + y_offset},
|
|
{.x = start_ref->notation_bounds.x + start_ref->notation_bounds.width, .y = start_head->center_y + y_offset},
|
|
below,
|
|
spacing,
|
|
start_ref->system_index,
|
|
start_ref->measure_index,
|
|
start_ref->note_index);
|
|
}
|
|
if (destination && destination_ref != nullptr && destination_head != nullptr) {
|
|
auto const* measure_ref = find_measure_ref(measure_refs, destination->measure_index);
|
|
if (measure_ref == nullptr) {
|
|
continue;
|
|
}
|
|
auto below = destination_ref->stem_up;
|
|
auto y_offset = scaled(tie_note_offset_spaces, spacing) * (below ? 1.0F : -1.0F);
|
|
add_tie_curve(
|
|
layout,
|
|
{.x = measure_ref->notation_bounds.x, .y = destination_head->center_y + y_offset},
|
|
{.x = destination_head->bounds.x - scaled(tie_endpoint_gap_spaces, spacing), .y = destination_head->center_y + y_offset},
|
|
below,
|
|
spacing,
|
|
destination_ref->system_index,
|
|
destination_ref->measure_index,
|
|
destination_ref->note_index);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
auto layout_measure(
|
|
vamp_zero::score_page_layout& layout,
|
|
vamp_zero::score_document const& score,
|
|
vamp_zero::score_measure const& measure,
|
|
luma::rect bounds,
|
|
std::size_t measure_number,
|
|
std::size_t system_index,
|
|
std::size_t measure_index,
|
|
std::float32_t staff_top,
|
|
std::float32_t spacing,
|
|
std::vector<laid_out_note_ref>& note_refs,
|
|
std::vector<laid_out_measure_ref>& measure_refs
|
|
) -> void {
|
|
add_text(layout, vamp_zero::score_layout_item_role::MEASURE_NUMBER, bounds.x + spacing * 0.20F, bounds.y + spacing * 0.05F, std::to_string(measure_number), 1, score_muted_ink(), spacing * 4.0F, system_index, measure_index);
|
|
|
|
auto notation_x = bounds.x + spacing * 1.15F;
|
|
auto notation_right_x = bounds.x + bounds.width - scaled(measure_right_guard_spaces, spacing);
|
|
auto notation_width = std::max<std::float32_t>(spacing * 3.0F, notation_right_x - notation_x);
|
|
auto notation_bounds = luma::rect{.x = notation_x, .y = bounds.y, .width = notation_width, .height = bounds.height};
|
|
measure_refs.push_back({
|
|
.measure_index = measure_index,
|
|
.notation_bounds = notation_bounds,
|
|
});
|
|
auto total_ticks = measure_ticks(measure);
|
|
auto elapsed = std::int64_t{};
|
|
auto accidentals = make_accidental_state(effective_key_signature(score, measure));
|
|
auto notes = std::vector<note_layout>{};
|
|
notes.reserve(measure.notes.size());
|
|
for (auto index = std::size_t{}; index < measure.notes.size(); ++index) {
|
|
auto const& note = measure.notes[index];
|
|
auto duration_ticks = spacing_ticks(note);
|
|
auto x = note_x(notation_x, notation_width, total_ticks, elapsed, duration_ticks);
|
|
auto const slash = note.notehead == "slash";
|
|
auto accidental_marks = notated_accidentals_for_note(accidentals, note, slash);
|
|
notes.push_back(build_note_layout(score, note, index, elapsed, duration_ticks, x, staff_top, spacing, accidental_marks));
|
|
elapsed += duration_ticks;
|
|
}
|
|
fit_note_spacing(notes, notation_bounds, spacing);
|
|
auto beam_groups = make_beam_groups(notes, staff_top, spacing);
|
|
for (auto& note : notes) {
|
|
refresh_note_visual_extents(note, spacing);
|
|
}
|
|
fit_note_spacing(notes, notation_bounds, spacing);
|
|
keep_notes_inside_right_guard(notes, notation_bounds, spacing);
|
|
for (auto& note : notes) {
|
|
refresh_note_visual_extents(note, spacing);
|
|
}
|
|
|
|
auto const notation_top_y = measure_notation_top_y(measure, notes, beam_groups, staff_top, spacing);
|
|
auto const notation_bottom_y = measure_notation_bottom_y(notes, beam_groups, staff_top, spacing);
|
|
auto const chord_y = chord_symbol_y(staff_top, spacing, notation_top_y);
|
|
auto const lyrics_y = lyric_y(staff_top, spacing, notation_bottom_y);
|
|
for (auto const& note : notes) {
|
|
note_refs.push_back(make_laid_out_note_ref(note, notation_bounds, system_index, measure_index));
|
|
}
|
|
if (!measure.section_label.empty()) {
|
|
auto section_y = std::min(staff_top - spacing * 4.70F, chord_y - spacing * 0.65F);
|
|
add_text(layout, vamp_zero::score_layout_item_role::SECTION_LABEL, bounds.x + spacing * 2.10F, section_y, measure.section_label, 2, score_accent_ink(), bounds.width - spacing * 2.6F, system_index, measure_index);
|
|
}
|
|
|
|
auto previous_chord = std::string{};
|
|
for (auto const& note : notes) {
|
|
if (note.source == nullptr || note.hidden || note.source->chord_symbol.empty() || note.source->chord_symbol == previous_chord) {
|
|
continue;
|
|
}
|
|
add_text(
|
|
layout,
|
|
vamp_zero::score_layout_item_role::CHORD_SYMBOL,
|
|
note.x - spacing * 1.15F,
|
|
chord_y,
|
|
note.source->chord_symbol,
|
|
2,
|
|
score_ink(),
|
|
std::min<std::float32_t>(spacing * 10.0F, bounds.width - (note.x - bounds.x)),
|
|
system_index,
|
|
measure_index,
|
|
note.note_index);
|
|
previous_chord = note.source->chord_symbol;
|
|
}
|
|
|
|
for (auto const& note : notes) {
|
|
if (note.hidden || note.rest || !has_stem(note.duration)) {
|
|
continue;
|
|
}
|
|
add_stroke_line(
|
|
layout,
|
|
vamp_zero::score_layout_item_role::STEM,
|
|
{.x = note.stem_x, .y = note.stem_base_y},
|
|
{.x = note.stem_x, .y = note.stem_tip_y},
|
|
at_least_one_pixel(scaled(stem_thickness_spaces, spacing)),
|
|
score_ink(),
|
|
system_index,
|
|
measure_index,
|
|
note.note_index);
|
|
}
|
|
for (auto const& group : beam_groups) {
|
|
add_stroke_line(
|
|
layout,
|
|
vamp_zero::score_layout_item_role::BEAM,
|
|
{.x = notes[group.first_note].stem_x, .y = group.beam_y},
|
|
{.x = notes[group.last_note].stem_x, .y = group.beam_y},
|
|
scaled(beam_thickness_spaces, spacing),
|
|
score_ink(),
|
|
system_index,
|
|
measure_index);
|
|
}
|
|
for (auto const& note : notes) {
|
|
if (note.hidden || note.rest || note.beamed || !has_flag(note.duration)) {
|
|
continue;
|
|
}
|
|
add_flag(layout, note, spacing, system_index, measure_index);
|
|
}
|
|
for (auto index = std::size_t{}; index < notes.size(); ++index) {
|
|
layout_note_symbols(layout, notes[index], staff_top, spacing, system_index, measure_index);
|
|
if (notes[index].source != nullptr && !notes[index].hidden && !notes[index].source->lyric.empty()) {
|
|
add_text(
|
|
layout,
|
|
vamp_zero::score_layout_item_role::LYRIC,
|
|
notes[index].x - spacing,
|
|
lyrics_y,
|
|
notes[index].source->lyric,
|
|
1,
|
|
score_muted_ink(),
|
|
spacing * 8.0F,
|
|
system_index,
|
|
measure_index,
|
|
notes[index].note_index);
|
|
}
|
|
}
|
|
layout_tuplets(layout, measure, notes, staff_top, spacing, system_index, measure_index);
|
|
}
|
|
|
|
auto draw_layout_item(luma::draw_context& context, vamp_zero::score_layout_item const& item) -> void {
|
|
switch (item.kind) {
|
|
case vamp_zero::score_layout_item_kind::FILL_RECT:
|
|
context.fill_rect(item.bounds, item.color);
|
|
break;
|
|
case vamp_zero::score_layout_item_kind::STROKE_RECT:
|
|
context.stroke_rect(item.bounds, item.thickness, item.color);
|
|
break;
|
|
case vamp_zero::score_layout_item_kind::STROKE_LINE:
|
|
context.stroke_line(item.from, item.to, item.thickness, item.color);
|
|
break;
|
|
case vamp_zero::score_layout_item_kind::CURVE:
|
|
draw_curve(context, item.from, item.control, item.to, item.thickness, item.color);
|
|
break;
|
|
case vamp_zero::score_layout_item_kind::GLYPH:
|
|
draw_music_origin(item.codepoint, item.origin, item.staff_spacing, item.color);
|
|
break;
|
|
case vamp_zero::score_layout_item_kind::TEXT: {
|
|
auto style = text_style(item.text_scale);
|
|
context.push_clip(item.bounds);
|
|
context.draw_text(item.text, item.origin, style, item.color);
|
|
context.pop_clip();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
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::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 layout = vamp_zero::score_page_layout{
|
|
.bounds = bounds,
|
|
.staff_spacing = 12.0F,
|
|
};
|
|
auto const spacing = layout.staff_spacing;
|
|
auto const margin = std::float32_t{28.0F};
|
|
auto const header_y = bounds.y + 14.0F;
|
|
|
|
add_fill_rect(layout, vamp_zero::score_layout_item_role::PAPER, bounds, score_paper());
|
|
add_text(layout, vamp_zero::score_layout_item_role::TITLE, bounds.x + margin, header_y, score.title, 3, score_ink(), bounds.width * 0.55F);
|
|
add_text(layout, vamp_zero::score_layout_item_role::PART, bounds.x + bounds.width - 220.0F, header_y + 2.0F, score.part, 3, score_accent_ink(), 190.0F);
|
|
add_text(layout, vamp_zero::score_layout_item_role::COMPOSER, bounds.x + margin, header_y + 34.0F, score.composer, 1, score_muted_ink(), bounds.width * 0.6F);
|
|
add_text(layout, vamp_zero::score_layout_item_role::PAGE_NUMBER, bounds.x + bounds.width - 122.0F, header_y + 39.0F, "PAGE " + std::to_string(page_index + 1U), 1, score_muted_ink(), 110.0F);
|
|
|
|
auto const content_x = bounds.x + margin;
|
|
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 system_height = (bounds.height - 108.0F) / static_cast<std::float32_t>(vamp_zero::score_systems_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 measure_refs = std::vector<laid_out_measure_ref>{};
|
|
|
|
for (auto system = std::size_t{}; system < vamp_zero::score_systems_per_page; ++system) {
|
|
auto const system_y = content_y + static_cast<std::float32_t>(system) * system_height;
|
|
auto const staff_top = system_y + spacing * 4.25F;
|
|
auto const range_index = first_system + system;
|
|
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);
|
|
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_area_width = std::max<std::float32_t>(spacing * 12.0F, content_x + content_width - measure_start_x);
|
|
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 < range->measure_count; ++measure) {
|
|
auto const measure_index = range->first_measure + measure;
|
|
auto const x = measure_start_x + static_cast<std::float32_t>(measure) * measure_width;
|
|
if (measure_index < score.measures.size()) {
|
|
layout_measure(
|
|
layout,
|
|
score,
|
|
score.measures[measure_index],
|
|
{.x = x, .y = system_y, .width = measure_width, .height = system_height},
|
|
measure_index + 1U,
|
|
system,
|
|
measure_index,
|
|
staff_top,
|
|
spacing,
|
|
note_refs,
|
|
measure_refs);
|
|
}
|
|
|
|
auto const bar_x = x + measure_width;
|
|
auto const final_bar = measure + 1U == range->measure_count || measure_index + 1U >= score.measures.size();
|
|
add_stroke_line(
|
|
layout,
|
|
final_bar ? vamp_zero::score_layout_item_role::FINAL_BARLINE : vamp_zero::score_layout_item_role::BARLINE,
|
|
{.x = bar_x, .y = staff_top},
|
|
{.x = bar_x, .y = staff_top + spacing * 4.0F},
|
|
at_least_one_pixel(scaled(final_bar ? thick_barline_thickness_spaces : thin_barline_thickness_spaces, spacing)),
|
|
score_ink(),
|
|
system,
|
|
measure_index);
|
|
}
|
|
}
|
|
|
|
layout_page_ties(layout, score, note_refs, measure_refs, spacing);
|
|
add_stroke_rect(layout, vamp_zero::score_layout_item_role::BORDER, bounds, 1.1F, score_border());
|
|
return layout;
|
|
}
|
|
|
|
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 layout = vamp_zero::layout_score_page(bounds, score, page_index);
|
|
for (auto const& item : layout.items) {
|
|
if (item.role == vamp_zero::score_layout_item_role::PAPER) {
|
|
draw_layout_item(context, item);
|
|
}
|
|
}
|
|
|
|
context.push_clip(bounds);
|
|
for (auto const& item : layout.items) {
|
|
if (item.role != vamp_zero::score_layout_item_role::PAPER && item.role != vamp_zero::score_layout_item_role::BORDER) {
|
|
draw_layout_item(context, item);
|
|
}
|
|
}
|
|
context.pop_clip();
|
|
|
|
for (auto const& item : layout.items) {
|
|
if (item.role == vamp_zero::score_layout_item_role::BORDER) {
|
|
draw_layout_item(context, item);
|
|
}
|
|
}
|
|
}
|