Files
product-vamp/tests/test.cpp
T
khodowany 6f3f26a3df
ci / checks (push) Failing after 23m53s
Type Cleanup & Rendering Fixes
2026-09-06 11:21:28 -05:00

479 lines
17 KiB
C++

#include <test.hpp>
#include <algorithm>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <initializer_list>
#include <limits>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include <vamp_zero/library.hpp>
#include <vamp_zero/score.hpp>
#include <vamp_zero/types.hpp>
namespace {
constexpr auto layout_any_index = std::numeric_limits<std::size_t>::max();
auto pitch(std::char_t letter, std::string accidental, std::int32_t octave = 4) -> vamp_zero::score_pitch {
return {
.letter = letter,
.octave = octave,
.accidental = std::move(accidental),
};
}
auto note(
std::initializer_list<vamp_zero::score_pitch> pitches,
std::bool_t tie_from_previous = false,
std::string notehead = {},
std::bool_t hidden = false
) -> vamp_zero::score_note {
return {
.kind = "note",
.hidden = hidden,
.duration_ticks = 4096,
.display_ticks = 4096,
.notehead = std::move(notehead),
.tie_from_previous = tie_from_previous,
.pitches = std::vector<vamp_zero::score_pitch>{pitches},
};
}
auto note_with_ticks(std::initializer_list<vamp_zero::score_pitch> pitches, std::int64_t ticks) -> vamp_zero::score_note {
auto value = note(pitches);
value.duration_ticks = ticks;
value.display_ticks = ticks;
return value;
}
auto rest(std::initializer_list<vamp_zero::score_pitch> pitches) -> vamp_zero::score_note {
return {
.kind = "rest",
.duration_ticks = 4096,
.display_ticks = 4096,
.pitches = std::vector<vamp_zero::score_pitch>{pitches},
};
}
auto mark_name(vamp_zero::score_accidental_mark mark) -> std::string {
switch (mark) {
case vamp_zero::score_accidental_mark::NONE:
return "none";
case vamp_zero::score_accidental_mark::FLAT:
return "flat";
case vamp_zero::score_accidental_mark::NATURAL:
return "natural";
case vamp_zero::score_accidental_mark::SHARP:
return "sharp";
}
return {};
}
auto mark_names(std::vector<vamp_zero::score_accidental_mark> const& marks) -> std::vector<std::string> {
auto names = std::vector<std::string>{};
names.reserve(marks.size());
for (auto mark : marks) {
names.push_back(mark_name(mark));
}
return names;
}
void expect_marks(std::vector<vamp_zero::score_accidental_mark> const& marks, std::initializer_list<std::string_view> expected) {
auto actual_names = mark_names(marks);
BOOST_REQUIRE_EQUAL(actual_names.size(), expected.size());
auto index = std::size_t{};
for (auto name : expected) {
BOOST_TEST(actual_names[index] == std::string{name});
++index;
}
}
auto score_with_measure(vamp_zero::score_measure measure, std::string key_signature = "C", std::string time_signature = "4/4", std::string clef = "treble") -> vamp_zero::score_document {
if (measure.key_signature.empty()) {
measure.key_signature = key_signature;
}
if (measure.time_signature.empty()) {
measure.time_signature = time_signature;
}
return {
.title = "Layout Tune",
.composer = "Composer",
.key_signature = std::move(key_signature),
.time_signature = std::move(time_signature),
.clef = std::move(clef),
.part = "Lead",
.measures = {std::move(measure)},
};
}
auto score_with_notes(std::vector<vamp_zero::score_note> notes, std::string key_signature = "C", std::string time_signature = "4/4", std::string clef = "treble") -> vamp_zero::score_document {
auto measure = vamp_zero::score_measure{
.time_signature = time_signature,
.key_signature = key_signature,
.notes = std::move(notes),
};
return score_with_measure(std::move(measure), std::move(key_signature), std::move(time_signature), std::move(clef));
}
auto layout_items(
vamp_zero::score_page_layout const& layout,
vamp_zero::score_layout_item_role role,
std::size_t system_index = layout_any_index,
std::size_t measure_index = layout_any_index,
std::size_t note_index = layout_any_index
) -> std::vector<vamp_zero::score_layout_item const*> {
auto result = std::vector<vamp_zero::score_layout_item const*>{};
for (auto const& item : layout.items) {
if (item.role != role) {
continue;
}
if (system_index != layout_any_index && item.system_index != system_index) {
continue;
}
if (measure_index != layout_any_index && item.measure_index != measure_index) {
continue;
}
if (note_index != layout_any_index && item.note_index != note_index) {
continue;
}
result.push_back(&item);
}
return result;
}
auto center_x(vamp_zero::score_layout_item const& item) -> std::float32_t {
return item.bounds.x + item.bounds.width * 0.5F;
}
auto center_y(vamp_zero::score_layout_item const& item) -> std::float32_t {
return item.bounds.y + item.bounds.height * 0.5F;
}
auto right_edge(vamp_zero::score_layout_item const& item) -> std::float32_t {
return item.bounds.x + item.bounds.width;
}
auto near(std::float32_t left, std::float32_t right) -> std::bool_t {
return std::abs(left - right) < 0.02F;
}
}
BOOST_AUTO_TEST_CASE(score_parser_preserves_notation_preamble) {
auto parsed = vamp_zero::parse_score_document(R"json(
{
"version": 1,
"metadata": {
"title": "Standard Tune",
"composer": "Composer",
"key_signature": "Eb",
"time_signature": "3/4"
},
"staves": [
{
"id": "staff-lead",
"name": "Lead",
"clef": "treble",
"role": "lead",
"measures": [
{
"id": "measure-1",
"time_signature": "3/4",
"key_signature": "Eb",
"voices": [
{
"id": "voice-1",
"notes": [
{
"id": "note-1",
"kind": "note",
"duration_ticks": 4096,
"tuplet_id": "triplet-1",
"pitches": [
{
"letter": "b",
"octave": 4,
"accidental": "b"
}
]
}
],
"tuplets": [
{
"id": "triplet-1",
"note_count": 3,
"space_count": 2
}
]
}
]
}
]
}
]
}
)json", "standard-tune.json");
BOOST_REQUIRE(static_cast<std::bool_t>(parsed));
BOOST_TEST(parsed->title == "Standard Tune");
BOOST_TEST(parsed->composer == "Composer");
BOOST_TEST(parsed->key_signature == "Eb");
BOOST_TEST(parsed->time_signature == "3/4");
BOOST_TEST(parsed->clef == "treble");
BOOST_TEST(parsed->part == "Lead");
BOOST_REQUIRE_EQUAL(parsed->measures.size(), 1U);
BOOST_TEST(parsed->measures.front().key_signature == "Eb");
BOOST_TEST(parsed->measures.front().time_signature == "3/4");
BOOST_REQUIRE_EQUAL(parsed->measures.front().notes.size(), 1U);
BOOST_TEST(parsed->measures.front().notes.front().tuplet_id == "triplet-1");
BOOST_REQUIRE_EQUAL(parsed->measures.front().tuplets.size(), 1U);
BOOST_TEST(parsed->measures.front().tuplets.front().id == "triplet-1");
BOOST_TEST(parsed->measures.front().tuplets.front().note_count == 3);
BOOST_TEST(parsed->measures.front().tuplets.front().space_count == 2);
}
BOOST_AUTO_TEST_CASE(accidentals_are_suppressed_when_the_key_signature_already_covers_the_pitch) {
auto c_major = vamp_zero::score_measure{
.key_signature = "C",
.notes = {
note({pitch('c', "n")}),
note({pitch('d', "n")}),
note({pitch('e', "n")}),
},
};
expect_marks(vamp_zero::notated_accidentals_for_measure(c_major, "C"), {"none", "none", "none"});
auto f_major = vamp_zero::score_measure{
.key_signature = "F",
.notes = {
note({pitch('b', "b")}),
note({pitch('e', "n")}),
},
};
expect_marks(vamp_zero::notated_accidentals_for_measure(f_major, "C"), {"none", "none"});
auto d_major = vamp_zero::score_measure{
.key_signature = "D",
.notes = {
note({pitch('f', "#")}),
note({pitch('c', "#")}),
},
};
expect_marks(vamp_zero::notated_accidentals_for_measure(d_major, "C"), {"none", "none"});
}
BOOST_AUTO_TEST_CASE(accidentals_follow_measure_state_after_the_first_printed_change) {
auto measure = vamp_zero::score_measure{
.key_signature = "F",
.notes = {
note({pitch('b', "b")}),
note({pitch('b', "n")}),
note({pitch('b', "n")}),
note({pitch('b', "b")}),
note({pitch('b', "b")}),
},
};
expect_marks(vamp_zero::notated_accidentals_for_measure(measure, "C"), {"none", "natural", "none", "flat", "none"});
}
BOOST_AUTO_TEST_CASE(measure_state_resets_for_each_measure) {
auto first = vamp_zero::score_measure{
.key_signature = "C",
.notes = {
note({pitch('f', "#")}),
note({pitch('f', "#")}),
},
};
expect_marks(vamp_zero::notated_accidentals_for_measure(first, "C"), {"sharp", "none"});
auto second = vamp_zero::score_measure{
.key_signature = "C",
.notes = {
note({pitch('f', "#")}),
},
};
expect_marks(vamp_zero::notated_accidentals_for_measure(second, "C"), {"sharp"});
}
BOOST_AUTO_TEST_CASE(tied_rests_hidden_and_slash_notes_do_not_emit_accidentals) {
auto measure = vamp_zero::score_measure{
.key_signature = "C",
.notes = {
note({pitch('f', "#")}, true),
rest({pitch('b', "b")}),
note({pitch('c', "#")}, false, "slash"),
note({pitch('d', "#")}, false, {}, true),
note({pitch('e', "b")}),
},
};
expect_marks(vamp_zero::notated_accidentals_for_measure(measure, "C"), {"none", "flat"});
}
BOOST_AUTO_TEST_CASE(score_layout_orders_preamble_on_staff_baselines) {
auto score = score_with_notes({note({pitch('e', "n")})}, "F", "4/4", "treble");
auto layout = vamp_zero::layout_score_page({.x = 0.0F, .y = 0.0F, .width = 976.0F, .height = 484.0F}, score, 0U);
auto clefs = layout_items(layout, vamp_zero::score_layout_item_role::CLEF, 0U);
auto keys = layout_items(layout, vamp_zero::score_layout_item_role::KEY_SIGNATURE, 0U);
auto times = layout_items(layout, vamp_zero::score_layout_item_role::TIME_SIGNATURE, 0U);
auto staff_lines = layout_items(layout, vamp_zero::score_layout_item_role::STAFF_LINE, 0U);
BOOST_REQUIRE_EQUAL(clefs.size(), 1U);
BOOST_REQUIRE(!keys.empty());
BOOST_REQUIRE(!times.empty());
BOOST_REQUIRE_GE(staff_lines.size(), 5U);
BOOST_TEST(clefs.front()->bounds.x < keys.front()->bounds.x);
BOOST_TEST(keys.front()->bounds.x < times.front()->bounds.x);
BOOST_TEST(near(clefs.front()->origin.y, staff_lines.front()->from.y + layout.staff_spacing * 3.5F));
BOOST_TEST(near(keys.front()->origin.y, staff_lines.front()->from.y + layout.staff_spacing * 2.5F));
BOOST_REQUIRE_GE(times.size(), 2U);
BOOST_TEST(near(times[0]->origin.y, staff_lines.front()->from.y + layout.staff_spacing * 1.5F));
BOOST_TEST(near(times[1]->origin.y, staff_lines.front()->from.y + layout.staff_spacing * 3.5F));
}
BOOST_AUTO_TEST_CASE(score_layout_attaches_stems_to_notehead_anchors) {
auto score = score_with_notes({note({pitch('d', "n")})});
auto layout = vamp_zero::layout_score_page({.x = 0.0F, .y = 0.0F, .width = 976.0F, .height = 484.0F}, score, 0U);
auto heads = layout_items(layout, vamp_zero::score_layout_item_role::NOTEHEAD, 0U, 0U, 0U);
auto stems = layout_items(layout, vamp_zero::score_layout_item_role::STEM, 0U, 0U, 0U);
BOOST_REQUIRE_EQUAL(heads.size(), 1U);
BOOST_REQUIRE_EQUAL(stems.size(), 1U);
BOOST_TEST(stems.front()->from.x < heads.front()->bounds.x + heads.front()->bounds.width);
BOOST_TEST(stems.front()->from.x > heads.front()->bounds.x + heads.front()->bounds.width - layout.staff_spacing * 0.25F);
BOOST_TEST(stems.front()->from.y > heads.front()->bounds.y);
BOOST_TEST(stems.front()->from.y < heads.front()->bounds.y + heads.front()->bounds.height);
BOOST_TEST(stems.front()->to.y < stems.front()->from.y);
}
BOOST_AUTO_TEST_CASE(score_layout_displaces_chord_seconds_and_columns_accidentals) {
auto score = score_with_notes({note({pitch('c', "#"), pitch('d', "#")})});
auto layout = vamp_zero::layout_score_page({.x = 0.0F, .y = 0.0F, .width = 976.0F, .height = 484.0F}, score, 0U);
auto heads = layout_items(layout, vamp_zero::score_layout_item_role::NOTEHEAD, 0U, 0U, 0U);
auto accidentals = layout_items(layout, vamp_zero::score_layout_item_role::ACCIDENTAL, 0U, 0U, 0U);
BOOST_REQUIRE_EQUAL(heads.size(), 2U);
BOOST_REQUIRE_EQUAL(accidentals.size(), 2U);
auto const* lower_head = *std::max_element(heads.begin(), heads.end(), [](auto const* left, auto const* right) {
return center_y(*left) < center_y(*right);
});
auto const* upper_head = *std::min_element(heads.begin(), heads.end(), [](auto const* left, auto const* right) {
return center_y(*left) < center_y(*right);
});
BOOST_TEST(center_x(*upper_head) > center_x(*lower_head) + layout.staff_spacing * 0.30F);
auto const* leftmost_head = *std::min_element(heads.begin(), heads.end(), [](auto const* left, auto const* right) {
return left->bounds.x < right->bounds.x;
});
auto const* leftmost_accidental = *std::min_element(accidentals.begin(), accidentals.end(), [](auto const* left, auto const* right) {
return left->bounds.x < right->bounds.x;
});
auto const* rightmost_accidental = *std::max_element(accidentals.begin(), accidentals.end(), [](auto const* left, auto const* right) {
return left->bounds.x < right->bounds.x;
});
for (auto const* accidental : accidentals) {
BOOST_TEST(accidental->bounds.x + accidental->bounds.width < leftmost_head->bounds.x);
}
BOOST_TEST(rightmost_accidental->bounds.x - leftmost_accidental->bounds.x > layout.staff_spacing * 0.50F);
}
BOOST_AUTO_TEST_CASE(score_layout_beams_eighth_tuplets_and_labels_them) {
auto first = note_with_ticks({pitch('c', "n")}, 2048);
auto second = note_with_ticks({pitch('d', "n")}, 2048);
auto third = note_with_ticks({pitch('e', "n")}, 2048);
first.tuplet_id = "triplet-1";
second.tuplet_id = "triplet-1";
third.tuplet_id = "triplet-1";
auto score = score_with_measure({
.time_signature = "4/4",
.key_signature = "C",
.notes = {std::move(first), std::move(second), std::move(third)},
.tuplets = {{
.id = "triplet-1",
.note_count = 3,
.space_count = 2,
}},
});
auto layout = vamp_zero::layout_score_page({.x = 0.0F, .y = 0.0F, .width = 976.0F, .height = 484.0F}, score, 0U);
auto beams = layout_items(layout, vamp_zero::score_layout_item_role::BEAM, 0U, 0U);
auto flags = layout_items(layout, vamp_zero::score_layout_item_role::FLAG, 0U, 0U);
auto tuplets = layout_items(layout, vamp_zero::score_layout_item_role::TUPLET, 0U, 0U);
BOOST_REQUIRE_EQUAL(beams.size(), 1U);
BOOST_TEST(flags.empty());
BOOST_REQUIRE_EQUAL(tuplets.size(), 1U);
BOOST_TEST(tuplets.front()->text == "3");
}
BOOST_AUTO_TEST_CASE(score_layout_keeps_dense_notation_inside_measure_guard) {
auto first = note_with_ticks({pitch('c', "#")}, 2048);
auto second = note_with_ticks({pitch('d', "#")}, 2048);
auto third = note_with_ticks({pitch('e', "n")}, 6144);
auto fourth = note_with_ticks({pitch('f', "#")}, 4096);
auto fifth = note_with_ticks({pitch('g', "#")}, 2048);
fourth.tie_to_next = true;
auto score = score_with_measure({
.time_signature = "4/4",
.key_signature = "C",
.notes = {std::move(first), std::move(second), std::move(third), std::move(fourth), std::move(fifth)},
});
auto layout = vamp_zero::layout_score_page({.x = 0.0F, .y = 0.0F, .width = 976.0F, .height = 484.0F}, score, 0U);
auto barlines = layout_items(layout, vamp_zero::score_layout_item_role::FINAL_BARLINE, 0U, 0U);
BOOST_REQUIRE_EQUAL(barlines.size(), 1U);
auto guarded_right = barlines.front()->from.x - layout.staff_spacing;
auto roles = {
vamp_zero::score_layout_item_role::NOTEHEAD,
vamp_zero::score_layout_item_role::ACCIDENTAL,
vamp_zero::score_layout_item_role::STEM,
vamp_zero::score_layout_item_role::FLAG,
vamp_zero::score_layout_item_role::BEAM,
vamp_zero::score_layout_item_role::DOT,
vamp_zero::score_layout_item_role::TIE,
};
for (auto role : roles) {
for (auto const* item : layout_items(layout, role, 0U, 0U)) {
BOOST_TEST(right_edge(*item) <= guarded_right);
}
}
}
BOOST_AUTO_TEST_CASE(score_layout_raises_chord_symbols_above_high_notation) {
auto high = note({pitch('c', "n", 6)});
high.chord_symbol = "Cmaj7";
auto score = score_with_notes({std::move(high)});
auto layout = vamp_zero::layout_score_page({.x = 0.0F, .y = 0.0F, .width = 976.0F, .height = 484.0F}, score, 0U);
auto chords = layout_items(layout, vamp_zero::score_layout_item_role::CHORD_SYMBOL, 0U, 0U);
BOOST_REQUIRE_EQUAL(chords.size(), 1U);
auto top_y = std::numeric_limits<std::float32_t>::max();
auto roles = {
vamp_zero::score_layout_item_role::NOTEHEAD,
vamp_zero::score_layout_item_role::ACCIDENTAL,
vamp_zero::score_layout_item_role::STEM,
vamp_zero::score_layout_item_role::FLAG,
vamp_zero::score_layout_item_role::BEAM,
vamp_zero::score_layout_item_role::DOT,
vamp_zero::score_layout_item_role::TUPLET,
vamp_zero::score_layout_item_role::TUPLET_BRACKET,
vamp_zero::score_layout_item_role::TIE,
};
for (auto role : roles) {
for (auto const* item : layout_items(layout, role, 0U, 0U)) {
top_y = std::min(top_y, item->bounds.y);
}
}
BOOST_TEST(right_edge(*chords.front()) <= layout.bounds.x + layout.bounds.width);
BOOST_TEST(chords.front()->bounds.y + chords.front()->bounds.height <= top_y - layout.staff_spacing * 0.40F);
}