Audio demo

generates and plays test tones using the MiniaudioBackend.

▶ Run in browser

Tags: 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())