Files
sonus/src/transport.cpp
khodowany dc46d83207
publish / publish (push) Successful in 13m11s
Cleanup
2026-10-08 02:13:33 -05:00

194 lines
8.4 KiB
C++

#include <sonus/transport.hpp>
#include <sonus/detail/errors.hpp>
#include "detail/transport_player.hpp"
#include <algorithm>
#include <cmath>
namespace {
auto failure(std::string text) -> std::unexpected<modulus::error> {
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<sonus::detail::transport_state> 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<sonus::prepared_transport, modulus::error> {
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<sonus::detail::transport_state>();
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<ma_decoder, decltype(&ma_decoder_uninit)>{&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<sonus::transport_progress> 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<sonus::transport_session, modulus::error> {
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);
}
}