@@ -48,6 +48,10 @@ jobs:
|
||||
restore-keys: conan-${{ runner.os }}-
|
||||
- name: Configure native release
|
||||
run: python3 modulus.py configure --target x86_64_gcc12_release
|
||||
- name: Check code style
|
||||
run: |
|
||||
python3 -m pip install --no-cache-dir -r build/build_tools/docker/format-requirements.txt
|
||||
python3 modulus.py format --check
|
||||
- name: Build native release
|
||||
run: python3 modulus.py build --target x86_64_gcc12_release
|
||||
- name: Test native release
|
||||
|
||||
@@ -18,6 +18,8 @@ Call `collect_finished()` periodically to release sounds that reach EOF without
|
||||
|
||||
## Build
|
||||
|
||||
Code style is managed by Modulus: run `./modulus.py format` or `./modulus.py format --check`. The generated Neovim integration uses the same pinned container formatter for C++ and Python, including existing format-on-save toggles. Configure first to prepare tooling; customize `tooling.editor.clang_format` and `tooling.format.exclude` in `modulus.json`, not generated editor files. See the shared Modulus README for the compact layout and no-alias policy.
|
||||
|
||||
Configure, build, and test the default target:
|
||||
|
||||
```sh
|
||||
|
||||
@@ -42,15 +42,12 @@ class sonus_conan(conan.ConanFile):
|
||||
"tests/*.cpp",
|
||||
"tests/*.hpp",
|
||||
)
|
||||
|
||||
def config_options(self):
|
||||
if self.settings.os == "Windows":
|
||||
del self.options.fPIC
|
||||
|
||||
def configure(self):
|
||||
if self.options.shared:
|
||||
del self.options.fPIC
|
||||
|
||||
def requirements(self):
|
||||
self.requires(
|
||||
"modulus/latest",
|
||||
@@ -63,7 +60,6 @@ class sonus_conan(conan.ConanFile):
|
||||
transitive_headers=False,
|
||||
transitive_libs=False,
|
||||
)
|
||||
|
||||
def generate(self):
|
||||
dependencies = conan.tools.cmake.CMakeDeps(self)
|
||||
dependencies.generate()
|
||||
@@ -73,14 +69,12 @@ class sonus_conan(conan.ConanFile):
|
||||
toolchain.variables["BUILD_SHARED_LIBS"] = bool(self.options.shared)
|
||||
toolchain.variables["BUILD_TESTING"] = bool(self.options.with_tests)
|
||||
toolchain.generate()
|
||||
|
||||
def build(self):
|
||||
cmake = conan.tools.cmake.CMake(self)
|
||||
cmake.configure()
|
||||
cmake.build()
|
||||
if self.options.with_tests and conan.tools.build.can_run(self):
|
||||
cmake.test(target="check")
|
||||
|
||||
def package_id(self):
|
||||
self.info.options.rm_safe("with_tests")
|
||||
for requirement in (
|
||||
@@ -90,11 +84,9 @@ class sonus_conan(conan.ConanFile):
|
||||
):
|
||||
if requirement in self.info.requires.pkg_names:
|
||||
self.info.requires.remove(requirement)
|
||||
|
||||
def package(self):
|
||||
cmake = conan.tools.cmake.CMake(self)
|
||||
cmake.install()
|
||||
|
||||
def package_info(self):
|
||||
self.cpp_info.set_property(
|
||||
"cmake_file_name",
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
#include <modulus/types.hpp>
|
||||
|
||||
|
||||
namespace sonus {
|
||||
|
||||
enum class performance_profile : std::uint8_t {
|
||||
@@ -47,6 +48,4 @@ struct playback_options {
|
||||
std::bool_t stream{true};
|
||||
};
|
||||
|
||||
using sound_handle = std::uint64_t;
|
||||
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
#include <modulus.hpp>
|
||||
|
||||
|
||||
namespace sonus::detail {
|
||||
|
||||
constexpr std::int32_t error_invalid_argument{1};
|
||||
|
||||
@@ -19,39 +19,31 @@ auto enumerate_playback_devices() -> std::expected<std::vector<sonus::device_inf
|
||||
class engine {
|
||||
public:
|
||||
static auto create(sonus::audio_config config = {}) -> std::expected<sonus::engine, modulus::error>;
|
||||
|
||||
engine(sonus::engine const &) = delete;
|
||||
auto operator=(sonus::engine const &) -> sonus::engine & = delete;
|
||||
engine(sonus::engine &&other) noexcept;
|
||||
auto operator=(sonus::engine &&other) noexcept -> sonus::engine &;
|
||||
~engine();
|
||||
|
||||
auto start() -> std::expected<void, modulus::error>;
|
||||
auto stop() -> void;
|
||||
auto started() const -> std::bool_t;
|
||||
auto sample_rate() const -> std::uint32_t;
|
||||
auto channels() const -> std::uint32_t;
|
||||
auto set_master_volume(std::float32_t volume) -> std::expected<void, modulus::error>;
|
||||
|
||||
auto play_file(std::filesystem::path const& path, sonus::playback_options options = {}) -> std::expected<sonus::sound_handle, modulus::error>;
|
||||
auto stop(sonus::sound_handle handle) -> std::expected<void, modulus::error>;
|
||||
auto play_file(std::filesystem::path const &path, sonus::playback_options options = {}) -> std::expected<std::uint64_t, modulus::error>;
|
||||
auto stop(std::uint64_t handle) -> std::expected<void, modulus::error>;
|
||||
auto stop_all() -> void;
|
||||
auto is_playing(sonus::sound_handle handle) -> std::expected<std::bool_t, modulus::error>;
|
||||
auto is_playing(std::uint64_t handle) -> std::expected<std::bool_t, modulus::error>;
|
||||
auto collect_finished() -> std::uint64_t;
|
||||
auto active_sound_count() -> std::uint64_t;
|
||||
|
||||
auto start_metronome(sonus::metronome_config config = {}) -> std::expected<void, modulus::error>;
|
||||
auto update_metronome(sonus::metronome_config config) -> std::expected<void, modulus::error>;
|
||||
auto stop_metronome() -> void;
|
||||
auto metronome_active() const -> std::bool_t;
|
||||
|
||||
auto render(std::float32_t *output, std::uint64_t frame_count) -> std::expected<void, modulus::error>;
|
||||
|
||||
private:
|
||||
struct impl;
|
||||
|
||||
explicit engine(std::unique_ptr<sonus::engine::impl> implementation) noexcept;
|
||||
|
||||
std::unique_ptr<sonus::engine::impl> implementation_;
|
||||
};
|
||||
|
||||
|
||||
+6
-2
@@ -16,6 +16,7 @@ COMMANDS = (
|
||||
"package",
|
||||
"lsp",
|
||||
"clangd",
|
||||
"format",
|
||||
)
|
||||
COMMON_DIR = pathlib.Path("build") / "build_common"
|
||||
TOOLS_DIR = pathlib.Path("build") / "build_tools"
|
||||
@@ -60,6 +61,9 @@ def runtime_ready(root):
|
||||
and (root / TOOLS_DIR / "clangd.py").is_file()
|
||||
and (root / RUNTIME_DIR / "modulus" / "cli.py").is_file()
|
||||
and (root / RUNTIME_DIR / "modulus" / "export.py").is_file()
|
||||
and (root / TOOLS_DIR / "format.py").is_file()
|
||||
and (root / RUNTIME_DIR / "modulus" / "formatting.py").is_file()
|
||||
and (root / RUNTIME_DIR / "modulus" / "format_layout.py").is_file()
|
||||
)
|
||||
|
||||
def optional_credentials():
|
||||
@@ -340,9 +344,9 @@ def ensure_runtime(root):
|
||||
|
||||
def delegate(command, argv):
|
||||
root = repo_root()
|
||||
if command == "clangd":
|
||||
if command in {"clangd", "format"}:
|
||||
if not runtime_ready(root):
|
||||
print("Modulus clangd tooling is missing; run ./modulus.py configure first", file=sys.stderr)
|
||||
print(f"Modulus {command} tooling is missing; run ./modulus.py configure first", file=sys.stderr)
|
||||
return 1
|
||||
else:
|
||||
ensure_runtime(root)
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
#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);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
#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> {
|
||||
|
||||
@@ -6,18 +6,20 @@
|
||||
#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) {
|
||||
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);
|
||||
}
|
||||
@@ -26,16 +28,19 @@ public:
|
||||
~read_guard() {
|
||||
owner_.active_render_count_.fetch_sub(1, std::memory_order_seq_cst);
|
||||
}
|
||||
auto get() const -> T const* { return value_; }
|
||||
|
||||
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 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_));
|
||||
@@ -44,7 +49,6 @@ public:
|
||||
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.
|
||||
@@ -52,7 +56,6 @@ public:
|
||||
retired_.clear();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
std::unique_ptr<T> current_{};
|
||||
std::vector<std::unique_ptr<T>> retired_{};
|
||||
|
||||
+23
-87
@@ -26,6 +26,7 @@
|
||||
#include "detail/render_snapshot.hpp"
|
||||
#include "detail/playback_devices.hpp"
|
||||
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto pi = 3.141592653589793238462643383279502884;
|
||||
@@ -110,19 +111,16 @@ 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() {
|
||||
if (initialized) {
|
||||
ma_sound_uninit(&sound);
|
||||
}
|
||||
}
|
||||
|
||||
static auto end_callback(void *user_data, ma_sound *) -> void {
|
||||
auto *self = static_cast<active_sound *>(user_data);
|
||||
if (self != nullptr) {
|
||||
@@ -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() {
|
||||
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() {
|
||||
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> {
|
||||
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;
|
||||
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,7 +372,6 @@ struct sonus::engine::impl {
|
||||
sounds.erase(found);
|
||||
return {};
|
||||
}
|
||||
|
||||
auto stop_all_sounds() -> void {
|
||||
auto lock = std::scoped_lock{mutex};
|
||||
for (auto &entry : sounds) {
|
||||
@@ -412,8 +379,7 @@ struct sonus::engine::impl {
|
||||
}
|
||||
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 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 {
|
||||
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 {
|
||||
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,7 +518,6 @@ 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 {
|
||||
if (metro == nullptr || !metro->enabled) {
|
||||
if (metro != nullptr) {
|
||||
@@ -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,7 +539,6 @@ 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;
|
||||
@@ -610,7 +550,6 @@ struct sonus::engine::impl {
|
||||
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) {
|
||||
@@ -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,7 +567,6 @@ 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);
|
||||
if (self == nullptr || output == nullptr) {
|
||||
@@ -685,14 +622,12 @@ 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;
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -735,7 +670,8 @@ 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;
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -4,23 +4,18 @@ import conan
|
||||
import conan.tools.build
|
||||
import conan.tools.cmake
|
||||
|
||||
|
||||
class sonus_test_package(conan.ConanFile):
|
||||
settings = "os", "arch", "compiler", "build_type"
|
||||
generators = "CMakeDeps", "CMakeToolchain"
|
||||
test_type = "explicit"
|
||||
|
||||
def requirements(self):
|
||||
self.requires(self.tested_reference_str)
|
||||
|
||||
def layout(self):
|
||||
conan.tools.cmake.cmake_layout(self)
|
||||
|
||||
def build(self):
|
||||
cmake = conan.tools.cmake.CMake(self)
|
||||
cmake.configure()
|
||||
cmake.build()
|
||||
|
||||
def test(self):
|
||||
if conan.tools.build.can_run(self):
|
||||
self.run(os.path.join(self.cpp.build.bindirs[0], "sonus-consumer"), env="conanrun")
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
|
||||
|
||||
auto main() -> int {
|
||||
auto config = sonus::audio_config{};
|
||||
config.no_device = true;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <sonus.hpp>
|
||||
#include "detail/playback_devices.hpp"
|
||||
|
||||
|
||||
BOOST_AUTO_TEST_CASE(sonus_updates_cannot_undo_concurrent_start_or_stop) {
|
||||
auto config = sonus::audio_config{};
|
||||
config.no_device = true;
|
||||
|
||||
@@ -8,16 +8,20 @@
|
||||
|
||||
#include "detail/render_snapshot.hpp"
|
||||
|
||||
|
||||
namespace {
|
||||
|
||||
struct snapshot {
|
||||
std::atomic<int> &live;
|
||||
std::array<int, 64> samples{};
|
||||
explicit snapshot(std::atomic<int>& count, int value = 0) : live(count) {
|
||||
explicit snapshot(std::atomic<int> &count, int value = 0)
|
||||
: live(count) {
|
||||
++live;
|
||||
samples.fill(value);
|
||||
}
|
||||
~snapshot() { --live; }
|
||||
~snapshot() {
|
||||
--live;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
+1
-42
@@ -15,6 +15,7 @@
|
||||
|
||||
#include <sonus.hpp>
|
||||
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto pi = 3.141592653589793238462643383279502884;
|
||||
@@ -64,13 +65,11 @@ auto write_u32_le(std::ofstream& output, std::uint32_t value) -> void {
|
||||
auto write_test_wav(std::filesystem::path const &path, std::uint32_t sample_rate, std::uint16_t channels, std::uint32_t frame_count, double frequency_hz) -> void {
|
||||
auto output = std::ofstream{path, std::ios::binary | std::ios::trunc};
|
||||
BOOST_REQUIRE(output.good());
|
||||
|
||||
auto const bits_per_sample = std::uint16_t{16};
|
||||
auto const bytes_per_sample = bits_per_sample / 8U;
|
||||
auto const block_align = static_cast<std::uint16_t>(channels * bytes_per_sample);
|
||||
auto const byte_rate = sample_rate * block_align;
|
||||
auto const data_size = frame_count * block_align;
|
||||
|
||||
write_bytes(output, "RIFF");
|
||||
write_u32_le(output, 36U + data_size);
|
||||
write_bytes(output, "WAVE");
|
||||
@@ -84,7 +83,6 @@ auto write_test_wav(std::filesystem::path const& path, std::uint32_t sample_rate
|
||||
write_u16_le(output, bits_per_sample);
|
||||
write_bytes(output, "data");
|
||||
write_u32_le(output, data_size);
|
||||
|
||||
for (auto frame = std::uint32_t{}; frame < frame_count; ++frame) {
|
||||
auto const t = static_cast<double>(frame) / static_cast<double>(sample_rate);
|
||||
auto const sample = static_cast<std::int16_t>(std::sin(2.0 * pi * frequency_hz * t) * 12000.0);
|
||||
@@ -92,7 +90,6 @@ auto write_test_wav(std::filesystem::path const& path, std::uint32_t sample_rate
|
||||
write_u16_le(output, static_cast<std::uint16_t>(sample));
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_REQUIRE(output.good());
|
||||
}
|
||||
|
||||
@@ -101,7 +98,6 @@ auto write_test_wav(std::filesystem::path const& path, std::uint32_t sample_rate
|
||||
BOOST_AUTO_TEST_CASE(sonus_validates_configs) {
|
||||
auto audio = sonus::audio_config{};
|
||||
BOOST_TEST(static_cast<bool>(sonus::validate(audio)));
|
||||
|
||||
audio.sample_rate = 0;
|
||||
BOOST_TEST(!sonus::validate(audio));
|
||||
audio = {};
|
||||
@@ -136,7 +132,6 @@ BOOST_AUTO_TEST_CASE(sonus_validates_configs) {
|
||||
audio = {};
|
||||
audio.playback_device_name.assign("a\0b", 3);
|
||||
BOOST_TEST(!sonus::validate(audio));
|
||||
|
||||
auto metronome = sonus::metronome_config{};
|
||||
BOOST_TEST(static_cast<bool>(sonus::validate(metronome)));
|
||||
metronome.bpm = 0;
|
||||
@@ -155,13 +150,11 @@ BOOST_AUTO_TEST_CASE(sonus_playback_devices_can_be_enumerated) {
|
||||
BOOST_TEST(!enumerated.error().message().empty());
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto const &device : *enumerated) {
|
||||
BOOST_TEST(!device.id.empty());
|
||||
BOOST_TEST(device.id.find('\0') == std::string::npos);
|
||||
BOOST_TEST(device.name.find('\0') == std::string::npos);
|
||||
}
|
||||
|
||||
if (!enumerated->empty()) {
|
||||
auto config = sonus::audio_config{};
|
||||
config.playback_device_id = enumerated->front().id;
|
||||
@@ -172,11 +165,9 @@ BOOST_AUTO_TEST_CASE(sonus_playback_devices_can_be_enumerated) {
|
||||
BOOST_AUTO_TEST_CASE(sonus_no_device_engine_renders_silence_when_idle) {
|
||||
auto created = sonus::engine::create(no_device_config());
|
||||
BOOST_REQUIRE(created);
|
||||
|
||||
auto samples = std::vector<std::float32_t>(2048U * created->channels(), 1.0F);
|
||||
auto rendered = created->render(samples.data(), 2048U);
|
||||
BOOST_REQUIRE(rendered);
|
||||
|
||||
BOOST_TEST(std::all_of(samples.begin(), samples.end(), [](auto sample) {
|
||||
return sample == 0.0F;
|
||||
}));
|
||||
@@ -185,7 +176,6 @@ BOOST_AUTO_TEST_CASE(sonus_no_device_engine_renders_silence_when_idle) {
|
||||
BOOST_AUTO_TEST_CASE(sonus_master_volume_can_be_changed_concurrently) {
|
||||
auto created = sonus::engine::create(no_device_config());
|
||||
BOOST_REQUIRE(created);
|
||||
|
||||
auto failed = std::atomic_bool{false};
|
||||
auto volume_worker = [&created, &failed](std::uint32_t offset) {
|
||||
for (auto index = std::uint32_t{}; index < 250U; ++index) {
|
||||
@@ -203,14 +193,12 @@ BOOST_AUTO_TEST_CASE(sonus_master_volume_can_be_changed_concurrently) {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
auto first = std::thread{volume_worker, 0U};
|
||||
auto second = std::thread{volume_worker, 37U};
|
||||
auto renderer = std::thread{render_worker};
|
||||
first.join();
|
||||
second.join();
|
||||
renderer.join();
|
||||
|
||||
BOOST_TEST(!failed.load(std::memory_order_acquire));
|
||||
BOOST_TEST(!created->set_master_volume(-0.01F));
|
||||
BOOST_TEST(!created->set_master_volume(1.01F));
|
||||
@@ -222,21 +210,17 @@ BOOST_AUTO_TEST_CASE(sonus_metronome_renders_accented_clicks) {
|
||||
config.channels = 2;
|
||||
auto created = sonus::engine::create(config);
|
||||
BOOST_REQUIRE(created);
|
||||
|
||||
auto metro = sonus::metronome_config{};
|
||||
metro.bpm = 120;
|
||||
metro.beats_per_bar = 4;
|
||||
metro.volume = 0.70F;
|
||||
metro.click_duration = std::chrono::milliseconds{25};
|
||||
BOOST_REQUIRE(created->start_metronome(metro));
|
||||
|
||||
auto samples = std::vector<std::float32_t>(config.sample_rate * config.channels);
|
||||
BOOST_REQUIRE(created->render(samples.data(), config.sample_rate));
|
||||
|
||||
auto downbeat_peak = peak(samples, 0, 2400, config.channels);
|
||||
auto beat_peak = peak(samples, config.sample_rate / 2, 2400, config.channels);
|
||||
auto between_peak = peak(samples, config.sample_rate / 4, 2400, config.channels);
|
||||
|
||||
BOOST_TEST(downbeat_peak > 0.2F);
|
||||
BOOST_TEST(beat_peak > 0.1F);
|
||||
BOOST_TEST(downbeat_peak > beat_peak * 1.2F);
|
||||
@@ -246,12 +230,10 @@ BOOST_AUTO_TEST_CASE(sonus_metronome_renders_accented_clicks) {
|
||||
BOOST_AUTO_TEST_CASE(sonus_metronome_update_keeps_inactive_metronome_stopped) {
|
||||
auto created = sonus::engine::create(no_device_config());
|
||||
BOOST_REQUIRE(created);
|
||||
|
||||
auto metro = sonus::metronome_config{};
|
||||
metro.bpm = 140;
|
||||
BOOST_REQUIRE(created->update_metronome(metro));
|
||||
BOOST_TEST(!created->metronome_active());
|
||||
|
||||
BOOST_REQUIRE(created->start_metronome(metro));
|
||||
BOOST_TEST(created->metronome_active());
|
||||
metro.bpm = 180;
|
||||
@@ -265,7 +247,6 @@ BOOST_AUTO_TEST_CASE(sonus_stopped_metronome_updates_stay_silent_and_restart) {
|
||||
config.channels = 2;
|
||||
auto created = sonus::engine::create(config);
|
||||
BOOST_REQUIRE(created);
|
||||
|
||||
for (auto index = std::uint32_t{}; index < 500U; ++index) {
|
||||
auto metro = sonus::metronome_config{};
|
||||
metro.bpm = 60U + (index % 180U);
|
||||
@@ -274,13 +255,11 @@ BOOST_AUTO_TEST_CASE(sonus_stopped_metronome_updates_stay_silent_and_restart) {
|
||||
BOOST_REQUIRE(created->update_metronome(metro));
|
||||
BOOST_TEST(!created->metronome_active());
|
||||
}
|
||||
|
||||
auto samples = std::vector<std::float32_t>(4096U * config.channels, 1.0F);
|
||||
BOOST_REQUIRE(created->render(samples.data(), 4096U));
|
||||
BOOST_TEST(std::all_of(samples.begin(), samples.end(), [](auto sample) {
|
||||
return sample == 0.0F;
|
||||
}));
|
||||
|
||||
auto metro = sonus::metronome_config{};
|
||||
metro.bpm = 120;
|
||||
BOOST_REQUIRE(created->start_metronome(metro));
|
||||
@@ -292,7 +271,6 @@ BOOST_AUTO_TEST_CASE(sonus_stopped_metronome_updates_stay_silent_and_restart) {
|
||||
BOOST_AUTO_TEST_CASE(sonus_metronome_can_be_updated_concurrently) {
|
||||
auto created = sonus::engine::create(no_device_config());
|
||||
BOOST_REQUIRE(created);
|
||||
|
||||
auto worker = [&created](std::uint32_t offset) {
|
||||
for (auto index = std::uint32_t{}; index < 100U; ++index) {
|
||||
auto metro = sonus::metronome_config{};
|
||||
@@ -304,14 +282,12 @@ BOOST_AUTO_TEST_CASE(sonus_metronome_can_be_updated_concurrently) {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
auto first = std::thread{worker, 0U};
|
||||
auto second = std::thread{worker, 11U};
|
||||
auto third = std::thread{worker, 23U};
|
||||
first.join();
|
||||
second.join();
|
||||
third.join();
|
||||
|
||||
auto samples = std::vector<std::float32_t>(4096U * created->channels());
|
||||
BOOST_TEST(static_cast<bool>(created->render(samples.data(), 4096U)));
|
||||
}
|
||||
@@ -319,7 +295,6 @@ BOOST_AUTO_TEST_CASE(sonus_metronome_can_be_updated_concurrently) {
|
||||
BOOST_AUTO_TEST_CASE(sonus_file_playback_reports_missing_files) {
|
||||
auto created = sonus::engine::create(no_device_config());
|
||||
BOOST_REQUIRE(created);
|
||||
|
||||
auto missing = std::filesystem::path{"/tmp/sonus-missing-file.wav"};
|
||||
auto played = created->play_file(missing);
|
||||
BOOST_TEST(!played);
|
||||
@@ -331,18 +306,14 @@ BOOST_AUTO_TEST_CASE(sonus_file_playback_renders_audio) {
|
||||
config.channels = 2;
|
||||
auto created = sonus::engine::create(config);
|
||||
BOOST_REQUIRE(created);
|
||||
|
||||
auto path = test_file("sonus-tone-long.wav");
|
||||
write_test_wav(path, config.sample_rate, 1, config.sample_rate, 440.0);
|
||||
|
||||
auto played = created->play_file(path);
|
||||
BOOST_REQUIRE(played);
|
||||
BOOST_TEST(created->active_sound_count() == 1U);
|
||||
|
||||
auto samples = std::vector<std::float32_t>(4096U * config.channels);
|
||||
BOOST_REQUIRE(created->render(samples.data(), 4096U));
|
||||
BOOST_TEST(peak(samples, 0, 4096U, config.channels) > 0.05F);
|
||||
|
||||
auto playing = created->is_playing(*played);
|
||||
BOOST_REQUIRE(playing);
|
||||
BOOST_TEST(*playing);
|
||||
@@ -354,16 +325,12 @@ BOOST_AUTO_TEST_CASE(sonus_finished_file_playback_can_be_collected) {
|
||||
config.channels = 2;
|
||||
auto created = sonus::engine::create(config);
|
||||
BOOST_REQUIRE(created);
|
||||
|
||||
auto path = test_file("sonus-tone-short.wav");
|
||||
write_test_wav(path, config.sample_rate, 1, 256U, 880.0);
|
||||
|
||||
auto played = created->play_file(path);
|
||||
BOOST_REQUIRE(played);
|
||||
|
||||
auto samples = std::vector<std::float32_t>(2048U * config.channels);
|
||||
BOOST_REQUIRE(created->render(samples.data(), 2048U));
|
||||
|
||||
auto playing = created->is_playing(*played);
|
||||
BOOST_REQUIRE(playing);
|
||||
BOOST_TEST(!*playing);
|
||||
@@ -376,10 +343,8 @@ BOOST_AUTO_TEST_CASE(sonus_repeated_short_sounds_are_retired) {
|
||||
config.channels = 2;
|
||||
auto created = sonus::engine::create(config);
|
||||
BOOST_REQUIRE(created);
|
||||
|
||||
auto path = test_file("sonus-tone-repeated.wav");
|
||||
write_test_wav(path, config.sample_rate, 1, 64U, 660.0);
|
||||
|
||||
auto samples = std::vector<std::float32_t>(512U * config.channels);
|
||||
auto collected = std::uint64_t{};
|
||||
for (auto index = 0; index < 20; ++index) {
|
||||
@@ -388,7 +353,6 @@ BOOST_AUTO_TEST_CASE(sonus_repeated_short_sounds_are_retired) {
|
||||
BOOST_REQUIRE(created->render(samples.data(), 512U));
|
||||
collected += created->collect_finished();
|
||||
}
|
||||
|
||||
BOOST_TEST(collected == 20U);
|
||||
BOOST_TEST(created->active_sound_count() == 0U);
|
||||
}
|
||||
@@ -399,10 +363,8 @@ BOOST_AUTO_TEST_CASE(sonus_no_device_render_serializes_with_control_paths) {
|
||||
config.channels = 2;
|
||||
auto created = sonus::engine::create(config);
|
||||
BOOST_REQUIRE(created);
|
||||
|
||||
auto path = test_file("sonus-tone-concurrent.wav");
|
||||
write_test_wav(path, config.sample_rate, 1, 512U, 330.0);
|
||||
|
||||
auto render_worker = [&created, channels = config.channels] {
|
||||
auto samples = std::vector<std::float32_t>(512U * channels);
|
||||
for (auto index = 0; index < 150; ++index) {
|
||||
@@ -420,7 +382,6 @@ BOOST_AUTO_TEST_CASE(sonus_no_device_render_serializes_with_control_paths) {
|
||||
} else {
|
||||
static_cast<void>(created->start_metronome(metro));
|
||||
}
|
||||
|
||||
auto played = created->play_file(path);
|
||||
if (played && index % 3 == 0) {
|
||||
static_cast<void>(created->stop(*played));
|
||||
@@ -428,12 +389,10 @@ BOOST_AUTO_TEST_CASE(sonus_no_device_render_serializes_with_control_paths) {
|
||||
static_cast<void>(created->collect_finished());
|
||||
}
|
||||
};
|
||||
|
||||
auto renderer = std::thread{render_worker};
|
||||
auto controller = std::thread{control_worker};
|
||||
renderer.join();
|
||||
controller.join();
|
||||
|
||||
created->stop_all();
|
||||
created->stop_metronome();
|
||||
BOOST_TEST(created->active_sound_count() == 0U);
|
||||
|
||||
Reference in New Issue
Block a user