This commit is contained in:
khodowany
2026-09-10 01:23:17 -05:00
commit d3749f894c
16 changed files with 1481 additions and 0 deletions
+26
View File
@@ -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
View File
@@ -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 {};
}