#include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { constexpr auto layout_any_index = std::numeric_limits::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 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{pitches}, }; } auto note_with_ticks(std::initializer_list 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 pitches) -> vamp_zero::score_note { return { .kind = "rest", .duration_ticks = 4096, .display_ticks = 4096, .pitches = std::vector{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 const& marks) -> std::vector { auto names = std::vector{}; names.reserve(marks.size()); for (auto mark : marks) { names.push_back(mark_name(mark)); } return names; } void expect_marks(std::vector const& marks, std::initializer_list 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 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 { auto result = std::vector{}; 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 bottom_edge(vamp_zero::score_layout_item const& item) -> std::float32_t { return item.bounds.y + item.bounds.height; } 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(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_places_lyrics_below_low_vocal_notation) { auto low = note({pitch('c', "n", 3)}); low.lyric = "low"; auto score = score_with_notes({std::move(low)}); auto layout = vamp_zero::layout_score_page({.x = 0.0F, .y = 0.0F, .width = 976.0F, .height = 484.0F}, score, 0U); auto lyrics = layout_items(layout, vamp_zero::score_layout_item_role::LYRIC, 0U, 0U, 0U); BOOST_REQUIRE_EQUAL(lyrics.size(), 1U); auto notation_bottom = std::numeric_limits::lowest(); 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::DOT, vamp_zero::score_layout_item_role::STAFF_LINE, }; for (auto role : roles) { for (auto const* item : layout_items(layout, role, 0U, 0U, 0U)) { notation_bottom = std::max(notation_bottom, bottom_edge(*item)); } } BOOST_TEST(lyrics.front()->bounds.y >= notation_bottom + layout.staff_spacing * 0.70F); } BOOST_AUTO_TEST_CASE(score_layout_ties_to_the_actual_next_measure_notehead) { auto tied = note_with_ticks({pitch('b', "b")}, 16384); tied.tie_to_next = true; auto target = note_with_ticks({pitch('b', "b")}, 4096); target.tie_from_previous = true; auto score = score_with_measure({ .time_signature = "4/4", .key_signature = "F", .notes = {std::move(tied)}, }, "F"); score.measures.push_back({ .time_signature = "4/4", .key_signature = "F", .notes = {std::move(target)}, }); auto layout = vamp_zero::layout_score_page({.x = 0.0F, .y = 0.0F, .width = 976.0F, .height = 484.0F}, score, 0U); auto ties = layout_items(layout, vamp_zero::score_layout_item_role::TIE, 0U, 0U, 0U); auto start_heads = layout_items(layout, vamp_zero::score_layout_item_role::NOTEHEAD, 0U, 0U, 0U); auto target_heads = layout_items(layout, vamp_zero::score_layout_item_role::NOTEHEAD, 0U, 1U, 0U); BOOST_REQUIRE_EQUAL(ties.size(), 1U); BOOST_REQUIRE_EQUAL(start_heads.size(), 1U); BOOST_REQUIRE_EQUAL(target_heads.size(), 1U); BOOST_TEST(ties.front()->from.x > right_edge(*start_heads.front())); BOOST_TEST(ties.front()->to.x < target_heads.front()->bounds.x); BOOST_TEST(ties.front()->to.x > right_edge(*start_heads.front())); } BOOST_AUTO_TEST_CASE(score_layout_draws_tie_continuations_across_system_breaks) { auto measures = std::vector{}; for (auto index = std::size_t{}; index < 5U; ++index) { auto value = note_with_ticks({pitch('a', "n")}, 16384); if (index == 3U) { value.tie_to_next = true; } if (index == 4U) { value.tie_from_previous = true; } measures.push_back({ .time_signature = "4/4", .key_signature = "C", .notes = {std::move(value)}, }); } auto score = vamp_zero::score_document{ .title = "Break Tie", .composer = "Composer", .key_signature = "C", .time_signature = "4/4", .clef = "treble", .part = "Lead", .measures = std::move(measures), }; auto layout = vamp_zero::layout_score_page({.x = 0.0F, .y = 0.0F, .width = 976.0F, .height = 484.0F}, score, 0U); auto outgoing = layout_items(layout, vamp_zero::score_layout_item_role::TIE, 0U, 3U, 0U); auto incoming = layout_items(layout, vamp_zero::score_layout_item_role::TIE, 1U, 4U, 0U); BOOST_REQUIRE_EQUAL(outgoing.size(), 1U); BOOST_REQUIRE_EQUAL(incoming.size(), 1U); BOOST_TEST(outgoing.front()->to.x > outgoing.front()->from.x); BOOST_TEST(incoming.front()->to.x > incoming.front()->from.x); } 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::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); }