Audio Playback

play, pause and stop tones, and change volume and pitch live.

▶ Run in browser

Tags: audio

AudioPlayer is the non-positional player, for music and interface sounds: its volume does not depend on where anything is. Give it a clip in stream and call play(), pause() and stop(). This example makes its clips with AudioClip.tone(frequency, duration=...), a sine wave generated in memory, so it ships no audio files; a file path works in stream just the same.

  • loop=True in the constructor makes each tone repeat. stream and loop cannot change under a live sound: writing one while the player is playing or paused raises during development, and a paused player’s play() resumes the old clip. So switching notes is stop(), assign stream, then play().

  • play() on a paused player resumes it where it left off, so the Pause action pauses a playing tone and resumes a paused one. is_playing() and is_paused() tell the two apart.

  • volume_db and pitch_scale are live: writing either one changes the sound that is already playing. Volume moves in 6 dB steps (half or double the amplitude) between -40 and +12 dB, and pitch in steps of 10% within the property’s own range of 0.5x to 2x.

  • get_playback_position() is the position in the clip, in seconds, which wraps back to zero each time a looping tone repeats.

Every control is an action in the class’s input_context, handled with @on_input, and every action has a Button(action=...) that presses it, so the keys, the mouse and a finger all work. Each button shows its key.

Source

  1"""Audio Playback: play, pause and stop tones, and change volume and pitch live.
  2
  3`AudioPlayer` is the non-positional player, for music and interface sounds: its
  4volume does not depend on where anything is. Give it a clip in `stream` and call
  5`play()`, `pause()` and `stop()`. This example makes its clips with
  6`AudioClip.tone(frequency, duration=...)`, a sine wave generated in memory, so it
  7ships no audio files; a file path works in `stream` just the same.
  8
  9- `loop=True` in the constructor makes each tone repeat. `stream` and `loop`
 10  cannot change under a live sound: writing one while the player is playing or
 11  paused raises during development, and a paused player's `play()` resumes the
 12  old clip. So switching notes is `stop()`, assign `stream`, then `play()`.
 13- `play()` on a paused player resumes it where it left off, so the Pause action
 14  pauses a playing tone and resumes a paused one. `is_playing()` and
 15  `is_paused()` tell the two apart.
 16- `volume_db` and `pitch_scale` are live: writing either one changes the sound
 17  that is already playing. Volume moves in 6 dB steps (half or double the
 18  amplitude) between -40 and +12 dB, and pitch in steps of 10% within the
 19  property's own range of 0.5x to 2x.
 20- `get_playback_position()` is the position in the clip, in seconds, which wraps
 21  back to zero each time a looping tone repeats.
 22
 23Every control is an action in the class's `input_context`, handled with
 24`@on_input`, and every action has a `Button(action=...)` that presses it, so the
 25keys, the mouse and a finger all work. Each button shows its key.
 26"""
 27
 28from simvx.core import (
 29    Anchor,
 30    AudioClip,
 31    AudioPlayer,
 32    Button,
 33    CanvasLayer,
 34    HBoxContainer,
 35    Key,
 36    Label,
 37    Node,
 38    VBoxContainer,
 39    on_input,
 40)
 41from simvx.graphics import App
 42
 43NOTES = {"C4": 261.63, "E4": 329.63, "G4": 392.00, "C5": 523.25}
 44VOLUME_STEP_DB = 6.0
 45VOLUME_MIN_DB, VOLUME_MAX_DB = -40.0, 12.0
 46PITCH_STEP = 1.1
 47
 48
 49class AudioDemo(Node):
 50    input_context = {
 51        "C4": [Key.KEY_1],
 52        "E4": [Key.KEY_2],
 53        "G4": [Key.KEY_3],
 54        "C5": [Key.KEY_4],
 55        "pause": [Key.SPACE],
 56        "stop": [Key.S],
 57        "volume_down": [Key.DOWN],
 58        "volume_up": [Key.UP],
 59        "pitch_down": [Key.LEFT],
 60        "pitch_up": [Key.RIGHT],
 61    }
 62
 63    def on_ready(self):
 64        self.tones = {note: AudioClip.tone(freq, duration=3.0) for note, freq in NOTES.items()}
 65        self.player = self.add_child(AudioPlayer(loop=True))
 66        self.note = ""
 67
 68        hud = self.add_child(CanvasLayer())
 69        self.status = hud.add_child(Label(anchor=Anchor.TOP_LEFT, inset=24, font_size=20))
 70        rows = hud.add_child(VBoxContainer(anchor=Anchor.CENTER_BOTTOM, inset=20))
 71        notes = rows.add_child(HBoxContainer())
 72        for note in NOTES:
 73            notes.add_child(Button(note, action=note))
 74        notes.add_child(Button("Pause", action="pause"))
 75        notes.add_child(Button("Stop", action="stop"))
 76        mix = rows.add_child(HBoxContainer())
 77        mix.add_child(Button("Volume -", action="volume_down"))
 78        mix.add_child(Button("Volume +", action="volume_up"))
 79        mix.add_child(Button("Pitch -", action="pitch_down"))
 80        mix.add_child(Button("Pitch +", action="pitch_up"))
 81        # A container keeps its default size until told to fit its children.
 82        rows.autosize_to_content()
 83
 84    @on_input(tuple(NOTES))
 85    def play_note(self, event):
 86        self.player.stop()
 87        self.player.stream = self.tones[event.action]
 88        self.player.play()
 89        self.note = event.action
 90
 91    @on_input("pause")
 92    def toggle_pause(self, event):
 93        if self.player.is_playing():
 94            self.player.pause()
 95        elif self.player.is_paused():
 96            self.player.play()
 97
 98    @on_input("stop")
 99    def stop_tone(self, event):
100        self.player.stop()
101        self.note = ""
102
103    @on_input(("volume_down", "volume_up"))
104    def change_volume(self, event):
105        step = VOLUME_STEP_DB if event.action == "volume_up" else -VOLUME_STEP_DB
106        self.player.volume_db = min(max(self.player.volume_db + step, VOLUME_MIN_DB), VOLUME_MAX_DB)
107
108    @on_input(("pitch_down", "pitch_up"))
109    def change_pitch(self, event):
110        self.player.pitch_scale *= PITCH_STEP if event.action == "pitch_up" else 1 / PITCH_STEP
111
112    def on_update(self, dt):
113        state = "Stopped"
114        if self.player.is_playing():
115            state = f"Playing {self.note}"
116        elif self.player.is_paused():
117            state = f"Paused {self.note}"
118        self.status.text = (
119            f"State: {state}\n"
120            f"Position: {self.player.get_playback_position():.1f} s\n"
121            f"Volume: {self.player.volume_db:+.0f} dB\n"
122            f"Pitch: {self.player.pitch_scale:.2f}x"
123        )
124
125
126if __name__ == "__main__":
127    App(width=800, height=400, title="Audio Playback").run(AudioDemo())