Audio ownership and playback
Use this reference to manage independent playback, voice controls and output
shutdown. For a first sound, follow
miniaudio preparation and output:
link Cheryl::Audio::Miniaudio, create an application-owned system, prepare a clip and
call play(). This page defines the lifetime and failure rules around that sequence.
The public audio contracts belong
to Engine and contain no SDK, device or graphics types. Applications own an audio
system independently of CE::Runtime::Context and compose a selected audio module through
its public CMake target. Engine has no reverse dependency on that module.
Clips and voices
CE::Audio::Clip::from_samples takes owned interleaved float PCM, with mono/stereo
channels and a sample rate between 8 and 192 kHz. Data is nonempty, frames are
complete, and every sample is finite. The nominal sample range is [-1, 1];
storage does not clamp samples or change their rate. Copies share immutable
storage. Moving preserves the original valid snapshot. Clip creation does not
open a device. Its frame count and duration describe source audio, independently
of a mixer’s output rate.
CE::Audio::System::play creates a voice with an independent cursor. Voices can share
a clip without sharing playback progress. stream opens file-backed playback for
music; the file must remain available until playback ends. Decode and initial file
opening are synchronous preparation operations, so applications perform them at
loading boundaries or dispatch owned decode requests through their worker groups.
flowchart LR
accTitle: Shared clips and independent voices
accDescr: An audio system retains two voices with independent playback cursors. Both voices can use the same immutable clip storage.
system["Audio system"] -->|retains playback| first["Voice A and cursor A"]
system -->|retains playback| second["Voice B and cursor B"]
first -->|reads| clip["Shared immutable clip"]
second -->|reads| clip
Voice and master volumes are normalized linear gains in [0, 1], with zero as
mute. Invalid/non-finite gains throw. Multiple voices are mixed; overlapping loud
sources can exceed nominal PCM amplitude and applications manage their mix levels.
Operation/state |
Behavior |
|---|---|
Initial playback |
Starts immediately, or starts |
|
Pauses a playing voice and preserves progress. |
|
Resumes a paused voice; other live states are unchanged. |
|
Leaves the voice |
Natural end |
Reports |
|
Requests source frame zero and starts playback. |
|
Changes the loop policy without restarting stopped/finished playback. |
System closure |
Surviving handles report |
Snapshots copy playback state, volume, loop policy and known source duration. Backends supporting outcomes also supply system/voice identity, generation and an immutable optional terminal fact as described below. They are observations, not an atomic freeze of native playback. Live decoder cursors are outside the initial API; reading them concurrently with mixing needs additional synchronization. No game callback runs on the mixing thread.
Playback outcomes
CE::Audio::playback_ended_event reports one logical terminal outcome for each
admitted playback generation. The miniaudio backend implements it; other audio
systems can leave this optional extension unsupported. Its plain PlaybackEnded
payload owns checked system/voice identity, generation, terminal reason/sequence/
observation time and cumulative loss information. It retains no Voice or native
objects. Voice identity stays stable across restart; a successful restart admits
the next generation and clears only that new generation’s terminal fact.
Terminal reason |
Selection |
|---|---|
|
Native EOF was observed before another terminal operation. |
|
Stop succeeded, including when a restart stops the old voice but cannot start its replacement. |
|
A new generation successfully replaced unfinished playback. |
|
Output closed before another terminal outcome was observed. |
|
Maintenance reclaimed an unreferenced inactive generation, such as paused playback. |
Failed initial play/stream admits no generation and fabricates no outcome. Replacement preparation failure preserves the old generation. Failure to stop it admits no successor. After a successful old stop, a failed replacement start ends the old generation as Stopped unless Completed was already observed. Successful restart marks an unfinished predecessor Replaced. Pause/resume and loop iterations do not create terminal outcomes.
Terminal facts never change within a generation: observing EOF before stop or
close preserves Completed. set_looping() cannot resurrect completed playback.
After output closes, VoiceSnapshot.state reports Closed while its terminal
reason remains available. These are owner-observed logical outcomes rather than
sample-accurate claims about a mixer race.
Publish on an explicit owner
Call audio->publish_events_to(bus.publisher()) to choose a weak route and
designate the calling thread as notification owner. The backend stages terminal
records under its audio mutex, and publish_notifications() invokes listeners
outside that mutex on the designated owner. Neither controls, close, maintenance
nor the mixer invoke game listeners directly. Without prior configuration, the
first pump designates its caller; an empty route discards publication attempts.
Route changes require that same owner outside an active pump.
Each pump observes completion even for externally retained voices and drains only through its entry sequence cutoff. A listener’s newly staged outcomes wait for a later pump; recursive pumping rejects. The source reserves terminal-counter capacity before native effects so admitted unfinished generations can still end during noexcept close. Observation storage is a separate bound: miniaudio defaults to 1,024 preallocated records, configurable when opening output. Overflow drops the oldest pending record rather than rejecting playback or waiting for observers.
notification_snapshot() exposes source sequence, closure, capacity, pending
count and loss count/range. System ID zero means unsupported; unsupported default
routing/pump operations throw. A retained Voice’s current-generation snapshot
recovers its terminal fact after a gap. Neither snapshot recovers superseded
generation history. Each retained record gets one publication attempt; bus/queue
failure has no automatic retry or exactly-once delivery guarantee.
sequenceDiagram
accTitle: Audio outcomes are delivered by the owner pump
accDescr: Stop stages a terminal outcome under the audio lock. The owner pump removes staged records and notifies listeners outside that lock. Explicit close can be followed by a final pump; destruction discards unpublished observations.
participant Game as Producer
participant Audio as Audio system
participant Owner as Notification owner
participant Bus as Chosen EventBus
participant Listener
Game->>Audio: voice.stop()
Audio->>Audio: Commit terminal fact and stage record
Audio-->>Game: return without callbacks
Owner->>Audio: publish_notifications()
Audio->>Bus: notify(playback_ended_event, record)
Bus->>Listener: callback(record)
Bus-->>Audio: return
Audio-->>Owner: return
Lifetime and scheduling
Controls, submissions, snapshots and maintenance are synchronized across application producers. Their order follows control admission, rather than a deterministic simulation clock. Device playback progresses on its own clock without render or simulation admission. Offline processing is explicitly selected; an unavailable native device must not silently produce inaudible success.
The system retains active playback after the application releases its voice
handle. Call maintain regularly to reclaim completed/stopped/paused voices that
have no application handles. Maintenance does not advance playback. Discarded
looping voices remain active until system closure; retain their handles when they
need individual control.
Create the system before playback. Settle all producers before destroying it.
close is idempotent, stops output, retires native voices before their mixer/device,
and leaves surviving handles safe. A system cannot reopen; construct another
system for a new session. Audio need not share the graphics owner’s thread, and
this contract does not add audio to the runtime’s existing adapter teardown.
For final observations, settle producers, call close(), then
publish_notifications() on the designated owner before releasing subscriptions
and the system. Publication remains allowed after close; destruction only cleans
up and discards unpublished records. The optional
runtime notification pump
does not transfer audio lifetime or producer settlement to Runtime.
Backends and limits
The miniaudio module supplies CPU decode, explicit device/offline output and streamed playback. Its independent consumer provides native observation without a game, window or graphics module. Linux root-assembly native stereo output and sustained WAV streaming are accepted; the module guide owns reusable validation and compressed-stream, composition and platform coverage limits.
World, entity and collision organization belongs to the game or its application-selected modules. FMOD is a later optional backend; Studio authoring formats/capabilities require a separate contract.