simvx.core.audio_protocol

Backend-agnostic audio interface.

The audio system talks to three thin structural :class:Protocol types:

  • class:

    AudioPlaybackBackend: start / stop / pause / live-update sounds.

  • class:

    AudioStreamingBackend: chunk-fed PCM streaming (for procedural synthesis, AudioWorklet, etc.).

  • class:

    AudioBusBackend: bus-layout reconciliation and capability advertisement.

A fourth, one-member facet, :class:TreeBoundAudioBackend, is implemented only by a backend that has no device clock of its own and paces itself by the engine.

A concrete backend implements whichever facets it can. The union

class:

AudioBackend keeps the legacy “everything in one Protocol” surface for callers that genuinely need all three (e.g. MiniaudioBackend, WebAudioBackend); narrower callers should depend on the smallest Protocol they actually use and reach the backend via

attr:

SceneTree.audio_playback / :attr:audio_streaming /

attr:

audio_buses so the type checker enforces the facet boundary.

Capabilities are a :class:enum.StrEnum so typos are caught at import. Existing string callers keep working: Capability.PLAY_BASIC compares equal to "play.basic" and round-trips through frozenset literally.

Channels are opaque integer handles allocated by the backend. The backend owns the mapping from handle to native voice; the Python layer treats them as bookmarks.

Module Contents

Classes

Capability

Capabilities a backend can advertise via :meth:AudioBusBackend.list_capabilities.

AudioPlaybackBackend

Backend facet for starting and controlling individual sounds.

AudioStreamingBackend

Backend facet for chunk-fed PCM streaming.

AudioStreamPacing

Backend facet that lets a chunk-fed producer size its writes.

AudioStreamDraining

Backend facet that lets a chunk-fed producer say it has finished.

AudioBusBackend

Backend facet for bus-layout reconciliation and capability discovery.

TreeBoundAudioBackend

Backend facet for a backend whose clock is a :class:SceneTree, not a device.

AudioBackend

Union Protocol: a backend that implements all three facets.

Data

__all__

CAPABILITIES_CORE

Capabilities every backend that participates in playback + streaming advertises.

PlayMode

Mode kwarg for :meth:AudioPlaybackBackend.play_audio.

API

simvx.core.audio_protocol.__all__

[‘AudioBackend’, ‘AudioBusBackend’, ‘AudioPlaybackBackend’, ‘AudioStreamDraining’, ‘AudioStreamPacin…

class simvx.core.audio_protocol.Capability[source]

Bases: enum.StrEnum

Capabilities a backend can advertise via :meth:AudioBusBackend.list_capabilities.

Members are strings so Capability.PLAY_BASIC in caps works against either a frozenset[Capability] or a legacy frozenset[str]. New capability flags must be declared here: string literals in backend code bypass the typo check and are banned.

Initialization

Initialize self. See help(type(self)) for accurate signature.

PLAY_BASIC

‘play.basic’

PLAY_2D

‘play.2d’

PLAY_3D

‘play.3d’

SPATIAL_HRTF

‘spatial.hrtf’

SPATIAL_DOPPLER

‘spatial.doppler’

STREAMING

‘streaming’

STREAMING_WAV

‘streaming.wav’

STREAMING_OGG

‘streaming.ogg’

STREAMING_MP3

‘streaming.mp3’

STREAMING_FLAC

‘streaming.flac’

EFFECT_GAIN

‘effect.gain’

EFFECT_FILTER_BIQUAD

‘effect.filter_biquad’

EFFECT_PARAMETRIC_EQ

‘effect.parametric_eq’

EFFECT_DELAY

‘effect.delay’

EFFECT_REVERB

‘effect.reverb’

EFFECT_COMPRESSOR

‘effect.compressor’

EFFECT_SOFTCLIP

‘effect.softclip’

__new__(*values)
__add__()
__contains__()
__delattr__()
__dir__()
__eq__()
__format__()
__ge__()
__getattribute__()
__getitem__()
__getnewargs__()
__getstate__()
__gt__()
__hash__()
__iter__()
__le__()
__len__()
__lt__()
__mod__()
__mul__()
__ne__()
__reduce__()
__reduce_ex__()
__repr__()
__rmod__()
__rmul__()
__setattr__()
__sizeof__()
__str__()
__subclasshook__()
capitalize()
casefold()
center()
count()
encode()
endswith()
expandtabs()
find()
format()
format_map()
index()
isalnum()
isalpha()
isascii()
isdecimal()
isdigit()
isidentifier()
islower()
isnumeric()
isprintable()
isspace()
istitle()
isupper()
join()
ljust()
lower()
lstrip()
partition()
removeprefix()
removesuffix()
replace()
rfind()
rindex()
rjust()
rpartition()
rsplit()
rstrip()
split()
splitlines()
startswith()
strip()
swapcase()
title()
translate()
upper()
zfill()
__deepcopy__(memo)
__copy__()
name()
value()
simvx.core.audio_protocol.CAPABILITIES_CORE: frozenset[simvx.core.audio_protocol.Capability]

‘frozenset(…)’

Capabilities every backend that participates in playback + streaming advertises.

The Null backend deliberately omits the streaming members because it does not implement :class:AudioStreamingBackend; see NullAudioBackend for its narrowed capability set.

simvx.core.audio_protocol.PlayMode

None

Mode kwarg for :meth:AudioPlaybackBackend.play_audio.

The Node-side dispatcher (_AudioPlaybackMixin._play_common) selects the mode from the player class; backends branch on it internally so the spatial / non-spatial code paths share one entry point.

class simvx.core.audio_protocol.AudioPlaybackBackend[source]

Bases: typing.Protocol

Backend facet for starting and controlling individual sounds.

The unified :meth:play_audio collapses the historical play_audio / play_audio_2d / play_audio_3d trio into one entry point parameterised by mode. Backends branch on mode internally; spatial kwargs (position, max_distance) are ignored when mode == "non_positional".

Channel lifetime. A channel id is owned by whoever started it and is given back with :meth:stop_audio, which must be safe to call on a channel that has already ended or already been released. A backend may not assume every channel is released: a caller can forget one, so a backend that holds per-channel resources reclaims the channels that have played out by itself rather than keeping them until shutdown.

Meth:

is_channel_active must answer False for both a finished channel and an unknown id, so the two are indistinguishable to the caller and reclaiming early is always safe.

play_audio(stream: simvx.core.audio.AudioClip, *, mode: simvx.core.audio_protocol.PlayMode = 'non_positional', position: Any = None, volume_db: float = 0.0, pitch: float = 1.0, loop: bool = False, bus: str = 'Master', max_distance: float = 100.0, from_position: float = 0.0, pan: float = 0.0, gain_db: float = 0.0) int | None[source]

Play stream. Returns the channel id or None on failure.

mode selects the spatial path:

  • "non_positional": background music / UI sounds. position

    • max_distance are ignored; bus defaults to "Master".

  • "2d": Node-side spatialization computes pan + volume_db from the listener; the backend just applies them before the first audio buffer is rendered. bus defaults to "SFX".

  • "3d": same as 2D, plus :meth:update_audio_3d per frame for Doppler. The Node pre-computes initial pitch and forwards it.

from_position seeks the playback cursor (in seconds) before the first sample is rendered. pan ([-1, 1], 0 = centre) is applied before sound.start() so spatial players’ first audio buffer is correctly panned.

stop_audio(channel_id: int) None[source]
pause_audio(channel_id: int) None[source]
resume_audio(channel_id: int) None[source]
update_audio_2d(channel_id: int, volume_db: float, pan: float) None[source]
update_audio_3d(channel_id: int, volume_db: float, pan: float, pitch: float) None[source]
set_pitch(channel_id: int, pitch: float) None[source]
set_listener_position(x: float, y: float, z: float) None[source]
set_listener_velocity(x: float, y: float, z: float) None[source]
set_listener_direction(x: float, y: float, z: float) None[source]
set_listener_world_up(x: float, y: float, z: float) None[source]
get_playback_position(channel_id: int) float[source]
is_channel_active(channel_id: int) bool[source]
shutdown() None[source]
__slots__

()

classmethod __init_subclass__(*args, **kwargs)
classmethod __class_getitem__(item)
class simvx.core.audio_protocol.AudioStreamingBackend[source]

Bases: typing.Protocol

Backend facet for chunk-fed PCM streaming.

Procedural synthesis (AudioSynth.attach_to), AudioWorklet feeds, and compressed-container streaming via native decoders all flow through these two methods. Backends that can’t stream (the Null backend, any no-device test stub) deliberately do NOT implement this Protocol: callers must isinstance(backend, AudioStreamingBackend) and raise

Class:

AudioCapabilityError (or warn-once and skip) when the facet is absent.

open_stream(*, volume_db: float = 0.0, bus: str = 'Master', buffer_seconds: float = 0.5, loop: bool = False, stream: simvx.core.audio.AudioClip | None = None) int[source]

Open a streaming channel for chunk-fed PCM playback.

If stream is provided and its container is a compressed format ("ogg"/"mp3"/"flac"), backends with a native streaming decoder open the file directly and ignore subsequent

Meth:

feed_audio_chunk calls. For "wav" or synthetic "pcm" streams, the caller is expected to feed raw int16 stereo bytes via :meth:feed_audio_chunk. Backends that cannot decode the requested container raise :class:AudioCapabilityError.

loop restarts the channel at the beginning when it plays out. It applies only when the backend owns the decoder: a chunk-fed channel loops by having its producer rewind and feed the file again, which is the only place that knows where the start is.

feed_audio_chunk(channel_id: int, chunk: bytes) None[source]
__slots__

()

classmethod __init_subclass__(*args, **kwargs)
classmethod __class_getitem__(item)
class simvx.core.audio_protocol.AudioStreamPacing[source]

Bases: typing.Protocol

Backend facet that lets a chunk-fed producer size its writes.

A producer with no idea how much room is left writes as fast as its caller ticks, which reads a file to its end in a fraction of the time the audio takes to play. These two members let it write exactly what fits, so the feed runs at the speed of the sound.

Kept out of :class:AudioStreamingBackend deliberately. That Protocol is runtime-checked to decide whether streaming is available at all, so widening it would take streaming away from every backend that has not implemented pacing yet. Producers isinstance-check this facet on its own and fall back to pacing themselves against the frame clock.

stream_format() tuple[int, int][source]

(sample_rate, channels) of the PCM this backend’s rings accept.

frames_available(channel_id: int) int[source]

Frames channel_id can accept right now; 0 when it is full.

Also 0 for a channel whose audio the backend decodes itself: such a channel has no ring, so there is nothing for a producer to write into it.

__slots__

()

classmethod __init_subclass__(*args, **kwargs)
classmethod __class_getitem__(item)
class simvx.core.audio_protocol.AudioStreamDraining[source]

Bases: typing.Protocol

Backend facet that lets a chunk-fed producer say it has finished.

Class:

AudioStreamingBackend gives a producer no way to end a stream. The only signal available is :meth:AudioPlaybackBackend.stop_audio, which tears the channel down at once and throws away everything the producer has written but the device has not yet played: up to a whole buffer of the end of the sound. This facet is how the producer says “that was the last chunk” instead, and lets the backend keep the channel alive until what is already queued has been heard.

Kept out of :class:AudioStreamingBackend for the same reason as

Class:

AudioStreamPacing: that Protocol is runtime-checked to decide whether streaming is available at all, so a backend that has not implemented draining yet would lose streaming entirely. Producers isinstance-check this facet on its own and fall back to the immediate stop, which is what they did before it existed.

end_of_stream(channel_id: int) None[source]

Declare that no further :meth:feed_audio_chunk call is coming.

The channel keeps playing what it already holds.

Meth:

AudioPlaybackBackend.is_channel_active answers False for it once that has played out, which is the producer’s cue to stop it. A no-op for a channel the backend decodes itself, for an unknown id, and on a second call.

__slots__

()

classmethod __init_subclass__(*args, **kwargs)
classmethod __class_getitem__(item)
class simvx.core.audio_protocol.AudioBusBackend[source]

Bases: typing.Protocol

Backend facet for bus-layout reconciliation and capability discovery.

sync_bus_layout(layout: simvx.core.audio_bus.AudioBusLayout) None[source]

Reconcile the backend’s native bus / effect graph against layout.

Cheap and idempotent: backends keep their own dict[bus_name, native_handle] and add/remove/update only what changed since the last call. Called automatically by the audio server when :class:AudioBusLayout or any bus’s effects list changes; manual call is safe.

list_capabilities() frozenset[simvx.core.audio_protocol.Capability][source]

Capability flags this backend supports.

Effect modules and Nodes call this before attempting to materialise a native feature; absent capabilities raise

Class:

AudioCapabilityError (strict) or warn-once (lenient).

__slots__

()

classmethod __init_subclass__(*args, **kwargs)
classmethod __class_getitem__(item)
class simvx.core.audio_protocol.TreeBoundAudioBackend[source]

Bases: typing.Protocol

Backend facet for a backend whose clock is a :class:SceneTree, not a device.

A backend driven by real audio hardware knows how much time has passed because the device tells it. A backend with no device – the silent

Class:

~simvx.core.audio_backend.NullAudioBackend is the only shipped one – has to be told, and several trees can be alive at once, so it must be told which tree’s ticks are its own.

Meth:

SceneTree.install_audio_backend isinstance-checks this facet and calls :meth:bind_tree on the backends that declare it. A backend that does not implement it is simply never bound; nothing else about installation changes.

bind_tree(tree: simvx.core.scene_tree.SceneTree) None[source]

Adopt tree as the clock this backend measures its playback against.

Called once per install, with the installing tree. A re-install onto a different tree calls it again with the new one. The backend is expected to hold the tree weakly: a finished scene’s node graph must not be kept alive by the audio backend that outlived it.

__slots__

()

classmethod __init_subclass__(*args, **kwargs)
classmethod __class_getitem__(item)
class simvx.core.audio_protocol.AudioBackend[source]

Bases: simvx.core.audio_protocol.AudioPlaybackBackend, simvx.core.audio_protocol.AudioStreamingBackend, simvx.core.audio_protocol.AudioBusBackend, typing.Protocol

Union Protocol: a backend that implements all three facets.

MiniaudioBackend (and its legacy fallback) and WebAudioBackend satisfy this. NullAudioBackend does NOT (it doesn’t implement

Class:

AudioStreamingBackend): callers that need streaming should isinstance-check :class:AudioStreamingBackend directly via

Attr:

SceneTree.audio_streaming.

play_audio(stream: simvx.core.audio.AudioClip, *, mode: simvx.core.audio_protocol.PlayMode = 'non_positional', position: Any = None, volume_db: float = 0.0, pitch: float = 1.0, loop: bool = False, bus: str = 'Master', max_distance: float = 100.0, from_position: float = 0.0, pan: float = 0.0, gain_db: float = 0.0) int | None
stop_audio(channel_id: int) None
pause_audio(channel_id: int) None
resume_audio(channel_id: int) None
update_audio_2d(channel_id: int, volume_db: float, pan: float) None
update_audio_3d(channel_id: int, volume_db: float, pan: float, pitch: float) None
set_pitch(channel_id: int, pitch: float) None
set_listener_position(x: float, y: float, z: float) None
set_listener_velocity(x: float, y: float, z: float) None
set_listener_direction(x: float, y: float, z: float) None
set_listener_world_up(x: float, y: float, z: float) None
get_playback_position(channel_id: int) float
is_channel_active(channel_id: int) bool
shutdown() None
__slots__

()

classmethod __init_subclass__(*args, **kwargs)
classmethod __class_getitem__(item)
open_stream(*, volume_db: float = 0.0, bus: str = 'Master', buffer_seconds: float = 0.5, loop: bool = False, stream: simvx.core.audio.AudioClip | None = None) int
feed_audio_chunk(channel_id: int, chunk: bytes) None
sync_bus_layout(layout: simvx.core.audio_bus.AudioBusLayout) None
list_capabilities() frozenset[simvx.core.audio_protocol.Capability]