@@ -0,0 +1,89 @@
|
||||
#include <sonus/engine.hpp>
|
||||
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
#include <sonus/detail/errors.hpp>
|
||||
|
||||
|
||||
namespace {
|
||||
|
||||
auto invalid(std::string message) -> modulus::error {
|
||||
return sonus::detail::make_error(sonus::detail::error_invalid_argument, 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;
|
||||
}
|
||||
|
||||
auto contains_null(std::string_view value) -> std::bool_t {
|
||||
return value.find('\0') != std::string_view::npos;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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"));
|
||||
}
|
||||
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_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"));
|
||||
}
|
||||
if (config.period_size_frames > 262144U) {
|
||||
return sonus::detail::fail_void(invalid("audio period_size_frames must be no greater than 262144"));
|
||||
}
|
||||
if (config.period_size_milliseconds > 1000U) {
|
||||
return sonus::detail::fail_void(invalid("audio period_size_milliseconds must be no greater than 1000"));
|
||||
}
|
||||
if (config.period_count > 16U) {
|
||||
return sonus::detail::fail_void(invalid("audio period_count must be no greater than 16"));
|
||||
}
|
||||
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 {};
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include <sonus/config.hpp>
|
||||
#include "render_snapshot.hpp"
|
||||
|
||||
|
||||
namespace sonus::detail {
|
||||
|
||||
struct metronome_render_config {
|
||||
std::bool_t enabled{false};
|
||||
std::uint32_t generation{0};
|
||||
std::uint32_t beats_per_bar{4};
|
||||
std::uint64_t frames_per_beat{24000};
|
||||
std::vector<std::float32_t> beat_click{};
|
||||
std::vector<std::float32_t> downbeat_click{};
|
||||
};
|
||||
|
||||
// The engine serializes control calls; only the audio reader touches render state.
|
||||
|
||||
class metronome_player {
|
||||
public:
|
||||
auto configure(sonus::metronome_config value, std::uint32_t sample_rate, std::bool_t enabled) -> void;
|
||||
auto stop() -> void;
|
||||
auto active() const -> std::bool_t;
|
||||
auto collect() -> void;
|
||||
auto render(std::float32_t *output, std::uint64_t frame_count, std::uint32_t channels) -> void;
|
||||
private:
|
||||
auto publish(std::unique_ptr<sonus::detail::metronome_render_config> next) -> void;
|
||||
auto reset() -> void;
|
||||
sonus::detail::render_snapshot<sonus::detail::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};
|
||||
};
|
||||
|
||||
}
|
||||
+12
-212
@@ -23,15 +23,13 @@
|
||||
#include <modulus.hpp>
|
||||
|
||||
#include <sonus/detail/errors.hpp>
|
||||
#include "detail/render_snapshot.hpp"
|
||||
#include "detail/metronome_player.hpp"
|
||||
#include "detail/playback_devices.hpp"
|
||||
#include "detail/sequence_player.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));
|
||||
}
|
||||
@@ -54,14 +52,6 @@ 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;
|
||||
}
|
||||
|
||||
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 += ':';
|
||||
@@ -94,19 +84,6 @@ auto to_miniaudio_profile(sonus::performance_profile profile) -> ma_performance_
|
||||
return ma_performance_profile_low_latency;
|
||||
}
|
||||
|
||||
auto make_click_samples(std::uint32_t sample_rate, std::uint32_t frame_count, std::float32_t frequency, std::float32_t gain) -> std::vector<std::float32_t> {
|
||||
auto result = std::vector<std::float32_t>(frame_count);
|
||||
auto const phase_step = (2.0 * pi * static_cast<double>(frequency)) / static_cast<double>(sample_rate);
|
||||
auto phase = 0.0;
|
||||
for (auto index = std::uint32_t{}; index < frame_count; ++index) {
|
||||
auto const progress = static_cast<double>(index) / static_cast<double>(frame_count);
|
||||
auto const envelope = (1.0 - progress) * (1.0 - progress);
|
||||
result[index] = static_cast<std::float32_t>(std::sin(phase) * envelope * static_cast<double>(gain));
|
||||
phase += phase_step;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
struct active_sound {
|
||||
ma_sound sound{};
|
||||
std::bool_t initialized{false};
|
||||
@@ -143,15 +120,6 @@ struct temporary_context {
|
||||
}
|
||||
};
|
||||
|
||||
struct metronome_render_config {
|
||||
std::bool_t enabled{false};
|
||||
std::uint32_t generation{0};
|
||||
std::uint32_t beats_per_bar{4};
|
||||
std::uint64_t frames_per_beat{24000};
|
||||
std::vector<std::float32_t> beat_click{};
|
||||
std::vector<std::float32_t> downbeat_click{};
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
struct sonus::engine::impl {
|
||||
@@ -170,15 +138,7 @@ struct sonus::engine::impl {
|
||||
std::uint64_t next_sound_handle{1};
|
||||
sonus::detail::sequence_player sequences{};
|
||||
std::atomic<std::float32_t> sequence_volume{0.70F};
|
||||
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};
|
||||
sonus::detail::metronome_player metronome{};
|
||||
impl() = default;
|
||||
impl(impl const &) = delete;
|
||||
auto operator=(impl const &) -> impl & = delete;
|
||||
@@ -188,7 +148,6 @@ struct sonus::engine::impl {
|
||||
auto initialize(sonus::audio_config value) -> std::expected<void, modulus::error> {
|
||||
config = std::move(value);
|
||||
sequence_volume.store(config.master_volume, std::memory_order_relaxed);
|
||||
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;
|
||||
@@ -259,7 +218,7 @@ struct sonus::engine::impl {
|
||||
static_cast<void>(ma_device_stop(&device));
|
||||
}
|
||||
started = false;
|
||||
collect_retired_metronome_configs_locked();
|
||||
metronome.collect();
|
||||
}
|
||||
auto shutdown() -> void {
|
||||
auto lock = std::scoped_lock{mutex};
|
||||
@@ -281,8 +240,6 @@ struct sonus::engine::impl {
|
||||
ma_context_uninit(&context);
|
||||
context_initialized = false;
|
||||
}
|
||||
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> {
|
||||
if (config.playback_device_id.empty() && config.playback_device_name.empty()) {
|
||||
@@ -342,7 +299,7 @@ struct sonus::engine::impl {
|
||||
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();
|
||||
metronome.collect();
|
||||
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<std::uint64_t>(miniaudio_error(result, "failed to load sound file"));
|
||||
@@ -405,13 +362,13 @@ struct sonus::engine::impl {
|
||||
auto lock = std::scoped_lock{mutex};
|
||||
sequences.collect();
|
||||
auto const collected = collect_finished_locked();
|
||||
collect_retired_metronome_configs_locked();
|
||||
metronome.collect();
|
||||
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();
|
||||
metronome.collect();
|
||||
return sounds.size();
|
||||
}
|
||||
auto store_metronome(sonus::metronome_config value, std::bool_t enable) -> std::expected<void, modulus::error> {
|
||||
@@ -420,62 +377,15 @@ struct sonus::engine::impl {
|
||||
return valid;
|
||||
}
|
||||
auto lock = std::scoped_lock{mutex};
|
||||
return publish_metronome_config_locked(value, enable);
|
||||
metronome.configure(value, config.sample_rate, enable || metronome.active());
|
||||
return {};
|
||||
}
|
||||
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 enable = current != nullptr && current->enabled;
|
||||
return publish_metronome_config_locked(value, enable);
|
||||
return store_metronome(value, false);
|
||||
}
|
||||
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;
|
||||
result->frames_per_beat = frames_per_beat;
|
||||
if (enable) {
|
||||
result->beat_click = make_click_samples(config.sample_rate, static_cast<std::uint32_t>(click_frames), value.beat_frequency_hz, value.volume * 0.64F);
|
||||
result->downbeat_click = make_click_samples(config.sample_rate, static_cast<std::uint32_t>(click_frames), value.downbeat_frequency_hz, value.volume);
|
||||
}
|
||||
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();
|
||||
metronome.stop();
|
||||
}
|
||||
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)) {
|
||||
@@ -515,7 +425,6 @@ struct sonus::engine::impl {
|
||||
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);
|
||||
@@ -524,59 +433,10 @@ struct sonus::engine::impl {
|
||||
std::fill(output, output + sample_count, 0.0F);
|
||||
return result;
|
||||
}
|
||||
mix_metronome(output, frame_count, guard.get());
|
||||
metronome.render(output, frame_count, config.channels);
|
||||
sequences.render(output, frame_count, config.channels, sequence_volume.load(std::memory_order_relaxed));
|
||||
return MA_SUCCESS;
|
||||
}
|
||||
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;
|
||||
}
|
||||
reset_metronome_render_state();
|
||||
return;
|
||||
}
|
||||
if (observed_metronome_generation != metro->generation) {
|
||||
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;
|
||||
click_frame_index = 0;
|
||||
click_is_downbeat = downbeat;
|
||||
click_frames_remaining = downbeat ? metro->downbeat_click.size() : metro->beat_click.size();
|
||||
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;
|
||||
if (click_frame_index < click.size()) {
|
||||
sample = click[click_frame_index];
|
||||
++click_frame_index;
|
||||
--click_frames_remaining;
|
||||
} else {
|
||||
click_frames_remaining = 0;
|
||||
}
|
||||
}
|
||||
if (sample != 0.0F) {
|
||||
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));
|
||||
}
|
||||
}
|
||||
--frames_until_next_beat;
|
||||
}
|
||||
}
|
||||
auto reset_metronome_render_state() -> void {
|
||||
beat_index = 0;
|
||||
frames_until_next_beat = 0;
|
||||
click_frame_index = 0;
|
||||
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);
|
||||
if (self == nullptr || output == nullptr) {
|
||||
@@ -590,40 +450,6 @@ struct sonus::engine::impl {
|
||||
}
|
||||
};
|
||||
|
||||
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"));
|
||||
}
|
||||
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_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"));
|
||||
}
|
||||
if (config.period_size_frames > 262144U) {
|
||||
return sonus::detail::fail_void(invalid("audio period_size_frames must be no greater than 262144"));
|
||||
}
|
||||
if (config.period_size_milliseconds > 1000U) {
|
||||
return sonus::detail::fail_void(invalid("audio period_size_milliseconds must be no greater than 1000"));
|
||||
}
|
||||
if (config.period_count > 16U) {
|
||||
return sonus::detail::fail_void(invalid("audio period_count must be no greater than 16"));
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
auto sonus::load_sample(std::filesystem::path const &path, std::uint32_t sample_rate) -> std::expected<sonus::sample_buffer, modulus::error> {
|
||||
if (path.empty() || sample_rate < 8000U || sample_rate > 192000U) {
|
||||
return sonus::detail::fail<sonus::sample_buffer>(invalid("invalid sample path or sample rate"));
|
||||
@@ -689,32 +515,6 @@ auto sonus::enumerate_playback_devices() -> std::expected<std::vector<sonus::dev
|
||||
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"));
|
||||
}
|
||||
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) {
|
||||
@@ -829,7 +629,7 @@ auto sonus::engine::stop_metronome() -> void {
|
||||
}
|
||||
|
||||
auto sonus::engine::metronome_active() const -> std::bool_t {
|
||||
return implementation_ && implementation_->metronome_enabled.load(std::memory_order_acquire);
|
||||
return implementation_ && implementation_->metronome.active();
|
||||
}
|
||||
|
||||
auto sonus::engine::render(std::float32_t *output, std::uint64_t frame_count) -> std::expected<void, modulus::error> {
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
#include "detail/metronome_player.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <utility>
|
||||
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto pi = 3.141592653589793238462643383279502884;
|
||||
|
||||
auto make_click_samples(std::uint32_t sample_rate, std::uint32_t frame_count, std::float32_t frequency, std::float32_t gain) -> std::vector<std::float32_t> {
|
||||
auto result = std::vector<std::float32_t>(frame_count);
|
||||
auto const phase_step = (2.0 * pi * static_cast<double>(frequency)) / static_cast<double>(sample_rate);
|
||||
auto phase = 0.0;
|
||||
for (auto index = std::uint32_t{}; index < frame_count; ++index) {
|
||||
auto const progress = static_cast<double>(index) / static_cast<double>(frame_count);
|
||||
auto const envelope = (1.0 - progress) * (1.0 - progress);
|
||||
result[index] = static_cast<std::float32_t>(std::sin(phase) * envelope * static_cast<double>(gain));
|
||||
phase += phase_step;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
auto sonus::detail::metronome_player::configure(sonus::metronome_config value, std::uint32_t sample_rate, std::bool_t enable) -> void {
|
||||
auto click_frames = static_cast<std::uint64_t>(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>(sample_rate) * 60.0) / static_cast<double>(value.bpm))));
|
||||
auto result = std::make_unique<sonus::detail::metronome_render_config>();
|
||||
result->enabled = enable;
|
||||
result->beats_per_bar = value.beats_per_bar;
|
||||
result->frames_per_beat = frames_per_beat;
|
||||
if (enable) {
|
||||
result->beat_click = make_click_samples(sample_rate, static_cast<std::uint32_t>(click_frames), value.beat_frequency_hz, value.volume * 0.64F);
|
||||
result->downbeat_click = make_click_samples(sample_rate, static_cast<std::uint32_t>(click_frames), value.downbeat_frequency_hz, value.volume);
|
||||
}
|
||||
publish(std::move(result));
|
||||
}
|
||||
|
||||
auto sonus::detail::metronome_player::stop() -> void {
|
||||
publish(std::make_unique<sonus::detail::metronome_render_config>());
|
||||
}
|
||||
|
||||
auto sonus::detail::metronome_player::active() const -> std::bool_t {
|
||||
return metronome_enabled.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
auto sonus::detail::metronome_player::collect() -> void {
|
||||
metronome_configs.collect();
|
||||
}
|
||||
|
||||
auto sonus::detail::metronome_player::publish(std::unique_ptr<sonus::detail::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 sonus::detail::metronome_player::render(std::float32_t *output, std::uint64_t frame_count, std::uint32_t channels) -> void {
|
||||
auto guard = metronome_configs.read();
|
||||
auto const *metro = guard.get();
|
||||
if (metro == nullptr || !metro->enabled) {
|
||||
if (metro != nullptr) {
|
||||
observed_metronome_generation = metro->generation;
|
||||
}
|
||||
reset();
|
||||
return;
|
||||
}
|
||||
if (observed_metronome_generation != metro->generation) {
|
||||
observed_metronome_generation = metro->generation;
|
||||
reset();
|
||||
}
|
||||
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_is_downbeat = downbeat;
|
||||
click_frames_remaining = downbeat ? metro->downbeat_click.size() : metro->beat_click.size();
|
||||
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;
|
||||
if (click_frame_index < click.size()) {
|
||||
sample = click[click_frame_index];
|
||||
++click_frame_index;
|
||||
--click_frames_remaining;
|
||||
} else {
|
||||
click_frames_remaining = 0;
|
||||
}
|
||||
}
|
||||
if (sample != 0.0F) {
|
||||
auto *frame_out = output + frame * channels;
|
||||
for (auto channel = std::uint32_t{}; channel < 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));
|
||||
}
|
||||
}
|
||||
--frames_until_next_beat;
|
||||
}
|
||||
}
|
||||
|
||||
auto sonus::detail::metronome_player::reset() -> void {
|
||||
beat_index = 0;
|
||||
frames_until_next_beat = 0;
|
||||
click_frame_index = 0;
|
||||
click_frames_remaining = 0;
|
||||
click_is_downbeat = false;
|
||||
}
|
||||
Reference in New Issue
Block a user