Tone Generation
publish / publish (push) Failing after 10m56s

This commit is contained in:
khodowany
2026-09-18 01:17:18 -05:00
parent 59b96ddf95
commit 95f6f6aaad
9 changed files with 465 additions and 1 deletions
+1
View File
@@ -27,6 +27,7 @@ endif()
add_library(sonus add_library(sonus
src/detail/errors.cpp src/detail/errors.cpp
src/engine.cpp src/engine.cpp
src/sequence.cpp
) )
target_include_directories(sonus PUBLIC target_include_directories(sonus PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include> $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
+11 -1
View File
@@ -1,6 +1,6 @@
# Sonus # Sonus
Sonus is a C++23 audio playback library built on miniaudio. It provides a small thread-safe engine for sound file playback and generated metronome clicks, with `std::expected`-based error reporting through Modulus. Sonus is a C++23 audio playback library built on miniaudio. It provides a small thread-safe engine for sound file playback, PCM sample sequences, and generated metronome clicks, with `std::expected`-based error reporting through Modulus.
Sonus keeps miniaudio out of public headers and is intended for embedded Linux applications where startup cost, predictable ownership, and low callback overhead matter. Sonus keeps miniaudio out of public headers and is intended for embedded Linux applications where startup cost, predictable ownership, and low callback overhead matter.
@@ -16,6 +16,16 @@ Control calls on one engine are serialized and may run concurrently with playbac
Call `collect_finished()` periodically to release sounds that reach EOF without further playback or status calls. Sonus compiles miniaudio internally; installed consumers link `sonus::sonus` and need only Modulus core and its Boost dependency. Miniaudio headers are needed only to build Sonus. Call `collect_finished()` periodically to release sounds that reach EOF without further playback or status calls. Sonus compiles miniaudio internally; installed consumers link `sonus::sonus` and need only Modulus core and its Boost dependency. Miniaudio headers are needed only to build Sonus.
## Sample Sequences
`sample_sequence` owns mono PCM buffers, frame-based sample events, a caller-provided release envelope, total duration, gain, and voice capacity. Each event selects a buffer and an independent start/duration/gain. The attack plays once; the half-open `[loop_start, loop_end)` region sustains until the event ends, then the stored release envelope is applied. Buffers must match the engine sample rate. Sonus does not interpret musical beats, pitches, repeats, or instrument definitions and does not synthesize sequence tones.
Use `prepared_sequence::create(sequence, sample_rate)` on a worker/control thread to validate data and preallocate the actual peak voice count. Move the result into `engine::play_sequence(prepared)` for serialized publication. The convenience `play_sequence(sample_sequence)` also prepares synchronously on its caller. `load_sample(path, sample_rate)` decodes bounded mono PCM through miniaudio off the callback; set its loop boundaries before preparation. Prepared sequences are single-use, move-only objects and cannot be mutated after validation.
The returned `playback_session` is a copyable thread-safe status/cancellation handle. `status()` distinguishes `PLAYING`, `COMPLETED`, `STOPPED`, and `FAILED`; natural completion includes the entire declared timeline and all release tails. Replacing a sequence, `stop_sequence()`, `stop_all()`, destruction, or `session.stop()` cancels it without reporting completion. Device `stop()` suspends rendering; use sequence cancellation when a session must not resume. Session status survives engine-side cleanup. Call `collect_finished()` periodically to release completed/cancelled PCM on a control thread. Keep handles away from final destruction in your own real-time callbacks.
One sequence is active per engine and can mix with file playback/metronome; master volume applies to it. Rendering uses the engine sample clock with no callback allocation, I/O, locking, trigonometry, or buffer destruction. Concurrent cancellation takes effect by the next audio buffer. Overlapping equal pitches have independent voices; capacity is checked including release tails, never handled by stealing notes. Preparation limits are 1,000,000 events, 4,096 samples, 64 MiB decoded PCM, 256 voices maximum (64 default), 24 hours, and a release envelope from one to zero of at most 500 ms. Gains/PCM must be finite and bounded; final output is clamped to unit range. Applications may impose stricter limits.
## Build ## 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. 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.
+4
View File
@@ -8,6 +8,7 @@
#include <modulus.hpp> #include <modulus.hpp>
#include <sonus/config.hpp> #include <sonus/config.hpp>
#include <sonus/sequence.hpp>
namespace sonus { namespace sonus {
@@ -33,6 +34,9 @@ public:
auto play_file(std::filesystem::path const &path, sonus::playback_options options = {}) -> std::expected<std::uint64_t, 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(std::uint64_t handle) -> std::expected<void, modulus::error>;
auto stop_all() -> void; auto stop_all() -> void;
auto play_sequence(sonus::sample_sequence sequence) -> std::expected<sonus::playback_session, modulus::error>;
auto play_sequence(sonus::prepared_sequence sequence) -> std::expected<sonus::playback_session, modulus::error>;
auto stop_sequence() -> void;
auto is_playing(std::uint64_t 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 collect_finished() -> std::uint64_t;
auto active_sound_count() -> std::uint64_t; auto active_sound_count() -> std::uint64_t;
+79
View File
@@ -0,0 +1,79 @@
#pragma once
#include <atomic>
#include <cstdint>
#include <expected>
#include <filesystem>
#include <memory>
#include <vector>
#include <modulus.hpp>
namespace sonus {
namespace detail {
struct sequence_state;
class sequence_player;
}
enum class playback_status : std::uint8_t {
PLAYING,
COMPLETED,
STOPPED,
FAILED,
};
struct sample_buffer {
std::uint32_t sample_rate{};
std::vector<std::float32_t> frames{};
std::uint64_t loop_start{};
std::uint64_t loop_end{};
};
struct sample_event {
std::uint32_t sample{};
std::uint64_t start_frame{};
std::uint64_t duration_frames{};
std::float32_t gain{1.0F};
};
struct sample_sequence {
std::vector<sonus::sample_buffer> samples{};
std::vector<sonus::sample_event> events{};
std::vector<std::float32_t> release_envelope{};
std::uint64_t duration_frames{};
std::uint32_t max_voices{64};
std::float32_t gain{0.25F};
};
class prepared_sequence {
public:
static auto create(sonus::sample_sequence sequence, std::uint32_t sample_rate) -> std::expected<sonus::prepared_sequence, modulus::error>;
prepared_sequence(prepared_sequence const &) = delete;
auto operator=(prepared_sequence const &) -> prepared_sequence & = delete;
prepared_sequence(prepared_sequence &&) noexcept;
auto operator=(prepared_sequence &&) noexcept -> prepared_sequence &;
~prepared_sequence();
private:
friend class sonus::detail::sequence_player;
explicit prepared_sequence(std::unique_ptr<sonus::detail::sequence_state> state);
std::unique_ptr<sonus::detail::sequence_state> state_;
};
class playback_session {
public:
playback_session() = default;
explicit playback_session(std::shared_ptr<std::atomic<sonus::playback_status>> status);
auto status() const -> sonus::playback_status;
auto stop() const -> void;
private:
std::shared_ptr<std::atomic<sonus::playback_status>> status_{};
};
auto load_sample(std::filesystem::path const &path, std::uint32_t sample_rate) -> std::expected<sonus::sample_buffer, modulus::error>;
}
+38
View File
@@ -0,0 +1,38 @@
#pragma once
#include <mutex>
#include <sonus/sequence.hpp>
#include "render_snapshot.hpp"
namespace sonus::detail {
struct sequence_voice {
std::size_t event{};
std::bool_t active{};
};
struct sequence_state {
sonus::sample_sequence sequence{};
std::shared_ptr<std::atomic<sonus::playback_status>> status{};
mutable std::vector<sonus::detail::sequence_voice> voices{};
mutable std::uint64_t frame{};
mutable std::size_t next_event{};
std::uint64_t end_frame{};
std::uint32_t sample_rate{};
};
class sequence_player {
public:
auto play(sonus::prepared_sequence sequence, std::uint32_t sample_rate) -> std::expected<sonus::playback_session, modulus::error>;
auto stop() -> void;
auto collect() -> void;
auto fail() -> void;
auto render(std::float32_t *output, std::uint64_t frames, std::uint32_t channels, std::float32_t gain) -> void;
private:
std::mutex mutex_{};
sonus::detail::render_snapshot<sonus::detail::sequence_state> snapshots_{};
};
}
+58
View File
@@ -25,6 +25,7 @@
#include <sonus/detail/errors.hpp> #include <sonus/detail/errors.hpp>
#include "detail/render_snapshot.hpp" #include "detail/render_snapshot.hpp"
#include "detail/playback_devices.hpp" #include "detail/playback_devices.hpp"
#include "detail/sequence_player.hpp"
namespace { namespace {
@@ -167,6 +168,8 @@ struct sonus::engine::impl {
mutable std::mutex render_mutex{}; mutable std::mutex render_mutex{};
std::unordered_map<std::uint64_t, std::unique_ptr<active_sound>> sounds{}; std::unordered_map<std::uint64_t, std::unique_ptr<active_sound>> sounds{};
std::uint64_t next_sound_handle{1}; 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{}; sonus::detail::render_snapshot<metronome_render_config> metronome_configs{};
std::atomic_bool metronome_enabled{false}; std::atomic_bool metronome_enabled{false};
std::uint32_t next_metronome_generation{1}; std::uint32_t next_metronome_generation{1};
@@ -184,6 +187,7 @@ struct sonus::engine::impl {
} }
auto initialize(sonus::audio_config value) -> std::expected<void, modulus::error> { auto initialize(sonus::audio_config value) -> std::expected<void, modulus::error> {
config = std::move(value); config = std::move(value);
sequence_volume.store(config.master_volume, std::memory_order_relaxed);
publish_metronome_locked(make_disabled_metronome_config_locked()); publish_metronome_locked(make_disabled_metronome_config_locked());
auto engine_config = ma_engine_config_init(); auto engine_config = ma_engine_config_init();
engine_config.noDevice = MA_TRUE; engine_config.noDevice = MA_TRUE;
@@ -268,6 +272,7 @@ struct sonus::engine::impl {
device_initialized = false; device_initialized = false;
} }
sounds.clear(); sounds.clear();
sequences.stop();
if (mixer_initialized) { if (mixer_initialized) {
ma_engine_uninit(&mixer); ma_engine_uninit(&mixer);
mixer_initialized = false; mixer_initialized = false;
@@ -315,6 +320,7 @@ struct sonus::engine::impl {
return sonus::detail::fail_void(miniaudio_error(result, "failed to set engine volume")); return sonus::detail::fail_void(miniaudio_error(result, "failed to set engine volume"));
} }
config.master_volume = volume; config.master_volume = volume;
sequence_volume.store(volume, std::memory_order_relaxed);
return {}; return {};
} }
auto play_file(std::filesystem::path const &path, sonus::playback_options options) -> std::expected<std::uint64_t, modulus::error> { auto play_file(std::filesystem::path const &path, sonus::playback_options options) -> std::expected<std::uint64_t, modulus::error> {
@@ -374,6 +380,7 @@ struct sonus::engine::impl {
} }
auto stop_all_sounds() -> void { auto stop_all_sounds() -> void {
auto lock = std::scoped_lock{mutex}; auto lock = std::scoped_lock{mutex};
sequences.stop();
for (auto &entry : sounds) { for (auto &entry : sounds) {
static_cast<void>(ma_sound_stop(&entry.second->sound)); static_cast<void>(ma_sound_stop(&entry.second->sound));
} }
@@ -396,6 +403,7 @@ struct sonus::engine::impl {
} }
auto collect_finished_sounds() -> std::uint64_t { auto collect_finished_sounds() -> std::uint64_t {
auto lock = std::scoped_lock{mutex}; auto lock = std::scoped_lock{mutex};
sequences.collect();
auto const collected = collect_finished_locked(); auto const collected = collect_finished_locked();
collect_retired_metronome_configs_locked(); collect_retired_metronome_configs_locked();
return collected; return collected;
@@ -512,10 +520,12 @@ struct sonus::engine::impl {
std::fill(output, output + sample_count, 0.0F); std::fill(output, output + sample_count, 0.0F);
auto result = ma_engine_read_pcm_frames(&mixer, output, frame_count, nullptr); auto result = ma_engine_read_pcm_frames(&mixer, output, frame_count, nullptr);
if (result != MA_SUCCESS) { if (result != MA_SUCCESS) {
sequences.fail();
std::fill(output, output + sample_count, 0.0F); std::fill(output, output + sample_count, 0.0F);
return result; return result;
} }
mix_metronome(output, frame_count, guard.get()); mix_metronome(output, frame_count, guard.get());
sequences.render(output, frame_count, config.channels, sequence_volume.load(std::memory_order_relaxed));
return MA_SUCCESS; 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 {
@@ -614,6 +624,54 @@ auto sonus::validate(sonus::audio_config const &config) -> std::expected<void, m
return {}; 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"));
}
auto decoder = ma_decoder{};
auto config = ma_decoder_config_init(ma_format_f32, 1U, sample_rate);
auto status = ma_decoder_init_file(path.string().c_str(), &config, &decoder);
if (status != MA_SUCCESS) {
return sonus::detail::fail<sonus::sample_buffer>(miniaudio_error(status, "failed to decode sample"));
}
auto guard = std::unique_ptr<ma_decoder, decltype(&ma_decoder_uninit)>{&decoder, &ma_decoder_uninit};
auto frames = ma_uint64{};
status = ma_decoder_get_length_in_pcm_frames(&decoder, &frames);
if (status != MA_SUCCESS || frames == 0U || frames > 16U * 1024U * 1024U) {
return sonus::detail::fail<sonus::sample_buffer>(invalid("sample length is unavailable or exceeds 64 MiB"));
}
auto result = sonus::sample_buffer{.sample_rate = sample_rate, .frames = std::vector<std::float32_t>(frames), .loop_start = 0U, .loop_end = frames};
auto read = ma_uint64{};
status = ma_decoder_read_pcm_frames(&decoder, result.frames.data(), frames, &read);
if ((status != MA_SUCCESS && status != MA_AT_END) || read != frames) {
return sonus::detail::fail<sonus::sample_buffer>(invalid("sample audio is truncated"));
}
return result;
}
auto sonus::engine::play_sequence(sonus::sample_sequence sequence) -> std::expected<sonus::playback_session, modulus::error> {
auto prepared = sonus::prepared_sequence::create(std::move(sequence), sample_rate());
if (!prepared) {
return std::unexpected(prepared.error());
}
return play_sequence(std::move(*prepared));
}
auto sonus::engine::play_sequence(sonus::prepared_sequence sequence) -> std::expected<sonus::playback_session, modulus::error> {
if (!implementation_) {
return sonus::detail::fail<sonus::playback_session>(unavailable("engine is not initialized"));
}
auto lock = std::scoped_lock{implementation_->mutex};
return implementation_->sequences.play(std::move(sequence), implementation_->config.sample_rate);
}
auto sonus::engine::stop_sequence() -> void {
if (implementation_) {
auto lock = std::scoped_lock{implementation_->mutex};
implementation_->sequences.stop();
}
}
auto sonus::enumerate_playback_devices() -> std::expected<std::vector<sonus::device_info>, modulus::error> { auto sonus::enumerate_playback_devices() -> std::expected<std::vector<sonus::device_info>, modulus::error> {
auto context = temporary_context{}; auto context = temporary_context{};
auto context_config = ma_context_config_init(); auto context_config = ma_context_config_init();
+169
View File
@@ -0,0 +1,169 @@
#include <sonus/sequence.hpp>
#include <algorithm>
#include <cmath>
#include <limits>
#include <utility>
#include <sonus/detail/errors.hpp>
#include "detail/sequence_player.hpp"
sonus::playback_session::playback_session(std::shared_ptr<std::atomic<sonus::playback_status>> status)
: status_{std::move(status)} {}
auto sonus::playback_session::status() const -> sonus::playback_status {
return status_ ? status_->load(std::memory_order_acquire) : sonus::playback_status::STOPPED;
}
auto sonus::playback_session::stop() const -> void {
if (status_) {
auto expected = sonus::playback_status::PLAYING;
status_->compare_exchange_strong(expected, sonus::playback_status::STOPPED, std::memory_order_acq_rel);
}
}
sonus::prepared_sequence::prepared_sequence(std::unique_ptr<sonus::detail::sequence_state> state)
: state_{std::move(state)} {}
sonus::prepared_sequence::prepared_sequence(prepared_sequence &&) noexcept = default;
auto sonus::prepared_sequence::operator=(prepared_sequence &&) noexcept -> prepared_sequence & = default;
sonus::prepared_sequence::~prepared_sequence() = default;
auto sonus::prepared_sequence::create(sonus::sample_sequence sequence, std::uint32_t sample_rate) -> std::expected<sonus::prepared_sequence, modulus::error> {
auto fail = [](std::string message) -> std::expected<sonus::prepared_sequence, modulus::error> {
return sonus::detail::fail<sonus::prepared_sequence>(sonus::detail::error_invalid_argument, std::move(message));
};
if (sample_rate < 8000U || sample_rate > 384000U || sequence.max_voices == 0U || sequence.max_voices > 256U || sequence.events.size() > 1000000U || sequence.samples.size() > 4096U || sequence.duration_frames == 0U || sequence.duration_frames > static_cast<std::uint64_t>(sample_rate) * 86400U || !std::isfinite(sequence.gain) || sequence.gain < 0.0F || sequence.gain > 1.0F) {
return fail("invalid sequence limits or gain");
}
if (sequence.release_envelope.empty() || sequence.release_envelope.size() > sample_rate / 2U || sequence.release_envelope.front() != 1.0F || sequence.release_envelope.back() != 0.0F || std::any_of(sequence.release_envelope.begin(), sequence.release_envelope.end(), [](auto value) { return !std::isfinite(value) || value < 0.0F || value > 1.0F; })) {
return fail("release envelope must run from one to zero within 500 milliseconds");
}
auto sample_count = std::uint64_t{};
for (auto const &sample : sequence.samples) {
sample_count += sample.frames.size();
if (sample_count > 16U * 1024U * 1024U || sample.sample_rate != sample_rate || sample.loop_start >= sample.loop_end || sample.loop_end > sample.frames.size() || std::any_of(sample.frames.begin(), sample.frames.end(), [](auto value) { return !std::isfinite(value) || std::abs(value) > 1.0F; })) {
return fail("invalid sample rate, loop, PCM data or sample memory limit");
}
}
auto endpoints = std::vector<std::pair<std::uint64_t, std::int32_t>>{};
endpoints.reserve(sequence.events.size() * 2U);
auto end = sequence.duration_frames;
auto previous_start = std::uint64_t{};
for (auto const &event : sequence.events) {
if (event.sample >= sequence.samples.size() || event.duration_frames == 0U || event.start_frame < previous_start || event.start_frame >= sequence.duration_frames || event.duration_frames > sequence.duration_frames - event.start_frame || !std::isfinite(event.gain) || event.gain < 0.0F || event.gain > 1.0F) {
return fail("invalid or unsorted sample event");
}
auto release_end = event.start_frame + event.duration_frames + sequence.release_envelope.size();
endpoints.emplace_back(event.start_frame, 1);
endpoints.emplace_back(release_end, -1);
end = std::max(end, release_end);
previous_start = event.start_frame;
}
std::sort(endpoints.begin(), endpoints.end());
auto voices = std::int32_t{};
auto peak = std::int32_t{};
for (auto const &[frame, change] : endpoints) {
voices += change;
peak = std::max(peak, voices);
if (voices > static_cast<std::int32_t>(sequence.max_voices)) {
return fail("sequence exceeds simultaneous voice capacity including releases");
}
}
auto next = std::make_unique<sonus::detail::sequence_state>();
next->voices.resize(static_cast<std::size_t>(peak));
next->sequence = std::move(sequence);
next->status = std::make_shared<std::atomic<sonus::playback_status>>(sonus::playback_status::PLAYING);
next->end_frame = end;
next->sample_rate = sample_rate;
return sonus::prepared_sequence{std::move(next)};
}
auto sonus::detail::sequence_player::play(sonus::prepared_sequence sequence, std::uint32_t sample_rate) -> std::expected<sonus::playback_session, modulus::error> {
if (!sequence.state_ || sequence.state_->sample_rate != sample_rate) {
return sonus::detail::fail<sonus::playback_session>(sonus::detail::error_invalid_argument, "empty sequence or incompatible sample rate");
}
auto next = std::move(sequence.state_);
auto result = sonus::playback_session{next->status};
auto lock = std::scoped_lock{mutex_};
if (auto current = snapshots_.current()) {
sonus::playback_session{current->status}.stop();
}
snapshots_.publish(std::move(next));
return result;
}
auto sonus::detail::sequence_player::stop() -> void {
auto lock = std::scoped_lock{mutex_};
if (auto current = snapshots_.current()) {
sonus::playback_session{current->status}.stop();
}
snapshots_.publish(nullptr);
}
auto sonus::detail::sequence_player::collect() -> void {
auto lock = std::scoped_lock{mutex_};
if (auto current = snapshots_.current(); current && current->status->load(std::memory_order_acquire) != sonus::playback_status::PLAYING) {
snapshots_.publish(nullptr);
}
snapshots_.collect();
}
auto sonus::detail::sequence_player::fail() -> void {
auto guard = snapshots_.read();
if (auto current = guard.get()) {
auto expected = sonus::playback_status::PLAYING;
current->status->compare_exchange_strong(expected, sonus::playback_status::FAILED, std::memory_order_release);
}
}
auto sonus::detail::sequence_player::render(std::float32_t *output, std::uint64_t frames, std::uint32_t channels, std::float32_t gain) -> void {
auto guard = snapshots_.read();
auto current = guard.get();
if (!current || current->status->load(std::memory_order_acquire) != sonus::playback_status::PLAYING) {
return;
}
auto const &sequence = current->sequence;
for (auto frame = std::uint64_t{}; frame < frames && current->frame < current->end_frame; ++frame, ++current->frame) {
for (auto &voice : current->voices) {
if (voice.active) {
auto const &event = sequence.events[voice.event];
if (current->frame >= event.start_frame + event.duration_frames + sequence.release_envelope.size()) {
voice.active = false;
}
}
}
while (current->next_event < sequence.events.size() && sequence.events[current->next_event].start_frame == current->frame) {
auto slot = std::find_if(current->voices.begin(), current->voices.end(), [](auto const &voice) { return !voice.active; });
if (slot == current->voices.end()) {
auto expected = sonus::playback_status::PLAYING;
current->status->compare_exchange_strong(expected, sonus::playback_status::FAILED, std::memory_order_release);
return;
}
*slot = {.event = current->next_event++, .active = true};
}
auto mixed = std::float32_t{};
for (auto const &voice : current->voices) {
if (!voice.active) {
continue;
}
auto const &event = sequence.events[voice.event];
auto const &sample = sequence.samples[event.sample];
auto age = current->frame - event.start_frame;
auto index = age < sample.loop_end ? age : sample.loop_start + (age - sample.loop_start) % (sample.loop_end - sample.loop_start);
auto release = age < event.duration_frames ? 1.0F : sequence.release_envelope[age - event.duration_frames];
mixed += sample.frames[index] * event.gain * release;
}
for (auto channel = std::uint32_t{}; channel < channels; ++channel) {
auto &destination = output[frame * channels + channel];
destination = std::clamp(destination + mixed * sequence.gain * gain, -1.0F, 1.0F);
}
}
if (current->frame == current->end_frame) {
auto expected = sonus::playback_status::PLAYING;
current->status->compare_exchange_strong(expected, sonus::playback_status::COMPLETED, std::memory_order_release);
}
}
+1
View File
@@ -7,6 +7,7 @@ add_executable(sonus-test
test.cpp test.cpp
render_snapshot.cpp render_snapshot.cpp
control.cpp control.cpp
sequence.cpp
) )
find_package(Threads REQUIRED) find_package(Threads REQUIRED)
target_include_directories(sonus-test PRIVATE target_include_directories(sonus-test PRIVATE
+104
View File
@@ -0,0 +1,104 @@
#include <boost/test/unit_test.hpp>
#include <array>
#include <thread>
#include <sonus.hpp>
namespace {
auto fixture() -> sonus::sample_sequence {
return {.samples = {{.sample_rate = 48000U, .frames = {0.1F, 0.2F, 0.3F, 0.4F}, .loop_start = 2U, .loop_end = 4U}},
.events = {{.sample = 0U, .start_frame = 2U, .duration_frames = 6U}},
.release_envelope = {1.0F, 0.5F, 0.0F},
.duration_frames = 12U,
.max_voices = 2U,
.gain = 1.0F};
}
}
BOOST_AUTO_TEST_CASE(sequence_sample_clock_and_completion) {
auto engine = sonus::engine::create({.sample_rate = 48000U, .channels = 1U, .master_volume = 1.0F, .no_device = true});
BOOST_REQUIRE(engine);
auto session = engine->play_sequence(fixture());
BOOST_REQUIRE(session);
auto output = std::array<std::float32_t, 12>{};
BOOST_REQUIRE(engine->render(output.data(), output.size()));
auto expected = std::array<std::float32_t, 12>{0.0F, 0.0F, 0.1F, 0.2F, 0.3F, 0.4F, 0.3F, 0.4F, 0.3F, 0.2F, 0.0F, 0.0F};
BOOST_TEST(output == expected, boost::test_tools::per_element());
BOOST_CHECK(session->status() == sonus::playback_status::COMPLETED);
engine->collect_finished();
BOOST_CHECK(session->status() == sonus::playback_status::COMPLETED);
}
BOOST_AUTO_TEST_CASE(sequence_cancel_replace_and_independent_unisons) {
auto engine = sonus::engine::create({.channels = 1U, .master_volume = 1.0F, .no_device = true});
BOOST_REQUIRE(engine);
auto value = fixture();
value.events.push_back({.sample = 0U, .start_frame = 2U, .duration_frames = 2U});
auto first = engine->play_sequence(value);
BOOST_REQUIRE(first);
auto output = std::array<std::float32_t, 4>{};
BOOST_REQUIRE(engine->render(output.data(), output.size()));
BOOST_TEST(output[2] == 0.2F);
auto second = engine->play_sequence(value);
BOOST_REQUIRE(second);
BOOST_CHECK(first->status() == sonus::playback_status::STOPPED);
second->stop();
BOOST_REQUIRE(engine->render(output.data(), output.size()));
BOOST_TEST(output[2] == 0.0F);
BOOST_CHECK(second->status() == sonus::playback_status::STOPPED);
value.max_voices = 1U;
BOOST_CHECK(!engine->play_sequence(value));
}
BOOST_AUTO_TEST_CASE(sequence_concurrent_control) {
auto engine = sonus::engine::create({.channels = 1U, .no_device = true});
BOOST_REQUIRE(engine);
auto render = std::thread{[&] {
auto output = std::array<std::float32_t, 128>{};
for (auto i = std::uint32_t{}; i < 1000U; ++i) {
static_cast<void>(engine->render(output.data(), output.size()));
}
}};
for (auto i = std::uint32_t{}; i < 1000U; ++i) {
auto session = engine->play_sequence(fixture());
if (session) {
session->stop();
}
engine->collect_finished();
}
render.join();
}
BOOST_AUTO_TEST_CASE(sequence_preparation_is_single_use_and_rate_checked) {
auto prepared = sonus::prepared_sequence::create(fixture(), 48000U);
BOOST_REQUIRE(prepared);
auto engine = sonus::engine::create({.channels = 1U, .master_volume = 1.0F, .no_device = true});
BOOST_REQUIRE(engine);
auto session = engine->play_sequence(std::move(*prepared));
BOOST_REQUIRE(session);
BOOST_CHECK(!engine->play_sequence(std::move(*prepared)));
auto output = std::array<std::float32_t, 12>{};
for (auto i = std::size_t{}; i < output.size(); ++i) {
BOOST_REQUIRE(engine->render(output.data() + i, 1U));
}
BOOST_TEST(output[8] == 0.3F);
BOOST_CHECK(session->status() == sonus::playback_status::COMPLETED);
auto invalid = fixture();
invalid.samples[0].loop_end = 0U;
BOOST_CHECK(!sonus::prepared_sequence::create(invalid, 48000U));
BOOST_CHECK(!sonus::prepared_sequence::create(fixture(), 44100U));
auto silent = fixture();
silent.samples.clear();
silent.events.clear();
auto quiet = engine->play_sequence(std::move(silent));
BOOST_REQUIRE(quiet);
BOOST_REQUIRE(engine->render(output.data(), output.size()));
BOOST_CHECK(quiet->status() == sonus::playback_status::COMPLETED);
for (auto value : output) {
BOOST_TEST(value == 0.0F);
}
}