|
|
|
@@ -26,6 +26,7 @@
|
|
|
|
|
#include "detail/render_snapshot.hpp"
|
|
|
|
|
#include "detail/playback_devices.hpp"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
namespace {
|
|
|
|
|
|
|
|
|
|
constexpr auto pi = 3.141592653589793238462643383279502884;
|
|
|
|
@@ -39,7 +40,7 @@ auto unavailable(std::string message) -> modulus::error {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto miniaudio_error(ma_result result, std::string action) -> modulus::error {
|
|
|
|
|
auto const* description = ma_result_description(result);
|
|
|
|
|
auto const *description = ma_result_description(result);
|
|
|
|
|
auto message = std::move(action);
|
|
|
|
|
if (description != nullptr && std::string_view{description} != "No error") {
|
|
|
|
|
message += ": ";
|
|
|
|
@@ -60,7 +61,7 @@ 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 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');
|
|
|
|
@@ -69,7 +70,7 @@ auto make_playback_device_id(ma_uint32 index, ma_device_info const& info) -> std
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto describe_playback_devices(ma_device_info const* devices, ma_uint32 count) -> std::vector<sonus::device_info> {
|
|
|
|
|
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) {
|
|
|
|
@@ -84,10 +85,10 @@ auto describe_playback_devices(ma_device_info const* devices, ma_uint32 count) -
|
|
|
|
|
|
|
|
|
|
auto to_miniaudio_profile(sonus::performance_profile profile) -> ma_performance_profile {
|
|
|
|
|
switch (profile) {
|
|
|
|
|
case sonus::performance_profile::LOW_LATENCY:
|
|
|
|
|
return ma_performance_profile_low_latency;
|
|
|
|
|
case sonus::performance_profile::CONSERVATIVE:
|
|
|
|
|
return ma_performance_profile_conservative;
|
|
|
|
|
case sonus::performance_profile::LOW_LATENCY:
|
|
|
|
|
return ma_performance_profile_low_latency;
|
|
|
|
|
case sonus::performance_profile::CONSERVATIVE:
|
|
|
|
|
return ma_performance_profile_conservative;
|
|
|
|
|
}
|
|
|
|
|
return ma_performance_profile_low_latency;
|
|
|
|
|
}
|
|
|
|
@@ -110,21 +111,18 @@ struct active_sound {
|
|
|
|
|
std::bool_t initialized{false};
|
|
|
|
|
std::bool_t loop{false};
|
|
|
|
|
std::atomic_bool finished{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(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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static auto end_callback(void* user_data, ma_sound*) -> void {
|
|
|
|
|
auto* self = static_cast<active_sound*>(user_data);
|
|
|
|
|
static auto end_callback(void *user_data, ma_sound *) -> void {
|
|
|
|
|
auto *self = static_cast<active_sound *>(user_data);
|
|
|
|
|
if (self != nullptr) {
|
|
|
|
|
self->finished.store(true, std::memory_order_release);
|
|
|
|
|
}
|
|
|
|
@@ -134,11 +132,9 @@ 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(temporary_context const &) = delete;
|
|
|
|
|
auto operator=(temporary_context const &) -> temporary_context & = delete;
|
|
|
|
|
~temporary_context() {
|
|
|
|
|
if (initialized) {
|
|
|
|
|
ma_context_uninit(&context);
|
|
|
|
@@ -169,32 +165,26 @@ struct sonus::engine::impl {
|
|
|
|
|
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::unordered_map<std::uint64_t, std::unique_ptr<active_sound>> sounds{};
|
|
|
|
|
std::uint64_t next_sound_handle{1};
|
|
|
|
|
sonus::detail::render_snapshot<metronome_render_config> metronome_configs{};
|
|
|
|
|
std::atomic_bool metronome_enabled{false};
|
|
|
|
|
std::uint32_t next_metronome_generation{1};
|
|
|
|
|
|
|
|
|
|
std::uint32_t observed_metronome_generation{0};
|
|
|
|
|
std::uint32_t beat_index{0};
|
|
|
|
|
std::uint64_t frames_until_next_beat{0};
|
|
|
|
|
std::size_t click_frame_index{0};
|
|
|
|
|
std::size_t click_frames_remaining{0};
|
|
|
|
|
std::bool_t click_is_downbeat{false};
|
|
|
|
|
|
|
|
|
|
impl() = default;
|
|
|
|
|
impl(impl const&) = delete;
|
|
|
|
|
auto operator=(impl const&) -> impl& = delete;
|
|
|
|
|
|
|
|
|
|
impl(impl const &) = delete;
|
|
|
|
|
auto operator=(impl const &) -> impl & = delete;
|
|
|
|
|
~impl() {
|
|
|
|
|
shutdown();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto initialize(sonus::audio_config value) -> std::expected<void, modulus::error> {
|
|
|
|
|
config = std::move(value);
|
|
|
|
|
publish_metronome_locked(make_disabled_metronome_config_locked());
|
|
|
|
|
|
|
|
|
|
auto engine_config = ma_engine_config_init();
|
|
|
|
|
engine_config.noDevice = MA_TRUE;
|
|
|
|
|
engine_config.channels = config.channels;
|
|
|
|
@@ -202,19 +192,16 @@ struct sonus::engine::impl {
|
|
|
|
|
engine_config.periodSizeInFrames = config.period_size_frames;
|
|
|
|
|
engine_config.periodSizeInMilliseconds = config.period_size_milliseconds;
|
|
|
|
|
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;
|
|
|
|
@@ -226,13 +213,11 @@ struct sonus::engine::impl {
|
|
|
|
|
device_config.performanceProfile = to_miniaudio_profile(config.performance);
|
|
|
|
|
device_config.dataCallback = sonus::engine::impl::device_callback;
|
|
|
|
|
device_config.pUserData = this;
|
|
|
|
|
|
|
|
|
|
auto selected = configure_playback_device(device_config);
|
|
|
|
|
if (!selected) {
|
|
|
|
|
shutdown();
|
|
|
|
|
return selected;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result = ma_device_init(context_initialized ? &context : nullptr, &device_config, &device);
|
|
|
|
|
if (result != MA_SUCCESS) {
|
|
|
|
|
shutdown();
|
|
|
|
@@ -240,10 +225,8 @@ struct sonus::engine::impl {
|
|
|
|
|
}
|
|
|
|
|
device_initialized = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto start() -> std::expected<void, modulus::error> {
|
|
|
|
|
auto lock = std::scoped_lock{mutex};
|
|
|
|
|
if (started) {
|
|
|
|
@@ -263,7 +246,6 @@ struct sonus::engine::impl {
|
|
|
|
|
started = true;
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto stop_device() -> void {
|
|
|
|
|
auto lock = std::scoped_lock{mutex};
|
|
|
|
|
if (!started) {
|
|
|
|
@@ -275,7 +257,6 @@ struct sonus::engine::impl {
|
|
|
|
|
started = false;
|
|
|
|
|
collect_retired_metronome_configs_locked();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto shutdown() -> void {
|
|
|
|
|
auto lock = std::scoped_lock{mutex};
|
|
|
|
|
if (device_initialized) {
|
|
|
|
@@ -298,26 +279,22 @@ struct sonus::engine::impl {
|
|
|
|
|
metronome_enabled.store(false, std::memory_order_release);
|
|
|
|
|
reset_metronome_render_state();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto configure_playback_device(ma_device_config& device_config) -> std::expected<void, modulus::error> {
|
|
|
|
|
auto configure_playback_device(ma_device_config &device_config) -> std::expected<void, modulus::error> {
|
|
|
|
|
if (config.playback_device_id.empty() && config.playback_device_name.empty()) {
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto context_config = ma_context_config_init();
|
|
|
|
|
auto result = ma_context_init(nullptr, 0, &context_config, &context);
|
|
|
|
|
if (result != MA_SUCCESS) {
|
|
|
|
|
return sonus::detail::fail_void(miniaudio_error(result, "failed to initialize miniaudio context"));
|
|
|
|
|
}
|
|
|
|
|
context_initialized = true;
|
|
|
|
|
|
|
|
|
|
ma_device_info* playback_devices = nullptr;
|
|
|
|
|
ma_device_info *playback_devices = nullptr;
|
|
|
|
|
auto playback_device_count = ma_uint32{};
|
|
|
|
|
result = ma_context_get_devices(&context, &playback_devices, &playback_device_count, nullptr, nullptr);
|
|
|
|
|
if (result != MA_SUCCESS) {
|
|
|
|
|
return sonus::detail::fail_void(miniaudio_error(result, "failed to enumerate playback devices"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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) {
|
|
|
|
@@ -325,16 +302,13 @@ struct sonus::engine::impl {
|
|
|
|
|
device_config.playback.pDeviceID = &playback_device_id;
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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> {
|
|
|
|
|
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};
|
|
|
|
|
auto result = ma_engine_set_volume(&mixer, volume);
|
|
|
|
|
if (result != MA_SUCCESS) {
|
|
|
|
@@ -343,55 +317,49 @@ struct sonus::engine::impl {
|
|
|
|
|
config.master_volume = volume;
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto play_file(std::filesystem::path const& path, sonus::playback_options options) -> std::expected<sonus::sound_handle, modulus::error> {
|
|
|
|
|
auto play_file(std::filesystem::path const &path, sonus::playback_options options) -> std::expected<std::uint64_t, modulus::error> {
|
|
|
|
|
if (path.empty()) {
|
|
|
|
|
return sonus::detail::fail<sonus::sound_handle>(sonus::detail::error_invalid_argument, "sound path must not be empty");
|
|
|
|
|
return sonus::detail::fail<std::uint64_t>(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());
|
|
|
|
|
return sonus::detail::fail<std::uint64_t>(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()));
|
|
|
|
|
return sonus::detail::fail<std::uint64_t>(sonus::detail::make_error(sonus::detail::error_not_found, "sound file not found: " + path.string()));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto sound = std::make_unique<active_sound>();
|
|
|
|
|
sound->loop = options.loop;
|
|
|
|
|
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};
|
|
|
|
|
static_cast<void>(collect_finished_locked());
|
|
|
|
|
collect_retired_metronome_configs_locked();
|
|
|
|
|
|
|
|
|
|
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"));
|
|
|
|
|
return sonus::detail::fail<std::uint64_t>(miniaudio_error(result, "failed to load sound file"));
|
|
|
|
|
}
|
|
|
|
|
sound->initialized = true;
|
|
|
|
|
result = ma_sound_set_end_callback(&sound->sound, active_sound::end_callback, sound.get());
|
|
|
|
|
if (result != MA_SUCCESS) {
|
|
|
|
|
return sonus::detail::fail<sonus::sound_handle>(miniaudio_error(result, "failed to set sound end callback"));
|
|
|
|
|
return sonus::detail::fail<std::uint64_t>(miniaudio_error(result, "failed to set sound end callback"));
|
|
|
|
|
}
|
|
|
|
|
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"));
|
|
|
|
|
return sonus::detail::fail<std::uint64_t>(miniaudio_error(result, "failed to start sound file"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto handle = allocate_sound_handle_locked();
|
|
|
|
|
if (!handle) {
|
|
|
|
|
return sonus::detail::fail<sonus::sound_handle>(handle.error());
|
|
|
|
|
return sonus::detail::fail<std::uint64_t>(handle.error());
|
|
|
|
|
}
|
|
|
|
|
sounds.emplace(*handle, std::move(sound));
|
|
|
|
|
return *handle;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto stop_sound(sonus::sound_handle handle) -> std::expected<void, modulus::error> {
|
|
|
|
|
auto stop_sound(std::uint64_t 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");
|
|
|
|
|
}
|
|
|
|
@@ -404,16 +372,14 @@ struct sonus::engine::impl {
|
|
|
|
|
sounds.erase(found);
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto stop_all_sounds() -> void {
|
|
|
|
|
auto lock = std::scoped_lock{mutex};
|
|
|
|
|
for (auto& entry : sounds) {
|
|
|
|
|
for (auto &entry : sounds) {
|
|
|
|
|
static_cast<void>(ma_sound_stop(&entry.second->sound));
|
|
|
|
|
}
|
|
|
|
|
sounds.clear();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto is_sound_playing(sonus::sound_handle handle) -> std::expected<std::bool_t, modulus::error> {
|
|
|
|
|
auto is_sound_playing(std::uint64_t handle) -> std::expected<std::bool_t, modulus::error> {
|
|
|
|
|
if (handle == 0) {
|
|
|
|
|
return sonus::detail::fail<std::bool_t>(sonus::detail::error_invalid_argument, "sound handle must not be zero");
|
|
|
|
|
}
|
|
|
|
@@ -428,65 +394,54 @@ struct sonus::engine::impl {
|
|
|
|
|
}
|
|
|
|
|
return ma_sound_is_playing(&found->second->sound) == MA_TRUE;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto collect_finished_sounds() -> std::uint64_t {
|
|
|
|
|
auto lock = std::scoped_lock{mutex};
|
|
|
|
|
auto const collected = collect_finished_locked();
|
|
|
|
|
collect_retired_metronome_configs_locked();
|
|
|
|
|
return collected;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto active_sound_count() -> std::uint64_t {
|
|
|
|
|
auto lock = std::scoped_lock{mutex};
|
|
|
|
|
static_cast<void>(collect_finished_locked());
|
|
|
|
|
collect_retired_metronome_configs_locked();
|
|
|
|
|
return sounds.size();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 lock = std::scoped_lock{mutex};
|
|
|
|
|
return publish_metronome_config_locked(value, enable);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto update_metronome(sonus::metronome_config value) -> std::expected<void, modulus::error> {
|
|
|
|
|
auto valid = sonus::validate(value);
|
|
|
|
|
if (!valid) {
|
|
|
|
|
return valid;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto lock = std::scoped_lock{mutex};
|
|
|
|
|
auto const* current = metronome_configs.current();
|
|
|
|
|
auto const *current = metronome_configs.current();
|
|
|
|
|
auto const enable = current != nullptr && current->enabled;
|
|
|
|
|
return publish_metronome_config_locked(value, enable);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto stop_metronome() -> void {
|
|
|
|
|
auto lock = std::scoped_lock{mutex};
|
|
|
|
|
publish_metronome_locked(make_disabled_metronome_config_locked());
|
|
|
|
|
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());
|
|
|
|
|
auto 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>(value.bpm))));
|
|
|
|
|
|
|
|
|
|
auto result = std::make_unique<metronome_render_config>();
|
|
|
|
|
result->enabled = enable;
|
|
|
|
|
result->beats_per_bar = value.beats_per_bar;
|
|
|
|
@@ -497,33 +452,27 @@ struct sonus::engine::impl {
|
|
|
|
|
}
|
|
|
|
|
return std::move(result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto make_disabled_metronome_config_locked() -> std::unique_ptr<metronome_render_config> {
|
|
|
|
|
auto result = std::make_unique<metronome_render_config>();
|
|
|
|
|
result->enabled = false;
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto publish_metronome_locked(std::unique_ptr<metronome_render_config> next) -> void {
|
|
|
|
|
next->generation = next_metronome_generation++;
|
|
|
|
|
if (next_metronome_generation == 0U) {
|
|
|
|
|
next_metronome_generation = 1U;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 {
|
|
|
|
|
metronome_configs.collect();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto allocate_sound_handle_locked() -> std::expected<sonus::sound_handle, modulus::error> {
|
|
|
|
|
if (sounds.size() >= static_cast<std::size_t>(std::numeric_limits<sonus::sound_handle>::max() - 1U)) {
|
|
|
|
|
return sonus::detail::fail<sonus::sound_handle>(sonus::detail::error_internal, "sound handle space exhausted");
|
|
|
|
|
auto allocate_sound_handle_locked() -> std::expected<std::uint64_t, modulus::error> {
|
|
|
|
|
if (sounds.size() >= static_cast<std::size_t>(std::numeric_limits<std::uint64_t>::max() - 1U)) {
|
|
|
|
|
return sonus::detail::fail<std::uint64_t>(sonus::detail::error_internal, "sound handle space exhausted");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto attempts = std::size_t{};
|
|
|
|
|
auto const max_attempts = sounds.size() + 1U;
|
|
|
|
|
while (attempts < max_attempts) {
|
|
|
|
@@ -536,14 +485,11 @@ struct sonus::engine::impl {
|
|
|
|
|
}
|
|
|
|
|
++attempts;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return sonus::detail::fail<sonus::sound_handle>(sonus::detail::error_internal, "failed to allocate sound handle");
|
|
|
|
|
return sonus::detail::fail<std::uint64_t>(sonus::detail::error_internal, "failed to allocate sound handle");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static auto sound_finished(active_sound const& sound) -> std::bool_t {
|
|
|
|
|
static auto sound_finished(active_sound const &sound) -> std::bool_t {
|
|
|
|
|
return !sound.loop && (sound.finished.load(std::memory_order_acquire) || ma_sound_at_end(&sound.sound) == MA_TRUE);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto collect_finished_locked() -> std::uint64_t {
|
|
|
|
|
auto collected = std::uint64_t{};
|
|
|
|
|
auto current = sounds.begin();
|
|
|
|
@@ -557,14 +503,11 @@ struct sonus::engine::impl {
|
|
|
|
|
}
|
|
|
|
|
return collected;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto render_to(std::float32_t* output, std::uint64_t frame_count) -> ma_result {
|
|
|
|
|
auto render_to(std::float32_t *output, std::uint64_t frame_count) -> ma_result {
|
|
|
|
|
if (output == nullptr || frame_count == 0U) {
|
|
|
|
|
return MA_SUCCESS;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto guard = metronome_configs.read();
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
@@ -575,8 +518,7 @@ struct sonus::engine::impl {
|
|
|
|
|
mix_metronome(output, frame_count, guard.get());
|
|
|
|
|
return MA_SUCCESS;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto mix_metronome(std::float32_t* output, std::uint64_t frame_count, metronome_render_config const* metro) -> void {
|
|
|
|
|
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;
|
|
|
|
@@ -588,7 +530,6 @@ struct sonus::engine::impl {
|
|
|
|
|
observed_metronome_generation = metro->generation;
|
|
|
|
|
reset_metronome_render_state();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (auto frame = std::uint64_t{}; frame < frame_count; ++frame) {
|
|
|
|
|
if (frames_until_next_beat == 0U) {
|
|
|
|
|
auto const downbeat = beat_index == 0U;
|
|
|
|
@@ -598,10 +539,9 @@ struct sonus::engine::impl {
|
|
|
|
|
beat_index = (beat_index + 1U) % std::max<std::uint32_t>(1U, metro->beats_per_bar);
|
|
|
|
|
frames_until_next_beat = metro->frames_per_beat;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto sample = 0.0F;
|
|
|
|
|
if (click_frames_remaining > 0U) {
|
|
|
|
|
auto const& click = click_is_downbeat ? metro->downbeat_click : metro->beat_click;
|
|
|
|
|
auto const &click = click_is_downbeat ? metro->downbeat_click : metro->beat_click;
|
|
|
|
|
if (click_frame_index < click.size()) {
|
|
|
|
|
sample = click[click_frame_index];
|
|
|
|
|
++click_frame_index;
|
|
|
|
@@ -610,9 +550,8 @@ struct sonus::engine::impl {
|
|
|
|
|
click_frames_remaining = 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (sample != 0.0F) {
|
|
|
|
|
auto* frame_out = output + frame * config.channels;
|
|
|
|
|
auto *frame_out = output + frame * config.channels;
|
|
|
|
|
for (auto channel = std::uint32_t{}; channel < config.channels; ++channel) {
|
|
|
|
|
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));
|
|
|
|
@@ -621,7 +560,6 @@ struct sonus::engine::impl {
|
|
|
|
|
--frames_until_next_beat;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto reset_metronome_render_state() -> void {
|
|
|
|
|
beat_index = 0;
|
|
|
|
|
frames_until_next_beat = 0;
|
|
|
|
@@ -629,13 +567,12 @@ struct sonus::engine::impl {
|
|
|
|
|
click_frames_remaining = 0;
|
|
|
|
|
click_is_downbeat = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
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);
|
|
|
|
|
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)));
|
|
|
|
@@ -643,7 +580,7 @@ struct sonus::engine::impl {
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
auto sonus::validate(sonus::audio_config const& config) -> std::expected<void, modulus::error> {
|
|
|
|
|
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"));
|
|
|
|
|
}
|
|
|
|
@@ -685,18 +622,16 @@ auto sonus::enumerate_playback_devices() -> std::expected<std::vector<sonus::dev
|
|
|
|
|
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;
|
|
|
|
|
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> {
|
|
|
|
|
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"));
|
|
|
|
|
}
|
|
|
|
@@ -715,7 +650,7 @@ auto sonus::validate(sonus::metronome_config const& config) -> std::expected<voi
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto sonus::validate(sonus::playback_options const& options) -> std::expected<void, modulus::error> {
|
|
|
|
|
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"));
|
|
|
|
|
}
|
|
|
|
@@ -735,11 +670,12 @@ auto sonus::engine::create(sonus::audio_config config) -> std::expected<sonus::e
|
|
|
|
|
return sonus::engine{std::move(implementation)};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sonus::engine::engine(std::unique_ptr<sonus::engine::impl> implementation) noexcept : implementation_{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;
|
|
|
|
|
sonus::engine::engine(sonus::engine &&other) noexcept = default;
|
|
|
|
|
|
|
|
|
|
auto sonus::engine::operator=(sonus::engine&& other) noexcept -> sonus::engine& = default;
|
|
|
|
|
auto sonus::engine::operator=(sonus::engine &&other) noexcept -> sonus::engine & = default;
|
|
|
|
|
|
|
|
|
|
sonus::engine::~engine() = default;
|
|
|
|
|
|
|
|
|
@@ -779,14 +715,14 @@ auto sonus::engine::set_master_volume(std::float32_t volume) -> std::expected<vo
|
|
|
|
|
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> {
|
|
|
|
|
auto sonus::engine::play_file(std::filesystem::path const &path, sonus::playback_options options) -> std::expected<std::uint64_t, modulus::error> {
|
|
|
|
|
if (!implementation_) {
|
|
|
|
|
return sonus::detail::fail<sonus::sound_handle>(sonus::detail::error_invalid_argument, "audio engine is empty");
|
|
|
|
|
return sonus::detail::fail<std::uint64_t>(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> {
|
|
|
|
|
auto sonus::engine::stop(std::uint64_t handle) -> std::expected<void, modulus::error> {
|
|
|
|
|
if (!implementation_) {
|
|
|
|
|
return sonus::detail::fail_void(sonus::detail::error_invalid_argument, "audio engine is empty");
|
|
|
|
|
}
|
|
|
|
@@ -799,7 +735,7 @@ auto sonus::engine::stop_all() -> void {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto sonus::engine::is_playing(sonus::sound_handle handle) -> std::expected<std::bool_t, modulus::error> {
|
|
|
|
|
auto sonus::engine::is_playing(std::uint64_t handle) -> std::expected<std::bool_t, modulus::error> {
|
|
|
|
|
if (!implementation_) {
|
|
|
|
|
return sonus::detail::fail<std::bool_t>(sonus::detail::error_invalid_argument, "audio engine is empty");
|
|
|
|
|
}
|
|
|
|
@@ -838,7 +774,7 @@ 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> {
|
|
|
|
|
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");
|
|
|
|
|
}
|
|
|
|
|