@@ -10,10 +10,16 @@ target_link_libraries(product-vamp-test PRIVATE
|
||||
target_compile_features(product-vamp-test PRIVATE cxx_std_23)
|
||||
add_test(NAME product-vamp COMMAND product-vamp-test)
|
||||
set_tests_properties(product-vamp PROPERTIES TIMEOUT 60)
|
||||
add_test(
|
||||
NAME product-vamp-generator
|
||||
COMMAND ${Python3_EXECUTABLE} -m unittest tests.test_score_generator
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||
)
|
||||
set_tests_properties(product-vamp-generator PROPERTIES TIMEOUT 60)
|
||||
add_custom_target(check
|
||||
COMMAND ${CMAKE_CTEST_COMMAND}
|
||||
--test-dir ${CMAKE_BINARY_DIR}
|
||||
-R ^product-vamp$
|
||||
-R ^product-vamp
|
||||
--output-on-failure
|
||||
--timeout 60
|
||||
USES_TERMINAL
|
||||
|
||||
@@ -158,6 +158,10 @@ 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;
|
||||
}
|
||||
@@ -448,6 +452,99 @@ BOOST_AUTO_TEST_CASE(score_layout_keeps_dense_notation_inside_measure_guard) {
|
||||
}
|
||||
}
|
||||
|
||||
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<std::float32_t>::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<vamp_zero::score_measure>{};
|
||||
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";
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from generator.score import normalize_score_document
|
||||
|
||||
|
||||
class CompactScoreJsonTests(unittest.TestCase):
|
||||
def test_compact_json_normalizes_notation_tokens(self) -> None:
|
||||
document = normalize_score_document({
|
||||
"title": "Compact Tune",
|
||||
"composer": "Composer",
|
||||
"key": "F",
|
||||
"time": "4/4",
|
||||
"clef": "treble",
|
||||
"part": "Vocal",
|
||||
"role": "vocal",
|
||||
"measures": [
|
||||
"[G-7] Bb4:1~ \"leaves\"",
|
||||
"[C7] ~Bb4:4 C4:4 \"drift\" D4:4 \"by\" E4:4 \"my\"",
|
||||
],
|
||||
})
|
||||
|
||||
staff = document["staves"][0]
|
||||
first_note = staff["measures"][0]["voices"][0]["notes"][0]
|
||||
second_measure_first_note = staff["measures"][1]["voices"][0]["notes"][0]
|
||||
|
||||
self.assertEqual(document["metadata"]["key_signature"], "F")
|
||||
self.assertEqual(staff["name"], "Vocal")
|
||||
self.assertEqual(first_note["duration_ticks"], 16384)
|
||||
self.assertEqual(first_note["chord_symbol"], "G-7")
|
||||
self.assertEqual(first_note["lyric"], "leaves")
|
||||
self.assertTrue(first_note["tie_to_next"])
|
||||
self.assertTrue(second_measure_first_note["tie_from_previous"])
|
||||
|
||||
def test_compact_json_normalizes_pitch_groups_and_tuplets(self) -> None:
|
||||
document = normalize_score_document({
|
||||
"title": "Practice",
|
||||
"key": "C",
|
||||
"time": "4/4",
|
||||
"measures": [
|
||||
"[G7] (G4 B4 F5):4 r:4",
|
||||
"{5:2 C4:4 D4:4 E4:4 F4:4 G4:4}",
|
||||
],
|
||||
})
|
||||
|
||||
first_measure_notes = document["staves"][0]["measures"][0]["voices"][0]["notes"]
|
||||
second_voice = document["staves"][0]["measures"][1]["voices"][0]
|
||||
tuplet_notes = second_voice["notes"]
|
||||
|
||||
self.assertEqual(len(first_measure_notes[0]["pitches"]), 3)
|
||||
self.assertEqual(first_measure_notes[1]["kind"], "rest")
|
||||
self.assertEqual(second_voice["tuplets"][0]["note_count"], 5)
|
||||
self.assertEqual(second_voice["tuplets"][0]["space_count"], 2)
|
||||
self.assertEqual(sum(note["duration_ticks"] for note in tuplet_notes), 8192)
|
||||
self.assertTrue(all(note["display_ticks"] == 4096 for note in tuplet_notes))
|
||||
|
||||
def test_compact_json_rejects_overfull_measure(self) -> None:
|
||||
with self.assertRaises(ValueError):
|
||||
normalize_score_document({
|
||||
"title": "Bad",
|
||||
"time": "4/4",
|
||||
"measures": ["C4:1 D4:4"],
|
||||
})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user