Audio demo¶
generates and plays test tones using the MiniaudioBackend.
▶ Run in browserTags: ui
Demonstrates:
AudioStreamPlayer with programmatically generated sine wave tones
Play, pause, resume, and stop controls via keyboard
Volume and pitch adjustment
On-screen status display
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
Source¶
1"""Audio demo: generates and plays test tones using the MiniaudioBackend.
2
3Demonstrates:
4- AudioStreamPlayer with programmatically generated sine wave tones
5- Play, pause, resume, and stop controls via keyboard
6- Volume and pitch adjustment
7- On-screen status display
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"""
17
18
19import numpy as np
20
21from simvx.core import (
22 AudioStream,
23 AudioStreamPlayer,
24 Input,
25 Key,
26 Node,
27 Property,
28 Text2D,
29 Vec2,
30)
31from simvx.graphics import App
32
33# Sample rate must match the backend (44100)
34SAMPLE_RATE = 44100
35
36
37def generate_tone(freq: float, duration: float = 2.0, volume: float = 0.3) -> AudioStream:
38 """Generate a sine-wave AudioStream at the given frequency."""
39 n_frames = int(SAMPLE_RATE * duration)
40 t = np.linspace(0, duration, n_frames, dtype=np.float32)
41
42 # Sine wave with fade-in/out to avoid clicks
43 fade_frames = min(int(SAMPLE_RATE * 0.02), n_frames // 4)
44 envelope = np.ones(n_frames, dtype=np.float32)
45 envelope[:fade_frames] = np.linspace(0, 1, fade_frames, dtype=np.float32)
46 envelope[-fade_frames:] = np.linspace(1, 0, fade_frames, dtype=np.float32)
47
48 mono = (np.sin(2 * np.pi * freq * t) * volume * envelope).astype(np.float32)
49
50 # Interleave to stereo
51 stereo = np.empty(n_frames * 2, dtype=np.float32)
52 stereo[0::2] = mono
53 stereo[1::2] = mono
54
55 stream = AudioStream(f"tone:{freq:.0f}Hz")
56 stream.backend_data = stereo
57 return stream
58
59
60class AudioDemo(Node):
61 """Interactive audio demo with keyboard controls."""
62
63 volume_db = Property(0.0, range=(-40.0, 12.0), hint="Master volume (dB)")
64 pitch_scale = Property(1.0, range=(0.25, 4.0), hint="Pitch multiplier")
65
66 def __init__(self, **kwargs):
67 super().__init__(**kwargs)
68 self._tones: dict[str, AudioStream] = {}
69 self._player: AudioStreamPlayer | None = None
70 self._status_label: Text2D | None = None
71 self._current_note: str = ""
72
73 def on_ready(self):
74 # Pre-generate tones for musical notes
75 notes = {"C4": 261.63, "E4": 329.63, "G4": 392.00, "C5": 523.25}
76 for name, freq in notes.items():
77 self._tones[name] = generate_tone(freq, duration=3.0)
78
79 # Create audio player
80 self._player = AudioStreamPlayer(name="Player")
81 self.add_child(self._player)
82
83 # Status text
84 self._status_label = Text2D(
85 text=(
86 "Audio Demo\n\n1-4: Play notes (C4, E4, G4, C5)\nSPACE: Pause/Resume\n"
87 "S: Stop\nUP/DOWN: Volume\nLEFT/RIGHT: Pitch\nESC: Quit"
88 ),
89 position=Vec2(40, 40),
90 name="Status",
91 )
92 self.add_child(self._status_label)
93
94 def on_process(self, delta: float):
95
96 player = self._player
97 if not player:
98 return
99
100 # Note selection (1-4 keys)
101 note_keys = {Key.KEY_1: "C4", Key.KEY_2: "E4", Key.KEY_3: "G4", Key.KEY_4: "C5"}
102 for key, note in note_keys.items():
103 if Input.is_key_just_pressed(key):
104 player.stream = self._tones[note]
105 player.volume_db = self.volume_db
106 player.pitch_scale = self.pitch_scale
107 player.loop = True
108 player.play()
109 self._current_note = note
110
111 # Pause / resume
112 if Input.is_key_just_pressed(Key.SPACE):
113 if player.is_playing():
114 player.pause()
115 elif player.is_paused():
116 player.play()
117
118 # Stop
119 if Input.is_key_just_pressed(Key.S):
120 player.stop()
121 self._current_note = ""
122
123 # Volume adjustment (6 dB step is clearly audible: half/double linear).
124 if Input.is_key_just_pressed(Key.UP):
125 self.volume_db = min(12.0, self.volume_db + 6.0)
126 player.volume_db = self.volume_db
127 if Input.is_key_just_pressed(Key.DOWN):
128 self.volume_db = max(-40.0, self.volume_db - 6.0)
129 player.volume_db = self.volume_db
130
131 # Pitch adjustment via `pitch_modulate` so the change propagates to
132 # the active backend channel; plain `pitch_scale = ...` only affects
133 # the *next* play(), not the currently-playing sound.
134 if Input.is_key_just_pressed(Key.RIGHT):
135 self.pitch_scale = min(4.0, self.pitch_scale * 1.1)
136 player.pitch_modulate(self.pitch_scale)
137 if Input.is_key_just_pressed(Key.LEFT):
138 self.pitch_scale = max(0.25, self.pitch_scale / 1.1)
139 player.pitch_modulate(self.pitch_scale)
140
141 # Update status display
142 if self._status_label:
143 state = "Stopped"
144 if player.is_playing():
145 state = f"Playing {self._current_note}"
146 elif player.is_paused():
147 state = f"Paused {self._current_note}"
148
149 pos = player.get_playback_position()
150 self._status_label.text = (
151 f"Audio Demo\n\n"
152 f"State: {state}\n"
153 f"Position: {pos:.1f}s\n"
154 f"Volume: {self.volume_db:.0f} dB\n"
155 f"Pitch: {self.pitch_scale:.2f}x\n\n"
156 f"1-4: Play notes (C4, E4, G4, C5)\n"
157 f"SPACE: Pause/Resume | S: Stop\n"
158 f"UP/DOWN: Volume | LEFT/RIGHT: Pitch\n"
159 f"ESC: Quit"
160 )
161
162
163if __name__ == "__main__":
164 app = App(width=800, height=400, title="SimVX Audio Demo")
165 app.run(AudioDemo())