Init
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
#include <sonus/detail/errors.hpp>
|
||||
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include <modulus.hpp>
|
||||
|
||||
auto sonus::detail::make_error(std::int32_t code, std::string message, modulus::source_location location) -> modulus::error {
|
||||
return modulus::make_error("sonus", code, std::move(message), location);
|
||||
}
|
||||
|
||||
auto sonus::detail::fail_void(modulus::error error) -> std::expected<void, modulus::error> {
|
||||
return std::unexpected(std::move(error));
|
||||
}
|
||||
|
||||
auto sonus::detail::fail_void(std::int32_t code, std::string message, modulus::source_location location) -> std::expected<void, modulus::error> {
|
||||
return sonus::detail::fail_void(sonus::detail::make_error(code, std::move(message), location));
|
||||
}
|
||||
|
||||
auto sonus::detail::fail_void(std::expected<void, modulus::error> const& result) -> std::expected<void, modulus::error> {
|
||||
assert(!result);
|
||||
return sonus::detail::fail_void(result.error());
|
||||
}
|
||||
+549
@@ -0,0 +1,549 @@
|
||||
#include <sonus/engine.hpp>
|
||||
|
||||
#define MINIAUDIO_IMPLEMENTATION
|
||||
#include <miniaudio.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <expected>
|
||||
#include <filesystem>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
|
||||
#include <modulus.hpp>
|
||||
|
||||
#include <sonus/detail/errors.hpp>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto pi = 3.141592653589793238462643383279502884;
|
||||
|
||||
auto invalid(std::string message) -> modulus::error {
|
||||
return sonus::detail::make_error(sonus::detail::error_invalid_argument, std::move(message));
|
||||
}
|
||||
|
||||
auto unavailable(std::string message) -> modulus::error {
|
||||
return sonus::detail::make_error(sonus::detail::error_unavailable, std::move(message));
|
||||
}
|
||||
|
||||
auto miniaudio_error(ma_result result, std::string action) -> modulus::error {
|
||||
auto const* description = ma_result_description(result);
|
||||
auto message = std::move(action);
|
||||
if (description != nullptr && std::string_view{description} != "No error") {
|
||||
message += ": ";
|
||||
message += description;
|
||||
}
|
||||
return unavailable(std::move(message));
|
||||
}
|
||||
|
||||
auto valid_unit_interval(std::float32_t value) -> std::bool_t {
|
||||
return std::isfinite(value) && value >= 0.0F && value <= 1.0F;
|
||||
}
|
||||
|
||||
auto valid_frequency(std::float32_t value) -> std::bool_t {
|
||||
return std::isfinite(value) && value >= 20.0F && value <= 20000.0F;
|
||||
}
|
||||
|
||||
struct active_sound {
|
||||
ma_sound sound{};
|
||||
std::bool_t initialized{false};
|
||||
|
||||
active_sound() = default;
|
||||
active_sound(active_sound const&) = delete;
|
||||
auto operator=(active_sound const&) -> active_sound& = delete;
|
||||
active_sound(active_sound&&) = delete;
|
||||
auto operator=(active_sound&&) -> active_sound& = delete;
|
||||
|
||||
~active_sound() {
|
||||
if (initialized) {
|
||||
ma_sound_uninit(&sound);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
struct sonus::engine::impl {
|
||||
sonus::audio_config config{};
|
||||
ma_engine mixer{};
|
||||
ma_device device{};
|
||||
std::bool_t mixer_initialized{false};
|
||||
std::bool_t device_initialized{false};
|
||||
std::bool_t started{false};
|
||||
mutable std::mutex mutex{};
|
||||
mutable std::mutex render_mutex{};
|
||||
std::unordered_map<sonus::sound_handle, std::unique_ptr<active_sound>> sounds{};
|
||||
sonus::sound_handle next_sound_handle{1};
|
||||
|
||||
std::atomic_bool metronome_enabled{false};
|
||||
std::atomic<std::uint32_t> metronome_generation{0};
|
||||
std::atomic<std::uint32_t> metronome_bpm{120};
|
||||
std::atomic<std::uint32_t> metronome_beats_per_bar{4};
|
||||
std::atomic<std::uint32_t> metronome_click_frames{1680};
|
||||
std::atomic<std::float32_t> metronome_volume{0.45F};
|
||||
std::atomic<std::float32_t> metronome_beat_frequency{1320.0F};
|
||||
std::atomic<std::float32_t> metronome_downbeat_frequency{1760.0F};
|
||||
|
||||
std::uint32_t observed_metronome_generation{0};
|
||||
std::uint32_t beat_index{0};
|
||||
std::uint64_t frames_until_next_beat{0};
|
||||
std::uint32_t click_frame_index{0};
|
||||
std::uint32_t click_frames_remaining{0};
|
||||
std::float32_t click_frequency{1320.0F};
|
||||
std::float32_t click_gain{0.0F};
|
||||
|
||||
impl() = default;
|
||||
impl(impl const&) = delete;
|
||||
auto operator=(impl const&) -> impl& = delete;
|
||||
|
||||
~impl() {
|
||||
shutdown();
|
||||
}
|
||||
|
||||
auto initialize(sonus::audio_config value) -> std::expected<void, modulus::error> {
|
||||
config = value;
|
||||
|
||||
auto engine_config = ma_engine_config_init();
|
||||
engine_config.noDevice = MA_TRUE;
|
||||
engine_config.channels = config.channels;
|
||||
engine_config.sampleRate = config.sample_rate;
|
||||
engine_config.noAutoStart = MA_TRUE;
|
||||
|
||||
auto result = ma_engine_init(&engine_config, &mixer);
|
||||
if (result != MA_SUCCESS) {
|
||||
return sonus::detail::fail_void(miniaudio_error(result, "failed to initialize miniaudio engine"));
|
||||
}
|
||||
mixer_initialized = true;
|
||||
|
||||
result = ma_engine_set_volume(&mixer, config.master_volume);
|
||||
if (result != MA_SUCCESS) {
|
||||
shutdown();
|
||||
return sonus::detail::fail_void(miniaudio_error(result, "failed to set engine volume"));
|
||||
}
|
||||
|
||||
if (!config.no_device) {
|
||||
auto device_config = ma_device_config_init(ma_device_type_playback);
|
||||
device_config.playback.format = ma_format_f32;
|
||||
device_config.playback.channels = config.channels;
|
||||
device_config.sampleRate = config.sample_rate;
|
||||
device_config.dataCallback = sonus::engine::impl::device_callback;
|
||||
device_config.pUserData = this;
|
||||
|
||||
result = ma_device_init(nullptr, &device_config, &device);
|
||||
if (result != MA_SUCCESS) {
|
||||
shutdown();
|
||||
return sonus::detail::fail_void(miniaudio_error(result, "failed to initialize playback device"));
|
||||
}
|
||||
device_initialized = true;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
auto start() -> std::expected<void, modulus::error> {
|
||||
auto lock = std::scoped_lock{mutex};
|
||||
if (started) {
|
||||
return {};
|
||||
}
|
||||
if (config.no_device) {
|
||||
started = true;
|
||||
return {};
|
||||
}
|
||||
if (!device_initialized) {
|
||||
return sonus::detail::fail_void(sonus::detail::error_unavailable, "playback device is not initialized");
|
||||
}
|
||||
auto result = ma_device_start(&device);
|
||||
if (result != MA_SUCCESS) {
|
||||
return sonus::detail::fail_void(miniaudio_error(result, "failed to start playback device"));
|
||||
}
|
||||
started = true;
|
||||
return {};
|
||||
}
|
||||
|
||||
auto stop_device() -> void {
|
||||
auto lock = std::scoped_lock{mutex};
|
||||
if (!started) {
|
||||
return;
|
||||
}
|
||||
if (device_initialized) {
|
||||
static_cast<void>(ma_device_stop(&device));
|
||||
}
|
||||
started = false;
|
||||
}
|
||||
|
||||
auto shutdown() -> void {
|
||||
{
|
||||
auto lock = std::scoped_lock{mutex};
|
||||
if (device_initialized) {
|
||||
if (started) {
|
||||
static_cast<void>(ma_device_stop(&device));
|
||||
started = false;
|
||||
}
|
||||
ma_device_uninit(&device);
|
||||
device_initialized = false;
|
||||
}
|
||||
sounds.clear();
|
||||
if (mixer_initialized) {
|
||||
ma_engine_uninit(&mixer);
|
||||
mixer_initialized = false;
|
||||
}
|
||||
metronome_enabled.store(false, std::memory_order_release);
|
||||
}
|
||||
}
|
||||
|
||||
auto set_master_volume(std::float32_t volume) -> std::expected<void, modulus::error> {
|
||||
auto valid = sonus::validate(sonus::audio_config{
|
||||
.sample_rate = config.sample_rate,
|
||||
.channels = config.channels,
|
||||
.master_volume = volume,
|
||||
.no_device = config.no_device,
|
||||
});
|
||||
if (!valid) {
|
||||
return valid;
|
||||
}
|
||||
|
||||
auto lock = std::scoped_lock{mutex};
|
||||
auto result = ma_engine_set_volume(&mixer, volume);
|
||||
if (result != MA_SUCCESS) {
|
||||
return sonus::detail::fail_void(miniaudio_error(result, "failed to set engine volume"));
|
||||
}
|
||||
config.master_volume = volume;
|
||||
return {};
|
||||
}
|
||||
|
||||
auto play_file(std::filesystem::path const& path, sonus::playback_options options) -> std::expected<sonus::sound_handle, modulus::error> {
|
||||
if (path.empty()) {
|
||||
return sonus::detail::fail<sonus::sound_handle>(sonus::detail::error_invalid_argument, "sound path must not be empty");
|
||||
}
|
||||
auto valid_options = sonus::validate(options);
|
||||
if (!valid_options) {
|
||||
return sonus::detail::fail<sonus::sound_handle>(valid_options.error());
|
||||
}
|
||||
auto file_error = std::error_code{};
|
||||
if (!std::filesystem::is_regular_file(path, file_error)) {
|
||||
return sonus::detail::fail<sonus::sound_handle>(sonus::detail::make_error(sonus::detail::error_not_found, "sound file not found: " + path.string()));
|
||||
}
|
||||
|
||||
auto sound = std::make_unique<active_sound>();
|
||||
auto const path_text = path.string();
|
||||
auto flags = static_cast<ma_uint32>(MA_SOUND_FLAG_NO_SPATIALIZATION);
|
||||
flags |= options.stream ? MA_SOUND_FLAG_STREAM : MA_SOUND_FLAG_DECODE;
|
||||
|
||||
auto lock = std::scoped_lock{mutex};
|
||||
auto result = ma_sound_init_from_file(&mixer, path_text.c_str(), flags, nullptr, nullptr, &sound->sound);
|
||||
if (result != MA_SUCCESS) {
|
||||
return sonus::detail::fail<sonus::sound_handle>(miniaudio_error(result, "failed to load sound file"));
|
||||
}
|
||||
sound->initialized = true;
|
||||
ma_sound_set_volume(&sound->sound, options.volume);
|
||||
ma_sound_set_looping(&sound->sound, options.loop ? MA_TRUE : MA_FALSE);
|
||||
result = ma_sound_start(&sound->sound);
|
||||
if (result != MA_SUCCESS) {
|
||||
return sonus::detail::fail<sonus::sound_handle>(miniaudio_error(result, "failed to start sound file"));
|
||||
}
|
||||
|
||||
auto handle = next_sound_handle++;
|
||||
if (next_sound_handle == 0) {
|
||||
next_sound_handle = 1;
|
||||
}
|
||||
sounds.emplace(handle, std::move(sound));
|
||||
return handle;
|
||||
}
|
||||
|
||||
auto stop_sound(sonus::sound_handle handle) -> std::expected<void, modulus::error> {
|
||||
if (handle == 0) {
|
||||
return sonus::detail::fail_void(sonus::detail::error_invalid_argument, "sound handle must not be zero");
|
||||
}
|
||||
auto lock = std::scoped_lock{mutex};
|
||||
auto found = sounds.find(handle);
|
||||
if (found == sounds.end()) {
|
||||
return sonus::detail::fail_void(sonus::detail::error_not_found, "sound handle not found");
|
||||
}
|
||||
static_cast<void>(ma_sound_stop(&found->second->sound));
|
||||
sounds.erase(found);
|
||||
return {};
|
||||
}
|
||||
|
||||
auto stop_all_sounds() -> void {
|
||||
auto lock = std::scoped_lock{mutex};
|
||||
for (auto& entry : sounds) {
|
||||
static_cast<void>(ma_sound_stop(&entry.second->sound));
|
||||
}
|
||||
sounds.clear();
|
||||
}
|
||||
|
||||
auto store_metronome(sonus::metronome_config value, std::bool_t enable) -> std::expected<void, modulus::error> {
|
||||
auto valid = sonus::validate(value);
|
||||
if (!valid) {
|
||||
return valid;
|
||||
}
|
||||
|
||||
auto click_frames = static_cast<std::uint64_t>(config.sample_rate) * static_cast<std::uint64_t>(value.click_duration.count()) / 1000U;
|
||||
click_frames = std::clamp<std::uint64_t>(click_frames, 1U, std::numeric_limits<std::uint32_t>::max());
|
||||
|
||||
metronome_bpm.store(value.bpm, std::memory_order_release);
|
||||
metronome_beats_per_bar.store(value.beats_per_bar, std::memory_order_release);
|
||||
metronome_click_frames.store(static_cast<std::uint32_t>(click_frames), std::memory_order_release);
|
||||
metronome_volume.store(value.volume, std::memory_order_release);
|
||||
metronome_beat_frequency.store(value.beat_frequency_hz, std::memory_order_release);
|
||||
metronome_downbeat_frequency.store(value.downbeat_frequency_hz, std::memory_order_release);
|
||||
if (enable) {
|
||||
metronome_enabled.store(true, std::memory_order_release);
|
||||
}
|
||||
metronome_generation.fetch_add(1, std::memory_order_acq_rel);
|
||||
return {};
|
||||
}
|
||||
|
||||
auto render_to(std::float32_t* output, std::uint64_t frame_count) -> ma_result {
|
||||
if (output == nullptr || frame_count == 0U) {
|
||||
return MA_SUCCESS;
|
||||
}
|
||||
auto const sample_count = frame_count * config.channels;
|
||||
std::fill(output, output + sample_count, 0.0F);
|
||||
auto result = ma_engine_read_pcm_frames(&mixer, output, frame_count, nullptr);
|
||||
if (result != MA_SUCCESS) {
|
||||
std::fill(output, output + sample_count, 0.0F);
|
||||
return result;
|
||||
}
|
||||
mix_metronome(output, frame_count);
|
||||
return MA_SUCCESS;
|
||||
}
|
||||
|
||||
auto mix_metronome(std::float32_t* output, std::uint64_t frame_count) -> void {
|
||||
auto const generation = metronome_generation.load(std::memory_order_acquire);
|
||||
auto const enabled = metronome_enabled.load(std::memory_order_acquire);
|
||||
if (!enabled) {
|
||||
observed_metronome_generation = generation;
|
||||
beat_index = 0;
|
||||
frames_until_next_beat = 0;
|
||||
click_frame_index = 0;
|
||||
click_frames_remaining = 0;
|
||||
return;
|
||||
}
|
||||
if (observed_metronome_generation != generation) {
|
||||
observed_metronome_generation = generation;
|
||||
beat_index = 0;
|
||||
frames_until_next_beat = 0;
|
||||
click_frame_index = 0;
|
||||
click_frames_remaining = 0;
|
||||
}
|
||||
|
||||
auto const bpm = std::max<std::uint32_t>(1U, metronome_bpm.load(std::memory_order_acquire));
|
||||
auto const beats_per_bar = std::max<std::uint32_t>(1U, metronome_beats_per_bar.load(std::memory_order_acquire));
|
||||
auto const click_frames = std::max<std::uint32_t>(1U, metronome_click_frames.load(std::memory_order_acquire));
|
||||
auto const volume = metronome_volume.load(std::memory_order_acquire);
|
||||
auto const beat_frequency = metronome_beat_frequency.load(std::memory_order_acquire);
|
||||
auto const downbeat_frequency = metronome_downbeat_frequency.load(std::memory_order_acquire);
|
||||
auto const frames_per_beat = std::max<std::uint64_t>(1U, static_cast<std::uint64_t>(std::llround((static_cast<double>(config.sample_rate) * 60.0) / static_cast<double>(bpm))));
|
||||
|
||||
for (auto frame = std::uint64_t{}; frame < frame_count; ++frame) {
|
||||
if (frames_until_next_beat == 0U) {
|
||||
auto const downbeat = beat_index == 0U;
|
||||
click_frame_index = 0;
|
||||
click_frames_remaining = click_frames;
|
||||
click_frequency = downbeat ? downbeat_frequency : beat_frequency;
|
||||
click_gain = volume * (downbeat ? 1.0F : 0.64F);
|
||||
beat_index = (beat_index + 1U) % beats_per_bar;
|
||||
frames_until_next_beat = frames_per_beat;
|
||||
}
|
||||
|
||||
auto sample = 0.0F;
|
||||
if (click_frames_remaining > 0U) {
|
||||
auto const t = static_cast<double>(click_frame_index) / static_cast<double>(config.sample_rate);
|
||||
auto const progress = static_cast<double>(click_frame_index) / static_cast<double>(click_frames);
|
||||
auto const envelope = (1.0 - progress) * (1.0 - progress);
|
||||
sample = static_cast<std::float32_t>(std::sin(2.0 * pi * static_cast<double>(click_frequency) * t) * envelope * static_cast<double>(click_gain));
|
||||
++click_frame_index;
|
||||
--click_frames_remaining;
|
||||
}
|
||||
|
||||
if (sample != 0.0F) {
|
||||
auto* frame_out = output + frame * config.channels;
|
||||
for (auto channel = std::uint32_t{}; channel < config.channels; ++channel) {
|
||||
frame_out[channel] = std::clamp(frame_out[channel] + sample, -1.0F, 1.0F);
|
||||
}
|
||||
}
|
||||
--frames_until_next_beat;
|
||||
}
|
||||
}
|
||||
|
||||
static auto device_callback(ma_device* device, void* output, void const*, ma_uint32 frame_count) -> void {
|
||||
auto* self = static_cast<sonus::engine::impl*>(device == nullptr ? nullptr : device->pUserData);
|
||||
if (self == nullptr || output == nullptr) {
|
||||
return;
|
||||
}
|
||||
auto result = self->render_to(static_cast<std::float32_t*>(output), frame_count);
|
||||
if (result != MA_SUCCESS) {
|
||||
auto const sample_count = static_cast<std::uint64_t>(frame_count) * self->config.channels;
|
||||
std::memset(output, 0, static_cast<std::size_t>(sample_count * sizeof(std::float32_t)));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
auto sonus::validate(sonus::audio_config const& config) -> std::expected<void, modulus::error> {
|
||||
if (config.sample_rate < 8000U || config.sample_rate > 384000U) {
|
||||
return sonus::detail::fail_void(invalid("audio sample_rate must be between 8000 and 384000"));
|
||||
}
|
||||
if (config.channels == 0U || config.channels > 8U) {
|
||||
return sonus::detail::fail_void(invalid("audio channels must be between 1 and 8"));
|
||||
}
|
||||
if (!valid_unit_interval(config.master_volume)) {
|
||||
return sonus::detail::fail_void(invalid("audio master_volume must be between 0 and 1"));
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
auto sonus::validate(sonus::metronome_config const& config) -> std::expected<void, modulus::error> {
|
||||
if (config.bpm == 0U || config.bpm > 400U) {
|
||||
return sonus::detail::fail_void(invalid("metronome bpm must be between 1 and 400"));
|
||||
}
|
||||
if (config.beats_per_bar == 0U || config.beats_per_bar > 16U) {
|
||||
return sonus::detail::fail_void(invalid("metronome beats_per_bar must be between 1 and 16"));
|
||||
}
|
||||
if (!valid_unit_interval(config.volume)) {
|
||||
return sonus::detail::fail_void(invalid("metronome volume must be between 0 and 1"));
|
||||
}
|
||||
if (!valid_frequency(config.beat_frequency_hz) || !valid_frequency(config.downbeat_frequency_hz)) {
|
||||
return sonus::detail::fail_void(invalid("metronome frequencies must be between 20 and 20000 Hz"));
|
||||
}
|
||||
if (config.click_duration.count() <= 0 || config.click_duration > std::chrono::milliseconds{250}) {
|
||||
return sonus::detail::fail_void(invalid("metronome click_duration must be between 1 and 250 milliseconds"));
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
auto sonus::validate(sonus::playback_options const& options) -> std::expected<void, modulus::error> {
|
||||
if (!valid_unit_interval(options.volume)) {
|
||||
return sonus::detail::fail_void(invalid("playback volume must be between 0 and 1"));
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
auto sonus::engine::create(sonus::audio_config config) -> std::expected<sonus::engine, modulus::error> {
|
||||
auto valid = sonus::validate(config);
|
||||
if (!valid) {
|
||||
return sonus::detail::fail<sonus::engine>(valid.error());
|
||||
}
|
||||
auto implementation = std::make_unique<sonus::engine::impl>();
|
||||
auto initialized = implementation->initialize(config);
|
||||
if (!initialized) {
|
||||
return sonus::detail::fail<sonus::engine>(initialized.error());
|
||||
}
|
||||
return sonus::engine{std::move(implementation)};
|
||||
}
|
||||
|
||||
sonus::engine::engine(std::unique_ptr<sonus::engine::impl> implementation) noexcept : implementation_{std::move(implementation)} {}
|
||||
|
||||
sonus::engine::engine(sonus::engine&& other) noexcept = default;
|
||||
|
||||
auto sonus::engine::operator=(sonus::engine&& other) noexcept -> sonus::engine& = default;
|
||||
|
||||
sonus::engine::~engine() = default;
|
||||
|
||||
auto sonus::engine::start() -> std::expected<void, modulus::error> {
|
||||
if (!implementation_) {
|
||||
return sonus::detail::fail_void(sonus::detail::error_invalid_argument, "audio engine is empty");
|
||||
}
|
||||
return implementation_->start();
|
||||
}
|
||||
|
||||
auto sonus::engine::stop() -> void {
|
||||
if (implementation_) {
|
||||
implementation_->stop_device();
|
||||
}
|
||||
}
|
||||
|
||||
auto sonus::engine::started() const -> std::bool_t {
|
||||
if (!implementation_) {
|
||||
return false;
|
||||
}
|
||||
auto lock = std::scoped_lock{implementation_->mutex};
|
||||
return implementation_->started;
|
||||
}
|
||||
|
||||
auto sonus::engine::sample_rate() const -> std::uint32_t {
|
||||
return implementation_ ? implementation_->config.sample_rate : 0U;
|
||||
}
|
||||
|
||||
auto sonus::engine::channels() const -> std::uint32_t {
|
||||
return implementation_ ? implementation_->config.channels : 0U;
|
||||
}
|
||||
|
||||
auto sonus::engine::set_master_volume(std::float32_t volume) -> std::expected<void, modulus::error> {
|
||||
if (!implementation_) {
|
||||
return sonus::detail::fail_void(sonus::detail::error_invalid_argument, "audio engine is empty");
|
||||
}
|
||||
return implementation_->set_master_volume(volume);
|
||||
}
|
||||
|
||||
auto sonus::engine::play_file(std::filesystem::path const& path, sonus::playback_options options) -> std::expected<sonus::sound_handle, modulus::error> {
|
||||
if (!implementation_) {
|
||||
return sonus::detail::fail<sonus::sound_handle>(sonus::detail::error_invalid_argument, "audio engine is empty");
|
||||
}
|
||||
return implementation_->play_file(path, options);
|
||||
}
|
||||
|
||||
auto sonus::engine::stop(sonus::sound_handle handle) -> std::expected<void, modulus::error> {
|
||||
if (!implementation_) {
|
||||
return sonus::detail::fail_void(sonus::detail::error_invalid_argument, "audio engine is empty");
|
||||
}
|
||||
return implementation_->stop_sound(handle);
|
||||
}
|
||||
|
||||
auto sonus::engine::stop_all() -> void {
|
||||
if (implementation_) {
|
||||
implementation_->stop_all_sounds();
|
||||
}
|
||||
}
|
||||
|
||||
auto sonus::engine::start_metronome(sonus::metronome_config config) -> std::expected<void, modulus::error> {
|
||||
if (!implementation_) {
|
||||
return sonus::detail::fail_void(sonus::detail::error_invalid_argument, "audio engine is empty");
|
||||
}
|
||||
return implementation_->store_metronome(config, true);
|
||||
}
|
||||
|
||||
auto sonus::engine::update_metronome(sonus::metronome_config config) -> std::expected<void, modulus::error> {
|
||||
if (!implementation_) {
|
||||
return sonus::detail::fail_void(sonus::detail::error_invalid_argument, "audio engine is empty");
|
||||
}
|
||||
return implementation_->store_metronome(config, false);
|
||||
}
|
||||
|
||||
auto sonus::engine::stop_metronome() -> void {
|
||||
if (!implementation_) {
|
||||
return;
|
||||
}
|
||||
implementation_->metronome_enabled.store(false, std::memory_order_release);
|
||||
implementation_->metronome_generation.fetch_add(1, std::memory_order_acq_rel);
|
||||
}
|
||||
|
||||
auto sonus::engine::metronome_active() const -> std::bool_t {
|
||||
return implementation_ && implementation_->metronome_enabled.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
auto sonus::engine::render(std::float32_t* output, std::uint64_t frame_count) -> std::expected<void, modulus::error> {
|
||||
if (!implementation_) {
|
||||
return sonus::detail::fail_void(sonus::detail::error_invalid_argument, "audio engine is empty");
|
||||
}
|
||||
if (!implementation_->config.no_device) {
|
||||
return sonus::detail::fail_void(sonus::detail::error_invalid_argument, "manual render requires no_device audio_config");
|
||||
}
|
||||
if (output == nullptr) {
|
||||
return sonus::detail::fail_void(sonus::detail::error_invalid_argument, "manual render output must not be null");
|
||||
}
|
||||
auto lock = std::scoped_lock{implementation_->render_mutex};
|
||||
auto result = implementation_->render_to(output, frame_count);
|
||||
if (result != MA_SUCCESS) {
|
||||
return sonus::detail::fail_void(miniaudio_error(result, "failed to render audio"));
|
||||
}
|
||||
return {};
|
||||
}
|
||||
Reference in New Issue
Block a user