|
|
|
@@ -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);
|
|
|
|
|
}
|
|
|
|
|
}
|