Files
product-vamp/src/vamp_zero/score.cpp
T
khodowany fe42d811fa
ci / checks (push) Successful in 42m54s
Formatting
2026-09-15 00:06:02 -05:00

2792 lines
115 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>
#include <vamp_zero/parse.hpp>
#include <vamp_zero/score_metrics.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 {
MAXIMA,
LONG,
BREVE,
WHOLE,
HALF,
QUARTER,
EIGHTH,
SIXTEENTH,
THIRTY_SECOND,
SIXTY_FOURTH,
ONE_TWENTY_EIGHTH,
TWO_FIFTY_SIXTH,
FIVE_TWELFTH,
TEN_TWENTY_FOURTH,
};
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>{};
for (auto const &glyph : vamp_zero::font_data::glyphs) {
codepoints.push_back(glyph.codepoint);
}
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 {
auto const &glyphs = vamp_zero::font_data::glyphs;
auto found = std::lower_bound(
glyphs.begin(), glyphs.end(), codepoint, [](auto const &g, std::int32_t value) { return g.codepoint < value; });
if (found == glyphs.end() || found->codepoint != codepoint) {
return {};
}
return {found->bounds[0], found->bounds[1], found->bounds[2], found->bounds[3]};
}
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(vamp_zero::score_written_duration const &written, double fallback_ticks = 4096.0) -> duration_shape {
constexpr auto names = std::array<std::string_view, 14>{"maxima",
"long",
"breve",
"whole",
"half",
"quarter",
"eighth",
"16th",
"32nd",
"64th",
"128th",
"256th",
"512th",
"1024th"};
auto found = std::find(names.begin(), names.end(), written.type);
if (found != names.end()) {
return {.base = static_cast<duration_base>(found - names.begin()), .dots = written.dots};
}
// Directly constructed layout documents use the same exact written-value lookup.
for (auto index = std::size_t{}; index < names.size(); ++index) {
for (auto dots = std::int32_t{}; dots <= 16; ++dots) {
if (std::abs(fallback_ticks - std::ldexp(131072.0, -static_cast<std::int32_t>(index)) * (2.0 - std::ldexp(1.0, -dots))) < 0.00001) {
return {.base = static_cast<duration_base>(index), .dots = dots};
}
}
}
return {};
}
auto spacing_ticks(vamp_zero::score_note const &note) -> double {
return note.duration_ticks;
}
auto measure_ticks(vamp_zero::score_measure const &measure) -> double {
if (measure.duration.numerator > 0) {
return measure.duration.quarters() * 4096.0;
}
auto total = 0.0;
for (auto const &note : measure.notes) {
total += spacing_ticks(note);
}
return std::max(1.0, total);
}
auto has_stem(duration_shape duration) -> std::bool_t {
return duration.base == duration_base::LONG || duration.base == duration_base::MAXIMA ||
duration.base >= duration_base::HALF;
}
auto flag_count(duration_shape duration) -> std::int32_t {
return std::max(
std::int32_t{}, static_cast<std::int32_t>(duration.base) - static_cast<std::int32_t>(duration_base::QUARTER));
}
auto has_flag(duration_shape duration) -> std::bool_t {
return flag_count(duration) > 0;
}
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_octave(std::string_view clef) -> std::int32_t {
if (clef.size() < 5 || clef[1] != ':' || clef[3] != ':') {
return 0;
}
auto octave = vamp_zero::parse_integer<std::int32_t>(clef.substr(4), "clef octave");
if (!octave || *octave < -3 || *octave > 3) {
return 0;
}
return *octave;
}
auto clef_bottom_line_pitch_index(std::string_view clef) -> std::int32_t {
if (clef.size() >= 5 && clef[1] == ':') {
auto line = clef[2] - '0';
auto octave = clef_octave(clef);
auto anchor = clef[0] == 'F' ? 3 * 7 + 3 : clef[0] == 'C' ? 4 * 7
: 4 * 7 + 4;
return anchor + octave * 7 - 2 * (line - 1);
}
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::DOUBLE_FLAT:
return 0xe264;
case vamp_zero::score_accidental_mark::DOUBLE_SHARP:
return 0xe263;
case vamp_zero::score_accidental_mark::TRIPLE_FLAT:
return 0xe266;
case vamp_zero::score_accidental_mark::TRIPLE_SHARP:
return 0xe265;
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 == "double-sharp") {
return 2;
}
if (accidental == "bb" || accidental == "flat-flat") {
return -2;
}
if (accidental == "###") {
return 3;
}
if (accidental == "bbb") {
return -3;
}
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 == -3) {
return vamp_zero::score_accidental_mark::TRIPLE_FLAT;
}
if (alteration == 3) {
return vamp_zero::score_accidental_mark::TRIPLE_SHARP;
}
if (alteration == -2) {
return vamp_zero::score_accidental_mark::DOUBLE_FLAT;
}
if (alteration == 2) {
return vamp_zero::score_accidental_mark::DOUBLE_SHARP;
}
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 && !pitch.explicit_accidental) {
return vamp_zero::score_accidental_mark::NONE;
}
auto const letter = letter_index(normalized_letter(pitch.letter));
auto const desired = pitch.alteration.value_or(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 && !pitch.explicit_accidental) {
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, !pitch.tie_from.empty() || (pitch.id.empty() && 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, &current) ||
previous.clef != current.clef || previous.key_alterations != current.key_alterations;
}
auto event_width(vamp_zero::score_note const &note) -> float {
if (note.hidden) {
return 8.0F;
}
auto accidentals = std::count_if(note.pitches.begin(), note.pitches.end(), [](auto const &p) {
return p.alteration.value_or(pitch_alteration(p.accidental)) != 0 || p.explicit_accidental;
});
return 30.0F + 12.0F * std::min<std::size_t>(3, accidentals) + note.written.dots * 8.0F +
std::max(0.0F, static_cast<float>(note.lyric.size()) * 7 - 30);
}
auto attribute_width(vamp_zero::score_attribute const &a) -> float {
auto result = a.clef.empty() ? 0.0F : 45.0F;
if (a.key_alterations) {
result += 35.0F + 22.0F * std::count_if(a.key_alterations->begin(), a.key_alterations->end(), [](std::int32_t v) {
return v != 0;
});
}
if (!a.time_signature.empty()) {
result += 50.0F;
}
return result;
}
auto measure_width(vamp_zero::score_measure const &measure) -> float {
auto width = 45.0F;
for (auto const &note : measure.notes) {
width += event_width(note);
}
auto harmony_width = 45.0F;
for (auto const &h : measure.harmonies) {
harmony_width += static_cast<float>(h.symbol.size()) * 14 + 16;
}
for (auto const &a : measure.attributes) {
width += attribute_width(a);
}
return std::max({150.0F, width, harmony_width});
}
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};
auto used_width = 0.0F;
for (auto measure_index = std::size_t{}; measure_index < score.measures.size(); ++measure_index) {
auto const &measure = score.measures[measure_index];
auto width = measure_width(measure);
if (current.measure_count != 0U &&
(current.measure_count >= vamp_zero::score_measures_per_system || used_width + width > 760.0F ||
notation_state_changes_before(score, measure_index))) {
ranges.push_back(current);
current = vamp_zero::score_system_range{.first_measure = measure_index, .measure_count = 0U};
used_width = 0;
}
if (width > 760.0F && measure.notes.size() > 1) {
auto first = std::size_t{};
while (first < measure.notes.size()) {
auto end = first;
auto fragment_width = 45.0F;
while (
end < measure.notes.size() && (end == first || fragment_width + event_width(measure.notes[end]) <= 660.0F)) {
fragment_width += event_width(measure.notes[end]);
++end;
}
// Keep an attached grace group with its metric anchor.
while (end < measure.notes.size() && measure.notes[end - 1].grace) {
++end;
}
ranges.push_back(
{.first_measure = measure_index, .measure_count = 1, .first_note = first, .note_count = end - first});
first = end;
}
current = {.first_measure = measure_index + 1};
used_width = 0;
continue;
}
++current.measure_count;
used_width += width;
}
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]);
}
struct system_metrics {
float above{5.5F};
float below{6.0F};
auto height() const -> float {
return (above + below) * 12.0F;
}
};
auto metrics_for(vamp_zero::score_document const &score, vamp_zero::score_system_range const &range) -> system_metrics {
auto result = system_metrics{};
for (auto mi = range.first_measure; mi < range.first_measure + range.measure_count; ++mi) {
auto const &measure = score.measures[mi];
if (!measure.section_label.empty()) {
result.above = std::max(result.above, 9.0F);
}
auto end = range.first_note + std::min(range.note_count, measure.notes.size() - range.first_note);
auto depth = std::int32_t{};
for (auto const &t : measure.tuplets) {
depth = std::max(depth, t.depth + 1);
}
for (auto ni = range.first_note; ni < end; ++ni) {
auto const &n = measure.notes[ni];
if (n.hidden) {
continue;
}
for (auto p : n.pitches) {
p.octave += n.display_octaves;
auto y = pitch_y(p, 0, 1, n.clef.empty() ? score.clef : n.clef);
result.above = std::max(result.above, -y + 4.0F + depth * 1.25F + (measure.harmonies.empty() ? 0.0F : 2.0F) + (measure.section_label.empty() ? 0.0F : 3.5F));
result.below = std::max(result.below, y + 4.0F + (n.lyric.empty() ? 0.0F : 2.0F));
}
}
}
return result;
}
auto page_starts(vamp_zero::score_document const &score, std::vector<vamp_zero::score_system_range> const &ranges) -> std::vector<std::size_t> {
auto starts = std::vector<std::size_t>{0};
auto height = 0.0F;
for (auto i = std::size_t{}; i < ranges.size(); ++i) {
auto next = std::min(376.0F, metrics_for(score, ranges[i]).height());
if (i > starts.back() && (height + next > 376.0F || i - starts.back() == vamp_zero::score_systems_per_page)) {
starts.push_back(i);
height = 0;
}
height += next;
}
return starts;
}
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{};
double elapsed_ticks{};
double 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{};
float size{1.0F};
float leading_space{};
};
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" || name.starts_with("f:") ? glyph_f_clef
: name.starts_with("c:") ? 0xe05c
: 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 = clef.size() >= 5 && clef[1] == ':' ? staff_top + spacing * (5 - (clef[2] - '0'))
: pitch_y(reference_pitch, staff_top, spacing, clef);
baseline_y += 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);
if (clef.size() >= 5 && clef[1] == ':') {
auto octave = clef_octave(clef);
if (octave) {
add_text(layout, vamp_zero::score_layout_item_role::CLEF, x + spacing, staff_top + spacing * (octave > 0 ? -2.5F : 5.5F), std::to_string(std::abs(octave) * 7 + 1), 1, score_ink(), spacing * 3, 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 layout_structured_key(vamp_zero::score_page_layout &layout, std::string_view clef, std::array<std::int32_t, 7> const &alterations, std::float32_t x, std::float32_t top, std::float32_t spacing, std::size_t system, std::array<std::int32_t, 7> const *previous = nullptr) -> std::float32_t {
auto start = x;
auto paint = [&](std::int32_t letter, std::int32_t alteration) {
auto glyph = accidental_glyph(alteration_mark(alteration));
if (!glyph) {
return;
}
auto pitch = vamp_zero::score_pitch{.letter = "cdefgab"[letter], .octave = 4};
while (pitch_y(pitch, top, spacing, clef) < top - spacing * 0.5F) {
--pitch.octave;
}
while (pitch_y(pitch, top, spacing, clef) > top + spacing * 3.5F) {
++pitch.octave;
}
add_glyph(layout, vamp_zero::score_layout_item_role::KEY_SIGNATURE, *glyph, origin_for_left_baseline(*glyph, x, pitch_y(pitch, top, spacing, clef), spacing), spacing, score_ink(), system);
x += glyph_width(*glyph, spacing) + spacing * 0.15F;
};
if (previous) {
for (auto i = std::int32_t{}; i < 7; ++i) {
if ((*previous)[i] && (*previous)[i] != alterations[i]) {
paint(i, 0);
}
}
}
auto flats = std::any_of(alterations.begin(), alterations.end(), [](std::int32_t a) { return a < 0; });
for (auto letter : std::string_view{flats ? "beadgcf" : "fcgdaeb"}) {
auto index = letter_index(letter);
if (alterations[index]) {
paint(index, alterations[index]);
}
}
return x - start + (x == start ? 0.0F : spacing * 0.55F);
}
auto time_signature_digit(std::char_t digit) -> std::optional<std::int32_t> {
if (digit == '+') {
return 0xe08c;
}
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 {
if (time_signature == "common" || time_signature == "cut") {
auto glyph = time_signature == "common" ? 0xe08a : 0xe08b;
add_glyph(layout, vamp_zero::score_layout_item_role::TIME_SIGNATURE, glyph, origin_for_left_baseline(glyph, x, staff_top + spacing * 2.0F, spacing), spacing, score_ink(), system_index);
return glyph_width(glyph, spacing) + spacing;
}
auto slash = time_signature.find('/');
if (slash == std::string_view::npos) {
return 0.0F;
}
auto group_end = time_signature.find('+', slash);
if (group_end != std::string_view::npos) {
auto width =
layout_time_signature(layout, time_signature.substr(0, group_end), x, staff_top, spacing, system_index);
layout_time_signature_run(layout, "+", x + width, staff_top + spacing * 2.0F, spacing, system_index);
width += spacing * 2;
return width + layout_time_signature(layout, time_signature.substr(group_end + 1), x + width, staff_top, spacing, system_index);
}
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::size_t first_note = 0) -> std::float32_t {
auto current_x = x + spacing * 0.35F;
auto clef = first_measure && !first_measure->clef.empty() ? std::string_view{first_measure->clef}
: std::string_view{score.clef};
auto key = first_measure ? first_measure->key_alterations : std::array<std::int32_t, 7>{};
auto time = effective_time_signature(score, first_measure);
if (first_measure && first_note > 0 && first_note < first_measure->notes.size()) {
auto onset = first_measure->notes[first_note].onset.quarters();
for (auto const &a : first_measure->attributes) {
if (a.onset.quarters() < onset) {
if (!a.clef.empty()) {
clef = a.clef;
}
if (a.key_alterations) {
key = *a.key_alterations;
}
if (!a.time_signature.empty()) {
time = a.time_signature;
}
}
}
}
current_x += layout_clef(layout, clef, current_x, staff_top, spacing, system_index);
auto key_width = layout_key_signature(layout, clef, first_measure == nullptr ? score.key_signature : effective_key_signature(score, *first_measure), current_x, staff_top, spacing, system_index);
if (first_measure && first_measure->structured_key) {
// The structured key is authoritative for minor, modal, and custom signatures.
layout.items.erase(std::remove_if(layout.items.begin(),
layout.items.end(),
[&](auto const &item) {
return item.system_index == system_index &&
item.role == vamp_zero::score_layout_item_role::KEY_SIGNATURE;
}),
layout.items.end());
auto previous =
first_measure > score.measures.data() && first_measure <= score.measures.data() + score.measures.size()
? &(first_measure - 1)->key_alterations
: nullptr;
key_width = layout_structured_key(layout, clef, key, current_x, staff_top, spacing, system_index, first_note ? nullptr : previous);
}
current_x += key_width;
if (key_width == 0.0F) {
current_x += spacing * 0.45F;
}
if (show_time_signature) {
current_x += layout_time_signature(layout, time, 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::MAXIMA:
return 0xe4e0;
case duration_base::LONG:
return 0xe4e1;
case duration_base::BREVE:
return 0xe4e2;
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_eighth + flag_count(duration) - 1;
}
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::MAXIMA:
return 0xe0a0;
case duration_base::LONG:
return 0xe0a1;
case duration_base::BREVE:
return 0xe0a0;
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 = std::bool_t{false};
for (auto &column : columns) {
auto fits = std::bool_t{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 = &note.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);
if (note.source->grace || note.source->cue) {
note.stem_tip_y = head->center_y + (note.stem_up ? -1.0F : 1.0F) * spacing * 2.5F;
}
}
auto build_note_layout(vamp_zero::score_document const &score, vamp_zero::score_note const &note, std::size_t note_index, double elapsed_ticks, double 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 = note.measure_rest
? duration_shape{.base = duration_base::WHOLE}
: duration_for(note.written, note.display_ticks > 0 ? note.display_ticks : note.duration_ticks);
auto slash = note.notehead == "slash";
auto result = note_layout{
.source = &note,
.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,
};
result.size = note.grace || note.cue ? 0.65F : 1.0F;
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 displayed_pitch = pitch;
displayed_pitch.octave += note.display_octaves;
auto yy = pitch_y(displayed_pitch, staff_top, spacing, note.clef.empty() ? score.clef : note.clef);
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 =
pitch.written.type.empty() ? result.notehead : notehead_codepoint(duration_for(pitch.written), false),
.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 = std::bool_t{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 * result.size);
head.bounds = glyph_bounds(head.codepoint, head.origin, spacing * result.size);
result.heads.push_back(head);
}
}
result.stem_up = note.stem == "up" || (note.stem != "down" && (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) && has_flag(note.duration) && !note.source->grace;
}
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 beam_type(note_layout const &note, std::int32_t level) -> std::string_view {
for (auto const &beam : note.source->beams) {
if (beam.level == level) {
return beam.type;
}
}
return {};
}
auto make_beam_groups(std::vector<note_layout> &notes, vamp_zero::score_measure const &measure, double offset_ticks, 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;
auto explicit_beam = beam_type(notes[index], 1);
if (!explicit_beam.empty()) {
while (end + 1U < notes.size() && beam_type(notes[end], 1) != "end" && eligible_for_beam(notes[end + 1]) &&
(beam_type(notes[end + 1], 1) == "continue" || beam_type(notes[end + 1], 1) == "end")) {
++end;
}
} else 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 onset = notes[index].elapsed_ticks + offset_ticks;
auto beat_end = (std::floor(onset / quarter_note_ticks) + 1) * quarter_note_ticks;
auto const *beat_pattern = &measure.beats;
auto beat_start = 0.0;
for (auto const &a : measure.attributes) {
if (!a.beats.empty() && a.onset.quarters() * 4096.0 <= onset) {
beat_pattern = &a.beats;
beat_start = a.onset.quarters() * 4096.0;
}
}
if (!beat_pattern->empty()) {
auto cycle = 0.0;
for (auto beat : *beat_pattern) {
cycle += beat.quarters() * quarter_note_ticks;
}
beat_end = beat_start + std::floor((onset - beat_start) / cycle) * cycle;
for (auto beat : *beat_pattern) {
beat_end += beat.quarters() * quarter_note_ticks;
if (beat_end > onset + 0.00001) {
break;
}
}
}
for (auto const &a : measure.attributes) {
if (!a.beats.empty() && a.onset.quarters() * 4096.0 > onset) {
beat_end = std::min(beat_end, a.onset.quarters() * 4096.0);
}
}
beat_end -= offset_ticks;
while (end + 1U < notes.size() && eligible_for_beam(notes[end + 1U]) && notes[end + 1U].source != nullptr &&
notes[end + 1U].source->grace == notes[index].source->grace && 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 = std::bool_t{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) + 2 * (flag_count(note.duration) - 1);
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 * note.size, 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) + 2 * (flag_count(note.duration) - 1);
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 * note.size);
}
struct secondary_stem_layout {
luma::point base;
luma::point tip;
std::optional<std::int32_t> flag;
};
auto secondary_stem(note_layout const &note, notehead_layout const &head, float spacing) -> std::optional<secondary_stem_layout> {
if (head.source_pitch_index >= note.source->pitches.size() || note.source->stem == "none") {
return std::nullopt;
}
auto const &tone = note.source->pitches[head.source_pitch_index];
if (tone.written.type.empty() || tone.written.type == note.source->written.type) {
return std::nullopt;
}
auto duration = duration_for(tone.written);
if (!has_stem(duration)) {
return std::nullopt;
}
auto up = !note.stem_up;
auto x = up ? head.bounds.x + head.bounds.width : head.bounds.x;
auto result = secondary_stem_layout{
{x, head.center_y}, {x, head.center_y + (up ? -1.0F : 1.0F) * spacing * 3.5F * note.size}, std::nullopt};
if (has_flag(duration)) {
result.flag = (up ? glyph_flag_eighth_up : glyph_flag_eighth_down) + 2 * (flag_count(duration) - 1);
}
return result;
}
auto refresh_note_visual_extents(note_layout &note, std::float32_t spacing) -> void {
update_note_visual_extents(note, spacing);
note.visual_left -= note.leading_space;
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 stem = secondary_stem(note, head, spacing)) {
include_visual_bounds(note,
{.x = stem->base.x,
.y = std::min(stem->base.y, stem->tip.y),
.width = 1,
.height = std::abs(stem->base.y - stem->tip.y)});
if (stem->flag) {
include_visual_bounds(note, glyph_bounds(*stem->flag, stem->tip, spacing * note.size));
}
}
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 y = note.source->rest_position ? pitch_y(*note.source->rest_position, staff_top, spacing, note.source->clef)
: rest_baseline_y(note.duration, staff_top, spacing);
auto origin = origin_for_centered_baseline(codepoint, note.x, y, spacing);
add_glyph(layout, vamp_zero::score_layout_item_role::REST, codepoint, origin, spacing, score_ink(), system_index, measure_index, note.note_index);
for (auto dot = std::int32_t{}; dot < note.duration.dots; ++dot) {
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 + dot * 0.6F), 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);
auto const &p = note.source->pitches[accidental.source_pitch_index];
if (p.accidental_parentheses || p.accidental_bracket) {
auto left = p.accidental_bracket ? 0xe26c : 0xe26a;
auto right = p.accidental_bracket ? 0xe26d : 0xe26b;
add_glyph(layout, vamp_zero::score_layout_item_role::ACCIDENTAL, left, origin_for_left_baseline(left, accidental.bounds.x - spacing * 0.7F, accidental.center_y, spacing), spacing, score_ink(), system_index, measure_index, note.note_index);
add_glyph(layout, vamp_zero::score_layout_item_role::ACCIDENTAL, right, origin_for_left_baseline(right, accidental.bounds.x + accidental.bounds.width + spacing * 0.1F, accidental.center_y, spacing), 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 * note.size, score_ink(), system_index, measure_index, note.note_index);
}
{
auto dot_x = note.visual_right - spacing * 0.65F;
for (auto const &head : note.heads) {
auto dots = note.source != nullptr && head.source_pitch_index < note.source->pitches.size() &&
!note.source->pitches[head.source_pitch_index].written.type.empty()
? note.source->pitches[head.source_pitch_index].written.dots
: note.duration.dots;
for (auto dot = std::int32_t{}; dot < dots; ++dot) {
add_glyph(layout, vamp_zero::score_layout_item_role::DOT, glyph_augmentation_dot, origin_for_centered_baseline(glyph_augmentation_dot, dot_x + spacing * dot * 0.6F, 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, double total_ticks, double elapsed_ticks, double 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 ||
std::find(notes[index].source->tuplet_ids.begin(), notes[index].source->tuplet_ids.end(), tuplet.id) !=
notes[index].source->tuplet_ids.end())) {
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 = tuplet.show_number == "none" ? std::string{} : std::to_string(tuplet.note_count);
if (tuplet.show_number == "both") {
text += ":" + std::to_string(tuplet.space_count);
}
auto y = top_y - spacing * (1.55F + 1.25F * tuplet.depth);
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 * 5.0F, system_index, measure_index, first);
if (tuplet.bracket == "yes" || (!same_beam && tuplet.bracket != "no")) {
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 ||
std::find(notes[index].source->tuplet_ids.begin(), notes[index].source->tuplet_ids.end(), tuplet.id) !=
notes[index].source->tuplet_ids.end())) {
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 pitch.id.empty() ? same_pitch(head.pitch, pitch)
: head.pitch.id == pitch.id || head.pitch.id == pitch.tie_to;
});
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.grace) {
continue;
}
if (note.kind == "rest" || note.kind == "spacer") {
return std::nullopt;
}
auto matching_pitch = std::find_if(note.pitches.begin(), note.pitches.end(), [&](auto const &candidate) {
return pitch.id.empty() ? same_pitch(candidate, pitch) : candidate.id == pitch.tie_to;
});
if (matching_pitch == note.pitches.end()) {
return std::nullopt;
}
if (note.tie_from_previous || matching_pitch->tie_from == pitch.id) {
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) {
if (!pitch.id.empty() && pitch.tie_to.empty()) {
continue;
}
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, std::size_t first_note = 0, std::size_t note_count = std::numeric_limits<std::size_t>::max()) -> void {
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 last_note = first_note + std::min(note_count, measure.notes.size() - first_note);
auto offset_ticks = 0.0;
for (auto index = std::size_t{}; index < first_note; ++index) {
offset_ticks += spacing_ticks(measure.notes[index]);
}
if (first_note < measure.notes.size() && !measure.notes[first_note].written.type.empty()) {
offset_ticks = measure.notes[first_note].onset.quarters() * 4096.0;
}
auto end_ticks = total_ticks;
if (last_note < measure.notes.size()) {
end_ticks = !measure.notes[last_note].written.type.empty() ? measure.notes[last_note].onset.quarters() * 4096.0
: offset_ticks;
}
if (last_note < measure.notes.size() && measure.notes[last_note].written.type.empty()) {
for (auto i = first_note; i < last_note; ++i) {
end_ticks += spacing_ticks(measure.notes[i]);
}
}
total_ticks = std::max(0.00001, end_ticks - offset_ticks);
auto elapsed = 0.0;
auto accidentals = make_accidental_state(effective_key_signature(score, measure));
if (measure.structured_key) {
accidentals.key_alterations = measure.key_alterations;
}
auto notes = std::vector<note_layout>{};
notes.reserve(measure.notes.size());
for (auto index = std::size_t{}; index < first_note; ++index) {
notated_accidentals_for_note(accidentals, measure.notes[index], measure.notes[index].notehead == "slash");
}
for (auto index = first_note; index < last_note; ++index) {
auto const &note = measure.notes[index];
auto duration_ticks = spacing_ticks(note);
if (!note.written.type.empty()) {
elapsed = note.onset.quarters() * 4096.0 - offset_ticks;
}
auto x = note_x(notation_x, notation_width, total_ticks, elapsed, duration_ticks);
for (auto const &a : measure.attributes) {
if (a.onset.quarters() * 4096.0 == elapsed + offset_ticks && a.key_alterations) {
accidentals.key_alterations = *a.key_alterations;
accidentals.measure_alterations.clear();
}
}
if (!note.written.type.empty() && !note.measure_rest) {
x = notation_x + spacing + (notation_width - spacing * 2) * static_cast<float>(elapsed / total_ticks);
}
if (note.grace) {
x -= spacing * 1.5F;
}
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));
for (auto const &a : measure.attributes) {
if (a.onset.quarters() * 4096.0 == elapsed + offset_ticks) {
notes.back().leading_space += attribute_width(a);
}
}
notes.back().visual_left -= notes.back().leading_space;
elapsed += duration_ticks;
}
fit_note_spacing(notes, notation_bounds, spacing);
auto beam_groups = make_beam_groups(notes, measure, offset_ticks, 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);
add_text(layout, vamp_zero::score_layout_item_role::MEASURE_NUMBER, bounds.x + spacing * 0.20F, chord_y - text_line_height(1) - 3.0F, std::to_string(measure_number), 1, score_muted_ink(), spacing * 4, system_index, measure_index);
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() && first_note == 0) {
auto section_y = chord_y - text_line_height(2) - text_line_height(1) - 9.0F;
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 at_time = [&](double ticks) {
if (notes.empty()) {
return notation_x;
}
auto before = notation_x;
auto before_time = 0.0;
for (auto const &n : notes) {
if (n.source->grace) {
continue;
}
if (std::abs(n.elapsed_ticks - ticks) < 0.00001) {
return n.x;
}
if (n.elapsed_ticks > ticks) {
return before + (n.x - before) * static_cast<float>((ticks - before_time) / (n.elapsed_ticks - before_time));
}
before = n.x;
before_time = n.elapsed_ticks;
}
return before + (notation_right_x - before) *
static_cast<float>((ticks - before_time) / std::max(0.00001, total_ticks - before_time));
};
for (auto const &h : measure.harmonies) {
auto time = h.onset.quarters() * 4096.0;
if (time < offset_ticks || time >= end_ticks) {
continue;
}
auto x = at_time(time - offset_ticks);
add_text(layout, vamp_zero::score_layout_item_role::CHORD_SYMBOL, x - spacing, chord_y, h.symbol, 2, score_ink(), std::max(spacing * 2, bounds.x + bounds.width - x), system_index, measure_index);
}
for (auto const &a : measure.attributes) {
auto time = a.onset.quarters() * 4096.0;
if (time < offset_ticks || time >= end_ticks) {
continue;
}
auto x = at_time(time - offset_ticks) - attribute_width(a);
if (!a.clef.empty()) {
x += layout_clef(layout, a.clef, x, staff_top, spacing * 0.75F, system_index);
}
if (a.key_alterations) {
x += layout_structured_key(layout, measure.clef, *a.key_alterations, x, staff_top, spacing * 0.75F, system_index, &measure.key_alterations);
}
if (!a.time_signature.empty()) {
layout_time_signature(layout, a.time_signature, x, staff_top, spacing * 0.75F, system_index);
}
}
auto measure_start = 0.0;
for (auto i = std::size_t{}; i < measure_index; ++i) {
measure_start += measure_ticks(score.measures[i]);
}
for (auto const &shift : score.octave_shifts) {
auto start = std::max(offset_ticks, shift.start.quarters() * 4096.0 - measure_start);
auto end = std::min(end_ticks, shift.end.quarters() * 4096.0 - measure_start);
if (start >= end) {
continue;
}
auto x0 = at_time(start - offset_ticks), x1 = at_time(end - offset_ticks);
auto above = shift.octaves < 0;
auto y = above ? notation_top_y - spacing * 2 : notation_bottom_y + spacing * 2;
auto label = std::to_string(std::abs(shift.octaves) * 7 + 1) + (above ? "va" : "vb");
add_text(layout, vamp_zero::score_layout_item_role::OCTAVE_SHIFT, x0, y - spacing, label, 1, score_ink(), spacing * 4, system_index, measure_index);
for (auto x = x0 + spacing * 3; x < x1; x += spacing) {
add_stroke_line(layout,
vamp_zero::score_layout_item_role::OCTAVE_SHIFT,
{.x = x, .y = y},
{.x = std::min(x + spacing * 0.5F, x1), .y = y},
1,
score_ink(),
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) || note.source->stem == "none") {
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 &group : beam_groups) {
for (auto index = group.first_note; index <= group.last_note; ++index) {
auto const &n = notes[index];
for (auto level = std::int32_t{2}; level <= flag_count(n.duration); ++level) {
auto type = beam_type(n, level);
auto next = index < group.last_note && flag_count(notes[index + 1].duration) >= level;
auto previous = index > group.first_note && flag_count(notes[index - 1].duration) >= level;
if (type == "backward hook" || type == "forward hook") {
next = false;
previous = false;
} else if (type == "end") {
next = false;
}
if (!next && previous) {
continue;
}
auto y = group.beam_y + (group.stem_up ? 1.0F : -1.0F) * spacing * 0.8F * (level - 1);
auto end = next ? notes[index + 1].stem_x
: n.stem_x + spacing * (type == "backward hook" || index == group.last_note ? -1.0F : 1.0F);
add_stroke_line(layout,
vamp_zero::score_layout_item_role::BEAM,
{.x = n.stem_x, .y = y},
{.x = end, .y = y},
scaled(beam_thickness_spaces, spacing),
score_ink(),
system_index,
measure_index,
n.note_index);
}
}
}
for (auto const &n : notes) {
if (n.hidden || n.rest) {
continue;
}
for (auto const &head : n.heads) {
auto stem = secondary_stem(n, head, spacing);
if (!stem) {
continue;
}
add_stroke_line(layout, vamp_zero::score_layout_item_role::STEM, stem->base, stem->tip, 1, score_ink(), system_index, measure_index, n.note_index);
if (stem->flag) {
add_glyph(layout, vamp_zero::score_layout_item_role::FLAG, *stem->flag, stem->tip, spacing * n.size, score_ink(), system_index, measure_index, n.note_index);
}
}
if (n.source->grace_slash) {
auto direction = n.stem_up ? 1.0F : -1.0F;
add_stroke_line(layout,
vamp_zero::score_layout_item_role::STEM,
{.x = n.stem_x - spacing * n.size * 0.5F, .y = n.stem_tip_y + direction * spacing * n.size * 1.5F},
{.x = n.stem_x + spacing * n.size, .y = n.stem_tip_y + direction * spacing * n.size * 0.3F},
1.0F,
score_ink(),
system_index,
measure_index,
n.note_index);
}
if (n.source->tremolo_marks) {
auto partner = std::find_if(notes.begin(), notes.end(), [&](auto const &candidate) {
return candidate.source->id == n.source->tremolo_partner;
});
if (n.source->tremolo_type == "stop" && partner != notes.end()) {
continue;
}
auto x0 = n.stem_x - spacing * 0.5F;
auto x1 = partner == notes.end() ? n.stem_x + spacing * 0.7F : partner->stem_x;
if (partner == notes.end() && n.source->tremolo_type == "start") {
x1 = notation_right_x;
}
if (n.source->tremolo_type == "stop") {
x0 = notation_x;
x1 = n.stem_x;
}
for (auto mark = std::int32_t{}; mark < n.source->tremolo_marks; ++mark) {
auto y = n.stem_tip_y + (n.stem_up ? 1.0F : -1.0F) * spacing * (1.0F + mark * 0.8F);
add_stroke_line(layout,
vamp_zero::score_layout_item_role::TREMOLO,
{.x = x0, .y = y + spacing * 0.3F},
{.x = x1, .y = y - spacing * 0.3F},
spacing * 0.45F,
score_ink(),
system_index,
measure_index,
n.note_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);
auto const &source = *notes[index].source;
if (source.written.ratio.numerator != source.written.ratio.denominator && source.tuplet_ids.empty() &&
!source.tremolo_marks && !source.hidden) {
auto label =
std::to_string(source.written.ratio.denominator) + ":" + std::to_string(source.written.ratio.numerator);
if (!source.written.normal_type.empty()) {
label += " " + source.written.normal_type + std::string(source.written.normal_dots, '.');
}
add_text(layout, vamp_zero::score_layout_item_role::TUPLET, notes[index].x - spacing, notation_top_y - spacing * 1.5F, label, 1, score_ink(), spacing * 9, system_index, measure_index, notes[index].note_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);
}
if (!source.lyric_extend.empty() && !source.hidden) {
auto end = index + 1 < notes.size() ? notes[index + 1].x - spacing : notation_right_x;
add_stroke_line(layout,
vamp_zero::score_layout_item_role::LYRIC,
{.x = notes[index].x + spacing, .y = lyrics_y + spacing},
{.x = end, .y = lyrics_y + spacing},
1,
score_ink(),
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);
style.size *= item.text_size_multiplier;
style.line_height *= item.text_size_multiplier;
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);
if (measure.structured_key) {
state.key_alterations = measure.key_alterations;
}
auto accidentals = std::vector<vamp_zero::score_accidental_mark>{};
for (auto const &note : measure.notes) {
for (auto const &a : measure.attributes) {
if (a.onset.quarters() == note.onset.quarters() && a.key_alterations) {
state.key_alterations = *a.key_alterations;
state.measure_alterations.clear();
}
}
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);
return page_starts(score, ranges).size();
}
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 {
if (bounds.width != 976.0F || bounds.height != 484.0F || bounds.x != 0 || bounds.y != 0) {
auto layout = layout_score_page({.x = 0, .y = 0, .width = 976, .height = 484}, score, page_index);
auto scale = std::min(bounds.width / 976.0F, bounds.height / 484.0F);
auto ox = bounds.x + (bounds.width - 976 * scale) / 2;
auto oy = bounds.y + (bounds.height - 484 * scale) / 2;
auto point = [&](luma::point p) {
return luma::point{.x = ox + p.x * scale, .y = oy + p.y * scale};
};
for (auto &item : layout.items) {
item.origin = point(item.origin);
item.from = point(item.from);
item.to = point(item.to);
item.control = point(item.control);
item.bounds = {.x = ox + item.bounds.x * scale,
.y = oy + item.bounds.y * scale,
.width = item.bounds.width * scale,
.height = item.bounds.height * scale};
item.thickness *= scale;
item.staff_spacing *= scale;
item.text_size_multiplier *= scale;
}
layout.staff_spacing *= scale;
layout.bounds = bounds;
return 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_ranges = make_score_system_ranges(score);
auto const starts = page_starts(score, system_ranges);
auto const first_system = page_index < starts.size() ? starts[page_index] : system_ranges.size();
auto const last_system = page_index + 1 < starts.size() ? starts[page_index + 1] : system_ranges.size();
auto note_refs = std::vector<laid_out_note_ref>{};
auto measure_refs = std::vector<laid_out_measure_ref>{};
auto system_y = content_y;
for (auto system = std::size_t{}; system < last_system - first_system; ++system) {
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;
auto metrics = metrics_for(score, *range);
auto system_height = std::min(376.0F, metrics.height());
auto spacing = std::min(12.0F, system_height / (metrics.above + metrics.below));
auto const staff_top = system_y + spacing * metrics.above;
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, range ? range->first_note : 0);
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 total_width = 0.0F;
for (auto i = std::size_t{}; i < range->measure_count; ++i) {
total_width += measure_width(score.measures[range->first_measure + i]);
}
auto x = measure_start_x;
for (auto measure = std::size_t{}; measure < range->measure_count; ++measure) {
auto const measure_index = range->first_measure + measure;
auto const width = measure_area_width * measure_width(score.measures[measure_index]) / total_width;
if (measure_index < score.measures.size()) {
layout_measure(layout,
score,
score.measures[measure_index],
{.x = x, .y = system_y, .width = width, .height = system_height},
measure_index + 1U,
system,
measure_index,
staff_top,
spacing,
note_refs,
measure_refs,
range->first_note,
range->note_count);
}
auto const bar_x = x + width;
x += width;
if (range->first_note +
std::min(range->note_count, score.measures[measure_index].notes.size() - range->first_note) <
score.measures[measure_index].notes.size()) {
continue;
}
auto const final_bar =
score.measures[measure_index].right_barline == "light-heavy" || 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);
}
system_y += system_height;
}
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);
vamp_zero::draw_score_layout(context, layout);
}
auto vamp_zero::draw_score_layout(luma::draw_context &context, vamp_zero::score_page_layout const &layout) -> void {
auto bounds = layout.bounds;
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);
}
}
}