#include #include #include "detail/transport_player.hpp" #include #include namespace { auto failure(std::string text) -> std::unexpected { return std::unexpected(sonus::detail::make_error(sonus::detail::error_invalid_argument, std::move(text))); } } sonus::detail::transport_state::~transport_state() { if (backing_ready) { ma_resource_manager_data_source_uninit(&backing); } if (resources_ready) { ma_resource_manager_uninit(&resources); } } sonus::prepared_transport::prepared_transport(std::unique_ptr state) : state_(std::move(state)) {} sonus::prepared_transport::prepared_transport(prepared_transport &&) noexcept = default; auto sonus::prepared_transport::operator=(prepared_transport &&) noexcept -> prepared_transport & = default; sonus::prepared_transport::~prepared_transport() = default; auto sonus::prepared_transport::create(sonus::transport_config config, std::uint32_t rate, std::uint32_t channels) -> std::expected { if (rate < 8000 || rate > 192000 || channels < 1 || channels > 2 || config.duration_frames == 0 || !std::isfinite(config.click_volume) || config.click_volume < 0 || config.click_volume > 1 || config.clicks.size() > 100000) { return failure("invalid recording transport configuration"); } auto state = std::make_unique(); state->sample_rate = rate; state->channels = channels; state->duration = config.duration_frames; state->scratch.resize(1024 * channels); if (!config.backing.empty()) { auto options = ma_resource_manager_config_init(); options.decodedFormat = ma_format_f32; options.decodedChannels = channels; options.decodedSampleRate = rate; if (ma_resource_manager_init(&options, &state->resources) != MA_SUCCESS) { return failure("cannot initialize recording stream"); } state->resources_ready = true; if (ma_resource_manager_data_source_init(&state->resources, config.backing.string().c_str(), MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_STREAM, nullptr, &state->backing) != MA_SUCCESS) { return failure("cannot open backing recording"); } state->backing_ready = true; ma_uint64 length{}; if (ma_resource_manager_data_source_get_length_in_pcm_frames(&state->backing, &length) != MA_SUCCESS || length < config.duration_frames) { return failure("backing recording is shorter than the performance"); } state->duration = length; } if (!config.clicks.empty()) { for (std::size_t index = 0; index < 2; ++index) { auto decoder = ma_decoder{}; auto options = ma_decoder_config_init(ma_format_f32, channels, rate); auto path = index == 0 ? config.beat1 : config.beat2; if (ma_decoder_init_file(path.string().c_str(), &options, &decoder) != MA_SUCCESS) { return failure("cannot decode metronome sample"); } auto guard = std::unique_ptr{&decoder, &ma_decoder_uninit}; ma_uint64 length{}, read{}; if (ma_decoder_get_length_in_pcm_frames(&decoder, &length) != MA_SUCCESS || length == 0 || length > rate * 4U) { return failure("invalid metronome sample length"); } state->samples[index].resize(length * channels); auto result = ma_decoder_read_pcm_frames(&decoder, state->samples[index].data(), length, &read); if ((result != MA_SUCCESS && result != MA_AT_END) || read != length) { return failure("truncated metronome sample"); } } if (!std::is_sorted(config.clicks.begin(), config.clicks.end(), [](auto const &a, auto const &b) { return a.frame < b.frame; }) || config.clicks.back().frame >= config.duration_frames) { return failure("invalid click schedule"); } } state->config = std::move(config); return sonus::prepared_transport{std::move(state)}; } sonus::transport_session::transport_session(std::shared_ptr progress) : progress_(std::move(progress)) {} auto sonus::transport_session::frames() const -> std::uint64_t { return progress_ ? progress_->frames.load(std::memory_order_acquire) : 0; } auto sonus::transport_session::status() const -> sonus::playback_status { return progress_ ? progress_->status.load(std::memory_order_acquire) : sonus::playback_status::STOPPED; } auto sonus::transport_session::stop() const -> void { if (progress_) { auto expected = sonus::playback_status::PLAYING; progress_->status.compare_exchange_strong(expected, sonus::playback_status::STOPPED); } } auto sonus::detail::transport_player::play(sonus::prepared_transport transport, std::uint32_t rate, std::uint32_t channels) -> std::expected { if (!transport.state_ || transport.state_->sample_rate != rate || transport.state_->channels != channels) { return failure("transport format does not match engine"); } stop(); auto session = sonus::transport_session{transport.state_->progress}; snapshots_.publish(std::move(transport.state_)); return session; } auto sonus::detail::transport_player::stop() -> void { if (auto state = snapshots_.current()) { sonus::transport_session{state->progress}.stop(); } snapshots_.publish(nullptr); } auto sonus::detail::transport_player::collect() -> void { if (auto state = snapshots_.current(); state && state->progress->status.load() != sonus::playback_status::PLAYING) { snapshots_.publish(nullptr); } snapshots_.collect(); } auto sonus::detail::transport_player::fail() -> void { auto guard = snapshots_.read(); if (auto state = guard.get()) { state->progress->status.store(sonus::playback_status::FAILED); } } auto sonus::detail::transport_player::render(std::float32_t *output, std::uint64_t frames, std::uint32_t channels, std::float32_t gain) -> void { auto guard = snapshots_.read(); auto state = guard.get(); if (!state || channels != state->channels) { return; } for (std::uint64_t offset = 0; offset < frames && state->cursor < state->duration;) { if (state->progress->status.load(std::memory_order_acquire) != sonus::playback_status::PLAYING) { return; } auto count = std::min({frames - offset, state->duration - state->cursor, std::uint64_t{1024}}); std::fill(state->scratch.begin(), state->scratch.end(), 0.F); if (state->backing_ready) { ma_uint64 read{}; auto result = ma_resource_manager_data_source_read_pcm_frames(&state->backing, state->scratch.data(), count, &read); if ((result != MA_SUCCESS && result != MA_AT_END) || read != count) { state->progress->status.store(sonus::playback_status::FAILED, std::memory_order_release); return; } } // 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; } auto const &sample = state->samples[click.downbeat ? 0 : 1]; auto start = std::max(click.frame, state->cursor); auto end = std::min({click.frame + sample.size() / channels, state->cursor + count, state->config.duration_frames}); for (auto frame = start; frame < end; ++frame) { for (std::uint32_t channel = 0; channel < channels; ++channel) { state->scratch[(frame - state->cursor) * channels + channel] += sample[(frame - click.frame) * channels + channel] * state->config.click_volume; } } } for (std::uint64_t index = 0; index < count * channels; ++index) { output[offset * channels + index] += state->scratch[index] * gain; } state->cursor += count; offset += count; state->progress->frames.store(state->cursor, std::memory_order_release); } if (state->cursor == state->duration) { auto expected = sonus::playback_status::PLAYING; state->progress->status.compare_exchange_strong(expected, sonus::playback_status::COMPLETED); } }