#include #include #include #include #include #include #include #include #include #include #include namespace { auto read(std::string const &name) -> std::string { auto input = std::ifstream{std::string{VAMP_TEST_PERFORMANCE_ASSETS_DIR} + "/" + name}; return {std::istreambuf_iterator{input}, {}}; } auto score() -> vamp_zero::score_document { auto parsed = vamp_zero::parse_notation_document(read("score.json"), "score.json"); BOOST_REQUIRE_MESSAGE(parsed, (parsed ? "" : parsed.error().describe())); return *parsed; } auto state(bool metronome = false, bool backing = false) -> vamp_zero::app_state { auto catalog = std::make_shared(); 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->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; result.selected_part_index = 1; result.active_part_id = "score"; result.score_session = 1; result.audio.metronome_enabled = metronome; result.audio.backing_enabled = backing; return result; } } BOOST_AUTO_TEST_CASE(recordings_and_clicks_share_clock_and_tail_is_unclicked) { for (auto metronome : {false, true}) { for (auto backing : {false, true}) { auto app = state(metronome, backing); auto document = score(); auto prepared = vamp_zero::prepare_performance(document, app, VAMP_TEST_PERFORMANCE_ASSETS_DIR, 1.F); if (!metronome && !backing) { BOOST_CHECK(!prepared); continue; } BOOST_REQUIRE_MESSAGE(prepared, (prepared ? "" : prepared.error().describe())); auto engine = sonus::engine::create({.sample_rate = 48000, .channels = 2, .master_volume = 1.F, .no_device = true}); BOOST_REQUIRE(engine); auto session = engine->play_transport(std::move(*prepared)); BOOST_REQUIRE(session); auto frames = backing ? 576000U : 288000U; auto buffer = std::array{}; for (unsigned cursor = 0; cursor < frames; cursor += 480) { BOOST_REQUIRE(engine->render(buffer.data(), 480)); BOOST_TEST(session->frames() == cursor + 480); if (cursor % 24000 == 0) { auto expected = backing ? 0.03125F : 0.F; if (metronome && cursor < 288000 && (!backing || cursor >= 96000)) { expected += cursor % 96000 == 0 ? .25F : .125F; } BOOST_TEST(buffer.front() == expected, boost::test_tools::tolerance(0.0001F)); } // Let the background stream worker refill between accelerated renders. if (backing && cursor % 48000 == 0) { std::this_thread::sleep_for(std::chrono::milliseconds{2}); } } BOOST_CHECK(session->status() == sonus::playback_status::COMPLETED); BOOST_TEST(vamp_zero::performance_at(document, 95999, 120).phase == "Count-in"); BOOST_TEST(vamp_zero::performance_at(document, 96000, 120).page == 0U); BOOST_TEST(vamp_zero::performance_at(document, 191999, 120).page == 0U); BOOST_TEST(vamp_zero::performance_at(document, 192000, 120).page == 1U); BOOST_TEST(vamp_zero::performance_at(document, 288000, 120).phase == "Sustain"); } } } BOOST_AUTO_TEST_CASE(manual_navigation_requires_both_toggles_off) { for (auto metro : {false, true}) { for (auto backing : {false, true}) { auto app = state(metro, backing); auto next = vamp_zero::apply_app_command(app, vamp_protocol::page_command::NEXT); BOOST_TEST(next.page_index == (metro || backing ? 0U : 1U)); next = vamp_zero::apply_app_command(next, vamp_protocol::page_command::SELECT); BOOST_CHECK(next.mode == vamp_zero::app_mode::PERFORMANCE_PART_MENU); } } } BOOST_AUTO_TEST_CASE(tempo_steps_use_global_recording_intersection_and_nearest_slower_tie) { auto app = state(); BOOST_TEST(vamp_zero::available_tempos(app).size() == 7U); app.mode = vamp_zero::app_mode::SETTINGS_MENU; app.selected_settings_index = 1; app.audio.tempo_bpm = 80; app = vamp_zero::apply_app_command(app, vamp_protocol::page_command::SELECT); BOOST_TEST(app.audio.backing_enabled); BOOST_TEST(app.audio.tempo_bpm == 120U); auto catalog = std::make_shared(*app.catalog); auto entry = catalog->audio_assets.back(); entry.bpm = 100; entry.path = "backing/fixture_100.wav"; catalog->audio_assets.push_back(entry); app.catalog = catalog; app.audio.backing_enabled = false; app.audio.tempo_bpm = 110; app = vamp_zero::apply_app_command(app, vamp_protocol::page_command::SELECT); BOOST_TEST(app.audio.tempo_bpm == 100U); catalog->parts.push_back({.song_id = "other"}); catalog->groups.push_back({.parts = {1}}); BOOST_TEST(vamp_zero::available_tempos(app).empty()); } BOOST_AUTO_TEST_CASE(stale_performance_updates_do_not_navigate_or_restart) { auto app = state(true, false); auto control = vamp_zero::control_state{{}, app}; control.update_performance(99, 1, "Playing", app.catalog); BOOST_TEST(control.snapshot().page_index == 0U); control.update_performance(1, 1, "Playing", app.catalog); BOOST_TEST(control.snapshot().page_index == 1U); control.finish_playback(99, {}, app.catalog); BOOST_CHECK(control.snapshot().mode == vamp_zero::app_mode::PERFORMANCE_SCORE); control.finish_playback(1, "Recording failed", app.catalog); control.update_performance(1, 0, "Playing", app.catalog); BOOST_TEST(control.snapshot().page_index == 1U); BOOST_TEST(control.snapshot().audio.phase == "Error"); control.apply(vamp_protocol::page_command::SELECT); control.finish_playback(1, {}, app.catalog); BOOST_CHECK(control.snapshot().mode == vamp_zero::app_mode::PERFORMANCE_PART_MENU); } 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); auto engine = sonus::engine::create({.sample_rate = 48000, .channels = 2, .no_device = true}); BOOST_REQUIRE(engine); auto session = engine->play_transport(std::move(*prepared)); BOOST_REQUIRE(session); session->stop(); std::array buffer{}; BOOST_REQUIRE(engine->render(buffer.data(), 480)); BOOST_TEST(session->frames() == 0U); BOOST_TEST(buffer.front() == 0.F); } BOOST_AUTO_TEST_CASE(page_timing_and_recording_manifests_reject_corruption) { auto document = boost::json::parse(read("score.json")); document.at("presentation").at("pages").as_array()[1].at("first_measure") = 1; BOOST_CHECK(!vamp_zero::parse_notation_document(boost::json::serialize(document), "score.json")); auto app = state(true); auto catalog = std::make_shared(*app.catalog); catalog->audio_assets.front().sha256 = std::string(64, '0'); app.catalog = catalog; BOOST_CHECK(!vamp_zero::prepare_performance(score(), app, VAMP_TEST_PERFORMANCE_ASSETS_DIR, 1.F)); } BOOST_AUTO_TEST_CASE(preparation_waits_for_pages_and_cancellation_discards_stale_work) { auto engine = sonus::engine::create({.sample_rate = 48000, .channels = 2, .no_device = true}); BOOST_REQUIRE(engine); auto controller = vamp_zero::audio_controller{{}, std::move(*engine)}; auto app = state(true); auto document = std::make_shared(score()); auto loaded = vamp_zero::score_load_state{.document = document}; BOOST_CHECK(!controller.tick(app, loaded, VAMP_TEST_PERFORMANCE_ASSETS_DIR)); BOOST_CHECK(!controller.position(*document, 120)); loaded.pages_ready = true; BOOST_CHECK(!controller.tick(app, loaded, VAMP_TEST_PERFORMANCE_ASSETS_DIR)); auto exited = vamp_zero::apply_app_command(app, vamp_protocol::page_command::SELECT); controller.sync(exited); for (unsigned i = 0; i < 20; ++i) { BOOST_CHECK(!controller.tick(exited, {}, VAMP_TEST_PERFORMANCE_ASSETS_DIR)); std::this_thread::sleep_for(std::chrono::milliseconds{1}); } BOOST_CHECK(!controller.position(*document, 120)); ++app.score_session; for (unsigned i = 0; i < 200 && !controller.position(*document, 120); ++i) { BOOST_CHECK(!controller.tick(app, loaded, VAMP_TEST_PERFORMANCE_ASSETS_DIR)); std::this_thread::sleep_for(std::chrono::milliseconds{1}); } BOOST_REQUIRE(controller.position(*document, 120)); controller.sync(exited); BOOST_CHECK(!controller.position(*document, 120)); }