Playback
publish / publish (push) Successful in 13m11s

This commit is contained in:
khodowany
2026-10-06 00:04:22 -05:00
parent 8bedccaa02
commit 3aa3a67e5d
9 changed files with 419 additions and 0 deletions
+37
View File
@@ -0,0 +1,37 @@
#pragma once
#include <sonus/transport.hpp>
#include "render_snapshot.hpp"
#include <miniaudio.h>
#include <array>
namespace sonus::detail {
struct transport_state {
ma_resource_manager resources{};
mutable ma_resource_manager_data_source backing{};
bool resources_ready{};
bool backing_ready{};
std::uint32_t sample_rate{};
std::uint32_t channels{};
std::uint64_t duration{};
mutable std::uint64_t cursor{};
sonus::transport_config config{};
std::array<std::vector<std::float32_t>, 2> samples{};
mutable std::vector<std::float32_t> scratch{};
std::shared_ptr<sonus::transport_progress> progress{std::make_shared<sonus::transport_progress>()};
~transport_state();
};
class transport_player {
public:
auto play(sonus::prepared_transport transport, std::uint32_t rate, std::uint32_t channels) -> std::expected<sonus::transport_session, modulus::error>;
auto stop() -> void;
auto collect() -> void;
auto fail() -> void;
auto render(std::float32_t *output, std::uint64_t frames, std::uint32_t channels, std::float32_t gain) -> void;
private:
sonus::detail::render_snapshot<sonus::detail::transport_state> snapshots_{};
};
}
+15
View File
@@ -26,6 +26,7 @@
#include "detail/metronome_player.hpp"
#include "detail/playback_devices.hpp"
#include "detail/sequence_player.hpp"
#include "detail/transport_player.hpp"
namespace {
@@ -137,6 +138,7 @@ struct sonus::engine::impl {
std::unordered_map<std::uint64_t, std::unique_ptr<active_sound>> sounds{};
std::uint64_t next_sound_handle{1};
sonus::detail::sequence_player sequences{};
sonus::detail::transport_player transport{};
std::atomic<std::float32_t> sequence_volume{0.70F};
sonus::detail::metronome_player metronome{};
impl() = default;
@@ -232,6 +234,7 @@ struct sonus::engine::impl {
}
sounds.clear();
sequences.stop();
transport.stop();
if (mixer_initialized) {
ma_engine_uninit(&mixer);
mixer_initialized = false;
@@ -338,6 +341,7 @@ struct sonus::engine::impl {
auto stop_all_sounds() -> void {
auto lock = std::scoped_lock{mutex};
sequences.stop();
transport.stop();
for (auto &entry : sounds) {
static_cast<void>(ma_sound_stop(&entry.second->sound));
}
@@ -361,6 +365,7 @@ struct sonus::engine::impl {
auto collect_finished_sounds() -> std::uint64_t {
auto lock = std::scoped_lock{mutex};
sequences.collect();
transport.collect();
auto const collected = collect_finished_locked();
metronome.collect();
return collected;
@@ -430,9 +435,11 @@ struct sonus::engine::impl {
auto result = ma_engine_read_pcm_frames(&mixer, output, frame_count, nullptr);
if (result != MA_SUCCESS) {
sequences.fail();
transport.fail();
std::fill(output, output + sample_count, 0.0F);
return result;
}
transport.render(output, frame_count, config.channels, sequence_volume.load(std::memory_order_relaxed));
metronome.render(output, frame_count, config.channels);
sequences.render(output, frame_count, config.channels, sequence_volume.load(std::memory_order_relaxed));
return MA_SUCCESS;
@@ -652,3 +659,11 @@ auto sonus::engine::render(std::float32_t *output, std::uint64_t frame_count) ->
}
return {};
}
auto sonus::engine::play_transport(sonus::prepared_transport transport) -> std::expected<sonus::transport_session, modulus::error> {
if (!implementation_) {
return sonus::detail::fail<sonus::transport_session>(unavailable("engine is not initialized"));
}
auto lock = std::scoped_lock{implementation_->mutex};
return implementation_->transport.play(std::move(transport), sample_rate(), channels());
}
+182
View File
@@ -0,0 +1,182 @@
#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;
}
}
for (auto const &click : state->config.clicks) {
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);
}
}