Source code for 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.
"""
from __future__ import annotations
import logging
from .._native import miniaudio_engine as _me
from ..audio_errors import AudioBackendUnavailable
from ._legacy import _LegacyMiniaudioBackend
from ._miniaudio import MiniaudioBackend
from ._null import NullAudioBackend
from ._shared import (
_DEFAULT_CHANNELS,
_DEFAULT_SAMPLE_RATE,
_NATIVE_REBUILD_HINT,
_legacy_allowed,
)
log = logging.getLogger(__name__)
__all__ = [
"MiniaudioBackend",
"_LegacyMiniaudioBackend",
"NullAudioBackend",
"make_backend",
]
_fallback_warned = False
[docs]
def make_backend(
sample_rate: int = _DEFAULT_SAMPLE_RATE,
nchannels: int = _DEFAULT_CHANNELS,
) -> MiniaudioBackend | _LegacyMiniaudioBackend | NullAudioBackend:
"""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.
"""
global _fallback_warned
# --- 1. native ----------------------------------------------------------
if _me.is_available():
try:
return MiniaudioBackend(sample_rate=sample_rate, nchannels=nchannels)
except Exception as exc:
if not _legacy_allowed():
raise AudioBackendUnavailable(
f"Native audio backend failed to initialise: {exc}. "
f"SIMVX_ALLOW_LEGACY_AUDIO=0 is set so the legacy fallback is "
f"disabled. {_NATIVE_REBUILD_HINT}"
) from exc
log.warning(
"MiniaudioBackend init failed (%s); trying legacy mixer.", exc
)
elif not _legacy_allowed():
raise AudioBackendUnavailable(
"Native audio extension is not built and SIMVX_ALLOW_LEGACY_AUDIO=0 "
f"is set. {_NATIVE_REBUILD_HINT}"
)
# --- 2. legacy ----------------------------------------------------------
if not _fallback_warned:
_fallback_warned = True
log.warning(
"Native audio extension not available; using legacy mixer "
"(100 ms latency). %s Or set SIMVX_ALLOW_LEGACY_AUDIO=0 to fail "
"loudly instead.",
_NATIVE_REBUILD_HINT,
)
try:
return _LegacyMiniaudioBackend(sample_rate=sample_rate, nchannels=nchannels)
except Exception as exc:
log.warning(
"_LegacyMiniaudioBackend init failed (%s); using silent NullAudioBackend. "
"Audio will be inaudible but the engine will run normally.",
exc,
)
# --- 3. null ------------------------------------------------------------
return NullAudioBackend(sample_rate=sample_rate, nchannels=nchannels)