#include #include #include #include #include #include namespace { struct recordings { std::filesystem::path root; recordings() { auto path = (std::filesystem::temp_directory_path() / "sonus-recordings-XXXXXX").string(); BOOST_REQUIRE(::mkdtemp(path.data())); root = path; write("backing.wav", 16, 1024, -1024); write("beat1.wav", 2, 8192, -8192); write("beat2.wav", 2, 4096, -4096); } ~recordings() { std::error_code error; std::filesystem::remove_all(root, error); } auto write(char const *name, unsigned frames, int left, int right) -> void { auto output = std::ofstream{root / name, std::ios::binary}; auto word = [&](unsigned value, unsigned bytes) { for (unsigned i = 0; i < bytes; ++i){ output.put(static_cast(value >> (8 * i))); } }; output.write("RIFF", 4); word(36 + frames * 4, 4); output.write("WAVEfmt ", 8); word(16, 4); word(1, 2); word(2, 2); word(48000, 4); word(192000, 4); word(4, 2); word(16, 2); output.write("data", 4); word(frames * 4, 4); for (unsigned i = 0; i < frames; ++i) { word(static_cast(left), 2); word(static_cast(right), 2); } } auto config() const -> sonus::transport_config { return {.backing = root / "backing.wav", .beat1 = root / "beat1.wav", .beat2 = root / "beat2.wav", .clicks = {{4, true}, {8, false}}, .duration_frames = 10, .click_volume = 1.F}; } }; } BOOST_AUTO_TEST_CASE(recording_transport_preserves_stereo_and_has_one_sample_clock) { auto files = recordings{}; auto prepared = sonus::prepared_transport::create(files.config(), 48000, 2); BOOST_REQUIRE(prepared); 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); std::array output{}; BOOST_REQUIRE(engine->render(output.data(), 20)); BOOST_TEST(session->frames() == 16U); BOOST_CHECK(session->status() == sonus::playback_status::COMPLETED); for (unsigned frame = 0; frame < 20; ++frame) { auto value = frame < 16 ? .03125F : 0.F; if (frame == 4 || frame == 5) { value += .25F; } if (frame == 8 || frame == 9) { value += .125F; } BOOST_TEST(output[frame * 2] == value); BOOST_TEST(output[frame * 2 + 1] == -value); } engine->collect_finished(); BOOST_CHECK(session->status() == sonus::playback_status::COMPLETED); } BOOST_AUTO_TEST_CASE(recording_transport_replacement_cancels_old_session_and_validates_inputs) { auto files = recordings{}; auto engine = sonus::engine::create({.sample_rate = 48000, .channels = 2, .no_device = true}); BOOST_REQUIRE(engine); auto config = files.config(); auto first = sonus::prepared_transport::create(config, 48000, 2); BOOST_REQUIRE(first); auto session = engine->play_transport(std::move(*first)); BOOST_REQUIRE(session); auto second = sonus::prepared_transport::create(config, 48000, 2); BOOST_REQUIRE(second); auto replacement = engine->play_transport(std::move(*second)); BOOST_REQUIRE(replacement); BOOST_CHECK(session->status() == sonus::playback_status::STOPPED); replacement->stop(); std::array output{}; BOOST_REQUIRE(engine->render(output.data(), 8)); BOOST_TEST(replacement->frames() == 0U); BOOST_TEST(output.front() == 0.F); config.duration_frames = 17; BOOST_CHECK(!sonus::prepared_transport::create(config, 48000, 2)); config = files.config(); config.beat1 = files.root / "missing.wav"; BOOST_CHECK(!sonus::prepared_transport::create(config, 48000, 2)); config = files.config(); config.clicks = {{9, true}, {4, false}}; BOOST_CHECK(!sonus::prepared_transport::create(config, 48000, 2)); config = files.config(); config.backing.clear(); config.clicks.push_back({10, true}); BOOST_CHECK(!sonus::prepared_transport::create(config, 48000, 2)); } BOOST_AUTO_TEST_CASE(recording_transport_long_schedule_preserves_overlaps_across_buffer_sizes) { auto files = recordings{}; files.write("beat1.wav", 1537, 1, -1); files.write("beat2.wav", 17, 2, -2); auto config = files.config(); config.backing.clear(); config.duration_frames = 40000; config.clicks.clear(); for (std::uint64_t i = 0; i < 2000; ++i) { config.clicks.push_back({i * 20, i % 7 == 0}); } auto expected = std::vector(config.duration_frames * 2); for (auto const &click : config.clicks) { auto length = click.downbeat ? 1537U : 17U; auto value = click.downbeat ? 1.F / 32768 : 2.F / 32768; for (auto frame = click.frame; frame < std::min(click.frame + length, config.duration_frames); ++frame) { expected[frame * 2] += value; expected[frame * 2 + 1] -= value; } } for (auto chunk : {317U, 1024U, 1701U}) { auto prepared = sonus::prepared_transport::create(config, 48000, 2); BOOST_REQUIRE(prepared); 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 actual = std::vector(expected.size()); for (std::uint64_t frame = 0; frame < config.duration_frames; frame += chunk) { BOOST_REQUIRE(engine->render(actual.data() + frame * 2, std::min(chunk, config.duration_frames - frame))); } BOOST_CHECK_EQUAL_COLLECTIONS(actual.begin(), actual.end(), expected.begin(), expected.end()); BOOST_CHECK(session->status() == sonus::playback_status::COMPLETED); BOOST_TEST(session->frames() == config.duration_frames); } }