simvx.core.audio_backend¶
Desktop audio backends for SimVX.
Three backends ship in this module:
MiniaudioBackend: the production path. Mixes audio natively viama_engine(CFFI wrapper atsimvx.core._native.miniaudio_engine). The native extension is built atuv pip installtime by the PEP 517 build hook inpackages/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 byminiaudio.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):
Native
MiniaudioBackend: picked when the extension is importable.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.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¶
Native-mixed audio backend on top of |
|
Silent backend implementing :class: |
Functions¶
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. Usemake_backend()for an automatic fallback to_LegacyMiniaudioBackendwith 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]¶
- 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 viafeed_audio_chunk; underrun produces silence (no glitch). Only applies to the chunk-fed PCM path: whenstreamis a compressed container, miniaudio’sma_sound_init_from_file(..., MA_SOUND_FLAG_STREAM)owns the buffering internally.Container routing (when
streamis provided):"wav"/"pcm"(synthetic) /None: open anma_pcm_rb-backed sound. Caller feeds raw int16 stereo bytes."ogg"/"mp3"/"flac": open the file directly viaSound.from_file(stream=True). Subsequent- meth:
feed_audio_chunkcalls are no-ops for this channel.
"unknown": raise :class:InvalidStreamError.
Pitch is intentionally not a parameter: streaming sounds are built with
pitch_enabled=Falseso the resampler can’t read past what the producer has written.set_pitchagainst 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_framesif 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.
- list_capabilities() frozenset[simvx.core.audio_protocol.Capability][source]¶
- sync_bus_layout(layout: simvx.core.audio_bus.AudioBusLayout) None[source]¶
Reconcile native
ma_sound_groupvolumes + effect chains againstlayout.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_backendwhen neither the native extension nor the legacyminiaudiopath can start (e.g. no audio device on the host, ALSA/Pulse not running, theminiaudioPython package not installed, or a sandboxed environment with no compiler).The engine, scene tree, and :class:
AudioStreamPlayernodes all operate normally: calls return valid channel IDs and- Meth:
is_channel_activebehaves 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 checkisinstance(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]¶
- 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):
Native
MiniaudioBackend(~20 ms latency). Selected when the compiled_simvx_miniaudio_engineextension imports cleanly.Legacy
_LegacyMiniaudioBackend(~100 ms latency). Selected when native is unavailable ANDSIMVX_ALLOW_LEGACY_AUDIOis not set to"0". Emits a one-time WARNING with rebuild instructions.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=0to refuse the fallback chain and raise :class:AudioBackendUnavailableif the native extension is missing or fails to initialise.