Audio Playback¶
play, pause and stop tones, and change volume and pitch live.
▶ Run in browserTags: 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=Truein the constructor makes each tone repeat.streamandloopcannot change under a live sound: writing one while the player is playing or paused raises during development, and a paused player’splay()resumes the old clip. So switching notes isstop(), assignstream, thenplay().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()andis_paused()tell the two apart.volume_dbandpitch_scaleare 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())