SimVX Audio System¶
Audio playback for background music, UI sounds, 2D positional effects, and 3D
spatial sound. Three player nodes feed a typed bus graph with per-bus effect
chains; an AudioSynth builds procedural audio with no asset files; a
scene-tree AudioListener node receives the spatialization. Backends are
pluggable: native MiniaudioBackend (CFFI over miniaudio’s ma_engine),
pure-Python _LegacyMiniaudioBackend, silent NullAudioBackend, and
WebAudioBackend for browser exports.
Features¶
Non-positional, 2D, and 3D player nodes with shared playback lifecycle.
Scene-tree
AudioListener2D/AudioListener3Dnodes that work likeCamera2D/Camera3D: most recently entered is current; lazy fallback if none is added.Typed audio buses (
AudioBus) withvolume_db,mute,solo,send_torouting, and per-bus effect chains.Procedural synthesis (
AudioSynth): oscillators, noise, envelopes, per-source filters, baked or live-streamed.Live property pushes:
volume_db,pan_override, andpitch_modulatepropagate to active channels without retriggering.Per-player fades:
fade_in(seconds),fade_out(seconds),crossfade_to(other, seconds)(replaces the removed bus-levelFadeEffect).queue_free_on_endauto-removes one-shot players when playback ends.Strict-mode error model: typed exceptions for unknown buses, unsupported capabilities, mid-playback property mutations, and missing backends.
Quick Start¶
1. Background music¶
from simvx.core import AudioStreamPlayer
music = AudioStreamPlayer(
stream="music/theme.ogg",
volume_db=-5.0,
loop=True,
autoplay=True,
bus="Music",
)
root.add_child(music)
2. 2D sound effects¶
from simvx.core import AudioStreamPlayer2D, Resource, Vec2
explosion = AudioStreamPlayer2D(
stream=Resource("game.assets", "explosion.wav"),
position=Vec2(200, 300),
max_distance=400.0,
bus="SFX",
)
root.add_child(explosion)
explosion.play()
3. 3D spatial audio (with explicit listener)¶
from simvx.core import AudioListener3D, AudioStreamPlayer3D, Vec3
from simvx.core.nodes_3d.camera import Camera3D
class MyScene(Node3D):
def on_ready(self):
camera = self.add_child(Camera3D())
# Parent the listener under the camera so its pose follows.
camera.add_child(AudioListener3D())
engine = AudioStreamPlayer3D(
stream="sfx/engine.ogg",
position=Vec3(10, 0, 5),
max_distance=100.0,
loop=True,
doppler_scale=0.5,
bus="SFX",
)
self.add_child(engine)
engine.play()
If no AudioListener3D is in the tree when an AudioStreamPlayer3D needs
one, the engine lazy-creates a fallback under the active Camera3D and
logs a one-time warning. Add an explicit listener to silence it.
4. Synthetic tones (no file)¶
from simvx.core import AudioStream, AudioStreamPlayer
beep = AudioStreamPlayer(stream=AudioStream.tone(440)) # 440 Hz, 1 s
root.add_child(beep)
beep.play()
AudioStream: audio sources¶
AudioStream and the AudioStreamPlayer* constructors accept:
Source |
Use case |
|---|---|
|
Filesystem audio file. |
|
Asset shipped inside a Python package. |
|
Raw |
|
Procedural sine with 20 ms fade-in/out. |
|
Wrap a float32 ndarray (interleaved stereo or mono). |
|
Synthetic placeholder with no audio data. Replaces the legacy |
The container format is sniffed from the file header (wav / ogg / mp3
/ flac / pcm for synthetic / unknown for failures); the streaming
open path uses it to pick the right decoder rather than guessing from the
extension.
Sharing a decoded stream between players:
from pathlib import Path
from simvx.core import AudioStream, AudioStreamPlayer
stream = AudioStream(Path("music/boss_fight.ogg"))
player1 = AudioStreamPlayer(stream=stream)
player2 = AudioStreamPlayer(stream=stream)
On the native backend, file-backed streams are opened directly via
ma_sound_init_from_file; ndarray-backed streams (synthetic /
AudioSynth.bake()) route through ma_audio_buffer. The legacy backend
caches decoded PCM in stream.backend_data after the first play so a
second player skips decode.
Supported formats: WAV (uncompressed; streaming path parses RIFF
chunks itself), OGG, MP3, FLAC (decoded by miniaudio on desktop; by
AudioContext.decodeAudioData in the browser for memory mode).
Audio Player Nodes¶
All three players share _AudioPlaybackMixin, which provides play,
stop, pause, is_playing, is_paused, fade_in, fade_out,
crossfade_to, pitch_modulate, and (on the non-positional player)
get_playback_position.
Property mutation policy. Each Property’s hint string carries a
[live] or [next_play] tag:
[live]Properties (volume_db,pitch_scale,pan_override, spatialmax_distance/attenuation/doppler_scale) push to the active backend channel on change: no retrigger needed.[next_play]Properties (bus,loop,autoplay,stream_mode,buffer_size,queue_free_on_end) require a freshplay()call to take effect. Mutating one mid-playback raisesAudioMutationDuringPlaybackErrorin strict mode (dev default) or warns once and defers in non-strict mode.
AudioStreamPlayer¶
Non-positional player for background music and UI sounds.
Property |
Default |
Range / values |
Policy |
|---|---|---|---|
|
|
|
live |
|
|
|
live (via |
|
|
enum |
next_play |
|
|
bool |
next_play |
|
|
bool |
next_play |
|
|
bool |
next_play |
|
|
|
next_play |
|
|
|
next_play |
Arbitrary bus strings outside the enum still work, but they must be
present in the active AudioBusLayout or playback raises
UnknownBusError.
Signals:
stream_open_failed(path: str): emitted whenstream_mode="streaming"cannot open or parse the source.
Methods:
play(from_position=0.0): start (or resume from pause) playback.from_positionis in seconds and seeks the source cursor before the first sample renders.stop(): stop the channel and close any streaming file handle.pause()/is_paused(): resumed viaplay()(withoutfrom_position, which would re-seek).is_playing(): true while not paused.fade_in(seconds): start (or continue) playback at silence and ramp up tovolume_dbover the duration.fade_out(seconds): linear-dB ramp to silence, thenstop().crossfade_to(other, seconds): startothersilently, fade self out while fading other in.pitch_modulate(scale): live pitch shift on an already-playing channel (clamped topitch_scalerange).get_playback_position() -> float: current cursor in seconds.
AudioStreamPlayer2D¶
2D positional player. Volume attenuates with distance from the current
AudioListener2D; stereo pan derives from horizontal offset.
Inherits the playback Properties above. Defaults that differ:
Property |
Default |
Range |
Policy |
|---|---|---|---|
|
|
enum as above |
next_play |
|
|
|
live |
|
|
|
live |
|
|
float in |
live |
|
|
RGBA |
: |
set_pan_and_gain(pan, gain_db) writes both Properties as a single
backend push for sample-accurate combined changes. get_gizmo_lines()
returns a 32-segment circle showing max_distance for the editor.
AudioStreamPlayer3D¶
3D spatial player. Volume attenuates with distance from the current
AudioListener3D; stereo pan is the dot product with the listener’s
right vector (forward × up); pitch shifts with radial velocity when
doppler_scale > 0.
Inherits the playback Properties above. Defaults that differ:
Property |
Default |
Range |
Policy |
|---|---|---|---|
|
|
enum as above |
next_play |
|
|
|
live |
|
|
|
live |
|
|
|
live |
|
|
float in |
live |
|
|
RGBA |
: |
get_gizmo_lines() returns three orthogonal circles forming a wireframe
sphere at max_distance. set_pan_and_gain(pan, gain_db) mirrors the 2D
variant.
AudioListener2D / AudioListener3D¶
Scene-tree nodes that receive spatial audio: the audio analogue of
Camera2D / Camera3D. Place one in the scene; the most recently
entered listener becomes the current one for its tree, and every
positional audio player computes attenuation, pan, and (for 3D) Doppler
relative to it.
from simvx.core import AudioListener3D, Vec3
from simvx.core.nodes_3d.camera import Camera3D
class MyScene(Node3D):
def on_ready(self):
cam = self.add_child(Camera3D())
cam.add_child(AudioListener3D()) # follows camera pose
Properties:
auto_current(bool, defaultTrue): become the current listener onenter_tree. SetFalseif you want to manage active listeners explicitly vialistener.make_current().
Methods:
make_current(): promote this listener to the active one for its scene tree; pushes the new pose to the backend immediately.
AudioListener3D extras:
velocity(Vec3, plain attribute): listener velocity in m/s for Doppler. Not a Property because Vec3 isn’t scalar-serialisable; assignments forward tobackend.set_listener_velocityso the native spatializer sees the velocity every push. The engine does not compute it automatically: set it from a camera-follow controller, or leave it at zero to disable Doppler regardless of any source’sdoppler_scale.
Orientation (forward, up, right) is inherited from Node3D, so
the listener’s rotation directly drives its pose.
Lazy fallback. Audio players reach the current listener via
tree.audio_listener_3d() / tree.audio_listener_2d(). If none has
been added, the engine auto-creates one parented to the active camera
and logs a one-time warning (audio.listener.autocreated_3d /
audio.listener.autocreated_2d). Add an explicit listener to silence
it. If there’s no camera either, the warning is
audio.listener.no_camera and the audio degrades to listener-at-origin.
The legacy singleton AudioListener.get() has been removed; calling it
raises AudioError with a migration pointer. The class survives only as
a stub for clean import errors during migration.
Audio Buses¶
The default layout has five buses, all using TitleCase Godot-convention
names (Master, Music, SFX, Voice, UI), each non-master bus
routed to Master via send_to. Bus names are case-sensitive:
"master" does not match "Master" and raises UnknownBusError with the
available names listed in the error message.
from simvx.core.audio_bus import AudioBusLayout
layout = AudioBusLayout.get_default()
layout.get_bus("Music").volume_db = -6.0
layout.get_bus("SFX").mute = True
# Effective volume walks the send_to chain to Master:
effective = layout.get_bus("SFX").effective_volume
AudioBus exposes:
Member |
Notes |
|---|---|
|
Property, range |
|
Property (bool). Walks: any muted bus along the chain forces effective volume to |
|
Property (bool). When any non-master bus is soloed, every non-solo, non-ancestor-of-solo, non-Master bus is gated to silence. Master is always exempt. |
|
Parent bus name (empty for root). Validated in |
|
|
|
Sum along the chain, with mute/solo gating. |
|
|
|
Per-bus DSP chain (see below). |
AudioBusLayout exposes add_bus, remove_bus (Master is protected),
get_bus, has_bus, buses, bus_names, to_dict, from_dict. The
singleton lives at AudioBusLayout.get_default(); reset() drops it
(for tests).
Each tick, SceneTree calls backend.sync_bus_layout(layout). The
native backend diffs (volume_db, mute, send_to) per bus and pushes
only on change, plus reconciles the effect chain whenever its signature
changes. The legacy backend re-snapshots gains per audio period during
mixing. The web backend diffs the layout when its drain runs (so volume
/ mute updates land live); its sync_bus_layout method itself is a
no-op since the JS bridge already drives off the per-drain diff.
Audio Effects¶
Buses route through chains of typed AudioEffect instances from
simvx.core.audio_effect. The Python layer owns parameters and ordering;
backends materialise the chain natively (ma_engine’s ma_node_graph on
desktop; Web Audio AudioNode chains on the web bridge).
from simvx.core import AudioBusLayout
from simvx.core.audio_effect import (
LowPassFilter, ReverbEffect, CompressorEffect, ParametricEQ, EQBand,
)
layout = AudioBusLayout.get_default()
music = layout.get_bus("Music")
music.add_effect(LowPassFilter(cutoff_hz=4000.0, q=1.2))
music.add_effect(ReverbEffect(room_size=0.7, wet=0.3, width=1.0, freeze=False))
sfx = layout.get_bus("SFX")
sfx.add_effect(CompressorEffect(threshold_db=-18.0, ratio=4.0, makeup_db=4.0))
sfx.add_effect(ParametricEQ(bands=[
EQBand(type="lowshelf", freq=120.0, gain_db=-3.0),
EQBand(type="peaking", freq=2500.0, q=1.4, gain_db=+2.0),
]))
Effects chain in declaration order (source → effect[0] → … → effect[N-1] → send_to). Disable an effect without removing it via
effect.enabled = False. The native backend rebuilds its ma_node chain
only when an effect’s signature changes (rounded parameter tuple), so
parameter sweeps are cheap.
Capability gating. Each effect class declares a required_capability
(a Capability StrEnum member). Backends advertise their supported set
via list_capabilities(); unsupported effects are skipped with a single
warning per (effect, backend) pair.
Backend |
Effect capabilities advertised |
|---|---|
|
|
|
none: the Python mixer can’t run effects at acceptable cost. |
|
|
|
none: silent. |
v1 effects:
Effect |
Parameters |
Native |
Web |
|---|---|---|---|
|
|
composed into output bus volume |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
notch effect node |
|
|
|
|
|
|
|
|
|
|
|
chain of peak/loshelf/hishelf nodes |
chain of |
|
|
tanh waveshaper |
|
|
|
feed-forward compressor |
|
Q on LP/HP/BP/notch is honoured on both web (true biquad) and native
(Phase 2 wired ma_lpf2/hpf2/bpf2 cascades with proper resonance).
ReverbEffect.freeze (infinite-sustain tail) and width (stereo width)
were similarly wired in Phase 2.
There is no FadeEffect. Per-player fades on the player node
(AudioStreamPlayer.fade_in / fade_out / crossfade_to) replace it:
fades are a property of the playing source, not the bus.
Procedural Synthesis (AudioSynth)¶
simvx.core.audio_synth provides oscillators, noise, envelopes,
per-source filters, and AudioSynth for runtime sound generation
without audio assets.
from simvx.core import AudioSynth, Oscillator, ADSR
# Bake a one-shot sound.
synth = AudioSynth()
synth.add(
Oscillator.sine(440.0),
envelope=ADSR(attack=0.01, decay=0.1, sustain=0.6, release=0.2),
gain=0.5,
)
pluck = synth.bake(duration=0.4) # → AudioStream
player.stream = pluck
player.play()
# Multiple voices mix together.
chord = AudioSynth()
chord.add(Oscillator.sine(220.0), gain=0.5)
chord.add(Oscillator.sine(330.0), gain=0.3) # fifth
chord.add(Oscillator.noise.white(seed=42), gain=0.1) # hiss
player.stream = chord.bake(duration=1.0)
bake() returns an AudioStream whose backend_data is a float32
interleaved-stereo ndarray; both desktop backends and the web backend
accept it directly. The stream carries the synth’s sample_rate and
channels so the backend doesn’t pitch-shift it against its own rate.
Live procedural (mutable parameters):
synth = AudioSynth()
vid = synth.add(Oscillator.sine(220.0), gain=0.5)
driver = synth.attach_to(player, chunk_seconds=0.1)
# Later, mid-game:
synth.set_param(vid, "freq", 880.0) # takes effect next chunk (<=100 ms)
attach_to(player, chunk_seconds=0.1, sample_rate=48000, channels=2)
adds a small _AudioSynthDriver Node as a child of player. The driver
calls backend.open_stream(bus=player.bus) and feeds
chunk_seconds * sample_rate samples per on_process tick. Returns the
driver so the caller can parent.remove_child(driver) to stop streaming.
The driver requires an AudioStreamingBackend; on NullAudioBackend
(playback-only) it raises AudioCapabilityError with remediation
pointing at the native install. The native path uses an ma_pcm_rb ring
buffer (default 0.5 s, override via MiniaudioBackend.open_stream( buffer_seconds=)); the legacy path appends to a Python bytearray; the
web path posts to an AudioWorkletNode. Underrun produces silence on all
three.
Per-source filters (one-pole). Attach a simple LP/HP to a single
voice via the filter= kwarg. Lighter than the bus-level
LowPassFilter / HighPassFilter effects (which are 2nd-order biquads
on web and on native); use these when you want the filter shape baked
into the source’s audio:
from simvx.core import AudioSynth, Oscillator, ADSR
from simvx.core.audio_synth import LowPass
synth = AudioSynth()
synth.add(
Oscillator.noise.white(),
envelope=ADSR(attack=0.001, decay=0.0, sustain=1.0, release=0.05),
filter=LowPass(800), # warm filtered-noise impact
gain=0.5,
)
impact = synth.bake(duration=0.1)
Building blocks (from simvx.core.audio_synth):
Class |
Notes |
|---|---|
|
Pure sine, phase-continuous across chunks. |
|
Pulse-width adjustable (clamped 0.01-0.99). |
|
Classic. |
|
Uniform-distribution white noise. |
|
Voss-McCartney pink noise (16 rows). |
|
All in seconds; sustain is a level. Release truncates if duration is short. |
|
Straight-line / exponential envelopes. |
|
First-order one-pole per-source filters (use |
AudioSynth.render_chunk(cursor_samples, n_samples, ...) is the
streaming primitive used by attach_to; envelopes are not applied
here (they belong to baked clips whose envelope spans the whole
duration).
Backends + Protocol split¶
The audio system talks to three thin structural Protocol types
(simvx.core.audio_protocol):
AudioPlaybackBackend: start / stop / pause / live-update sounds, plus listener pose endpoints (set_listener_position/_velocity/_direction/_world_up).AudioStreamingBackend:open_stream/feed_audio_chunkfor chunk-fed PCM (AudioSynth, AudioWorklet, compressed-container streaming).AudioBusBackend:sync_bus_layout+list_capabilities.
AudioBackend is a union Protocol over all three: kept for callers
that genuinely need every facet (e.g. MiniaudioBackend). Narrower
callers should depend on the smallest Protocol they actually use and
reach the backend via SceneTree.audio_playback, audio_streaming,
or audio_buses so the type checker enforces the boundary:
from simvx.core.audio_protocol import AudioStreamingBackend
backend = self.tree.audio_streaming # None or AudioStreamingBackend
if backend is None:
raise AudioCapabilityError(
"streaming",
backend=type(self.tree.audio_backend).__name__,
advertised=self.tree.audio_backend.list_capabilities(),
remediation="Install the native extension or use stream_mode='memory'.",
)
Capability is a StrEnum, so Capability.PLAY_BASIC in caps works
against either a frozenset[Capability] or a legacy frozenset[str],
and string literals in backend code are caught by the type checker.
Members: PLAY_BASIC, PLAY_2D, PLAY_3D, SPATIAL_HRTF,
SPATIAL_DOPPLER, STREAMING, STREAMING_WAV, STREAMING_OGG,
STREAMING_MP3, STREAMING_FLAC, EFFECT_GAIN, EFFECT_FILTER_BIQUAD,
EFFECT_PARAMETRIC_EQ, EFFECT_DELAY, EFFECT_REVERB,
EFFECT_COMPRESSOR, EFFECT_SOFTCLIP.
Resolution. make_backend(sample_rate=48000, nchannels=2) resolves
at runtime in this order: the runtime never invokes a C compiler;
that’s the install-time build hook’s job (see below):
Native
MiniaudioBackend(~20 ms latency, GIL-immune mixing). Selected when the compiled_simvx_miniaudio_engineextension imports cleanly._LegacyMiniaudioBackend(~100 ms pure-Python mixer running on miniaudio’s audio thread). Selected when the native extension is unavailable andSIMVX_ALLOW_LEGACY_AUDIO != "0". Supports every playback / streaming / bus call: only latency differs. Advertises noeffect.*capabilities, so bus effects are skipped on this path.NullAudioBackend(silent). Selected when even legacy can’t start. Calls return valid channel ids; nothing is heard. Does not implementAudioStreamingBackend, soAudioSynth.attach_toand streaming player modes raiseAudioCapabilityErrorhere.
MiniaudioBackend (and the legacy + null backends) register an atexit
hook so miniaudio’s audio thread is joined cleanly even when callers
bypass App.quit() with sys.exit.
Note
Native HRTF spatializer is deferred. The native backend still computes
3D pan + attenuation in Python (via _compute_3d_state) and pushes the
result per frame via update_audio_3d. The C-side ma_engine spatializer
(true HRTF, hardware Doppler) is wired but not yet used. Tracked in
BUGS.md as bug-audio-native-ma-engine-spatializer-dead-code
(Phase 1c). Current 3D audio is correct but does not yet use the
canonical native path.
Install + runtime modes¶
See Installation for the full install matrix. In summary, three install-time env vars control how the C extension is compiled:
Env var |
Behaviour at install |
|---|---|
(default) |
Tries to compile. On failure prints a stderr WARNING; install succeeds. |
|
Skips compile entirely. Quiet. |
|
Fails install if compile fails (for CI). |
…and one runtime env var controls the fallback chain:
Env var |
Effect |
|---|---|
(default) |
Native → legacy (with WARNING) → null (with WARNING). |
|
Native or |
To rebuild the extension after install without re-running pip:
uv run --with setuptools simvx build-audio
Strict mode + error types¶
SIMVX_AUDIO_STRICT (default "1", dev) controls whether historically
silent code paths raise or warn-once. Shipping games typically set
SIMVX_AUDIO_STRICT=0 so a misconfigured asset doesn’t crash the
player; development runs strict so bugs surface early.
All audio exceptions live in simvx.core.audio_errors and inherit from
AudioError:
Exception |
Raised when |
|---|---|
|
Base class for |
|
Backend selection or initialisation failed (with |
|
A player references a bus that isn’t in the active layout. Message lists the available bus names. |
|
Stream source unrecognised or unsupported by the active backend; also raised for |
|
Backend doesn’t advertise the requested capability (e.g. streaming on |
|
A |
Two helpers gate strict vs lenient mode:
warn_once(key, msg, *args, exc_info=False): log a WARNING the first timekeyis seen, then suppress. Use for non-fatal failures insideon_processticks (would otherwise flood the log).raise_or_warn(exc, *, key, message): re-raise wrapped inAudioErrorunder strict mode; otherwise warn once. Use at cleanup boundaries where the surrounding code can continue.
Performance Tips¶
Use OGG / FLAC for music: smaller file than WAV.
Use WAV for short SFX: fast decode, no codec overhead.
Limit active 3D sounds: each 3D player runs Doppler math and a backend update every
on_processtick.Share decoded streams: construct
AudioStream(...)once and pass it to multiple players (backend_datacaches the PCM on the legacy path; the native path opens the file once per sound).Bake long synths once:
AudioSynth.bake()is pure numpy; the resultingAudioStreamplays through any backend with no per-frame cost. Reserveattach_tofor synths whose parameters actually need to change mid-play.Push spatial properties through Properties, not retriggers: the
[live]Properties (volume_db,max_distance,pan_override, …) reach the backend without astop()/play()cycle.
Troubleshooting¶
No sound at all. Check the resolved backend by enabling WARNING logs:
import logging; logging.basicConfig(level=logging.WARNING)
A “Native audio extension not available; using legacy mixer” or
“using silent NullAudioBackend” line identifies which fallback is in
use. To get the native mixer back, run
uv run --with setuptools simvx build-audio.
UnknownBusError. Your player’s bus doesn’t match a bus name in
the layout. Bus names are case-sensitive: "sfx" is not "SFX". The
error message lists the available names; either fix the typo or call
AudioBusLayout.get_default().add_bus("MyBus", send_to="Master")
before referencing it.
AudioCapabilityError: streaming. The active backend doesn’t
implement AudioStreamingBackend: usually because you’re on
NullAudioBackend (no audio device or miniaudio package missing).
Install the native extension, or switch to stream_mode="memory" /
AudioSynth.bake() instead of attach_to.
AudioMutationDuringPlaybackError. You wrote a next_play Property
(e.g. player.loop = True) while a channel was active. Stop the player,
change the property, then play() again. Set SIMVX_AUDIO_STRICT=0 to
defer the change to the next play() instead of raising.
Bus muted. Walk the layout:
AudioBusLayout.get_default().get_bus("Music").effective_volume. Any
muted bus along the send_to chain returns -80 dB. If you’ve used
solo anywhere, remember solo on a non-master bus gates every non-solo
non-ancestor non-Master bus to silence.
3D panning stuck / silent. Make sure you have an AudioListener3D
in the scene (or that the auto-created fallback warning fired). For
Doppler, set listener.velocity = Vec3(...) from a camera-follow
controller: the engine doesn’t compute it for you.
Crackling / stuttering on the legacy backend. Known limitation of the Python mixer (audio thread runs under the GIL). Build the native extension: it moves mixing into C off-thread.
stream_open_failed signal fires. stream_mode="streaming"
validates the container from the file header. WAV runs through the
engine-side header parser; OGG / MP3 / FLAC hand off to the backend’s
native decoder. Synthetic PCM streams (from_pcm / tone) can’t stream;
use stream_mode="memory". Connect to the signal to fall back to a
placeholder.
Example: Complete Game Audio¶
from simvx.core import (
AudioListener3D,
AudioStreamPlayer,
AudioStreamPlayer3D,
Input,
InputMap,
MouseButton,
Node,
Vec3,
)
from simvx.core.nodes_3d.camera import Camera3D
from simvx.graphics import App
class GameScene(Node):
def __init__(self):
super().__init__()
# Background music on the Music bus.
music = AudioStreamPlayer(
stream="music/gameplay.ogg",
volume_db=-8.0,
loop=True,
autoplay=True,
bus="Music",
)
self.add_child(music)
# UI click sound on the UI bus.
self.ui_click = AudioStreamPlayer(
stream="ui/button_click.wav",
bus="UI",
)
self.add_child(self.ui_click)
# 3D ambient (waterfall) on SFX.
waterfall = AudioStreamPlayer3D(
stream="ambient/waterfall.ogg",
position=Vec3(20, 0, 10),
max_distance=30.0,
loop=True,
autoplay=True,
bus="SFX",
)
self.add_child(waterfall)
# Camera + AudioListener3D parented to it so the listener pose
# follows the camera every frame.
camera = Camera3D(position=(0, 5, 10))
self.add_child(camera)
camera.add_child(AudioListener3D())
def on_ready(self):
# InputMap.add_action must live in on_ready (web exporter skips main()).
InputMap.add_action("click", [MouseButton.LEFT])
def on_process(self, dt):
if Input.is_action_just_pressed("click"):
self.ui_click.play()
if __name__ == "__main__":
app = App(width=1280, height=720, title="Game")
app.run(GameScene())
API Reference¶
packages/core/src/simvx/core/audio.py:AudioStream,AudioStreamPlayer,AudioStreamPlayer2D,AudioStreamPlayer3D.packages/core/src/simvx/core/audio_listener.py:AudioListener2D,AudioListener3D,_autocreate_listener_*.packages/core/src/simvx/core/audio_bus.py:AudioBus,AudioBusLayout.packages/core/src/simvx/core/audio_effect.py:AudioEffectand v1 effect subclasses (GainEffect,LowPassFilter,HighPassFilter,BandPassFilter,NotchFilter,DelayEffect,ReverbEffect,ParametricEQ/EQBand,SoftClipEffect,CompressorEffect). Per-player fades live on the player node (fade_in,fade_out,crossfade_to); there is noFadeEffect.packages/core/src/simvx/core/audio_synth.py:AudioSynth,Oscillator,WhiteNoise,PinkNoise,ADSR,Linear,Exponential,LowPass,HighPass,AudioSource,Envelope,Filter.packages/core/src/simvx/core/audio_protocol.py:AudioPlaybackBackend,AudioStreamingBackend,AudioBusBackend, unionAudioBackend,CapabilityStrEnum,CAPABILITIES_CORE,PlayMode.packages/core/src/simvx/core/audio_errors.py:AudioError,AudioBackendUnavailable,UnknownBusError,InvalidStreamError,AudioCapabilityError,AudioMutationDuringPlaybackError,warn_once,raise_or_warn,STRICTflag.packages/core/src/simvx/core/audio_backend.py:MiniaudioBackend,_LegacyMiniaudioBackend,NullAudioBackend,make_backend.packages/core/src/simvx/core/_native/miniaudio_engine.py: CFFI wrapper around the compiled_simvx_miniaudio_engineextension.packages/core/src/simvx/core/_native/miniaudio_engine_build.py: CFFI build script invoked bysimvx build-audio.packages/web/src/simvx/web/audio/web_backend.py:WebAudioBackend(Pyodide-side).packages/web/src/simvx/web/runtime/js/audio_bridge.js: JS bridge that materialises the bus / effect graph in Web Audio.packages/graphics/examples/audio_demo.py: interactive demo.packages/core/tests/test_audio*.py: test coverage.