JSON Score
ci / checks (push) Successful in 39m41s

This commit is contained in:
khodowany
2026-09-07 21:12:22 -05:00
parent 9a3ec7caa5
commit 900604b9fb
15 changed files with 1065 additions and 5634 deletions
+7 -1
View File
@@ -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
+97
View File
@@ -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";
+68
View File
@@ -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()