@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#include <mutex>
|
||||
|
||||
#include <sonus/sequence.hpp>
|
||||
#include "render_snapshot.hpp"
|
||||
|
||||
|
||||
namespace sonus::detail {
|
||||
|
||||
struct sequence_voice {
|
||||
std::size_t event{};
|
||||
std::bool_t active{};
|
||||
};
|
||||
|
||||
struct sequence_state {
|
||||
sonus::sample_sequence sequence{};
|
||||
std::shared_ptr<std::atomic<sonus::playback_status>> status{};
|
||||
mutable std::vector<sonus::detail::sequence_voice> voices{};
|
||||
mutable std::uint64_t frame{};
|
||||
mutable std::size_t next_event{};
|
||||
std::uint64_t end_frame{};
|
||||
std::uint32_t sample_rate{};
|
||||
};
|
||||
|
||||
class sequence_player {
|
||||
public:
|
||||
auto play(sonus::prepared_sequence sequence, std::uint32_t sample_rate) -> std::expected<sonus::playback_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:
|
||||
std::mutex mutex_{};
|
||||
sonus::detail::render_snapshot<sonus::detail::sequence_state> snapshots_{};
|
||||
};
|
||||
|
||||
}
|
||||
@@ -25,6 +25,7 @@
|
||||
#include <sonus/detail/errors.hpp>
|
||||
#include "detail/render_snapshot.hpp"
|
||||
#include "detail/playback_devices.hpp"
|
||||
#include "detail/sequence_player.hpp"
|
||||
|
||||
|
||||
namespace {
|
||||
@@ -167,6 +168,8 @@ struct sonus::engine::impl {
|
||||
mutable std::mutex render_mutex{};
|
||||
std::unordered_map<std::uint64_t, std::unique_ptr<active_sound>> sounds{};
|
||||
std::uint64_t next_sound_handle{1};
|
||||
sonus::detail::sequence_player sequences{};
|
||||
std::atomic<std::float32_t> sequence_volume{0.70F};
|
||||
sonus::detail::render_snapshot<metronome_render_config> metronome_configs{};
|
||||
std::atomic_bool metronome_enabled{false};
|
||||
std::uint32_t next_metronome_generation{1};
|
||||
@@ -184,6 +187,7 @@ struct sonus::engine::impl {
|
||||
}
|
||||
auto initialize(sonus::audio_config value) -> std::expected<void, modulus::error> {
|
||||
config = std::move(value);
|
||||
sequence_volume.store(config.master_volume, std::memory_order_relaxed);
|
||||
publish_metronome_locked(make_disabled_metronome_config_locked());
|
||||
auto engine_config = ma_engine_config_init();
|
||||
engine_config.noDevice = MA_TRUE;
|
||||
@@ -268,6 +272,7 @@ struct sonus::engine::impl {
|
||||
device_initialized = false;
|
||||
}
|
||||
sounds.clear();
|
||||
sequences.stop();
|
||||
if (mixer_initialized) {
|
||||
ma_engine_uninit(&mixer);
|
||||
mixer_initialized = false;
|
||||
@@ -315,6 +320,7 @@ struct sonus::engine::impl {
|
||||
return sonus::detail::fail_void(miniaudio_error(result, "failed to set engine volume"));
|
||||
}
|
||||
config.master_volume = volume;
|
||||
sequence_volume.store(volume, std::memory_order_relaxed);
|
||||
return {};
|
||||
}
|
||||
auto play_file(std::filesystem::path const &path, sonus::playback_options options) -> std::expected<std::uint64_t, modulus::error> {
|
||||
@@ -374,6 +380,7 @@ struct sonus::engine::impl {
|
||||
}
|
||||
auto stop_all_sounds() -> void {
|
||||
auto lock = std::scoped_lock{mutex};
|
||||
sequences.stop();
|
||||
for (auto &entry : sounds) {
|
||||
static_cast<void>(ma_sound_stop(&entry.second->sound));
|
||||
}
|
||||
@@ -396,6 +403,7 @@ struct sonus::engine::impl {
|
||||
}
|
||||
auto collect_finished_sounds() -> std::uint64_t {
|
||||
auto lock = std::scoped_lock{mutex};
|
||||
sequences.collect();
|
||||
auto const collected = collect_finished_locked();
|
||||
collect_retired_metronome_configs_locked();
|
||||
return collected;
|
||||
@@ -512,10 +520,12 @@ struct sonus::engine::impl {
|
||||
std::fill(output, output + sample_count, 0.0F);
|
||||
auto result = ma_engine_read_pcm_frames(&mixer, output, frame_count, nullptr);
|
||||
if (result != MA_SUCCESS) {
|
||||
sequences.fail();
|
||||
std::fill(output, output + sample_count, 0.0F);
|
||||
return result;
|
||||
}
|
||||
mix_metronome(output, frame_count, guard.get());
|
||||
sequences.render(output, frame_count, config.channels, sequence_volume.load(std::memory_order_relaxed));
|
||||
return MA_SUCCESS;
|
||||
}
|
||||
auto mix_metronome(std::float32_t *output, std::uint64_t frame_count, metronome_render_config const *metro) -> void {
|
||||
@@ -614,6 +624,54 @@ auto sonus::validate(sonus::audio_config const &config) -> std::expected<void, m
|
||||
return {};
|
||||
}
|
||||
|
||||
auto sonus::load_sample(std::filesystem::path const &path, std::uint32_t sample_rate) -> std::expected<sonus::sample_buffer, modulus::error> {
|
||||
if (path.empty() || sample_rate < 8000U || sample_rate > 192000U) {
|
||||
return sonus::detail::fail<sonus::sample_buffer>(invalid("invalid sample path or sample rate"));
|
||||
}
|
||||
auto decoder = ma_decoder{};
|
||||
auto config = ma_decoder_config_init(ma_format_f32, 1U, sample_rate);
|
||||
auto status = ma_decoder_init_file(path.string().c_str(), &config, &decoder);
|
||||
if (status != MA_SUCCESS) {
|
||||
return sonus::detail::fail<sonus::sample_buffer>(miniaudio_error(status, "failed to decode sample"));
|
||||
}
|
||||
auto guard = std::unique_ptr<ma_decoder, decltype(&ma_decoder_uninit)>{&decoder, &ma_decoder_uninit};
|
||||
auto frames = ma_uint64{};
|
||||
status = ma_decoder_get_length_in_pcm_frames(&decoder, &frames);
|
||||
if (status != MA_SUCCESS || frames == 0U || frames > 16U * 1024U * 1024U) {
|
||||
return sonus::detail::fail<sonus::sample_buffer>(invalid("sample length is unavailable or exceeds 64 MiB"));
|
||||
}
|
||||
auto result = sonus::sample_buffer{.sample_rate = sample_rate, .frames = std::vector<std::float32_t>(frames), .loop_start = 0U, .loop_end = frames};
|
||||
auto read = ma_uint64{};
|
||||
status = ma_decoder_read_pcm_frames(&decoder, result.frames.data(), frames, &read);
|
||||
if ((status != MA_SUCCESS && status != MA_AT_END) || read != frames) {
|
||||
return sonus::detail::fail<sonus::sample_buffer>(invalid("sample audio is truncated"));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
auto sonus::engine::play_sequence(sonus::sample_sequence sequence) -> std::expected<sonus::playback_session, modulus::error> {
|
||||
auto prepared = sonus::prepared_sequence::create(std::move(sequence), sample_rate());
|
||||
if (!prepared) {
|
||||
return std::unexpected(prepared.error());
|
||||
}
|
||||
return play_sequence(std::move(*prepared));
|
||||
}
|
||||
|
||||
auto sonus::engine::play_sequence(sonus::prepared_sequence sequence) -> std::expected<sonus::playback_session, modulus::error> {
|
||||
if (!implementation_) {
|
||||
return sonus::detail::fail<sonus::playback_session>(unavailable("engine is not initialized"));
|
||||
}
|
||||
auto lock = std::scoped_lock{implementation_->mutex};
|
||||
return implementation_->sequences.play(std::move(sequence), implementation_->config.sample_rate);
|
||||
}
|
||||
|
||||
auto sonus::engine::stop_sequence() -> void {
|
||||
if (implementation_) {
|
||||
auto lock = std::scoped_lock{implementation_->mutex};
|
||||
implementation_->sequences.stop();
|
||||
}
|
||||
}
|
||||
|
||||
auto sonus::enumerate_playback_devices() -> std::expected<std::vector<sonus::device_info>, modulus::error> {
|
||||
auto context = temporary_context{};
|
||||
auto context_config = ma_context_config_init();
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
#include <sonus/sequence.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <utility>
|
||||
|
||||
#include <sonus/detail/errors.hpp>
|
||||
#include "detail/sequence_player.hpp"
|
||||
|
||||
|
||||
sonus::playback_session::playback_session(std::shared_ptr<std::atomic<sonus::playback_status>> status)
|
||||
: status_{std::move(status)} {}
|
||||
|
||||
auto sonus::playback_session::status() const -> sonus::playback_status {
|
||||
return status_ ? status_->load(std::memory_order_acquire) : sonus::playback_status::STOPPED;
|
||||
}
|
||||
|
||||
auto sonus::playback_session::stop() const -> void {
|
||||
if (status_) {
|
||||
auto expected = sonus::playback_status::PLAYING;
|
||||
status_->compare_exchange_strong(expected, sonus::playback_status::STOPPED, std::memory_order_acq_rel);
|
||||
}
|
||||
}
|
||||
|
||||
sonus::prepared_sequence::prepared_sequence(std::unique_ptr<sonus::detail::sequence_state> state)
|
||||
: state_{std::move(state)} {}
|
||||
|
||||
sonus::prepared_sequence::prepared_sequence(prepared_sequence &&) noexcept = default;
|
||||
|
||||
auto sonus::prepared_sequence::operator=(prepared_sequence &&) noexcept -> prepared_sequence & = default;
|
||||
|
||||
sonus::prepared_sequence::~prepared_sequence() = default;
|
||||
|
||||
auto sonus::prepared_sequence::create(sonus::sample_sequence sequence, std::uint32_t sample_rate) -> std::expected<sonus::prepared_sequence, modulus::error> {
|
||||
auto fail = [](std::string message) -> std::expected<sonus::prepared_sequence, modulus::error> {
|
||||
return sonus::detail::fail<sonus::prepared_sequence>(sonus::detail::error_invalid_argument, std::move(message));
|
||||
};
|
||||
if (sample_rate < 8000U || sample_rate > 384000U || sequence.max_voices == 0U || sequence.max_voices > 256U || sequence.events.size() > 1000000U || sequence.samples.size() > 4096U || sequence.duration_frames == 0U || sequence.duration_frames > static_cast<std::uint64_t>(sample_rate) * 86400U || !std::isfinite(sequence.gain) || sequence.gain < 0.0F || sequence.gain > 1.0F) {
|
||||
return fail("invalid sequence limits or gain");
|
||||
}
|
||||
if (sequence.release_envelope.empty() || sequence.release_envelope.size() > sample_rate / 2U || sequence.release_envelope.front() != 1.0F || sequence.release_envelope.back() != 0.0F || std::any_of(sequence.release_envelope.begin(), sequence.release_envelope.end(), [](auto value) { return !std::isfinite(value) || value < 0.0F || value > 1.0F; })) {
|
||||
return fail("release envelope must run from one to zero within 500 milliseconds");
|
||||
}
|
||||
auto sample_count = std::uint64_t{};
|
||||
for (auto const &sample : sequence.samples) {
|
||||
sample_count += sample.frames.size();
|
||||
if (sample_count > 16U * 1024U * 1024U || sample.sample_rate != sample_rate || sample.loop_start >= sample.loop_end || sample.loop_end > sample.frames.size() || std::any_of(sample.frames.begin(), sample.frames.end(), [](auto value) { return !std::isfinite(value) || std::abs(value) > 1.0F; })) {
|
||||
return fail("invalid sample rate, loop, PCM data or sample memory limit");
|
||||
}
|
||||
}
|
||||
auto endpoints = std::vector<std::pair<std::uint64_t, std::int32_t>>{};
|
||||
endpoints.reserve(sequence.events.size() * 2U);
|
||||
auto end = sequence.duration_frames;
|
||||
auto previous_start = std::uint64_t{};
|
||||
for (auto const &event : sequence.events) {
|
||||
if (event.sample >= sequence.samples.size() || event.duration_frames == 0U || event.start_frame < previous_start || event.start_frame >= sequence.duration_frames || event.duration_frames > sequence.duration_frames - event.start_frame || !std::isfinite(event.gain) || event.gain < 0.0F || event.gain > 1.0F) {
|
||||
return fail("invalid or unsorted sample event");
|
||||
}
|
||||
auto release_end = event.start_frame + event.duration_frames + sequence.release_envelope.size();
|
||||
endpoints.emplace_back(event.start_frame, 1);
|
||||
endpoints.emplace_back(release_end, -1);
|
||||
end = std::max(end, release_end);
|
||||
previous_start = event.start_frame;
|
||||
}
|
||||
std::sort(endpoints.begin(), endpoints.end());
|
||||
auto voices = std::int32_t{};
|
||||
auto peak = std::int32_t{};
|
||||
for (auto const &[frame, change] : endpoints) {
|
||||
voices += change;
|
||||
peak = std::max(peak, voices);
|
||||
if (voices > static_cast<std::int32_t>(sequence.max_voices)) {
|
||||
return fail("sequence exceeds simultaneous voice capacity including releases");
|
||||
}
|
||||
}
|
||||
auto next = std::make_unique<sonus::detail::sequence_state>();
|
||||
next->voices.resize(static_cast<std::size_t>(peak));
|
||||
next->sequence = std::move(sequence);
|
||||
next->status = std::make_shared<std::atomic<sonus::playback_status>>(sonus::playback_status::PLAYING);
|
||||
next->end_frame = end;
|
||||
next->sample_rate = sample_rate;
|
||||
return sonus::prepared_sequence{std::move(next)};
|
||||
}
|
||||
|
||||
auto sonus::detail::sequence_player::play(sonus::prepared_sequence sequence, std::uint32_t sample_rate) -> std::expected<sonus::playback_session, modulus::error> {
|
||||
if (!sequence.state_ || sequence.state_->sample_rate != sample_rate) {
|
||||
return sonus::detail::fail<sonus::playback_session>(sonus::detail::error_invalid_argument, "empty sequence or incompatible sample rate");
|
||||
}
|
||||
auto next = std::move(sequence.state_);
|
||||
auto result = sonus::playback_session{next->status};
|
||||
auto lock = std::scoped_lock{mutex_};
|
||||
if (auto current = snapshots_.current()) {
|
||||
sonus::playback_session{current->status}.stop();
|
||||
}
|
||||
snapshots_.publish(std::move(next));
|
||||
return result;
|
||||
}
|
||||
|
||||
auto sonus::detail::sequence_player::stop() -> void {
|
||||
auto lock = std::scoped_lock{mutex_};
|
||||
if (auto current = snapshots_.current()) {
|
||||
sonus::playback_session{current->status}.stop();
|
||||
}
|
||||
snapshots_.publish(nullptr);
|
||||
}
|
||||
|
||||
auto sonus::detail::sequence_player::collect() -> void {
|
||||
auto lock = std::scoped_lock{mutex_};
|
||||
if (auto current = snapshots_.current(); current && current->status->load(std::memory_order_acquire) != sonus::playback_status::PLAYING) {
|
||||
snapshots_.publish(nullptr);
|
||||
}
|
||||
snapshots_.collect();
|
||||
}
|
||||
|
||||
auto sonus::detail::sequence_player::fail() -> void {
|
||||
auto guard = snapshots_.read();
|
||||
if (auto current = guard.get()) {
|
||||
auto expected = sonus::playback_status::PLAYING;
|
||||
current->status->compare_exchange_strong(expected, sonus::playback_status::FAILED, std::memory_order_release);
|
||||
}
|
||||
}
|
||||
|
||||
auto sonus::detail::sequence_player::render(std::float32_t *output, std::uint64_t frames, std::uint32_t channels, std::float32_t gain) -> void {
|
||||
auto guard = snapshots_.read();
|
||||
auto current = guard.get();
|
||||
if (!current || current->status->load(std::memory_order_acquire) != sonus::playback_status::PLAYING) {
|
||||
return;
|
||||
}
|
||||
auto const &sequence = current->sequence;
|
||||
for (auto frame = std::uint64_t{}; frame < frames && current->frame < current->end_frame; ++frame, ++current->frame) {
|
||||
for (auto &voice : current->voices) {
|
||||
if (voice.active) {
|
||||
auto const &event = sequence.events[voice.event];
|
||||
if (current->frame >= event.start_frame + event.duration_frames + sequence.release_envelope.size()) {
|
||||
voice.active = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
while (current->next_event < sequence.events.size() && sequence.events[current->next_event].start_frame == current->frame) {
|
||||
auto slot = std::find_if(current->voices.begin(), current->voices.end(), [](auto const &voice) { return !voice.active; });
|
||||
if (slot == current->voices.end()) {
|
||||
auto expected = sonus::playback_status::PLAYING;
|
||||
current->status->compare_exchange_strong(expected, sonus::playback_status::FAILED, std::memory_order_release);
|
||||
return;
|
||||
}
|
||||
*slot = {.event = current->next_event++, .active = true};
|
||||
}
|
||||
auto mixed = std::float32_t{};
|
||||
for (auto const &voice : current->voices) {
|
||||
if (!voice.active) {
|
||||
continue;
|
||||
}
|
||||
auto const &event = sequence.events[voice.event];
|
||||
auto const &sample = sequence.samples[event.sample];
|
||||
auto age = current->frame - event.start_frame;
|
||||
auto index = age < sample.loop_end ? age : sample.loop_start + (age - sample.loop_start) % (sample.loop_end - sample.loop_start);
|
||||
auto release = age < event.duration_frames ? 1.0F : sequence.release_envelope[age - event.duration_frames];
|
||||
mixed += sample.frames[index] * event.gain * release;
|
||||
}
|
||||
for (auto channel = std::uint32_t{}; channel < channels; ++channel) {
|
||||
auto &destination = output[frame * channels + channel];
|
||||
destination = std::clamp(destination + mixed * sequence.gain * gain, -1.0F, 1.0F);
|
||||
}
|
||||
}
|
||||
if (current->frame == current->end_frame) {
|
||||
auto expected = sonus::playback_status::PLAYING;
|
||||
current->status->compare_exchange_strong(expected, sonus::playback_status::COMPLETED, std::memory_order_release);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user