Skip to content

Commit 99ea0b8

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

21 files changed

+652
-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

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
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: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
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+
18+
@onready var _master_bus_index: int = AudioServer.get_bus_index("Master")
19+
20+
var _tween: Tween
21+
var _scheduled_song_start_time: float
22+
var _scheduled_song_time: float
23+
var _next_playback: AudioStreamPlaybackScheduled
24+
var _prev_scheduled_beat_count: int = song_beat_count
25+
26+
27+
func _ready() -> void:
28+
_update_max_fps(10)
29+
_update_song_beat_count(8)
30+
31+
# Both scheduled and non-scheduled players run simultaneously, but only one
32+
# set is playing audio at a time. By default, the scheduled players are muted.
33+
$Song.volume_linear = 0
34+
$Metronome.volume_linear = 0
35+
$SongScheduled.volume_linear = 0
36+
$MetronomeScheduled.volume_linear = 0
37+
_on_use_play_scheduled_check_button_toggled(use_play_scheduled_toggle.button_pressed)
38+
39+
# Scheduled players. Schedule for 1 second in the future.
40+
_scheduled_song_start_time = AudioServer.get_absolute_time() + 1
41+
print("Scheduled song starting at ", _scheduled_song_start_time)
42+
_next_playback = $SongScheduled.play_scheduled(_scheduled_song_start_time)
43+
_next_playback.set_scheduled_end_time(_scheduled_song_start_time + (60 / bpm * song_beat_count))
44+
_prev_scheduled_beat_count = song_beat_count
45+
$MetronomeScheduled.start(_scheduled_song_start_time)
46+
_scheduled_song_time = _scheduled_song_start_time
47+
48+
# Non-scheduled players. Wait 1 second, then start playing.
49+
await get_tree().create_timer(1).timeout
50+
var sys_time := Time.get_ticks_usec() / 1000000.0
51+
$Song.play()
52+
$Metronome.start(sys_time)
53+
54+
55+
func _process(_delta: float) -> void:
56+
var abs_time := AudioServer.get_absolute_time()
57+
var game_time := Time.get_ticks_usec() / 1000000.0
58+
59+
# Show the new game/audio times.
60+
game_time_label.text = "Game Time: %.4f" % game_time
61+
audio_time_label.text = "Audio Time: %.4f" % abs_time
62+
63+
var song_length := 60 / bpm * _prev_scheduled_beat_count
64+
65+
# If for some reason there isn't a song playing right now (e.g. game is in a
66+
# background tab on web), seek to the correct time and play the song.
67+
if abs_time > _scheduled_song_time + song_length:
68+
var missed_loops := floori((abs_time - _scheduled_song_time) / song_length)
69+
_scheduled_song_time += missed_loops * song_length
70+
var playback: AudioStreamPlaybackScheduled
71+
playback = $SongScheduled.play_scheduled(abs_time + 0.1, abs_time + 0.1 - _scheduled_song_time)
72+
playback.set_scheduled_end_time(_scheduled_song_time + song_length)
73+
_prev_scheduled_beat_count = song_beat_count
74+
song_length = 60 / bpm * _prev_scheduled_beat_count
75+
76+
# Schedule the next song loop manually.
77+
if abs_time > _scheduled_song_time:
78+
_scheduled_song_time += song_length
79+
_next_playback = $SongScheduled.play_scheduled(_scheduled_song_time)
80+
_next_playback.set_scheduled_end_time(_scheduled_song_time + (60 / bpm * song_beat_count))
81+
_prev_scheduled_beat_count = song_beat_count
82+
83+
84+
func _update_max_fps(max_fps: int) -> void:
85+
Engine.max_fps = max_fps
86+
ProjectSettings.set("application/run/max_fps", max_fps)
87+
max_fps_slider.value = max_fps
88+
max_fps_spinbox.value = max_fps
89+
90+
91+
func _update_song_beat_count(beat_count: int) -> void:
92+
song_beat_count = beat_count
93+
beat_count_slider.value = beat_count
94+
beat_count_spinbox.value = beat_count
95+
96+
# Update the next playback's length with the new song beat count.
97+
if _next_playback:
98+
_next_playback.set_scheduled_end_time(_scheduled_song_time + (60 / bpm * song_beat_count))
99+
_prev_scheduled_beat_count = song_beat_count
100+
101+
102+
func _on_max_fps_h_slider_value_changed(value: float) -> void:
103+
_update_max_fps(int(value))
104+
105+
106+
func _on_max_fps_spin_box_value_changed(value: float) -> void:
107+
_update_max_fps(int(value))
108+
109+
110+
func _on_song_beat_count_h_slider_value_changed(value: float) -> void:
111+
_update_song_beat_count(int(value))
112+
113+
114+
func _on_song_beat_count_spin_box_value_changed(value: float) -> void:
115+
_update_song_beat_count(int(value))
116+
117+
118+
func _on_use_play_scheduled_check_button_toggled(toggled_on: bool) -> void:
119+
if _tween:
120+
_tween.kill()
121+
122+
if toggled_on:
123+
_tween = create_tween().parallel()
124+
_tween.tween_property($Song, "volume_linear", 0, 0.2)
125+
_tween.tween_property($Metronome, "volume_linear", 0, 0.2)
126+
_tween.tween_property($SongScheduled, "volume_linear", db_to_linear(SONG_VOLUME_DB), 0.2)
127+
_tween.tween_property($MetronomeScheduled, "volume_linear", 1, 0.2)
128+
else:
129+
_tween = create_tween().parallel()
130+
_tween.tween_property($SongScheduled, "volume_linear", 0, 0.2)
131+
_tween.tween_property($MetronomeScheduled, "volume_linear", 0, 0.2)
132+
_tween.tween_property($Song, "volume_linear", db_to_linear(SONG_VOLUME_DB), 0.2)
133+
_tween.tween_property($Metronome, "volume_linear", 1, 0.2)
134+
135+
beat_count_slider.editable = toggled_on
136+
beat_count_spinbox.editable = toggled_on
137+
138+
139+
func _on_volume_h_slider_value_changed(value: float) -> void:
140+
AudioServer.set_bus_volume_linear(_master_bus_index, value)

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)