@@ -20,7 +20,7 @@ inline auto add_timing(boost::json::value &value) -> void {
|
||||
music.at("metadata").as_object()["song_id"] = "fixture";
|
||||
auto &measures = music.at("staves").as_array().front().at("measures").as_array();
|
||||
auto &pages = value.at("presentation").at("pages").as_array();
|
||||
while (measures.size() < pages.size()) {
|
||||
while (measures.size() < 2 * pages.size() - 1) {
|
||||
measures.push_back(boost::json::object{});
|
||||
}
|
||||
auto beat = [](std::size_t n) { return boost::json::object{{"numerator", n}, {"denominator", 1}}; };
|
||||
@@ -32,15 +32,43 @@ inline auto add_timing(boost::json::value &value) -> void {
|
||||
}
|
||||
}
|
||||
std::size_t cursor{};
|
||||
auto systems = boost::json::array{};
|
||||
for (std::size_t i = 0; i < pages.size(); ++i) {
|
||||
auto count = i + 1 == pages.size() ? measures.size() - cursor : 1U;
|
||||
auto count = i + 1 == pages.size() ? measures.size() - cursor : 2U;
|
||||
auto &page = pages[i].as_object();
|
||||
page["first_measure"] = cursor + 1;
|
||||
page["measure_count"] = count;
|
||||
page["start_beat"] = beat(cursor * 4);
|
||||
page["end_beat"] = beat((cursor + count) * 4);
|
||||
cursor += count;
|
||||
auto members = boost::json::array{};
|
||||
for (std::size_t j = 0; j < (i + 1 == pages.size() ? 1U : 2U); ++j) {
|
||||
auto n = i + 1 == pages.size() ? count : 1U;
|
||||
auto labels = boost::json::array{};
|
||||
for (std::size_t k = 0; k < n; ++k) {
|
||||
labels.push_back(boost::json::object{{"measure", cursor + k + 1}, {"bounds", boost::json::array{30 + k * 30, 70 + j * 220, 42 + k * 30, 82 + j * 220}}});
|
||||
}
|
||||
auto system = boost::json::object{{"first_measure", cursor + 1}, {"measure_count", n}, {"start_beat", beat(cursor * 4)}, {"end_beat", beat((cursor + n) * 4)}, {"labels", labels}};
|
||||
members.push_back(system);
|
||||
systems.push_back(system);
|
||||
cursor += n;
|
||||
}
|
||||
page["systems"] = members;
|
||||
}
|
||||
auto playback = boost::json::array{};
|
||||
for (std::size_t i = 0; i < systems.size(); ++i) {
|
||||
auto page = pages[std::min(i / 2, pages.size() - 1)].as_object();
|
||||
auto members = boost::json::array{systems[i]};
|
||||
if (i + 1 < systems.size()) {
|
||||
members.push_back(systems[i + 1]);
|
||||
}
|
||||
page["systems"] = members;
|
||||
page["first_measure"] = members.front().at("first_measure");
|
||||
page["start_beat"] = members.front().at("start_beat");
|
||||
page["end_beat"] = members.back().at("end_beat");
|
||||
page["measure_count"] = members.back().at("first_measure").to_number<std::size_t>() + members.back().at("measure_count").to_number<std::size_t>() - members.front().at("first_measure").to_number<std::size_t>();
|
||||
playback.push_back(page);
|
||||
}
|
||||
value.at("presentation").as_object()["playback_pages"] = playback;
|
||||
value.as_object()["performance"] = boost::json::object{{"song_id", "fixture"}, {"duration", beat(measures.size() * 4)}, {"count_in_beats", 4}, {"clicks", clicks}};
|
||||
}
|
||||
|
||||
|
||||
Vendored
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
Vendored
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 67 KiB |
+2
-2
@@ -245,7 +245,7 @@ BOOST_AUTO_TEST_CASE(empty_catalog_navigation_and_main_menu_wrap) {
|
||||
BOOST_AUTO_TEST_CASE(song_part_navigation_preserves_selection_and_remote_display) {
|
||||
auto parts = std::vector<vamp_zero::library_part>{};
|
||||
for (auto name : {"Acoustic", "Electric", "Vocal"}) {
|
||||
parts.push_back({.id = name, .title = "Song", .composer = "Composer", .part = name, .total_pages = 3});
|
||||
parts.push_back({.id = name, .title = "Song", .composer = "Composer", .part = name, .total_pages = 3, .playback_pages = 5});
|
||||
}
|
||||
auto state = vamp_zero::app_state{
|
||||
.catalog = std::make_shared<vamp_zero::catalog const>(vamp_zero::group_ordered_parts(std::move(parts))),
|
||||
@@ -264,7 +264,7 @@ BOOST_AUTO_TEST_CASE(song_part_navigation_preserves_selection_and_remote_display
|
||||
BOOST_REQUIRE(vamp_pico::parse_remote_display_body(vamp_zero::remote_display_text(state), remote));
|
||||
BOOST_TEST(remote.active);
|
||||
BOOST_TEST(std::string{remote.part} == name);
|
||||
BOOST_TEST(remote.pages == 3U);
|
||||
BOOST_TEST(remote.pages == 5U);
|
||||
for (auto index = std::uint32_t{}; index < 4U; ++index) {
|
||||
state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::NEXT);
|
||||
}
|
||||
|
||||
@@ -285,7 +285,7 @@ BOOST_AUTO_TEST_CASE(catalog_reload_uses_cached_metadata_without_parsing_score_j
|
||||
// Complete cached metadata must not require parsing or retaining the score body.
|
||||
db.query("INSERT INTO songs(id, title, composer, source_json, part, source_path, "
|
||||
"key_signature, measure_count, total_pages, title_image) "
|
||||
"VALUES('tune', 'Metadata Song', 'Composer', '{}', 'Lead', 'songs/tune.json', 'F', 3, 2, '0000000000000000000000000000000000000000000000000000000000000000')");
|
||||
"VALUES('tune', 'Metadata Song', 'Composer', '{\"presentation\":{\"playback_pages\":[{},{},{}]}}', 'Lead', 'songs/tune.json', 'F', 3, 2, '0000000000000000000000000000000000000000000000000000000000000000')");
|
||||
auto songs = db.rows("songs");
|
||||
for (auto attempt = std::uint32_t{}; attempt < 2U; ++attempt) {
|
||||
auto performance = db.performance();
|
||||
@@ -295,6 +295,7 @@ BOOST_AUTO_TEST_CASE(catalog_reload_uses_cached_metadata_without_parsing_score_j
|
||||
BOOST_TEST(performance->parts[0].key_signature == "F");
|
||||
BOOST_TEST(performance->parts[0].measure_count == 3U);
|
||||
BOOST_TEST(performance->parts[0].total_pages == 2U);
|
||||
BOOST_TEST(performance->parts[0].playback_pages == 3U);
|
||||
}
|
||||
BOOST_TEST(db.rows("songs") == songs, boost::test_tools::per_element());
|
||||
auto song = db.score("tune");
|
||||
|
||||
@@ -21,6 +21,7 @@ import generator.engraving.types
|
||||
import generator.guitar
|
||||
import generator.model
|
||||
import generator.musicxml
|
||||
import generator.performance
|
||||
import tests.score_events
|
||||
import tests.arrangement_fixtures
|
||||
import tests.test_accompaniment
|
||||
@@ -139,6 +140,56 @@ class EngravingTests(unittest.TestCase):
|
||||
actual = generator.engraving.raster.rasterize_page(layout.pages[0], self.fonts)
|
||||
with PIL.Image.open('tests/fixtures/notation_python.png') as expected:
|
||||
self.assertIsNone(PIL.ImageChops.difference(actual, expected.convert('RGB')).getbbox())
|
||||
def test_playback_windows_reuse_complete_systems_and_finish_on_top(self):
|
||||
for count in (1, 2, 3, 4):
|
||||
with self.subTest(systems=count):
|
||||
source = tests.test_musicxml.parse(tests.test_accompaniment.xml_bars([tests.test_musicxml.xml_note()] * count))
|
||||
style = generator.engraving.types.Style()
|
||||
index = generator.engraving.notation.ScoreIndex(source)
|
||||
systems = [generator.engraving.layout.make_system(index, i * 4, (i + 1) * 4, self.fonts, style) for i in range(count)]
|
||||
pages, playback = generator.engraving.layout.page_views(systems, source, self.fonts, style, index=index)
|
||||
generator.engraving.audit.audit_layout(source, generator.engraving.types.LayoutDocument(pages, style, ''), self.fonts)
|
||||
generator.engraving.audit.audit_playback(pages, playback, style, self.fonts)
|
||||
self.assertEqual(len(pages), (count + 1) // 2)
|
||||
self.assertEqual(len(playback), count)
|
||||
self.assertEqual([[s.start for s in p.systems] for p in playback], [[i * 4, (i + 1) * 4] if i + 1 < count else [i * 4] for i in range(count)])
|
||||
self.assertLess(playback[-1].systems[0].box.top, style.height / 2)
|
||||
for sequence in (pages, playback):
|
||||
for page in sequence:
|
||||
self.assertEqual(len({s.scale for s in page.systems}), 1)
|
||||
normal = [generator.performance.page_timing(source, p) for p in pages]
|
||||
sliding = [generator.performance.page_timing(source, p) for p in playback]
|
||||
generator.performance.validate_views(source, normal, sliding)
|
||||
def test_transition_pair_of_tall_systems_fits_at_shared_scale(self):
|
||||
short = tests.test_musicxml.xml_note()
|
||||
tall = tests.test_musicxml.xml_note('C', 2, 'half', octave=2) + tests.test_musicxml.xml_note('C', 2, 'half', octave=7)
|
||||
source = tests.test_musicxml.parse(tests.test_accompaniment.xml_bars([short, tall, tall, short]))
|
||||
style = generator.engraving.types.Style()
|
||||
index = generator.engraving.notation.ScoreIndex(source)
|
||||
systems = [generator.engraving.layout.make_system(index, i * 4, (i + 1) * 4, self.fonts, style) for i in range(4)]
|
||||
pages, playback = generator.engraving.layout.page_views(systems, source, self.fonts, style)
|
||||
generator.engraving.audit.audit_playback(pages, playback, style, self.fonts)
|
||||
self.assertEqual(playback[1].systems[0].scale, pages[0].systems[0].scale)
|
||||
self.assertLess(playback[1].systems[1].box.bottom, style.height - style.margin_y)
|
||||
def test_measure_numbers_clear_high_notes_and_follow_barlines(self):
|
||||
source = tests.test_musicxml.parse(tests.test_accompaniment.xml_bars([tests.test_musicxml.xml_note('C', octave=7)] * 3))
|
||||
for i, measure in enumerate(source['staves'][0]['measures']):
|
||||
measure['number'] = str(169 + i)
|
||||
layout = self.layout(source)
|
||||
for page in layout.pages:
|
||||
for system in page.systems:
|
||||
labels = [p for p in system.primitives if p.role == 'measure_number']
|
||||
self.assertEqual(len({round(p.points[0][1], 6) for p in labels}), 1)
|
||||
bars = [p for p in system.primitives if p.role == 'barline']
|
||||
for i, label in enumerate(labels):
|
||||
center = (label.box.left + label.box.right) / 2
|
||||
if i == 0:
|
||||
self.assertAlmostEqual(center, layout.style.margin_x + 1, places=2)
|
||||
else:
|
||||
self.assertTrue(any(abs(center - p.points[0][0]) < system.scale for p in bars))
|
||||
for other in system.primitives:
|
||||
if other is not label:
|
||||
self.assertFalse(label.box.overlaps(other.box, 3 * system.scale))
|
||||
def test_aligned_bands_match_full_pages_with_spans_tab_and_sampling_phases(self):
|
||||
tied = tests.test_musicxml.xml_note('E', 2, 'half', extra='<tie type="start"/>') + tests.test_musicxml.xml_note('E', 2, 'half', extra='<tie type="stop"/>')
|
||||
content = ''.join(tests.test_musicxml.xml_note('G', 1, 'eighth') for _ in range(8))
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <vamp_zero/notation.hpp>
|
||||
#include <vamp_zero/page_control.hpp>
|
||||
#include <vamp_zero/performance.hpp>
|
||||
#include <vamp_zero/score.hpp>
|
||||
|
||||
|
||||
namespace {
|
||||
@@ -29,7 +30,7 @@ auto state(bool metronome = false, bool backing = false) -> vamp_zero::app_state
|
||||
auto audio = vamp_zero::parse_audio_assets(read("manifest.audio"));
|
||||
BOOST_REQUIRE(audio);
|
||||
catalog->audio_assets = *audio;
|
||||
catalog->parts.push_back({.id = "score", .title = "Fixture", .part = "Vocal", .total_pages = 2, .song_id = "fixture"});
|
||||
catalog->parts.push_back({.id = "score", .title = "Fixture", .part = "Vocal", .total_pages = 2, .song_id = "fixture", .playback_pages = 3});
|
||||
catalog->groups.push_back({.title = "Fixture", .parts = {0}});
|
||||
auto result = vamp_zero::app_state{.catalog = catalog, .mode = vamp_zero::app_mode::PERFORMANCE_SCORE};
|
||||
result.selected_song_index = 1;
|
||||
@@ -139,6 +140,94 @@ BOOST_AUTO_TEST_CASE(stale_performance_updates_do_not_navigate_or_restart) {
|
||||
BOOST_CHECK(control.snapshot().mode == vamp_zero::app_mode::PERFORMANCE_PART_MENU);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(playback_advances_one_system_and_highlights_each_measure) {
|
||||
auto document = score();
|
||||
BOOST_TEST(document.pages.size() == 2U);
|
||||
BOOST_TEST(document.playback_pages.size() == 3U);
|
||||
for (auto bpm : {60U, 90U, 120U}) {
|
||||
auto count_in = vamp_zero::frame_at({4, 1}, bpm);
|
||||
BOOST_CHECK(!vamp_zero::performance_at(document, count_in - 1, bpm).measure);
|
||||
for (std::size_t m = 0; m < document.measure_count; ++m) {
|
||||
auto frame = count_in + vamp_zero::frame_at(document.measure_boundaries[m], bpm);
|
||||
auto now = vamp_zero::performance_at(document, frame, bpm);
|
||||
BOOST_REQUIRE(now.measure);
|
||||
BOOST_TEST(*now.measure == m + 1);
|
||||
BOOST_TEST(now.page == std::min(m, std::size_t{2}));
|
||||
if (m) {
|
||||
BOOST_TEST(*vamp_zero::performance_at(document, frame - 1, bpm).measure == m);
|
||||
}
|
||||
}
|
||||
}
|
||||
BOOST_TEST(document.playback_pages.back().systems.size() == 1U);
|
||||
auto app = state(true);
|
||||
auto control = vamp_zero::control_state{{}, app};
|
||||
control.update_performance(1, 2, "Sustain", app.catalog, 3);
|
||||
auto revision = control.snapshot().input_revision;
|
||||
control.update_performance(1, 2, "Sustain", app.catalog, 4);
|
||||
BOOST_TEST(*control.snapshot().active_measure == 4U);
|
||||
BOOST_TEST(control.snapshot().input_revision == revision + 1);
|
||||
auto remote = vamp_zero::remote_display_text(control.snapshot());
|
||||
BOOST_TEST(remote.find("page=3\npages=3") != std::string::npos);
|
||||
control.finish_playback(1, "failed", app.catalog);
|
||||
BOOST_CHECK(!control.snapshot().active_measure);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(fractional_pickups_and_unequal_measures_use_exact_boundaries) {
|
||||
auto document = score();
|
||||
document.measure_boundaries = {{0, 1}, {1, 2}, {7, 2}, {41, 6}, {65, 6}, {89, 6}};
|
||||
document.playback_pages[1].start_beat = {1, 2};
|
||||
document.playback_pages[2].start_beat = {7, 2};
|
||||
document.duration = {89, 6};
|
||||
for (auto bpm : {60U, 110U, 120U}) {
|
||||
for (std::size_t i = 1; i < document.measure_count; ++i) {
|
||||
auto frame = vamp_zero::frame_at({4, 1}, bpm) + vamp_zero::frame_at(document.measure_boundaries[i], bpm);
|
||||
BOOST_TEST(*vamp_zero::performance_at(document, frame - 1, bpm).measure == i);
|
||||
BOOST_TEST(*vamp_zero::performance_at(document, frame, bpm).measure == i + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(highlight_geometry_follows_letterboxed_page_and_active_view) {
|
||||
auto document = score();
|
||||
for (auto bounds : {luma::rect{0, 0, 976, 484}, luma::rect{10, 20, 488, 484}, luma::rect{10, 20, 1952, 968}}) {
|
||||
auto image = vamp_zero::page_rectangle(bounds);
|
||||
auto a = vamp_zero::measure_highlight(document.playback_pages[0], 2, image);
|
||||
auto b = vamp_zero::measure_highlight(document.playback_pages[1], 2, image);
|
||||
BOOST_REQUIRE(a);
|
||||
BOOST_REQUIRE(b);
|
||||
auto scale = image.width / 976.F;
|
||||
BOOST_TEST(b->x == image.x + 27.F * scale);
|
||||
BOOST_TEST(b->y == image.y + 67.F * scale);
|
||||
BOOST_TEST(a->y - b->y == 220.F * scale);
|
||||
BOOST_TEST(b->width == 18.F * scale);
|
||||
BOOST_CHECK(!vamp_zero::measure_highlight(document.playback_pages[0], {}, image));
|
||||
BOOST_CHECK(!vamp_zero::measure_highlight(document.playback_pages[0], 5, image));
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(playback_parser_requires_complete_views_and_safe_label_geometry) {
|
||||
auto original = boost::json::parse(read("score.json"));
|
||||
for (auto fault : {"missing", "pair", "label", "bounds", "time", "path"}) {
|
||||
auto value = original;
|
||||
auto &presentation = value.at("presentation").as_object();
|
||||
auto &views = presentation.at("playback_pages").as_array();
|
||||
if (std::string_view{fault} == "missing") {
|
||||
presentation.erase("playback_pages");
|
||||
} else if (std::string_view{fault} == "pair") {
|
||||
views[1] = views[0];
|
||||
} else if (std::string_view{fault} == "label") {
|
||||
views[0].at("systems").as_array()[0].at("labels").as_array()[0].at("measure") = 99;
|
||||
} else if (std::string_view{fault} == "bounds") {
|
||||
views[0].at("systems").as_array()[0].at("labels").as_array()[0].at("bounds").as_array()[2] = 1000;
|
||||
} else if (std::string_view{fault} == "time") {
|
||||
views[1].at("start_beat").at("numerator") = 3;
|
||||
} else {
|
||||
views[1].at("path") = "../outside.png";
|
||||
}
|
||||
BOOST_CHECK_MESSAGE(!vamp_zero::parse_notation_document(boost::json::serialize(value), "score.json"), fault);
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(recording_transport_cancellation_does_not_advance_clock) {
|
||||
auto prepared = vamp_zero::prepare_performance(score(), state(true), VAMP_TEST_PERFORMANCE_ASSETS_DIR, 1.F);
|
||||
BOOST_REQUIRE(prepared);
|
||||
|
||||
@@ -19,9 +19,19 @@ def fixture(root):
|
||||
music['staves'][0]['measures'].append(copy.deepcopy(first))
|
||||
music['staves'][0]['measures'].extend({**copy.deepcopy(first), 'ending_sustain': True} for _ in range(3))
|
||||
music['metadata']['song_id'] = 'fixture'
|
||||
pages = [dict(path=f'{i + 1}.png', sha256='0' * 64, first_measure=i + 1, measure_count=1 if i == 0 else 4,
|
||||
start_beat=generator.model.rational(i * 4), end_beat=generator.model.rational(4 if i == 0 else 20)) for i in range(2)]
|
||||
bundle = dict(music=music, presentation=dict(width=976, height=484, pages=pages), performance=generator.performance.timing(music))
|
||||
|
||||
def system(first, count, y):
|
||||
return dict(first_measure=first, measure_count=count, start_beat=generator.model.rational((first - 1) * 4),
|
||||
end_beat=generator.model.rational((first + count - 1) * 4),
|
||||
labels=[dict(measure=m, bounds=[30 + 40 * (m - first), y, 42 + 40 * (m - first), y + 12]) for m in range(first, first + count)])
|
||||
|
||||
def page(index, systems):
|
||||
return dict(path=f'{index}.png', sha256='0' * 64, first_measure=systems[0]['first_measure'],
|
||||
measure_count=sum(s['measure_count'] for s in systems), start_beat=systems[0]['start_beat'], end_beat=systems[-1]['end_beat'], systems=systems)
|
||||
|
||||
pages = [page(1, [system(1, 1, 70), system(2, 1, 290)]), page(2, [system(3, 3, 70)])]
|
||||
playback = [pages[0], page(3, [system(2, 1, 70), system(3, 3, 290)]), pages[1]]
|
||||
bundle = dict(music=music, presentation=dict(width=976, height=484, pages=pages, playback_pages=playback), performance=generator.performance.timing(music))
|
||||
(root / 'score.json').write_text(json.dumps(bundle))
|
||||
entries = []
|
||||
# Constant PCM makes accents, recorded count-in and the silent click tail observable.
|
||||
@@ -79,6 +89,28 @@ class PerformanceTests(unittest.TestCase):
|
||||
bundle['presentation']['pages'][1]['start_beat'] = generator.model.rational(3)
|
||||
with self.assertRaisesRegex(ValueError, 'timing'):
|
||||
generator.performance.validate_pages(bundle['music'], bundle['presentation']['pages'])
|
||||
def test_playback_metadata_rejects_missing_views_wrong_pairs_and_bad_labels(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = pathlib.Path(temporary)
|
||||
fixture(root)
|
||||
bundle = json.loads((root / 'score.json').read_text())
|
||||
pages = bundle['presentation']['pages']
|
||||
playback = bundle['presentation']['playback_pages']
|
||||
generator.performance.validate_views(bundle['music'], pages, playback)
|
||||
for fault in ('missing', 'pair', 'bounds', 'label', 'timing'):
|
||||
altered = copy.deepcopy(playback)
|
||||
if fault == 'missing':
|
||||
altered.pop()
|
||||
elif fault == 'pair':
|
||||
altered[1] = copy.deepcopy(altered[0])
|
||||
elif fault == 'bounds':
|
||||
altered[0]['systems'][0]['labels'][0]['bounds'][0] = -1
|
||||
elif fault == 'label':
|
||||
altered[0]['systems'][0]['labels'][0]['measure'] = 2
|
||||
else:
|
||||
altered[1]['start_beat'] = generator.model.rational(3)
|
||||
with self.subTest(fault=fault), self.assertRaises(ValueError):
|
||||
generator.performance.validate_views(bundle['music'], pages, altered)
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser()
|
||||
|
||||
@@ -213,8 +213,10 @@ class ScoreBundleTests(unittest.TestCase):
|
||||
page.write_bytes(data)
|
||||
document = generator.musicxml.load_musicxml(pathlib.Path('tests/fixtures/engraving.xml'))
|
||||
bundle = {'music': document, 'presentation': {'engine': generator.engraving.validation.ENGINE_NAME, 'width': 976, 'height': 484, 'pages': [{'path': 'page.png', 'sha256': hashlib.sha256(data).hexdigest()}]}}
|
||||
times = generator.performance.boundaries(document)
|
||||
bundle['presentation']['pages'][0].update(first_measure=1, measure_count=len(times) - 1, start_beat=generator.model.rational(0), end_beat=generator.model.rational(times[-1]))
|
||||
fonts = generator.engraving.fonts.Fonts(pathlib.Path('package/assets/fonts/petaluma'))
|
||||
layout = generator.engraving.layout.layout_score(document, generator.engraving.types.Style(), fonts)
|
||||
bundle['presentation']['pages'][0].update(generator.performance.page_timing(document, layout.pages[0]))
|
||||
bundle['presentation']['playback_pages'] = copy.deepcopy(bundle['presentation']['pages'])
|
||||
bundle['performance'] = generator.performance.timing(document)
|
||||
path = assets / 'score.json'
|
||||
path.write_text(json.dumps(bundle))
|
||||
|
||||
+28
-3
@@ -14,6 +14,10 @@
|
||||
#include <vamp_pico/screen.hpp>
|
||||
#include <vamp_zero/menu_view.hpp>
|
||||
#include <vamp_zero/page_control.hpp>
|
||||
#include <vamp_zero/notation.hpp>
|
||||
#include <vamp_zero/performance.hpp>
|
||||
#include <vamp_zero/score.hpp>
|
||||
#include <vamp_zero/color.hpp>
|
||||
|
||||
|
||||
namespace {
|
||||
@@ -133,12 +137,21 @@ auto preview(vamp_zero::app_state const &state, std::string const &name, vamp_pi
|
||||
auto context = image_context{};
|
||||
vamp_zero::draw_header(context, {0.0F, 0.0F, 1024.0F, 600.0F}, state);
|
||||
if (state.mode == vamp_zero::app_mode::PERFORMANCE_SCORE) {
|
||||
auto source = boost::json::parse(read_bytes(std::filesystem::path{VAMP_TEST_ASSETS_DIR} / "songs/autumn_leaves_electric.json"));
|
||||
auto path = std::filesystem::path{VAMP_TEST_ASSETS_DIR} / "songs" / std::string{source.at("presentation").at("pages").as_array().front().at("path").as_string()};
|
||||
auto source = vamp_zero::parse_notation_document(read_bytes(std::filesystem::path{VAMP_TEST_ASSETS_DIR} / "songs/autumn_leaves_electric.json"), "score.json");
|
||||
BOOST_REQUIRE(source);
|
||||
auto playback = vamp_zero::automatic(state.audio);
|
||||
auto const &pages = playback ? source->playback_pages : source->pages;
|
||||
auto const &page = pages.at(state.page_index);
|
||||
auto path = std::filesystem::path{VAMP_TEST_ASSETS_DIR} / "songs" / page.path;
|
||||
auto sheet = LoadImage(path.c_str());
|
||||
BOOST_REQUIRE(sheet.data);
|
||||
ImageDraw(&context.image, sheet, {0, 0, 976, 484}, {24, 98, 976, 484}, WHITE);
|
||||
UnloadImage(sheet);
|
||||
if (playback) {
|
||||
if (auto highlight = vamp_zero::measure_highlight(page, state.active_measure, {24, 98, 976, 484})) {
|
||||
context.stroke_rect(*highlight, 1.5F, vamp_zero::to_luma(vamp_style::accent));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
vamp_zero::draw_menu(context, {0.0F, 80.0F, 1024.0F, 520.0F}, state);
|
||||
}
|
||||
@@ -184,7 +197,7 @@ BOOST_AUTO_TEST_CASE(device_menu_rendering_uses_petaluma_and_fits_both_displays)
|
||||
auto catalog = std::make_shared<vamp_zero::catalog>();
|
||||
auto score = boost::json::parse(read_bytes(std::filesystem::path{VAMP_TEST_ASSETS_DIR} / "songs/autumn_leaves_electric.json"));
|
||||
auto title = std::string{score.at("presentation").at("title_image").as_string()};
|
||||
catalog->parts.push_back({.id = "electric", .title = "Autumn Leaves", .composer = "Joseph Kosma", .part = "Electric", .total_pages = score.at("presentation").at("pages").as_array().size(), .title_image = title, .song_id = "autumn_leaves"});
|
||||
catalog->parts.push_back({.id = "electric", .title = "Autumn Leaves", .composer = "Joseph Kosma", .part = "Electric", .total_pages = score.at("presentation").at("pages").as_array().size(), .title_image = title, .song_id = "autumn_leaves", .playback_pages = score.at("presentation").at("playback_pages").as_array().size()});
|
||||
catalog->groups.push_back({"Autumn Leaves", "Joseph Kosma", {0U}});
|
||||
catalog->audio_assets.push_back({.path = "backing/autumn_leaves_120.wav", .song_id = "autumn_leaves", .bpm = 120});
|
||||
state.catalog = catalog;
|
||||
@@ -206,9 +219,21 @@ BOOST_AUTO_TEST_CASE(device_menu_rendering_uses_petaluma_and_fits_both_displays)
|
||||
state.active_part_id = "electric";
|
||||
for (auto phase : {"Manual", "Preparing", "Count-in", "Playing", "Sustain"}) {
|
||||
state.audio.phase = phase;
|
||||
state.audio.backing_enabled = std::string_view{phase} != "Manual";
|
||||
state.audio.metronome_enabled = state.audio.backing_enabled;
|
||||
state.active_measure = std::string_view{phase} == "Playing" ? std::optional<std::size_t>{1} : std::nullopt;
|
||||
preview(state, phase);
|
||||
}
|
||||
state.audio.phase = "Playing";
|
||||
state.page_index = 1;
|
||||
state.active_measure = score.at("presentation").at("playback_pages").as_array()[1].at("first_measure").to_number<std::size_t>();
|
||||
preview(state, "transition");
|
||||
state.page_index = catalog->parts[0].playback_pages - 1;
|
||||
state.active_measure = score.at("music").at("staves").as_array()[0].at("measures").as_array().size();
|
||||
state.audio.phase = "Sustain";
|
||||
preview(state, "final_line");
|
||||
state.audio.phase = "Error";
|
||||
state.active_measure.reset();
|
||||
state.audio.playback_error = "Recording unavailable";
|
||||
preview(state, "audio_error");
|
||||
state.mode = vamp_zero::app_mode::PERFORMANCE_PART_MENU;
|
||||
|
||||
Reference in New Issue
Block a user