Cleanup
publish / publish (push) Successful in 14m29s

This commit is contained in:
khodowany
2026-09-10 09:22:15 -05:00
parent 1241f3eb71
commit b58664d5f4
16 changed files with 582 additions and 68 deletions
+30
View File
@@ -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;
}
}
+63
View File
@@ -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
View File
@@ -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"));