Source code for simvx.core.audio_errors
"""Typed audio errors and the strict-mode flag.
The audio system raises typed exceptions instead of returning ``None`` or
silently falling back. Callers can ``except AudioError`` (or any subclass)
at well-defined boundaries; deep code paths never swallow.
Strict mode (``SIMVX_AUDIO_STRICT``, default ``"1"``) controls whether
historically-silent code paths raise (strict, dev default) or warn-once
(non-strict, release builds). Shipping games typically set
``SIMVX_AUDIO_STRICT=0`` so a misconfigured asset doesn't crash the
player; development runs at strict to surface bugs early.
"""
from __future__ import annotations
import logging
import os
__all__ = [
"STRICT",
"AudioError",
"AudioBackendUnavailable",
"UnknownBusError",
"InvalidStreamError",
"AudioCapabilityError",
"AudioMutationDuringPlaybackError",
"warn_once",
"raise_or_warn",
]
STRICT: bool = os.environ.get("SIMVX_AUDIO_STRICT", "1") != "0"
_log = logging.getLogger("simvx.audio")
_warned_keys: set[str] = set()
[docs]
def warn_once(key: str, msg: str, *args: object, exc_info: bool = False) -> None:
"""Emit ``msg`` at WARNING level the first time ``key`` is seen, then suppress.
Use for non-fatal audio failures where flooding the log would be worse
than missing a few subsequent occurrences (e.g. a backend call that
fails on every ``on_process`` tick).
"""
if key in _warned_keys:
return
_warned_keys.add(key)
_log.warning(msg, *args, exc_info=exc_info)
[docs]
def raise_or_warn(
exc: BaseException,
*,
key: str,
message: str,
) -> None:
"""In strict mode, re-raise ``exc`` wrapped in :class:`AudioError`. Otherwise warn-once.
Use at boundaries where the surrounding code can continue without the
operation that just failed (e.g. cleanup paths, best-effort releases).
Hot-path code that fundamentally needs the operation to succeed should
raise directly rather than route through this helper.
"""
if STRICT:
raise AudioError(message) from exc
warn_once(key, "%s: %s", message, exc, exc_info=True)
[docs]
class AudioError(Exception):
"""Base class for every audio-system exception."""
[docs]
class AudioBackendUnavailable(AudioError):
"""The requested audio backend is missing or failed to initialise.
Raised at backend-selection time (``make_backend``) or at first audio
operation when the native extension is absent and the user hasn't
opted into a fallback.
"""
[docs]
class UnknownBusError(AudioError):
"""Bus name not found in the active layout.
Raised by ``AudioBusLayout.get_bus`` and every backend's bus resolver
when a player references a bus that hasn't been declared. The error
message lists the available bus names to help the caller fix the
typo or register the missing bus.
"""
def __init__(self, name: str, *, available: list[str]) -> None:
super().__init__(
f"Unknown audio bus: {name!r}. Available: {sorted(available)}. "
f"Register the bus via AudioBusLayout.get_default().add_bus(...) "
f"before referencing it from a player."
)
self.name = name
self.available = list(available)
[docs]
class InvalidStreamError(AudioError):
"""Stream source is unrecognised or unsupported by the active backend."""
[docs]
class AudioCapabilityError(AudioError):
"""The active backend doesn't support the requested capability.
Use when a player asks for something the backend can't deliver
(e.g. ``set_pitch`` on a streaming sound on a backend that can't
pitch-shift streams). Includes the capability name + the backend's
advertised capability set so the caller knows what's available. An
optional ``remediation`` string is appended verbatim to point the
caller at the fix.
Accepts ``capability`` and the ``advertised`` set as either ``str``
or :class:`simvx.core.audio_protocol.Capability`. Members are stored
as plain strings so the error remains hashable/serialisable.
"""
def __init__(
self,
capability: str | object,
*,
backend: str,
advertised: set[str] | set[object] | frozenset[object],
remediation: str | None = None,
) -> None:
cap_str = str(capability)
advertised_strs = {str(c) for c in advertised}
message = (
f"Audio backend {backend!r} does not support capability {cap_str!r}. "
f"Advertised capabilities: {sorted(advertised_strs)}."
)
if remediation:
message = f"{message} {remediation}"
super().__init__(message)
self.capability = cap_str
self.backend = backend
self.advertised = advertised_strs
self.remediation = remediation
[docs]
class AudioMutationDuringPlaybackError(AudioError):
"""A next-play-only Property was mutated while a channel was active.
Raised in strict mode when a Property declared with
``mutation_policy="next_play"`` (e.g. ``loop``, ``stream_mode``) is
written while the player has an active channel. Non-strict mode logs
a one-time warning and silently defers the change to the next
``play()`` call.
"""
def __init__(self, prop_name: str, *, player: str) -> None:
super().__init__(
f"Cannot mutate {prop_name!r} on {player!r} mid-playback. "
f"Stop the player first, change the property, then call play() again. "
f"Set SIMVX_AUDIO_STRICT=0 to defer instead of raising."
)
self.prop_name = prop_name
self.player = player