Spatial Audio

3D positional sound with distance attenuation.

📄 Docs only

Tags: audio 3d spatial

What it demonstrates

  • AudioPlayer3D attached to a moving mesh so the mix pans and attenuates with distance.

  • AudioListener3D parented under the Camera3D, so the camera pose is the ear.

  • A procedurally generated looping tone via AudioClip.tone() (no external asset).

  • Live spatial state: listener->source distance changes as the source orbits the listener.

Source

 1"""Spatial Audio: 3D positional sound with distance attenuation.
 2
 3# /// simvx
 4# tags = ["audio", "3d", "spatial"]
 5# web = { root = "SpatialAudio", width = 800, height = 600, responsive = true, disabled = true, reason = "Audio playback needs a device; the web backend renders the scene but stays silent." }
 6# ///
 7
 8## What it demonstrates
 9- `AudioPlayer3D` attached to a moving mesh so the mix pans and attenuates with distance.
10- `AudioListener3D` parented under the `Camera3D`, so the camera pose is the ear.
11- A procedurally generated looping tone via `AudioClip.tone()` (no external asset).
12- Live spatial state: listener->source distance changes as the source orbits the listener.
13"""
14
15import math
16
17from simvx.core import (
18    AudioClip,
19    AudioListener3D,
20    AudioPlayer3D,
21    Camera3D,
22    DirectionalLight3D,
23    Material,
24    Mesh,
25    MeshInstance3D,
26    Node,
27    Vec3,
28)
29from simvx.graphics import App
30
31ORBIT_RADIUS = 6.0  # World units the source orbits at.
32ORBIT_SPEED = 0.6  # Radians per second.
33
34
35class SpatialAudio(Node):
36    def on_ready(self):
37        # Camera looks down the orbit plane from above and behind; it is the ear.
38        cam = self.add_child(Camera3D(name="Camera", position=Vec3(0, 4, 9), look_at=Vec3(0, 0, 0), fov=60.0))
39        cam.add_child(AudioListener3D(name="Listener"))
40        self.add_child(DirectionalLight3D())  # so the source and marker are lit
41
42        # A small marker mesh at the listener origin for visual reference.
43        origin = self.add_child(MeshInstance3D(name="ListenerMarker"))
44        origin.mesh = Mesh.cube()
45        origin.scale = Vec3(0.4, 0.4, 0.4)
46        origin.material = Material(colour=(0.9, 0.9, 0.2, 1.0), roughness=0.6)
47
48        # The moving sound source: a small sphere that orbits the listener.
49        self.source = self.add_child(MeshInstance3D(name="Source", position=Vec3(ORBIT_RADIUS, 0, 0)))
50        self.source.mesh = Mesh.sphere()
51        self.source.scale = Vec3(0.5, 0.5, 0.5)
52        self.source.material = Material(colour=(0.2, 0.6, 1.0, 1.0), roughness=0.3)
53
54        # The 3D audio player rides on the source; its world position drives the mix.
55        self.player = self.source.add_child(AudioPlayer3D(name="Tone"))
56        self.player.stream = AudioClip.tone(330.0, duration=2.0, volume=0.4)
57        self.player.loop = True
58        self.player.max_distance = 12.0  # Inaudible past this radius.
59        self.player.attenuation = 2.0  # Inverse-square falloff.
60        self.player.play()
61
62        self._angle = 0.0
63
64    def on_update(self, dt: float):
65        # Orbit the source around the listener so distance and direction change.
66        self._angle += ORBIT_SPEED * dt
67        self.source.position = Vec3(math.cos(self._angle) * ORBIT_RADIUS, 0.0, math.sin(self._angle) * ORBIT_RADIUS)
68
69
70if __name__ == "__main__":
71    App(title="Spatial Audio", width=800, height=600).run(SpatialAudio())