Playback Mode Pages
ci / checks (push) Successful in 50m29s

This commit is contained in:
khodowany
2026-10-06 23:11:25 -05:00
parent 125aaa1637
commit d2f493c17b
33 changed files with 620 additions and 130 deletions
+31 -3
View File
@@ -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}};
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 67 KiB

+2 -2
View File
@@ -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);
}
+2 -1
View File
@@ -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");
+51
View File
@@ -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))
+90 -1
View File
@@ -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);
+35 -3
View File
@@ -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()
+4 -2
View File
@@ -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
View File
@@ -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;