Skip to content

Commit 87e5fb0

Browse files
Add an improved sound scheduling demo (scheduled metronome)
Compared to #1199, this includes a "Song Beat Count" that dynamically changes the loop, which utilizes scheduled_end_time in AudioStreamPlaybackScheduled.
1 parent c97a648 commit 87e5fb0

19 files changed

+690
-0
lines changed
Binary file not shown.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
[remap]
2+
3+
importer="wav"
4+
type="AudioStreamWAV"
5+
uid="uid://j8yec16ugbbv"
6+
path="res://.godot/imported/Perc_MetronomeQuartz_hi.wav-812497d02260463d68888c4f5101e271.sample"
7+
8+
[deps]
9+
10+
source_file="res://Perc_MetronomeQuartz_hi.wav"
11+
dest_files=["res://.godot/imported/Perc_MetronomeQuartz_hi.wav-812497d02260463d68888c4f5101e271.sample"]
12+
13+
[params]
14+
15+
force/8_bit=false
16+
force/mono=false
17+
force/max_rate=false
18+
force/max_rate_hz=44100
19+
edit/trim=false
20+
edit/normalize=false
21+
edit/loop_mode=0
22+
edit/loop_begin=0
23+
edit/loop_end=-1
24+
compress/mode=2

audio/scheduled_metronome/README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Scheduled Metronome Demo
2+
3+
Godot project for showcasing `AudioStreamPlayer.play_scheduled()`. Plays a song
4+
on loop with a metronome.
5+
6+
The metronome sound was recorded by Ludwig Peter Müller in December 2020 under
7+
the "Creative Commons CC0 1.0 Universal" license.
8+
9+
Language: GDScript
10+
11+
Renderer: Compatibility
12+
13+
Check out this demo on the asset library: (TBD)
14+
15+
## Things to try
16+
17+
- Swap between `play` and `play_scheduled` for the metronome ticks.
18+
- Adjust max FPS to showcase its effect on the metronome.
19+
20+
## Screenshots
21+
22+
![Screenshot](screenshots/scheduled-metronome.png)

audio/scheduled_metronome/icon.svg

Lines changed: 82 additions & 0 deletions
Loading
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
[remap]
2+
3+
importer="texture"
4+
type="CompressedTexture2D"
5+
uid="uid://neinc785lt3k"
6+
path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"
7+
metadata={
8+
"vram_texture": false
9+
}
10+
11+
[deps]
12+
13+
source_file="res://icon.svg"
14+
dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"]
15+
16+
[params]
17+
18+
compress/mode=0
19+
compress/high_quality=false
20+
compress/lossy_quality=0.7
21+
compress/hdr_compression=1
22+
compress/normal_map=0
23+
compress/channel_pack=0
24+
mipmaps/generate=false
25+
mipmaps/limit=-1
26+
roughness/mode=0
27+
roughness/src_normal=""
28+
process/fix_alpha_border=true
29+
process/premult_alpha=false
30+
process/normal_map_invert_y=false
31+
process/hdr_as_srgb=false
32+
process/hdr_clamp_exposure=false
33+
process/size_limit=0
34+
detect_3d/compress_to=1
35+
svg/scale=1.0
36+
editor/scale_with_editor_scale=false
37+
editor/convert_colors_with_editor_theme=false

audio/scheduled_metronome/icon.webp

1.96 KB
Binary file not shown.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
[remap]
2+
3+
importer="texture"
4+
type="CompressedTexture2D"
5+
uid="uid://cbj2pph8lw003"
6+
path="res://.godot/imported/icon.webp-e94f9a68b0f625a567a797079e4d325f.ctex"
7+
metadata={
8+
"vram_texture": false
9+
}
10+
11+
[deps]
12+
13+
source_file="res://icon.webp"
14+
dest_files=["res://.godot/imported/icon.webp-e94f9a68b0f625a567a797079e4d325f.ctex"]
15+
16+
[params]
17+
18+
compress/mode=0
19+
compress/high_quality=false
20+
compress/lossy_quality=0.7
21+
compress/hdr_compression=1
22+
compress/normal_map=0
23+
compress/channel_pack=0
24+
mipmaps/generate=false
25+
mipmaps/limit=-1
26+
roughness/mode=0
27+
roughness/src_normal=""
28+
process/fix_alpha_border=true
29+
process/premult_alpha=false
30+
process/normal_map_invert_y=false
31+
process/hdr_as_srgb=false
32+
process/hdr_clamp_exposure=false
33+
process/size_limit=0
34+
detect_3d/compress_to=1

audio/scheduled_metronome/main.gd

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
extends Node2D
2+
3+
const SONG_VOLUME_DB = -18
4+
5+
@export_category("Song Settings")
6+
@export var bpm: float = 130
7+
@export var song_beat_count: int = 32
8+
9+
@export_category("Nodes")
10+
@export var use_play_scheduled_toggle: CheckButton
11+
@export var max_fps_slider: HSlider
12+
@export var max_fps_spinbox: SpinBox
13+
@export var beat_count_slider: HSlider
14+
@export var beat_count_spinbox: SpinBox
15+
@export var game_time_label: Label
16+
@export var audio_time_label: Label
17+
@export var loop_settings_container: VBoxContainer
18+
@export var stop_curr_loop_button: Button
19+
@export var cancel_next_loop_button: Button
20+
21+
@onready var _master_bus_index: int = AudioServer.get_bus_index("Master")
22+
23+
var _tween: Tween
24+
var _scheduled_song_start_time: float
25+
var _scheduled_song_time: float
26+
var _curr_playback: AudioStreamPlaybackScheduled
27+
var _next_playback: AudioStreamPlaybackScheduled
28+
var _prev_scheduled_beat_count: int = song_beat_count
29+
30+
31+
func _ready() -> void:
32+
_update_max_fps(10)
33+
_update_song_beat_count(32)
34+
35+
# Both scheduled and non-scheduled players run simultaneously, but only one
36+
# set is playing audio at a time. By default, the scheduled players are muted.
37+
$Song.volume_linear = 0
38+
$Metronome.volume_linear = 0
39+
$SongScheduled.volume_linear = 0
40+
$MetronomeScheduled.volume_linear = 0
41+
_on_use_play_scheduled_check_button_toggled(use_play_scheduled_toggle.button_pressed)
42+
43+
# Scheduled players. Schedule for 1 second in the future.
44+
_scheduled_song_start_time = AudioServer.get_absolute_time() + 1
45+
print("Scheduled song starting at ", _scheduled_song_start_time)
46+
_next_playback = $SongScheduled.play_scheduled(_scheduled_song_start_time)
47+
_next_playback.scheduled_end_time = _scheduled_song_start_time + (60 / bpm * song_beat_count)
48+
_prev_scheduled_beat_count = song_beat_count
49+
$MetronomeScheduled.start(_scheduled_song_start_time)
50+
_scheduled_song_time = _scheduled_song_start_time
51+
52+
# Non-scheduled players. Wait 1 second, then start playing.
53+
await get_tree().create_timer(1).timeout
54+
var sys_time := Time.get_ticks_usec() / 1000000.0
55+
$Song.play()
56+
$Metronome.start(sys_time)
57+
58+
59+
func _process(_delta: float) -> void:
60+
var abs_time := AudioServer.get_absolute_time()
61+
var game_time := Time.get_ticks_usec() / 1000000.0
62+
63+
# Show the new game/audio times.
64+
game_time_label.text = "Game Time: %.4f" % game_time
65+
audio_time_label.text = "Audio Time: %.4f" % abs_time
66+
67+
var song_length := 60 / bpm * _prev_scheduled_beat_count
68+
69+
# If for some reason there isn't a song playing right now (e.g. game is in a
70+
# background tab on web), seek to the correct time and play the song.
71+
if abs_time > _scheduled_song_time + song_length:
72+
var missed_loops := floori((abs_time - _scheduled_song_time) / song_length)
73+
_scheduled_song_time += missed_loops * song_length
74+
var playback: AudioStreamPlaybackScheduled
75+
playback = $SongScheduled.play_scheduled(abs_time + 0.1, abs_time + 0.1 - _scheduled_song_time)
76+
playback.scheduled_end_time = _scheduled_song_time + song_length
77+
_prev_scheduled_beat_count = song_beat_count
78+
song_length = 60 / bpm * _prev_scheduled_beat_count
79+
80+
# Schedule the next song loop manually.
81+
if abs_time > _scheduled_song_time:
82+
_curr_playback = _next_playback
83+
_scheduled_song_time += song_length
84+
_next_playback = $SongScheduled.play_scheduled(_scheduled_song_time)
85+
_next_playback.scheduled_end_time = _scheduled_song_time + (60 / bpm * song_beat_count)
86+
_prev_scheduled_beat_count = song_beat_count
87+
if use_play_scheduled_toggle.button_pressed:
88+
stop_curr_loop_button.disabled = not _curr_playback.is_playing()
89+
cancel_next_loop_button.disabled = not _next_playback.is_scheduled()
90+
91+
92+
func _update_max_fps(max_fps: int) -> void:
93+
Engine.max_fps = max_fps
94+
ProjectSettings.set("application/run/max_fps", max_fps)
95+
max_fps_slider.value = max_fps
96+
max_fps_spinbox.value = max_fps
97+
98+
99+
func _update_song_beat_count(beat_count: int) -> void:
100+
song_beat_count = beat_count
101+
beat_count_slider.value = beat_count
102+
beat_count_spinbox.value = beat_count
103+
104+
# Update the next playback's length with the new song beat count.
105+
if _next_playback:
106+
_next_playback.scheduled_end_time = _scheduled_song_time + (60 / bpm * song_beat_count)
107+
_prev_scheduled_beat_count = song_beat_count
108+
109+
110+
func _on_max_fps_h_slider_value_changed(value: float) -> void:
111+
_update_max_fps(int(value))
112+
113+
114+
func _on_max_fps_spin_box_value_changed(value: float) -> void:
115+
_update_max_fps(int(value))
116+
117+
118+
func _on_song_beat_count_h_slider_value_changed(value: float) -> void:
119+
_update_song_beat_count(int(value))
120+
121+
122+
func _on_song_beat_count_spin_box_value_changed(value: float) -> void:
123+
_update_song_beat_count(int(value))
124+
125+
126+
func _on_use_play_scheduled_check_button_toggled(toggled_on: bool) -> void:
127+
if _tween:
128+
_tween.kill()
129+
130+
if toggled_on:
131+
_tween = create_tween().parallel()
132+
_tween.tween_property($Song, "volume_linear", 0, 0.2)
133+
_tween.tween_property($Metronome, "volume_linear", 0, 0.2)
134+
_tween.tween_property($SongScheduled, "volume_linear", db_to_linear(SONG_VOLUME_DB), 0.2)
135+
_tween.tween_property($MetronomeScheduled, "volume_linear", 1, 0.2)
136+
else:
137+
_tween = create_tween().parallel()
138+
_tween.tween_property($SongScheduled, "volume_linear", 0, 0.2)
139+
_tween.tween_property($MetronomeScheduled, "volume_linear", 0, 0.2)
140+
_tween.tween_property($Song, "volume_linear", db_to_linear(SONG_VOLUME_DB), 0.2)
141+
_tween.tween_property($Metronome, "volume_linear", 1, 0.2)
142+
143+
loop_settings_container.visible = toggled_on
144+
beat_count_slider.editable = toggled_on
145+
beat_count_spinbox.editable = toggled_on
146+
if toggled_on:
147+
if _curr_playback:
148+
stop_curr_loop_button.disabled = not _curr_playback.is_playing()
149+
if _next_playback:
150+
cancel_next_loop_button.disabled = not _next_playback.is_scheduled()
151+
else:
152+
stop_curr_loop_button.disabled = true
153+
cancel_next_loop_button.disabled = true
154+
155+
156+
func _on_volume_h_slider_value_changed(value: float) -> void:
157+
AudioServer.set_bus_volume_linear(_master_bus_index, value)
158+
159+
160+
func _on_stop_curr_button_pressed() -> void:
161+
if _curr_playback:
162+
_curr_playback.stop()
163+
stop_curr_loop_button.release_focus()
164+
stop_curr_loop_button.disabled = true
165+
166+
167+
func _on_cancel_next_button_pressed() -> void:
168+
if _next_playback:
169+
_next_playback.cancel()
170+
cancel_next_loop_button.release_focus()
171+
cancel_next_loop_button.disabled = true

audio/scheduled_metronome/main.gd.uid

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
uid://mwio0eujos2s

0 commit comments

Comments
 (0)