Audio Playback¶
AudioPlayer with procedurally generated tones, volume, and pitch.
▶ Run in browserTags: audio
Demonstrates:
AudioClip.tone() for a procedural sine tone, so the example ships no asset files
AudioPlayer play / pause / resume / stop
Live volume_db and pitch_scale adjustment on an already-playing sound
get_playback_position() for a running position readout
Controls: 1-4: Play tones (C4=262Hz, E4=330Hz, G4=392Hz, C5=523Hz) SPACE: Pause / resume current tone S: Stop all audio UP/DOWN: Adjust volume LEFT/RIGHT: Adjust pitch ESC: Quit (or click the buttons along the bottom)
Source¶
1"""Audio Playback: AudioPlayer with procedurally generated tones, volume, and pitch.
2
3Demonstrates:
4- AudioClip.tone() for a procedural sine tone, so the example ships no asset files
5- AudioPlayer play / pause / resume / stop
6- Live volume_db and pitch_scale adjustment on an already-playing sound
7- get_playback_position() for a running position readout
8
9Controls:
10 1-4: Play tones (C4=262Hz, E4=330Hz, G4=392Hz, C5=523Hz)
11 SPACE: Pause / resume current tone
12 S: Stop all audio
13 UP/DOWN: Adjust volume
14 LEFT/RIGHT: Adjust pitch
15 ESC: Quit
16 (or click the buttons along the bottom)
17"""
18
19from simvx.core import (
20 AnchorPreset,
21 AudioClip,
22 AudioPlayer,
23 Button,
24 HBoxContainer,
25 Input,
26 Key,
27 Node,
28 Text2D,
29 Vec2,
30)
31from simvx.graphics import App
32
33# The notes offered by the 1-4 keys and by the on-screen buttons.
34NOTES = {"C4": 261.63, "E4": 329.63, "G4": 392.00, "C5": 523.25}
35NOTE_KEYS = (Key.KEY_1, Key.KEY_2, Key.KEY_3, Key.KEY_4)
36
37# 6 dB per step is clearly audible: half or double the linear amplitude.
38VOLUME_STEP_DB = 6.0
39VOLUME_MIN_DB, VOLUME_MAX_DB = -40.0, 12.0
40PITCH_STEP = 1.1
41
42BUTTON_GAP = 10.0
43STRIP_MARGIN = 20.0
44
45
46class AudioDemo(Node):
47 """Interactive audio demo driven by the keyboard and by on-screen buttons."""
48
49 input_actions = {"quit": [Key.ESCAPE]}
50
51 def __init__(self, **kwargs):
52 super().__init__(**kwargs)
53 self._tones: dict[str, AudioClip] = {}
54 self._player: AudioPlayer | None = None
55 self._status_label: Text2D | None = None
56 self._current_note: str = ""
57
58 def on_ready(self):
59 # AudioClip.tone() bakes the PCM into the clip, so no file is decoded.
60 self._tones = {name: AudioClip.tone(freq, duration=3.0) for name, freq in NOTES.items()}
61
62 self._player = self.add_child(AudioPlayer(name="Player"))
63 # `loop` only takes effect on the next play(), so set it before the first one.
64 self._player.loop = True
65
66 self._status_label = self.add_child(Text2D(text="", position=Vec2(40, 40), name="Status"))
67
68 # Bottom control strip. The container owns the per-child positions and the
69 # anchor plus margins keep the whole strip centred as the window resizes,
70 # so nothing here recomputes a position per frame.
71 controls = HBoxContainer(name="Controls")
72 controls.separation = BUTTON_GAP
73 for note in NOTES:
74 controls.add_child(Button(note, on_press=lambda n=note: self._play_note(n), size_x=64, size_y=40))
75 controls.add_child(Button("Pause", on_press=self._toggle_pause, size_x=80, size_y=40))
76 controls.add_child(Button("Stop", on_press=self._stop, size_x=80, size_y=40))
77
78 strip = controls.get_minimum_size()
79 controls.set_anchor_preset(AnchorPreset.CENTER_BOTTOM)
80 controls.margin_left = -strip.x / 2
81 controls.margin_right = strip.x / 2
82 controls.margin_top = -(strip.y + STRIP_MARGIN)
83 controls.margin_bottom = -STRIP_MARGIN
84 self.add_child(controls)
85
86 self._update_status()
87
88 # --- Shared control handlers (keyboard and buttons both call these) ---
89
90 def _play_note(self, note: str):
91 player = self._player
92 if not player:
93 return
94 # stop() first: play() on a paused player resumes the previous tone
95 # instead of starting the newly assigned stream.
96 player.stop()
97 player.stream = self._tones[note]
98 player.play()
99 self._current_note = note
100
101 def _toggle_pause(self):
102 player = self._player
103 if not player:
104 return
105 if player.is_playing():
106 player.pause()
107 elif player.is_paused():
108 player.play()
109
110 def _stop(self):
111 player = self._player
112 if not player:
113 return
114 player.stop()
115 self._current_note = ""
116
117 def on_update(self, delta: float):
118 player = self._player
119 if not player:
120 return
121
122 if Input.is_action_just_pressed("quit"):
123 self.app.quit()
124 return
125
126 for key, note in zip(NOTE_KEYS, NOTES, strict=True):
127 if Input.is_key_just_pressed(key):
128 self._play_note(note)
129
130 if Input.is_key_just_pressed(Key.SPACE):
131 self._toggle_pause()
132
133 if Input.is_key_just_pressed(Key.S):
134 self._stop()
135
136 # volume_db and pitch_scale are live: writing either one mid-playback is
137 # pushed straight to the active channel, and both clamp to their declared
138 # Property range (pitch_scale to [0.5, 2.0]).
139 if Input.is_key_just_pressed(Key.UP):
140 player.volume_db = min(VOLUME_MAX_DB, player.volume_db + VOLUME_STEP_DB)
141 if Input.is_key_just_pressed(Key.DOWN):
142 player.volume_db = max(VOLUME_MIN_DB, player.volume_db - VOLUME_STEP_DB)
143 if Input.is_key_just_pressed(Key.RIGHT):
144 player.pitch_scale *= PITCH_STEP
145 if Input.is_key_just_pressed(Key.LEFT):
146 player.pitch_scale /= PITCH_STEP
147
148 self._update_status()
149
150 def _update_status(self):
151 player = self._player
152 if not (self._status_label and player):
153 return
154 state = "Stopped"
155 if player.is_playing():
156 state = f"Playing {self._current_note}"
157 elif player.is_paused():
158 state = f"Paused {self._current_note}"
159
160 self._status_label.text = (
161 f"Audio Demo\n\n"
162 f"State: {state}\n"
163 f"Position: {player.get_playback_position():.1f}s\n"
164 f"Volume: {player.volume_db:.0f} dB\n"
165 f"Pitch: {player.pitch_scale:.2f}x\n\n"
166 f"1-4: Play notes (C4, E4, G4, C5)\n"
167 f"SPACE: Pause/Resume | S: Stop\n"
168 f"UP/DOWN: Volume | LEFT/RIGHT: Pitch\n"
169 f"ESC: Quit (or click the buttons below)"
170 )
171
172
173if __name__ == "__main__":
174 app = App(width=800, height=400, title="SimVX Audio Demo")
175 app.run(AudioDemo())