Audio Playback

AudioPlayer with procedurally generated tones, volume, and pitch.

▶ Run in browser

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