miniaudio reference
Cheryl::Audio::Miniaudio implements Engine’s
audio ownership and playback contracts.
It owns CPU file decoding, native output, mixing, streamed music and voice lifetime.
It links Engine publicly and its audio SDK privately; it needs no window, graphics
module or UI toolkit. Public headers contain no miniaudio types.
Use Preparation and output for decoding, playback and shutdown; Dependency configuration for a standalone or supplied implementation; and Acceptance boundaries to distinguish offline mixer coverage from device observations.
Selection and dependency
Select CHERYL_BUILD_AUDIO_MINIAUDIO=ON at the repository root and link:
target_link_libraries(game PRIVATE Cheryl::Engine Cheryl::Audio::Miniaudio)
This module defaults to ON at the repository root. Disable it for assemblies
that do not need audio. Its standalone entry point locates the enclosing checkout;
set CHERYL_REPOSITORY_ROOT to select another absolute checkout path.
It reuses Cheryl::Engine or bootstraps Engine through the common module helper.
Preparation and output
#include <cheryl/backends/miniaudio.h>
auto audio = CE::Audio::Miniaudio::System::open_device();
const auto effect = CE::Audio::Miniaudio::decode_file("effect.wav");
auto voice = audio->play(effect, {.volume = 0.5f});
auto music = audio->stream("music.flac", {.volume = 0.25f, .looping = true});
// During normal application maintenance:
audio->maintain();
// After application producers settle:
audio->close();
decode_file supports the SDK’s built-in WAV, FLAC and MP3 decoders and returns
immutable source-rate mono/stereo PCM. It opens no device. Whole-file decode has
a caller-selected byte budget (64 MiB by default); over-budget, empty, malformed,
unsupported format and non-finite sample data fail without publishing a clip.
Ogg/Vorbis and additional custom codecs are outside this initial selection.
open_device requests stereo 48 kHz by default; supported mono/stereo output is
8–192 kHz. output_format reports the mixer’s selected format, which the device
backend may convert to its hardware format. The system excludes Null from native
backend selection and throws when device initialization fails. backend_name
identifies the selected native backend. Offline output uses open_offline, opens
no device and reports offline; render alone advances its clock. Native systems
reject manual mixing. Offline output spans contain complete interleaved frames.
Successful renders fill the whole span, including silence before playback or after
voice retirement, and advance the clock by the requested frame count. Voices bypass
pitch processing at matching sample rates; differing source rates still use the
SDK’s rate conversion. Cheryl exposes no pitch or spatialization control.
File stream opening waits for initial readiness; subsequent decode-ahead uses the SDK’s resource-manager worker. Keep the file and its contents stable until playback ends. Restart prepares a fresh node and cursor to discard processing caches; for streams it reopens the file. Preparation failure preserves the previous voice. A native start failure leaves it stopped. Control changes take effect at backend processing boundaries. Buffered decoded PCM can delay a looping-policy change; already queued device output cannot be withdrawn.
Playback notifications
Use publish_events_to() and publish_notifications() for the
logical outcome contract. This standalone
example opens no device and advances no mixer clock. Include the existing module
header plus <optional> and <vector>:
CE::Events::EventBus events;
std::optional<CE::Audio::PlaybackEnded> ended;
auto audio = CE::Audio::Miniaudio::System::open_offline({}, {.capacity = 1024});
audio->publish_events_to(events.publisher());
const auto listener = events.register_listener(
CE::Audio::playback_ended_event,
[&ended](const CE::Audio::PlaybackEnded& outcome) { ended = outcome; }
);
auto voice = audio->play(CE::Audio::Clip::from_samples({2, 48000}, std::vector<float>(128, 0.25f)));
voice->stop();
audio->publish_notifications(); // ended now identifies this voice/generation as Stopped.
audio->close();
audio->publish_notifications(); // Allowed after close; the terminal fact is not duplicated.
events.unregister_and_wait(listener);
Callbacks run only from the owner pump, outside audio locks. Retain and filter
system/voice/generation IDs when several outputs share a bus. A source’s
notification_snapshot() and retained voice->snapshot().terminal reconcile
current state after bounded observation loss. They do not replay earlier
generations. The direct play/control exception remains authoritative.
Runtime notification pump
After Runtime initializes its platform services, normally during game init_platform(),
an application-owned miniaudio system can attach to maintenance:
audio->publish_events_to(engine.events().publisher());
audio_pump_ = audio->pump_with(engine.services());
Declare CE::Runtime::ServiceAttachment audio_pump_; as an owning member and
retain it through the wanted passes. pump_with() captures only weak internal
audio access, so it holds no borrowed System pointer and does not keep output
alive. It pumps notifications during platform maintenance, including after an
explicit close. It does not open/close output, call maintain() or advance the
audio clock. Ordinary voice reclamation remains the application’s maintenance
responsibility.
During platform cleanup after producers settle:
audio->close();
audio->publish_notifications(); // Explicit final outcomes before detach.
audio_pump_.reset();
Keep output and the pump’s designated publication owner consistent. Releasing the system first silently disables its weak pump; destruction never invokes listeners. Runtime owns neither application audio shutdown nor playback policy.
Dependency configuration
Miniaudio is selected for its available source, permissive licensing and support for decoding, mixing and streaming.
The owner reuses a supplied miniaudio::miniaudio or miniaudio target. Otherwise
it uses CHERYL_MINIAUDIO_SOURCE or the pinned extern/miniaudio submodule at
0.11.25 (9634bedb5b5a2ca38c1ee7108a9358a4e233f14d). Supplied dependencies must
provide the high-level engine, resource manager, decoding and device APIs with
matching public compile definitions. The private SDK support target compiles only
miniaudio.c; it does not configure upstream extra nodes, codecs, examples or
installation, and it does not rewrite host cache options.
The static implementation is incorporated only by this audio owner. Shared SDK consumers receive its Cheryl interfaces rather than a separate miniaudio target, implementation or public miniaudio header requirement; static SDKs retain the required archive closure.
Miniaudio offers the Unlicense and MIT No Attribution; its notices remain in LICENSE. Linux output loads supported native audio backends dynamically and links Threads, the platform loader and the math library. Engine-only selection neither discovers nor compiles miniaudio.
Repeating root-assembly validation
Reuse build/testing-audio when its toolchain matches; it
keeps the optional owner separate from Engine-only and native/graphics selections.
Initialize the pinned SDK and Engine prerequisites from
setup. These commands need CMake 3.28 or newer, Ninja
and a C++23 toolchain, and preserve the caller’s working directory.
Rerun cases only when related source/configuration changes invalidate their evidence. The complete owner selection below includes decode, mixing and short-stream cases; narrow it for a focused change and avoid repeating cases through aggregates.
(
set -e
cd "$(git rev-parse --show-toplevel)"
cmake -S . -B build/testing-audio -G Ninja \
-DCMAKE_BUILD_TYPE=Release -DCMAKE_POLICY_VERSION_MINIMUM=3.5 \
-DCMAKE_CXX_SCAN_FOR_MODULES=OFF \
-DCMAKE_GTEST_DISCOVER_TESTS_DISCOVERY_MODE=PRE_TEST \
-DCHERYL_LOG_PROFILE=developer \
-DCHERYL_BUILD_NATIVE_GLFW=OFF -DCHERYL_BUILD_OPENGL=OFF \
-DCHERYL_BUILD_UI_TGUI=OFF -DCHERYL_BUILD_UI_RMLUI=OFF \
-DCHERYL_BUILD_AUDIO_MINIAUDIO=ON -DCHERYL_MINIAUDIO_SOURCE= \
-DCHERYL_BUILD_TESTS=ON -DCHERYL_BUILD_CONSUMER_TESTS=ON \
-DCHERYL_BUILD_ALL_TESTS=OFF -DCHERYL_BUILD_ACCEPTANCE_TESTS=OFF \
-DCHERYL_BUILD_DEMO=OFF
cmake --build build/testing-audio --parallel "$(nproc)" --target \
tests-audio-miniaudio consumer-module-audio-miniaudio
ctest --test-dir build/testing-audio --parallel "$(nproc)" --output-on-failure \
--no-tests=error -R '^tests-audio-miniaudio\.(audio_mix|audio_decode|audio_stream|audio_events)\.'
./build/testing-audio/cheryl-audio-miniaudio-consumer --offline
)
Acceptance boundaries
tests-audio-miniaudio owns offline PCM, mixing/control, owned-source, file decode,
short-stream, playback generation/outcome, bounded notification and lifetime regressions.
Outcome checks include restart failure arbitration, retained EOF, owner/cutoff
rules, post-close pumping and destructor silence without opening a device.
all-audio-miniaudio and the root all-tests
reuse those case objects. consumer-module-audio-miniaudio supplies an independent
audio consumer and SDK-free first-include probe. None requires a display or device.
The consumer defaults to --offline. Its --device mode opens native output and
generates its own 20-second stereo WAV: left/right alternate each second, and pitch
steps through 220/440/660/880 Hz. It requires no external media. --producers adds
an independent 200 ms, 440 Hz two-channel effect every two seconds. It continues
after music pauses or stops; music commands and v control only the stream, while
m controls all output. Maintenance continues while stdin waits. --stream=FILE
selects another stable WAV/FLAC/MP3 source. The consumer settles its producer before closing output and
then observes a handle surviving destruction.
Command |
Action |
|---|---|
|
Pause and resume music. |
|
Stop and restart music from the beginning. |
|
Toggle looping. |
|
Submit three overlapping short effects without retaining handles. |
|
Music or master volume, in |
|
Observe state or close output. |
Linux Release root-assembly WAV/FLAC/MP3 decoding, offline mixing/control/short-stream regressions, consumer/header checks and native stereo output/sustained WAV streaming are accepted. Native acceptance covers music/effect controls, independent producers, loop/restart and shutdown/relaunch. WAV coverage uses generated PCM16 fixtures, byte-budget boundaries, missing/corrupt paths and owned PCM after source-file removal. Short-stream offline cases use an initially buffered WAV and do not establish sustained decode-ahead, native latency, audible channel routing or device shutdown. Owned codec fixtures cover whole-clip FLAC/MP3 decoding; the native QA baseline uses a long WAV and does not establish compressed-stream seek/loop behavior. Supplied-SDK and standalone configurations remain unaccepted composition variants until explicitly selected and built. Other platforms remain deferred under the platform plan.
Repeating native WAV validation
Reuse the matching build/testing-audio consumer from the root-assembly procedure
above. Native checks require a working audio backend/server/device and audible
stereo output; no display or external media is needed. The generated 20-second WAV
exceeds the SDK’s two one-second stream pages. Offline success does not establish
audible channel routing, sustained decode-ahead or device shutdown. If stereo
output or a usable device is unavailable, provide it before requesting native
acceptance. A real backend must be reported; offline/Null is outside native scope.
Run each launch, complete the observations, then enter q before the next launch:
(
set -e
cd "$(git rev-parse --show-toplevel)"
./build/testing-audio/cheryl-audio-miniaudio-consumer --device
./build/testing-audio/cheryl-audio-miniaudio-consumer --device --producers
)
Listen past the initial two seconds. The generated stream alternates left/right once per second and cycles through 220/440/660/880 Hz; it continues for 20 seconds without dropouts or becoming silent when terminal input is idle. Check left/right with stereo output, rather than a mono speaker configuration.
Enter
p, wait, thenr: the stream pauses and continues its previous sequence. In the producer launch, an independent 200 ms, 440 Hz two-channel effect continues every two seconds while music is paused or stopped. Music commands andvaffect only the stream;maffects all output. Entererepeatedly: three distinct overlapping effects finish after their handles are discarded, without cutting off music.Enter
v 0, thenv 0.5: music alone mutes/restores. Enterm 0, thenm 0.5: all output mutes/restores while playback time continues. An invalid gain such asv -1reports a command error and preserves the previous gain/playback.Enter
s, thenr: stopped music remains stopped. Enterx: it restarts at the initial low left-channel tone rather than replaying an old cached segment. Leave looping off for a full 20-second playback;treportsFinishedandrleaves it finished. Enterl, thenx, and listen through the 20-second boundary: playback loops and remainsPlayingwithout a multi-second gap or stale segment.In both launches, close with
qwhile music/effects are active, then relaunch. Shutdown joins the producer, releases the device without hanging or trailing playback, and reports the surviving music handle asClosedafter destruction. Relaunch obtains usable output again. Audible artifact/latency observations are separate from the sample-level offline regressions.
These observations cover the generated long WAV. Native compressed-stream seek/loop requires a stable long FLAC or MP3 fixture when selected. Standalone and supplied-SDK composition, device hotplug and other platforms retain separate acceptance requirements.