From dc46d832071174035394795db5e74565928c2dd3 Mon Sep 17 00:00:00 2001 From: khodowany Date: Thu, 8 Oct 2026 02:13:33 -0500 Subject: [PATCH] Cleanup --- src/detail/transport_player.hpp | 1 + src/transport.cpp | 13 +++++++++++- tests/transport.cpp | 37 +++++++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/src/detail/transport_player.hpp b/src/detail/transport_player.hpp index f48882f..002d47e 100644 --- a/src/detail/transport_player.hpp +++ b/src/detail/transport_player.hpp @@ -16,6 +16,7 @@ struct transport_state { std::uint32_t channels{}; std::uint64_t duration{}; mutable std::uint64_t cursor{}; + mutable std::size_t first_live_click{}; sonus::transport_config config{}; std::array, 2> samples{}; mutable std::vector scratch{}; diff --git a/src/transport.cpp b/src/transport.cpp index b3dc6e7..e4e4797 100644 --- a/src/transport.cpp +++ b/src/transport.cpp @@ -155,7 +155,18 @@ auto sonus::detail::transport_player::render(std::float32_t *output, std::uint64 return; } } - for (auto const &click : state->config.clicks) { + // Retain overlapping tails, but never revisit an expired schedule prefix. + // The cursor is owned by the render thread and needs no allocation or lock. + while (state->first_live_click < state->config.clicks.size()) { + auto const &click = state->config.clicks[state->first_live_click]; + auto length = state->samples[click.downbeat ? 0 : 1].size() / channels; + if (click.frame > state->cursor || length > state->cursor - click.frame) { + break; + } + ++state->first_live_click; + } + for (auto index = state->first_live_click; index < state->config.clicks.size(); ++index) { + auto const &click = state->config.clicks[index]; if (click.frame >= state->cursor + count) { break; } diff --git a/tests/transport.cpp b/tests/transport.cpp index c66860f..eca3536 100644 --- a/tests/transport.cpp +++ b/tests/transport.cpp @@ -109,3 +109,40 @@ BOOST_AUTO_TEST_CASE(recording_transport_replacement_cancels_old_session_and_val 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); + } +}