Skip to content

Commit 5d9f8f3

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

19 files changed

+691
-0
lines changed
31.8 KB
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
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
1.96 KB
Loading
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: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
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: float = 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: float = AudioServer.get_absolute_time()
61+
var game_time: float = 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 beat_time: float = 60.0 / bpm
68+
var song_length: float = beat_time * _prev_scheduled_beat_count
69+
70+
# If for some reason there isn't a song playing right now (e.g. game is in a
71+
# background tab on web), seek to the correct time and play the song.
72+
if abs_time > _scheduled_song_time + song_length:
73+
var missed_loops: int = floori((abs_time - _scheduled_song_time) / song_length)
74+
_scheduled_song_time += missed_loops * song_length
75+
var playback: AudioStreamPlaybackScheduled
76+
playback = $SongScheduled.play_scheduled(abs_time + 0.1, abs_time + 0.1 - _scheduled_song_time)
77+
playback.scheduled_end_time = _scheduled_song_time + song_length
78+
_prev_scheduled_beat_count = song_beat_count
79+
song_length = beat_time * _prev_scheduled_beat_count
80+
81+
# Schedule the next song loop manually.
82+
if abs_time > _scheduled_song_time:
83+
_curr_playback = _next_playback
84+
_scheduled_song_time += song_length
85+
_next_playback = $SongScheduled.play_scheduled(_scheduled_song_time)
86+
_next_playback.scheduled_end_time = _scheduled_song_time + (beat_time * song_beat_count)
87+
_prev_scheduled_beat_count = song_beat_count
88+
if use_play_scheduled_toggle.button_pressed:
89+
stop_curr_loop_button.disabled = not _curr_playback.is_playing()
90+
cancel_next_loop_button.disabled = not _next_playback.is_scheduled()
91+
92+
93+
func _update_max_fps(max_fps: int) -> void:
94+
Engine.max_fps = max_fps
95+
ProjectSettings.set("application/run/max_fps", max_fps)
96+
max_fps_slider.value = max_fps
97+
max_fps_spinbox.value = max_fps
98+
99+
100+
func _update_song_beat_count(beat_count: int) -> void:
101+
song_beat_count = beat_count
102+
beat_count_slider.value = beat_count
103+
beat_count_spinbox.value = beat_count
104+
105+
# Update the next playback's length with the new song beat count.
106+
if _next_playback:
107+
_next_playback.scheduled_end_time = _scheduled_song_time + (60 / bpm * song_beat_count)
108+
_prev_scheduled_beat_count = song_beat_count
109+
110+
111+
func _on_max_fps_h_slider_value_changed(value: float) -> void:
112+
_update_max_fps(int(value))
113+
114+
115+
func _on_max_fps_spin_box_value_changed(value: float) -> void:
116+
_update_max_fps(int(value))
117+
118+
119+
func _on_song_beat_count_h_slider_value_changed(value: float) -> void:
120+
_update_song_beat_count(int(value))
121+
122+
123+
func _on_song_beat_count_spin_box_value_changed(value: float) -> void:
124+
_update_song_beat_count(int(value))
125+
126+
127+
func _on_use_play_scheduled_check_button_toggled(toggled_on: bool) -> void:
128+
if _tween:
129+
_tween.kill()
130+
131+
if toggled_on:
132+
_tween = create_tween().parallel()
133+
_tween.tween_property($Song, "volume_linear", 0, 0.2)
134+
_tween.tween_property($Metronome, "volume_linear", 0, 0.2)
135+
_tween.tween_property($SongScheduled, "volume_linear", db_to_linear(SONG_VOLUME_DB), 0.2)
136+
_tween.tween_property($MetronomeScheduled, "volume_linear", 1, 0.2)
137+
else:
138+
_tween = create_tween().parallel()
139+
_tween.tween_property($SongScheduled, "volume_linear", 0, 0.2)
140+
_tween.tween_property($MetronomeScheduled, "volume_linear", 0, 0.2)
141+
_tween.tween_property($Song, "volume_linear", db_to_linear(SONG_VOLUME_DB), 0.2)
142+
_tween.tween_property($Metronome, "volume_linear", 1, 0.2)
143+
144+
loop_settings_container.visible = toggled_on
145+
beat_count_slider.editable = toggled_on
146+
beat_count_spinbox.editable = toggled_on
147+
if toggled_on:
148+
if _curr_playback:
149+
stop_curr_loop_button.disabled = not _curr_playback.is_playing()
150+
if _next_playback:
151+
cancel_next_loop_button.disabled = not _next_playback.is_scheduled()
152+
else:
153+
stop_curr_loop_button.disabled = true
154+
cancel_next_loop_button.disabled = true
155+
156+
157+
func _on_volume_h_slider_value_changed(value: float) -> void:
158+
AudioServer.set_bus_volume_linear(_master_bus_index, value)
159+
160+
161+
func _on_stop_curr_button_pressed() -> void:
162+
if _curr_playback:
163+
_curr_playback.stop()
164+
stop_curr_loop_button.release_focus()
165+
stop_curr_loop_button.disabled = true
166+
167+
168+
func _on_cancel_next_button_pressed() -> void:
169+
if _next_playback:
170+
_next_playback.cancel()
171+
cancel_next_loop_button.release_focus()
172+
cancel_next_loop_button.disabled = true
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)