@@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <string_view>
|
||||
|
||||
#include <sonus/config.hpp>
|
||||
|
||||
namespace sonus::detail {
|
||||
|
||||
inline auto find_playback_device(std::span<device_info const> devices, std::string_view id, std::string_view name) -> std::optional<std::size_t> {
|
||||
if (!id.empty()) {
|
||||
for (auto index = std::size_t{}; index < devices.size(); ++index) {
|
||||
if (devices[index].id == id) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!name.empty()) {
|
||||
for (auto index = std::size_t{}; index < devices.size(); ++index) {
|
||||
if (devices[index].name == name) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace sonus::detail {
|
||||
|
||||
// Publishers/collectors must be serialized; readers never lock or destroy snapshots.
|
||||
template<typename T>
|
||||
class render_snapshot {
|
||||
static_assert(std::atomic<std::uint32_t>::is_always_lock_free);
|
||||
static_assert(std::atomic<T const*>::is_always_lock_free);
|
||||
|
||||
public:
|
||||
class read_guard {
|
||||
public:
|
||||
explicit read_guard(render_snapshot& owner) : owner_(owner) {
|
||||
owner_.active_render_count_.fetch_add(1, std::memory_order_seq_cst);
|
||||
value_ = owner_.active_.load(std::memory_order_seq_cst);
|
||||
}
|
||||
read_guard(read_guard const&) = delete;
|
||||
auto operator=(read_guard const&) -> read_guard& = delete;
|
||||
~read_guard() {
|
||||
owner_.active_render_count_.fetch_sub(1, std::memory_order_seq_cst);
|
||||
}
|
||||
auto get() const -> T const* { return value_; }
|
||||
|
||||
private:
|
||||
render_snapshot& owner_;
|
||||
T const* value_{};
|
||||
};
|
||||
|
||||
auto read() -> read_guard { return read_guard{*this}; }
|
||||
auto current() const -> T const* { return current_.get(); }
|
||||
|
||||
auto publish(std::unique_ptr<T> next) -> void {
|
||||
if (current_) {
|
||||
retired_.push_back(std::move(current_));
|
||||
}
|
||||
current_ = std::move(next);
|
||||
active_.store(current_.get(), std::memory_order_seq_cst);
|
||||
collect();
|
||||
}
|
||||
|
||||
auto collect() -> void {
|
||||
// SC orders publication/count observation against reader entry/pointer load:
|
||||
// a reader either contributes to the count or sees the new snapshot.
|
||||
if (active_render_count_.load(std::memory_order_seq_cst) == 0U) {
|
||||
retired_.clear();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
std::unique_ptr<T> current_{};
|
||||
std::vector<std::unique_ptr<T>> retired_{};
|
||||
std::atomic<T const*> active_{nullptr};
|
||||
std::atomic<std::uint32_t> active_render_count_{0};
|
||||
};
|
||||
|
||||
}
|
||||
+106
-63
@@ -23,6 +23,8 @@
|
||||
#include <modulus.hpp>
|
||||
|
||||
#include <sonus/detail/errors.hpp>
|
||||
#include "detail/render_snapshot.hpp"
|
||||
#include "detail/playback_devices.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -58,6 +60,28 @@ auto contains_null(std::string_view value) -> std::bool_t {
|
||||
return value.find('\0') != std::string_view::npos;
|
||||
}
|
||||
|
||||
auto make_playback_device_id(ma_uint32 index, ma_device_info const& info) -> std::string {
|
||||
auto result = std::to_string(index);
|
||||
result += ':';
|
||||
result += (info.isDefault == MA_TRUE ? '1' : '0');
|
||||
result += ':';
|
||||
result += info.name;
|
||||
return result;
|
||||
}
|
||||
|
||||
auto describe_playback_devices(ma_device_info const* devices, ma_uint32 count) -> std::vector<sonus::device_info> {
|
||||
auto result = std::vector<sonus::device_info>{};
|
||||
result.reserve(count);
|
||||
for (auto index = ma_uint32{}; index < count; ++index) {
|
||||
result.push_back(sonus::device_info{
|
||||
.id = make_playback_device_id(index, devices[index]),
|
||||
.name = devices[index].name,
|
||||
.is_default = devices[index].isDefault == MA_TRUE,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
auto to_miniaudio_profile(sonus::performance_profile profile) -> ma_performance_profile {
|
||||
switch (profile) {
|
||||
case sonus::performance_profile::LOW_LATENCY:
|
||||
@@ -107,6 +131,21 @@ struct active_sound {
|
||||
}
|
||||
};
|
||||
|
||||
struct temporary_context {
|
||||
ma_context context{};
|
||||
std::bool_t initialized{false};
|
||||
|
||||
temporary_context() = default;
|
||||
temporary_context(temporary_context const&) = delete;
|
||||
auto operator=(temporary_context const&) -> temporary_context& = delete;
|
||||
|
||||
~temporary_context() {
|
||||
if (initialized) {
|
||||
ma_context_uninit(&context);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
struct metronome_render_config {
|
||||
std::bool_t enabled{false};
|
||||
std::uint32_t generation{0};
|
||||
@@ -116,11 +155,6 @@ struct metronome_render_config {
|
||||
std::vector<std::float32_t> downbeat_click{};
|
||||
};
|
||||
|
||||
struct retired_metronome_config {
|
||||
std::unique_ptr<metronome_render_config> config{};
|
||||
std::uint64_t retire_epoch{0};
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
struct sonus::engine::impl {
|
||||
@@ -138,11 +172,8 @@ struct sonus::engine::impl {
|
||||
std::unordered_map<sonus::sound_handle, std::unique_ptr<active_sound>> sounds{};
|
||||
sonus::sound_handle next_sound_handle{1};
|
||||
|
||||
std::unique_ptr<metronome_render_config> metronome_config{};
|
||||
std::vector<retired_metronome_config> retired_metronome_configs{};
|
||||
std::atomic<metronome_render_config const*> active_metronome_config{nullptr};
|
||||
sonus::detail::render_snapshot<metronome_render_config> metronome_configs{};
|
||||
std::atomic_bool metronome_enabled{false};
|
||||
std::atomic<std::uint64_t> render_epoch{0};
|
||||
std::uint32_t next_metronome_generation{1};
|
||||
|
||||
std::uint32_t observed_metronome_generation{0};
|
||||
@@ -242,6 +273,7 @@ struct sonus::engine::impl {
|
||||
static_cast<void>(ma_device_stop(&device));
|
||||
}
|
||||
started = false;
|
||||
collect_retired_metronome_configs_locked();
|
||||
}
|
||||
|
||||
auto shutdown() -> void {
|
||||
@@ -264,14 +296,11 @@ struct sonus::engine::impl {
|
||||
context_initialized = false;
|
||||
}
|
||||
metronome_enabled.store(false, std::memory_order_release);
|
||||
active_metronome_config.store(nullptr, std::memory_order_release);
|
||||
metronome_config.reset();
|
||||
retired_metronome_configs.clear();
|
||||
reset_metronome_render_state();
|
||||
}
|
||||
|
||||
auto configure_playback_device(ma_device_config& device_config) -> std::expected<void, modulus::error> {
|
||||
if (config.playback_device_name.empty()) {
|
||||
if (config.playback_device_id.empty() && config.playback_device_name.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -289,23 +318,21 @@ struct sonus::engine::impl {
|
||||
return sonus::detail::fail_void(miniaudio_error(result, "failed to enumerate playback devices"));
|
||||
}
|
||||
|
||||
for (auto index = ma_uint32{}; index < playback_device_count; ++index) {
|
||||
if (config.playback_device_name == playback_devices[index].name) {
|
||||
playback_device_id = playback_devices[index].id;
|
||||
device_config.playback.pDeviceID = &playback_device_id;
|
||||
return {};
|
||||
}
|
||||
auto devices = describe_playback_devices(playback_devices, playback_device_count);
|
||||
auto selected = sonus::detail::find_playback_device(devices, config.playback_device_id, config.playback_device_name);
|
||||
if (selected) {
|
||||
playback_device_id = playback_devices[*selected].id;
|
||||
device_config.playback.pDeviceID = &playback_device_id;
|
||||
return {};
|
||||
}
|
||||
|
||||
return sonus::detail::fail_void(sonus::detail::error_not_found, "playback device not found: " + config.playback_device_name);
|
||||
auto selector = config.playback_device_id.empty() ? config.playback_device_name : config.playback_device_id;
|
||||
return sonus::detail::fail_void(sonus::detail::error_not_found, "playback device not found: " + selector);
|
||||
}
|
||||
|
||||
auto set_master_volume(std::float32_t volume) -> std::expected<void, modulus::error> {
|
||||
auto next_config = config;
|
||||
next_config.master_volume = volume;
|
||||
auto valid = sonus::validate(next_config);
|
||||
if (!valid) {
|
||||
return valid;
|
||||
if (!valid_unit_interval(volume)) {
|
||||
return sonus::detail::fail_void(invalid("audio master_volume must be between 0 and 1"));
|
||||
}
|
||||
|
||||
auto lock = std::scoped_lock{mutex};
|
||||
@@ -422,19 +449,20 @@ struct sonus::engine::impl {
|
||||
return valid;
|
||||
}
|
||||
|
||||
auto next = make_metronome_config(value, enable);
|
||||
if (!next) {
|
||||
return sonus::detail::fail_void(next.error());
|
||||
}
|
||||
|
||||
auto lock = std::scoped_lock{mutex};
|
||||
publish_metronome_locked(std::move(*next));
|
||||
collect_retired_metronome_configs_locked();
|
||||
return {};
|
||||
return publish_metronome_config_locked(value, enable);
|
||||
}
|
||||
|
||||
auto update_metronome(sonus::metronome_config value) -> std::expected<void, modulus::error> {
|
||||
return store_metronome(value, metronome_enabled.load(std::memory_order_acquire));
|
||||
auto valid = sonus::validate(value);
|
||||
if (!valid) {
|
||||
return valid;
|
||||
}
|
||||
|
||||
auto lock = std::scoped_lock{mutex};
|
||||
auto const* current = metronome_configs.current();
|
||||
auto const enable = current != nullptr && current->enabled;
|
||||
return publish_metronome_config_locked(value, enable);
|
||||
}
|
||||
|
||||
auto stop_metronome() -> void {
|
||||
@@ -443,6 +471,17 @@ struct sonus::engine::impl {
|
||||
collect_retired_metronome_configs_locked();
|
||||
}
|
||||
|
||||
auto publish_metronome_config_locked(sonus::metronome_config value, std::bool_t enable) -> std::expected<void, modulus::error> {
|
||||
auto next = make_metronome_config(value, enable);
|
||||
if (!next) {
|
||||
return sonus::detail::fail_void(next.error());
|
||||
}
|
||||
|
||||
publish_metronome_locked(std::move(*next));
|
||||
collect_retired_metronome_configs_locked();
|
||||
return {};
|
||||
}
|
||||
|
||||
auto make_metronome_config(sonus::metronome_config value, std::bool_t enable) -> std::expected<std::unique_ptr<metronome_render_config>, modulus::error> {
|
||||
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());
|
||||
@@ -471,25 +510,13 @@ struct sonus::engine::impl {
|
||||
next_metronome_generation = 1U;
|
||||
}
|
||||
|
||||
auto previous = std::move(metronome_config);
|
||||
auto* next_pointer = next.get();
|
||||
metronome_config = std::move(next);
|
||||
active_metronome_config.store(next_pointer, std::memory_order_release);
|
||||
metronome_enabled.store(next_pointer->enabled, std::memory_order_release);
|
||||
if (previous) {
|
||||
retired_metronome_configs.push_back(retired_metronome_config{
|
||||
.config = std::move(previous),
|
||||
.retire_epoch = render_epoch.load(std::memory_order_acquire),
|
||||
});
|
||||
}
|
||||
auto const enabled = next->enabled;
|
||||
metronome_configs.publish(std::move(next));
|
||||
metronome_enabled.store(enabled, std::memory_order_release);
|
||||
}
|
||||
|
||||
auto collect_retired_metronome_configs_locked() -> void {
|
||||
auto const completed_epoch = render_epoch.load(std::memory_order_acquire);
|
||||
auto first_removed = std::remove_if(retired_metronome_configs.begin(), retired_metronome_configs.end(), [completed_epoch](retired_metronome_config const& retired) {
|
||||
return retired.retire_epoch < completed_epoch;
|
||||
});
|
||||
retired_metronome_configs.erase(first_removed, retired_metronome_configs.end());
|
||||
metronome_configs.collect();
|
||||
}
|
||||
|
||||
auto allocate_sound_handle_locked() -> std::expected<sonus::sound_handle, modulus::error> {
|
||||
@@ -536,13 +563,7 @@ struct sonus::engine::impl {
|
||||
return MA_SUCCESS;
|
||||
}
|
||||
|
||||
struct epoch_guard {
|
||||
std::atomic<std::uint64_t>& epoch;
|
||||
|
||||
~epoch_guard() {
|
||||
epoch.fetch_add(1, std::memory_order_acq_rel);
|
||||
}
|
||||
} guard{render_epoch};
|
||||
auto guard = metronome_configs.read();
|
||||
|
||||
auto const sample_count = frame_count * config.channels;
|
||||
std::fill(output, output + sample_count, 0.0F);
|
||||
@@ -551,12 +572,11 @@ struct sonus::engine::impl {
|
||||
std::fill(output, output + sample_count, 0.0F);
|
||||
return result;
|
||||
}
|
||||
mix_metronome(output, frame_count);
|
||||
mix_metronome(output, frame_count, guard.get());
|
||||
return MA_SUCCESS;
|
||||
}
|
||||
|
||||
auto mix_metronome(std::float32_t* output, std::uint64_t frame_count) -> void {
|
||||
auto const* metro = active_metronome_config.load(std::memory_order_acquire);
|
||||
auto mix_metronome(std::float32_t* output, std::uint64_t frame_count, metronome_render_config const* metro) -> void {
|
||||
if (metro == nullptr || !metro->enabled) {
|
||||
if (metro != nullptr) {
|
||||
observed_metronome_generation = metro->generation;
|
||||
@@ -594,7 +614,8 @@ struct sonus::engine::impl {
|
||||
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);
|
||||
auto const mixed = static_cast<std::float32_t>(frame_out[channel] + sample);
|
||||
frame_out[channel] = std::clamp(mixed, static_cast<std::float32_t>(-1.0F), static_cast<std::float32_t>(1.0F));
|
||||
}
|
||||
}
|
||||
--frames_until_next_beat;
|
||||
@@ -632,11 +653,14 @@ auto sonus::validate(sonus::audio_config const& config) -> std::expected<void, m
|
||||
if (!valid_unit_interval(config.master_volume)) {
|
||||
return sonus::detail::fail_void(invalid("audio master_volume must be between 0 and 1"));
|
||||
}
|
||||
if (contains_null(config.playback_device_id)) {
|
||||
return sonus::detail::fail_void(invalid("audio playback_device_id must not contain null bytes"));
|
||||
}
|
||||
if (contains_null(config.playback_device_name)) {
|
||||
return sonus::detail::fail_void(invalid("audio playback_device_name must not contain null bytes"));
|
||||
}
|
||||
if (config.no_device && !config.playback_device_name.empty()) {
|
||||
return sonus::detail::fail_void(invalid("audio playback_device_name requires a playback device"));
|
||||
if (config.no_device && (!config.playback_device_id.empty() || !config.playback_device_name.empty())) {
|
||||
return sonus::detail::fail_void(invalid("audio playback device selectors require a playback device"));
|
||||
}
|
||||
if (config.period_size_frames != 0U && config.period_size_milliseconds != 0U) {
|
||||
return sonus::detail::fail_void(invalid("audio period_size_frames and period_size_milliseconds are mutually exclusive"));
|
||||
@@ -653,6 +677,25 @@ auto sonus::validate(sonus::audio_config const& config) -> std::expected<void, m
|
||||
return {};
|
||||
}
|
||||
|
||||
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();
|
||||
auto result = ma_context_init(nullptr, 0, &context_config, &context.context);
|
||||
if (result != MA_SUCCESS) {
|
||||
return sonus::detail::fail<std::vector<sonus::device_info>>(miniaudio_error(result, "failed to initialize miniaudio context"));
|
||||
}
|
||||
context.initialized = true;
|
||||
|
||||
ma_device_info* playback_devices = nullptr;
|
||||
auto playback_device_count = ma_uint32{};
|
||||
result = ma_context_get_devices(&context.context, &playback_devices, &playback_device_count, nullptr, nullptr);
|
||||
if (result != MA_SUCCESS) {
|
||||
return sonus::detail::fail<std::vector<sonus::device_info>>(miniaudio_error(result, "failed to enumerate playback devices"));
|
||||
}
|
||||
|
||||
return describe_playback_devices(playback_devices, playback_device_count);
|
||||
}
|
||||
|
||||
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"));
|
||||
|
||||
Reference in New Issue
Block a user