+199
-104
@@ -24,6 +24,8 @@
|
||||
#include <boost/asio/use_future.hpp>
|
||||
#include <boost/json/object.hpp>
|
||||
#include <boost/json/parse.hpp>
|
||||
#include <boost/json/serialize.hpp>
|
||||
#include <boost/json/array.hpp>
|
||||
|
||||
#include <noto.hpp>
|
||||
|
||||
@@ -275,45 +277,24 @@ auto has_value(noto::query_result const& result, std::size_t column, std::string
|
||||
}
|
||||
|
||||
auto score_asset_json(std::string title, std::string key = "C") -> std::string {
|
||||
return std::string{R"json({
|
||||
"metadata": {
|
||||
"title": ")json"} + title + R"json(",
|
||||
"composer": "Composer",
|
||||
"key_signature": ")json" + key + R"json(",
|
||||
"time_signature": "4/4"
|
||||
},
|
||||
"staves": [
|
||||
{
|
||||
"id": "staff-lead",
|
||||
"name": "Lead",
|
||||
"clef": "treble",
|
||||
"role": "lead",
|
||||
"measures": [
|
||||
{
|
||||
"id": "measure-1",
|
||||
"time_signature": "4/4",
|
||||
"key_signature": ")json" + key + R"json(",
|
||||
"voices": [
|
||||
{
|
||||
"id": "voice-1",
|
||||
"notes": [
|
||||
{
|
||||
"id": "note-1",
|
||||
"kind": "note",
|
||||
"duration_ticks": 4096,
|
||||
"pitches": [
|
||||
{"letter": "c", "octave": 4, "accidental": "n"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
)json";
|
||||
auto value = boost::json::parse(R"json({
|
||||
"schema_version":2,
|
||||
"metadata":{"title":"Fixture","composer":"Composer","key_signature":"C","time_signature":"4/4"},
|
||||
"staves":[{"id":"staff-lead","name":"Lead","role":"lead","clef":{"sign":"G","line":2,"octave_change":0},
|
||||
"measures":[{"id":"measure-1","duration":{"numerator":4,"denominator":1},"key_signature":"C","time_signature":"4/4",
|
||||
"key":{"alterations":[0,0,0,0,0,0,0]},"meter":{"groups":[{"beats":[4],"unit":4}]},
|
||||
"clef":{"sign":"G","line":2,"octave_change":0},
|
||||
"voices":[{"id":"voice-1","notes":[{"id":"note-1","kind":"note",
|
||||
"onset":{"numerator":0,"denominator":1},"duration":{"numerator":4,"denominator":1},
|
||||
"written":{"type":"whole","dots":0},
|
||||
"pitches":[{"id":"pitch-1","letter":"c","octave":4,"alter":0,
|
||||
"duration":{"numerator":4,"denominator":1},"written":{"type":"whole","dots":0}}]}]}]}]}]
|
||||
})json");
|
||||
auto& root = value.as_object();
|
||||
root["metadata"].as_object()["title"] = title;
|
||||
root["metadata"].as_object()["key_signature"] = key;
|
||||
root["staves"].as_array()[0].as_object()["measures"].as_array()[0].as_object()["key_signature"] = key;
|
||||
return boost::json::serialize(value);
|
||||
}
|
||||
|
||||
auto navigation_state() -> vamp_zero::app_state {
|
||||
@@ -504,75 +485,180 @@ BOOST_AUTO_TEST_CASE(main_menu_metronome_setting_survives_score_selection) {
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(score_parser_preserves_notation_preamble) {
|
||||
auto parsed = vamp_zero::parse_score_document(R"json(
|
||||
{
|
||||
"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));
|
||||
auto parsed = vamp_zero::parse_score_document(score_asset_json("Standard Tune", "Eb"), "standard-tune.json");
|
||||
BOOST_REQUIRE(static_cast<bool>(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->time_signature == "4/4");
|
||||
BOOST_TEST(parsed->clef == "G:2:0");
|
||||
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_TEST(parsed->measures.front().duration.numerator == 4);
|
||||
BOOST_TEST(parsed->measures.front().notes.front().written.type == "whole");
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(score_parser_loads_the_generated_catalogue) {
|
||||
auto count = std::size_t{};
|
||||
for (auto const& entry : std::filesystem::recursive_directory_iterator{VAMP_TEST_ASSETS_DIR}) {
|
||||
if (entry.path().extension() != ".json") continue;
|
||||
auto input = std::ifstream{entry.path()};
|
||||
auto json = std::string{std::istreambuf_iterator<char>{input}, std::istreambuf_iterator<char>{}};
|
||||
auto parsed = vamp_zero::parse_score_document(json, entry.path().string());
|
||||
BOOST_TEST_CONTEXT(entry.path().string()) {
|
||||
BOOST_REQUIRE(parsed.has_value());
|
||||
BOOST_TEST(vamp_zero::score_page_count(*parsed) > 0U);
|
||||
}
|
||||
++count;
|
||||
}
|
||||
BOOST_TEST(count > 0U);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(score_parser_rejects_old_schema_and_extra_voices) {
|
||||
BOOST_TEST(!vamp_zero::parse_score_document(R"({"metadata":{"title":"Old"},"staves":[]})").has_value());
|
||||
auto source = boost::json::parse(score_asset_json("Fixture"));
|
||||
auto& voices = source.as_object()["staves"].as_array()[0].as_object()["measures"].as_array()[0].as_object()["voices"].as_array();
|
||||
auto copy = voices[0];
|
||||
voices.push_back(copy);
|
||||
BOOST_TEST(!vamp_zero::parse_score_document(boost::json::serialize(source)).has_value());
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(score_parser_rejects_invalid_rational_and_dangling_tie) {
|
||||
auto source = boost::json::parse(score_asset_json("Fixture"));
|
||||
auto& n = source.as_object()["staves"].as_array()[0].as_object()["measures"].as_array()[0].as_object()["voices"].as_array()[0].as_object()["notes"].as_array()[0].as_object();
|
||||
n["duration"].as_object()["denominator"] = 0;
|
||||
BOOST_TEST(!vamp_zero::parse_score_document(boost::json::serialize(source)).has_value());
|
||||
n["duration"].as_object()["denominator"] = 1;
|
||||
n["pitches"].as_array()[0].as_object()["tie_to"] = "missing";
|
||||
BOOST_TEST(!vamp_zero::parse_score_document(boost::json::serialize(source)).has_value());
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(score_layout_supports_all_written_durations_and_multiple_dots) {
|
||||
auto names = std::vector<std::string>{"maxima","long","breve","whole","half","quarter","eighth","16th","32nd","64th","128th","256th","512th","1024th"};
|
||||
for (auto const& name : names) {
|
||||
auto n = note({pitch('c',"n",4)});
|
||||
n.written = {.type=name,.dots=2};
|
||||
n.pitches.front().written = n.written;
|
||||
auto layout = vamp_zero::layout_score_page({.x=0,.y=0,.width=976,.height=484},score_with_notes({n}),0);
|
||||
auto dots = layout_items(layout,vamp_zero::score_layout_item_role::DOT,0,0,0);
|
||||
BOOST_TEST(dots.size()==2U);
|
||||
for (auto const& item : layout.items) if (item.kind == vamp_zero::score_layout_item_kind::GLYPH) {
|
||||
BOOST_TEST(item.bounds.width > 0);
|
||||
BOOST_TEST(item.bounds.height > 0);
|
||||
}
|
||||
n.kind = "rest";
|
||||
n.pitches.clear();
|
||||
layout = vamp_zero::layout_score_page({.x=0,.y=0,.width=976,.height=484},score_with_notes({n}),0);
|
||||
auto rests = layout_items(layout,vamp_zero::score_layout_item_role::REST,0,0,0);
|
||||
BOOST_REQUIRE_EQUAL(rests.size(),1U);
|
||||
BOOST_TEST(rests[0]->bounds.width > 0);
|
||||
BOOST_TEST(rests[0]->bounds.height > 0);
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(score_layout_paginates_dense_measures_without_losing_events) {
|
||||
auto notes = std::vector<vamp_zero::score_note>{};
|
||||
for (int i = 0; i < 128; ++i) {
|
||||
auto n = note_with_ticks({pitch('c',"#",4)},128);
|
||||
n.written = {.type="128th"};
|
||||
n.duration = {1,32};
|
||||
n.onset = {i,32};
|
||||
notes.push_back(n);
|
||||
}
|
||||
auto score = score_with_notes(notes);
|
||||
auto total = std::size_t{};
|
||||
auto pages = vamp_zero::score_page_count(score);
|
||||
BOOST_TEST(pages>1U);
|
||||
for (auto page = std::size_t{}; page < pages; ++page) {
|
||||
auto layout = vamp_zero::layout_score_page({.x=0,.y=0,.width=976,.height=484},score,page);
|
||||
for (auto const& item : layout.items) if (item.role == vamp_zero::score_layout_item_role::NOTEHEAD) {
|
||||
++total;
|
||||
BOOST_TEST(item.bounds.x >= 0);
|
||||
BOOST_TEST(item.bounds.x+item.bounds.width <= 976);
|
||||
BOOST_TEST(item.bounds.y >= 0);
|
||||
BOOST_TEST(item.bounds.y+item.bounds.height <= 484);
|
||||
}
|
||||
}
|
||||
BOOST_TEST(total==notes.size());
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(score_layout_uses_independent_harmony_onsets) {
|
||||
auto source = boost::json::parse(score_asset_json("Fixture"));
|
||||
auto& m = source.as_object()["staves"].as_array()[0].as_object()["measures"].as_array()[0].as_object();
|
||||
m["harmonies"] = boost::json::parse(R"([{"id":"h1","onset":{"numerator":2,"denominator":1},"kind":"major","symbol":"F"}])");
|
||||
auto score = vamp_zero::parse_score_document(boost::json::serialize(source));
|
||||
BOOST_REQUIRE(score.has_value());
|
||||
auto layout = vamp_zero::layout_score_page({.x=0,.y=0,.width=976,.height=484},*score,0);
|
||||
auto heads = layout_items(layout,vamp_zero::score_layout_item_role::NOTEHEAD,0,0);
|
||||
auto chords = layout_items(layout,vamp_zero::score_layout_item_role::CHORD_SYMBOL,0,0);
|
||||
BOOST_REQUIRE_EQUAL(heads.size(),1U);
|
||||
BOOST_REQUIRE_EQUAL(chords.size(),1U);
|
||||
BOOST_TEST(chords[0]->origin.x > heads[0]->origin.x);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(score_layout_keeps_section_measure_and_harmony_labels_separate) {
|
||||
auto score = score_with_notes({note({pitch('e', "n", 5)})});
|
||||
score.measures[0].section_label = "Melody";
|
||||
score.measures[0].harmonies.push_back({"h1", {0,1}, "Cmaj7", "major-seventh"});
|
||||
for (auto bounds : {luma::rect{.x=0,.y=0,.width=976,.height=484}, luma::rect{.x=0,.y=0,.width=480,.height=320}}) {
|
||||
auto layout = vamp_zero::layout_score_page(bounds, score, 0);
|
||||
auto sections = layout_items(layout, vamp_zero::score_layout_item_role::SECTION_LABEL);
|
||||
auto numbers = layout_items(layout, vamp_zero::score_layout_item_role::MEASURE_NUMBER);
|
||||
auto chords = layout_items(layout, vamp_zero::score_layout_item_role::CHORD_SYMBOL);
|
||||
BOOST_REQUIRE_EQUAL(sections.size(), 1U);
|
||||
BOOST_REQUIRE_EQUAL(numbers.size(), 1U);
|
||||
BOOST_REQUIRE_EQUAL(chords.size(), 1U);
|
||||
BOOST_TEST(sections[0]->bounds.y >= 0);
|
||||
BOOST_TEST(bottom_edge(*sections[0]) < numbers[0]->bounds.y);
|
||||
BOOST_TEST(bottom_edge(*numbers[0]) < chords[0]->bounds.y);
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(score_layout_ties_only_the_linked_chord_tone) {
|
||||
auto source = boost::json::parse(score_asset_json("Partial tie"));
|
||||
auto& ns = source.as_object()["staves"].as_array()[0].as_object()["measures"].as_array()[0].as_object()["voices"].as_array()[0].as_object()["notes"].as_array();
|
||||
auto first = ns[0].as_object();
|
||||
first["id"] = "a";
|
||||
first["duration"].as_object()["numerator"] = 2;
|
||||
first["written"].as_object()["type"] = "half";
|
||||
auto& p = first["pitches"].as_array()[0].as_object();
|
||||
p["id"] = "a-c";
|
||||
p["duration"] = first["duration"];
|
||||
p["written"] = first["written"];
|
||||
auto other = p;
|
||||
other["id"] = "a-e";
|
||||
other["letter"] = "e";
|
||||
first["pitches"].as_array().push_back(other);
|
||||
auto second = first;
|
||||
second["id"] = "b";
|
||||
second["onset"].as_object()["numerator"] = 2;
|
||||
second["pitches"].as_array()[0].as_object()["id"] = "b-c";
|
||||
second["pitches"].as_array()[0].as_object()["tie_from"] = "a-c";
|
||||
second["pitches"].as_array()[1].as_object()["id"] = "b-g";
|
||||
second["pitches"].as_array()[1].as_object()["letter"] = "g";
|
||||
first["pitches"].as_array()[0].as_object()["tie_to"] = "b-c";
|
||||
ns = {first, second};
|
||||
auto parsed = vamp_zero::parse_score_document(boost::json::serialize(source));
|
||||
BOOST_REQUIRE(parsed.has_value());
|
||||
auto layout = vamp_zero::layout_score_page({.x=0,.y=0,.width=976,.height=484}, *parsed, 0);
|
||||
BOOST_TEST(layout_items(layout, vamp_zero::score_layout_item_role::NOTEHEAD).size() == 4U);
|
||||
BOOST_TEST(layout_items(layout, vamp_zero::score_layout_item_role::TIE).size() == 1U);
|
||||
ns[0].as_object()["pitches"].as_array()[1].as_object()["duration"].as_object()["numerator"] = 0;
|
||||
BOOST_TEST(!vamp_zero::parse_score_document(boost::json::serialize(source)).has_value());
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(score_layout_clears_secondary_chord_stems_above_notation) {
|
||||
auto n = note({pitch('c', "#", 5), pitch('e', "n", 5)});
|
||||
n.written = {.type="quarter", .dots=2};
|
||||
n.pitches[0].written = n.written;
|
||||
n.pitches[1].written = {.type="eighth"};
|
||||
auto score = score_with_notes({n});
|
||||
score.measures[0].harmonies.push_back({"h",{0,1},"Cdim","diminished"});
|
||||
auto layout = vamp_zero::layout_score_page({.x=0,.y=0,.width=976,.height=484}, score, 0);
|
||||
auto chords = layout_items(layout, vamp_zero::score_layout_item_role::CHORD_SYMBOL);
|
||||
BOOST_REQUIRE_EQUAL(chords.size(), 1U);
|
||||
for (auto role : {vamp_zero::score_layout_item_role::STEM, vamp_zero::score_layout_item_role::FLAG})
|
||||
for (auto item : layout_items(layout, role)) BOOST_TEST(bottom_edge(*chords[0]) < item->bounds.y);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(accidentals_are_suppressed_when_the_key_signature_already_covers_the_pitch) {
|
||||
@@ -813,6 +899,15 @@ BOOST_AUTO_TEST_CASE(database_import_upserts_catalog_rows_and_keeps_unmanaged_ro
|
||||
{noto::sqlite_value{score_asset_json("Custom Tune")}});
|
||||
BOOST_REQUIRE(inserted_custom);
|
||||
|
||||
write_text(assets / "songs" / "custom.json", score_asset_json("Catalog Collision"));
|
||||
BOOST_REQUIRE(run_import(config, assets));
|
||||
auto custom = query_database(database_path, "SELECT title, catalog_managed FROM songs WHERE id = 'custom'");
|
||||
BOOST_REQUIRE(custom);
|
||||
BOOST_REQUIRE_EQUAL(custom->rows.size(), 1U);
|
||||
BOOST_TEST(text_value(custom->rows[0][0]) == "Custom Tune");
|
||||
BOOST_TEST(integer_value(custom->rows[0][1]) == 0);
|
||||
std::filesystem::remove(assets / "songs" / "custom.json");
|
||||
|
||||
std::filesystem::remove(song_path);
|
||||
BOOST_REQUIRE(run_import(config, assets));
|
||||
auto counts = query_database(database_path, "SELECT sum(CASE WHEN id = 'tune' THEN 1 ELSE 0 END), sum(CASE WHEN id = 'custom' THEN 1 ELSE 0 END) FROM songs");
|
||||
@@ -919,7 +1014,7 @@ BOOST_AUTO_TEST_CASE(score_page_count_uses_forced_notation_breaks) {
|
||||
.measures = std::move(measures),
|
||||
};
|
||||
|
||||
BOOST_TEST(vamp_zero::score_page_count(score) == 2U);
|
||||
BOOST_TEST(vamp_zero::score_page_count(score) == 3U);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(score_layout_attaches_stems_to_notehead_anchors) {
|
||||
|
||||
Reference in New Issue
Block a user