simvx.core.audio_backend

Desktop audio backends for SimVX.

Three backends ship in this module:

  • MiniaudioBackend: the production path. Mixes audio natively via ma_engine (CFFI wrapper at simvx.core._native.miniaudio_engine). The native extension is built at uv pip install time by the PEP 517 build hook in packages/core/build_audio_ext_hook.py. If that build is skipped or fails, the runtime falls back to legacy.

  • _LegacyMiniaudioBackend: the pure-Python fallback. Mixes in a numpy generator callback driven by miniaudio.PlaybackDevice. Slower (target latency 100 ms vs 20 ms for the native path) and runs on the Python audio thread under the GIL, so it’s vulnerable to underruns from heavy main-thread frames.

  • NullAudioBackend: silent no-op backend used when no audio device is available (sandboxed CI, headless containers).

make_backend() resolution order, with loud warnings at every fallback (never silent degradation):

  1. Native MiniaudioBackend: picked when the extension is importable.

  2. Legacy _LegacyMiniaudioBackend: picked when native is unavailable AND a real audio device opens. Emits a one-time WARNING explaining the latency hit and the rebuild command.

  3. Null NullAudioBackend: picked when neither native nor legacy can start (no audio device). Emits a one-time WARNING.

Set SIMVX_ALLOW_LEGACY_AUDIO=0 to disable the legacy/null fallbacks and raise :class:AudioBackendUnavailable instead.

The runtime never invokes a C compiler. Use simvx build-audio (or uv pip install --reinstall -e packages/core) to (re)build the extension manually.

Module Contents

Classes

MiniaudioBackend

Native-mixed audio backend on top of ma_engine.

NullAudioBackend

Silent backend implementing :class:AudioPlaybackBackend + :class:AudioBusBackend.

Functions

make_backend

Pick the best available audio backend, falling back loudly on failure.

Data

API

simvx.core.audio_backend.log

‘getLogger(…)’

simvx.core.audio_backend.__all__

[‘MiniaudioBackend’, ‘_LegacyMiniaudioBackend’, ‘NullAudioBackend’, ‘make_backend’]

class simvx.core.audio_backend.MiniaudioBackend(sample_rate: int = _DEFAULT_SAMPLE_RATE, nchannels: int = _DEFAULT_CHANNELS)[source]

Native-mixed audio backend on top of ma_engine.

All mixing, spatialization, and DSP runs in native C: Python only pushes parameter updates. Target latency: buffersize_msec=20 (vs the legacy backend’s 100 ms).

The native extension must be built once after install:

simvx build-audio

If the extension is missing, the constructor raises MiniaudioEngineUnavailable. Use make_backend() for an automatic fallback to _LegacyMiniaudioBackend with a one-time warning.

Initialization

play_audio(stream: simvx.core.audio.AudioStream, *, mode: str = '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]
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]
get_playback_position(channel_id: int) float[source]
is_channel_active(channel_id: int) bool[source]
open_stream(*, volume_db: float = 0.0, bus: str = 'Master', buffer_seconds: float = 0.5, stream: simvx.core.audio.AudioStream | None = None) int[source]

Open a streaming channel.

buffer_seconds (default 0.5 s) trades latency for underrun tolerance. At 48 kHz stereo s16 = ~96 KB. Drop to 0.1 s for low-latency interactive synthesis; bump to 1-2 s for music streams under heavy CPU load. Producer pushes int16 stereo bytes via feed_audio_chunk; underrun produces silence (no glitch). Only applies to the chunk-fed PCM path: when stream is a compressed container, miniaudio’s ma_sound_init_from_file(..., MA_SOUND_FLAG_STREAM) owns the buffering internally.

Container routing (when stream is provided):

  • "wav" / "pcm" (synthetic) / None: open an ma_pcm_rb-backed sound. Caller feeds raw int16 stereo bytes.

  • "ogg" / "mp3" / "flac": open the file directly via Sound.from_file(stream=True). Subsequent

    meth:

    feed_audio_chunk calls are no-ops for this channel.

  • "unknown": raise :class:InvalidStreamError.

Pitch is intentionally not a parameter: streaming sounds are built with pitch_enabled=False so the resampler can’t read past what the producer has written. set_pitch against the returned channel raises :class:AudioCapabilityError.

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

Push raw int16 stereo bytes into the channel’s ring buffer.

Misaligned chunks (length not a multiple of bytes-per-frame) have the trailing partial frame trimmed. When the ring is full the excess is silently dropped: callers can poll entry.keeper.available_write_frames if they need to know.

set_listener_position(x: float, y: float, z: float) None[source]

Forward listener position to the native ma_engine spatializer.

set_listener_velocity(x: float, y: float, z: float) None[source]

Forward listener velocity (m/s) to the native engine for Doppler.

set_listener_direction(x: float, y: float, z: float) None[source]

Forward listener forward vector to the native engine.

set_listener_world_up(x: float, y: float, z: float) None[source]

Forward listener world-up vector to the native engine.

shutdown() None[source]
list_capabilities() frozenset[simvx.core.audio_protocol.Capability][source]
sync_bus_layout(layout: simvx.core.audio_bus.AudioBusLayout) None[source]

Reconcile native ma_sound_group volumes + effect chains against layout.

Cheap and idempotent: pushes only on change. Called by the audio server every frame; safe to call manually.

class simvx.core.audio_backend.NullAudioBackend(sample_rate: int = _DEFAULT_SAMPLE_RATE, nchannels: int = _DEFAULT_CHANNELS)[source]

Silent backend implementing :class:AudioPlaybackBackend + :class:AudioBusBackend.

Selected automatically by :func:make_backend when neither the native extension nor the legacy miniaudio path can start (e.g. no audio device on the host, ALSA/Pulse not running, the miniaudio Python package not installed, or a sandboxed environment with no compiler).

The engine, scene tree, and :class:AudioStreamPlayer nodes all operate normally: calls return valid channel IDs and

Meth:

is_channel_active behaves consistently, but nothing is rendered to a device.

NullBackend does NOT implement :class:AudioStreamingBackend. Code paths that need streaming (procedural synth via

Class:

AudioSynth.attach_to, AudioWorklet feeds, etc.) must check isinstance(backend, AudioStreamingBackend) and raise / warn-once when it’s absent. Advertised capabilities are narrowed to {Capability.PLAY_BASIC} so effect modules don’t try to instantiate native nodes on the null path.

Initialization

play_audio(stream: simvx.core.audio.AudioStream, *, mode: str = '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]
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]
list_capabilities() frozenset[simvx.core.audio_protocol.Capability][source]
sync_bus_layout(layout: simvx.core.audio_bus.AudioBusLayout) None[source]
simvx.core.audio_backend.make_backend(sample_rate: int = _DEFAULT_SAMPLE_RATE, nchannels: int = _DEFAULT_CHANNELS) simvx.core.audio_backend.MiniaudioBackend | simvx.core.audio_backend._LegacyMiniaudioBackend | simvx.core.audio_backend.NullAudioBackend[source]

Pick the best available audio backend, falling back loudly on failure.

Resolution order (the runtime never invokes a C compiler: that’s the install-time build hook’s job):

  1. Native MiniaudioBackend (~20 ms latency). Selected when the compiled _simvx_miniaudio_engine extension imports cleanly.

  2. Legacy _LegacyMiniaudioBackend (~100 ms latency). Selected when native is unavailable AND SIMVX_ALLOW_LEGACY_AUDIO is not set to "0". Emits a one-time WARNING with rebuild instructions.

  3. Null NullAudioBackend (silent). Selected when neither native nor legacy can start: typically a sandboxed CI without an audio device. Emits another one-time WARNING.

Set SIMVX_ALLOW_LEGACY_AUDIO=0 to refuse the fallback chain and raise :class:AudioBackendUnavailable if the native extension is missing or fails to initialise.