diff --git a/audio/scheduled_metronome/Perc_MetronomeQuartz_hi.wav b/audio/scheduled_metronome/Perc_MetronomeQuartz_hi.wav new file mode 100644 index 0000000000..e410e1fba3 Binary files /dev/null and b/audio/scheduled_metronome/Perc_MetronomeQuartz_hi.wav differ diff --git a/audio/scheduled_metronome/Perc_MetronomeQuartz_hi.wav.import b/audio/scheduled_metronome/Perc_MetronomeQuartz_hi.wav.import new file mode 100644 index 0000000000..c328844d6f --- /dev/null +++ b/audio/scheduled_metronome/Perc_MetronomeQuartz_hi.wav.import @@ -0,0 +1,24 @@ +[remap] + +importer="wav" +type="AudioStreamWAV" +uid="uid://j8yec16ugbbv" +path="res://.godot/imported/Perc_MetronomeQuartz_hi.wav-812497d02260463d68888c4f5101e271.sample" + +[deps] + +source_file="res://Perc_MetronomeQuartz_hi.wav" +dest_files=["res://.godot/imported/Perc_MetronomeQuartz_hi.wav-812497d02260463d68888c4f5101e271.sample"] + +[params] + +force/8_bit=false +force/mono=false +force/max_rate=false +force/max_rate_hz=44100 +edit/trim=false +edit/normalize=false +edit/loop_mode=0 +edit/loop_begin=0 +edit/loop_end=-1 +compress/mode=2 diff --git a/audio/scheduled_metronome/README.md b/audio/scheduled_metronome/README.md new file mode 100644 index 0000000000..0cced2a745 --- /dev/null +++ b/audio/scheduled_metronome/README.md @@ -0,0 +1,22 @@ +# Scheduled Metronome Demo + +Godot project for showcasing `AudioStreamPlayer.play_scheduled()`. Plays a song +on loop with a metronome. + +The metronome sound was recorded by Ludwig Peter Müller in December 2020 under +the "Creative Commons CC0 1.0 Universal" license. + +Language: GDScript + +Renderer: Compatibility + +Check out this demo on the asset library: (TBD) + +## Things to try + +- Swap between `play` and `play_scheduled` for the metronome ticks. +- Adjust max FPS to showcase its effect on the metronome. + +## Screenshots + +![Screenshot](screenshots/scheduled-metronome.png) diff --git a/audio/scheduled_metronome/icon.svg b/audio/scheduled_metronome/icon.svg new file mode 100644 index 0000000000..6c4f26c62c --- /dev/null +++ b/audio/scheduled_metronome/icon.svg @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + diff --git a/audio/scheduled_metronome/icon.svg.import b/audio/scheduled_metronome/icon.svg.import new file mode 100644 index 0000000000..c2316b92a4 --- /dev/null +++ b/audio/scheduled_metronome/icon.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://neinc785lt3k" +path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://icon.svg" +dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/audio/scheduled_metronome/icon.webp b/audio/scheduled_metronome/icon.webp new file mode 100644 index 0000000000..777a9adb9d Binary files /dev/null and b/audio/scheduled_metronome/icon.webp differ diff --git a/audio/scheduled_metronome/icon.webp.import b/audio/scheduled_metronome/icon.webp.import new file mode 100644 index 0000000000..33b68d9bf8 --- /dev/null +++ b/audio/scheduled_metronome/icon.webp.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cbj2pph8lw003" +path="res://.godot/imported/icon.webp-e94f9a68b0f625a567a797079e4d325f.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://icon.webp" +dest_files=["res://.godot/imported/icon.webp-e94f9a68b0f625a567a797079e4d325f.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/audio/scheduled_metronome/main.gd b/audio/scheduled_metronome/main.gd new file mode 100644 index 0000000000..29dde4744b --- /dev/null +++ b/audio/scheduled_metronome/main.gd @@ -0,0 +1,171 @@ +extends Node2D + +const SONG_VOLUME_DB = -18 + +@export_category("Song Settings") +@export var bpm: float = 130 +@export var song_beat_count: int = 32 + +@export_category("Nodes") +@export var use_play_scheduled_toggle: CheckButton +@export var max_fps_slider: HSlider +@export var max_fps_spinbox: SpinBox +@export var beat_count_slider: HSlider +@export var beat_count_spinbox: SpinBox +@export var game_time_label: Label +@export var audio_time_label: Label +@export var loop_settings_container: VBoxContainer +@export var stop_curr_loop_button: Button +@export var cancel_next_loop_button: Button + +@onready var _master_bus_index: int = AudioServer.get_bus_index("Master") + +var _tween: Tween +var _scheduled_song_start_time: float +var _scheduled_song_time: float +var _curr_playback: AudioStreamPlaybackScheduled +var _next_playback: AudioStreamPlaybackScheduled +var _prev_scheduled_beat_count: int = song_beat_count + + +func _ready() -> void: + _update_max_fps(10) + _update_song_beat_count(32) + + # Both scheduled and non-scheduled players run simultaneously, but only one + # set is playing audio at a time. By default, the scheduled players are muted. + $Song.volume_linear = 0 + $Metronome.volume_linear = 0 + $SongScheduled.volume_linear = 0 + $MetronomeScheduled.volume_linear = 0 + _on_use_play_scheduled_check_button_toggled(use_play_scheduled_toggle.button_pressed) + + # Scheduled players. Schedule for 1 second in the future. + _scheduled_song_start_time = AudioServer.get_absolute_time() + 1 + print("Scheduled song starting at ", _scheduled_song_start_time) + _next_playback = $SongScheduled.play_scheduled(_scheduled_song_start_time) + _next_playback.scheduled_end_time = _scheduled_song_start_time + (60 / bpm * song_beat_count) + _prev_scheduled_beat_count = song_beat_count + $MetronomeScheduled.start(_scheduled_song_start_time) + _scheduled_song_time = _scheduled_song_start_time + + # Non-scheduled players. Wait 1 second, then start playing. + await get_tree().create_timer(1).timeout + var sys_time := Time.get_ticks_usec() / 1000000.0 + $Song.play() + $Metronome.start(sys_time) + + +func _process(_delta: float) -> void: + var abs_time := AudioServer.get_absolute_time() + var game_time := Time.get_ticks_usec() / 1000000.0 + + # Show the new game/audio times. + game_time_label.text = "Game Time: %.4f" % game_time + audio_time_label.text = "Audio Time: %.4f" % abs_time + + var song_length := 60 / bpm * _prev_scheduled_beat_count + + # If for some reason there isn't a song playing right now (e.g. game is in a + # background tab on web), seek to the correct time and play the song. + if abs_time > _scheduled_song_time + song_length: + var missed_loops := floori((abs_time - _scheduled_song_time) / song_length) + _scheduled_song_time += missed_loops * song_length + var playback: AudioStreamPlaybackScheduled + playback = $SongScheduled.play_scheduled(abs_time + 0.1, abs_time + 0.1 - _scheduled_song_time) + playback.scheduled_end_time = _scheduled_song_time + song_length + _prev_scheduled_beat_count = song_beat_count + song_length = 60 / bpm * _prev_scheduled_beat_count + + # Schedule the next song loop manually. + if abs_time > _scheduled_song_time: + _curr_playback = _next_playback + _scheduled_song_time += song_length + _next_playback = $SongScheduled.play_scheduled(_scheduled_song_time) + _next_playback.scheduled_end_time = _scheduled_song_time + (60 / bpm * song_beat_count) + _prev_scheduled_beat_count = song_beat_count + if use_play_scheduled_toggle.button_pressed: + stop_curr_loop_button.disabled = not _curr_playback.is_playing() + cancel_next_loop_button.disabled = not _next_playback.is_scheduled() + + +func _update_max_fps(max_fps: int) -> void: + Engine.max_fps = max_fps + ProjectSettings.set("application/run/max_fps", max_fps) + max_fps_slider.value = max_fps + max_fps_spinbox.value = max_fps + + +func _update_song_beat_count(beat_count: int) -> void: + song_beat_count = beat_count + beat_count_slider.value = beat_count + beat_count_spinbox.value = beat_count + + # Update the next playback's length with the new song beat count. + if _next_playback: + _next_playback.scheduled_end_time = _scheduled_song_time + (60 / bpm * song_beat_count) + _prev_scheduled_beat_count = song_beat_count + + +func _on_max_fps_h_slider_value_changed(value: float) -> void: + _update_max_fps(int(value)) + + +func _on_max_fps_spin_box_value_changed(value: float) -> void: + _update_max_fps(int(value)) + + +func _on_song_beat_count_h_slider_value_changed(value: float) -> void: + _update_song_beat_count(int(value)) + + +func _on_song_beat_count_spin_box_value_changed(value: float) -> void: + _update_song_beat_count(int(value)) + + +func _on_use_play_scheduled_check_button_toggled(toggled_on: bool) -> void: + if _tween: + _tween.kill() + + if toggled_on: + _tween = create_tween().parallel() + _tween.tween_property($Song, "volume_linear", 0, 0.2) + _tween.tween_property($Metronome, "volume_linear", 0, 0.2) + _tween.tween_property($SongScheduled, "volume_linear", db_to_linear(SONG_VOLUME_DB), 0.2) + _tween.tween_property($MetronomeScheduled, "volume_linear", 1, 0.2) + else: + _tween = create_tween().parallel() + _tween.tween_property($SongScheduled, "volume_linear", 0, 0.2) + _tween.tween_property($MetronomeScheduled, "volume_linear", 0, 0.2) + _tween.tween_property($Song, "volume_linear", db_to_linear(SONG_VOLUME_DB), 0.2) + _tween.tween_property($Metronome, "volume_linear", 1, 0.2) + + loop_settings_container.visible = toggled_on + beat_count_slider.editable = toggled_on + beat_count_spinbox.editable = toggled_on + if toggled_on: + if _curr_playback: + stop_curr_loop_button.disabled = not _curr_playback.is_playing() + if _next_playback: + cancel_next_loop_button.disabled = not _next_playback.is_scheduled() + else: + stop_curr_loop_button.disabled = true + cancel_next_loop_button.disabled = true + + +func _on_volume_h_slider_value_changed(value: float) -> void: + AudioServer.set_bus_volume_linear(_master_bus_index, value) + + +func _on_stop_curr_button_pressed() -> void: + if _curr_playback: + _curr_playback.stop() + stop_curr_loop_button.release_focus() + stop_curr_loop_button.disabled = true + + +func _on_cancel_next_button_pressed() -> void: + if _next_playback: + _next_playback.cancel() + cancel_next_loop_button.release_focus() + cancel_next_loop_button.disabled = true diff --git a/audio/scheduled_metronome/main.gd.uid b/audio/scheduled_metronome/main.gd.uid new file mode 100644 index 0000000000..28eac5b8c9 --- /dev/null +++ b/audio/scheduled_metronome/main.gd.uid @@ -0,0 +1 @@ +uid://mwio0eujos2s diff --git a/audio/scheduled_metronome/main.tscn b/audio/scheduled_metronome/main.tscn new file mode 100644 index 0000000000..f9efb4c1aa --- /dev/null +++ b/audio/scheduled_metronome/main.tscn @@ -0,0 +1,209 @@ +[gd_scene load_steps=7 format=3 uid="uid://b3scmm5r6a23q"] + +[ext_resource type="Script" uid="uid://mwio0eujos2s" path="res://main.gd" id="1_ig7tw"] +[ext_resource type="Script" uid="uid://bwqpovn6r5q7q" path="res://metronome.gd" id="2_0xm2m"] +[ext_resource type="AudioStream" uid="uid://c2b3dcgll0ae4" path="res://track.ogg" id="2_1bvp3"] +[ext_resource type="AudioStream" uid="uid://j8yec16ugbbv" path="res://Perc_MetronomeQuartz_hi.wav" id="3_lquwl"] +[ext_resource type="Script" uid="uid://f4crwo4mmo3t" path="res://metronome_scheduled.gd" id="5_lquwl"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_7mycd"] +content_margin_left = 8.0 +content_margin_top = 8.0 +content_margin_right = 8.0 +content_margin_bottom = 8.0 +bg_color = Color(0.1, 0.1, 0.1, 0.6) +corner_radius_top_left = 3 +corner_radius_top_right = 3 +corner_radius_bottom_right = 3 +corner_radius_bottom_left = 3 +corner_detail = 5 + +[node name="Main" type="Node2D" node_paths=PackedStringArray("use_play_scheduled_toggle", "max_fps_slider", "max_fps_spinbox", "beat_count_slider", "beat_count_spinbox", "game_time_label", "audio_time_label", "loop_settings_container", "stop_curr_loop_button", "cancel_next_loop_button")] +script = ExtResource("1_ig7tw") +use_play_scheduled_toggle = NodePath("Control/VBoxContainer/HBoxContainer/VBoxContainer/UsePlayScheduled/CheckButton") +max_fps_slider = NodePath("Control/VBoxContainer/HBoxContainer/VBoxContainer/MaxFps/CenterContainer/HSlider") +max_fps_spinbox = NodePath("Control/VBoxContainer/HBoxContainer/VBoxContainer/MaxFps/SpinBox") +beat_count_slider = NodePath("Control/VBoxContainer/HBoxContainer/LoopSettings/SongBeatCount/Ranges/CenterContainer/HSlider") +beat_count_spinbox = NodePath("Control/VBoxContainer/HBoxContainer/LoopSettings/SongBeatCount/Ranges/CenterContainer2/SpinBox") +game_time_label = NodePath("Control/VBoxContainer/HBoxContainer/TimeLabels/GameTimeLabel") +audio_time_label = NodePath("Control/VBoxContainer/HBoxContainer/TimeLabels/AudioTimeLabel") +loop_settings_container = NodePath("Control/VBoxContainer/HBoxContainer/LoopSettings") +stop_curr_loop_button = NodePath("Control/VBoxContainer/HBoxContainer/LoopSettings/StopCurrButton") +cancel_next_loop_button = NodePath("Control/VBoxContainer/HBoxContainer/LoopSettings/CancelNextButton") + +[node name="Song" type="AudioStreamPlayer" parent="."] +stream = ExtResource("2_1bvp3") +volume_db = -18.0 +parameters/looping = true + +[node name="Metronome" type="AudioStreamPlayer" parent="."] +stream = ExtResource("3_lquwl") +script = ExtResource("2_0xm2m") + +[node name="SongScheduled" type="ScheduledAudioStreamPlayer" parent="."] +stream = ExtResource("2_1bvp3") +volume_db = -18.0 +max_polyphony = 2 + +[node name="MetronomeScheduled" type="ScheduledAudioStreamPlayer" parent="."] +stream = ExtResource("3_lquwl") +max_polyphony = 2 +script = ExtResource("5_lquwl") + +[node name="Control" type="Control" parent="."] +layout_mode = 3 +anchors_preset = 0 +offset_right = 1152.0 +offset_bottom = 648.0 + +[node name="VBoxContainer" type="VBoxContainer" parent="Control"] +layout_mode = 0 +offset_right = 1152.0 +offset_bottom = 648.0 +theme_override_constants/separation = 30 + +[node name="PanelContainer" type="PanelContainer" parent="Control/VBoxContainer"] +layout_mode = 2 +theme_override_styles/panel = SubResource("StyleBoxFlat_7mycd") + +[node name="DescriptionLabel" type="Label" parent="Control/VBoxContainer/PanelContainer"] +custom_minimum_size = Vector2(900, 0) +layout_mode = 2 +text = "This demo showcases the difference between play() and play_scheduled() by playing a looping song with a metronome. + +With play(): +- Metronome ticks can only be played on drawn frames, so low FPS can cause ticks to play very out of sync. +- Even at high FPS, ticks are always aligned with the beginning of audio mix chunks. At a default mix rate of 44100 Hz, this is roughly every 11ms. + +With play_scheduled(): +- Metronome ticks can play consistently with exact audio frame precision, regardless of FPS. +- Song loops can be manually scheduled, allowing them to be dynamically changed. +- It's recommended to use a `max_polyphony` of at least 2 to avoid cutting off currently playing audio when scheduling the next play. This demo uses a `max_polyphony` of 2 on the scheduled players. + +Try changing the settings below!" +autowrap_mode = 2 + +[node name="HBoxContainer" type="HBoxContainer" parent="Control/VBoxContainer"] +layout_mode = 2 +theme_override_constants/separation = 60 + +[node name="VBoxContainer" type="VBoxContainer" parent="Control/VBoxContainer/HBoxContainer"] +layout_mode = 2 +theme_override_constants/separation = 16 + +[node name="UsePlayScheduled" type="HBoxContainer" parent="Control/VBoxContainer/HBoxContainer/VBoxContainer"] +layout_mode = 2 + +[node name="CheckButton" type="CheckButton" parent="Control/VBoxContainer/HBoxContainer/VBoxContainer/UsePlayScheduled"] +layout_mode = 2 +text = "Use play_scheduled()" + +[node name="MaxFps" type="HBoxContainer" parent="Control/VBoxContainer/HBoxContainer/VBoxContainer"] +layout_mode = 2 +theme_override_constants/separation = 12 + +[node name="Label" type="Label" parent="Control/VBoxContainer/HBoxContainer/VBoxContainer/MaxFps"] +layout_mode = 2 +text = "Max FPS" + +[node name="CenterContainer" type="CenterContainer" parent="Control/VBoxContainer/HBoxContainer/VBoxContainer/MaxFps"] +layout_mode = 2 + +[node name="HSlider" type="HSlider" parent="Control/VBoxContainer/HBoxContainer/VBoxContainer/MaxFps/CenterContainer"] +custom_minimum_size = Vector2(200, 0) +layout_mode = 2 +min_value = 10.0 +max_value = 1000.0 +value = 10.0 +exp_edit = true + +[node name="SpinBox" type="SpinBox" parent="Control/VBoxContainer/HBoxContainer/VBoxContainer/MaxFps"] +layout_mode = 2 +min_value = 10.0 +max_value = 1000.0 +value = 10.0 + +[node name="Volume" type="HBoxContainer" parent="Control/VBoxContainer/HBoxContainer/VBoxContainer"] +layout_mode = 2 +theme_override_constants/separation = 12 + +[node name="Label" type="Label" parent="Control/VBoxContainer/HBoxContainer/VBoxContainer/Volume"] +layout_mode = 2 +text = "Volume" + +[node name="CenterContainer" type="CenterContainer" parent="Control/VBoxContainer/HBoxContainer/VBoxContainer/Volume"] +layout_mode = 2 + +[node name="HSlider" type="HSlider" parent="Control/VBoxContainer/HBoxContainer/VBoxContainer/Volume/CenterContainer"] +custom_minimum_size = Vector2(200, 0) +layout_mode = 2 +max_value = 2.0 +step = 0.1 +value = 1.0 + +[node name="LoopSettings" type="VBoxContainer" parent="Control/VBoxContainer/HBoxContainer"] +layout_mode = 2 +theme_override_constants/separation = 12 + +[node name="Label" type="Label" parent="Control/VBoxContainer/HBoxContainer/LoopSettings"] +layout_mode = 2 +theme_override_font_sizes/font_size = 20 +text = "Song Loop Settings" + +[node name="SongBeatCount" type="VBoxContainer" parent="Control/VBoxContainer/HBoxContainer/LoopSettings"] +layout_mode = 2 + +[node name="Label" type="Label" parent="Control/VBoxContainer/HBoxContainer/LoopSettings/SongBeatCount"] +layout_mode = 2 +text = "Song Beat Count (affects next loop)" + +[node name="Ranges" type="HBoxContainer" parent="Control/VBoxContainer/HBoxContainer/LoopSettings/SongBeatCount"] +layout_mode = 2 +theme_override_constants/separation = 12 + +[node name="CenterContainer" type="CenterContainer" parent="Control/VBoxContainer/HBoxContainer/LoopSettings/SongBeatCount/Ranges"] +layout_mode = 2 + +[node name="HSlider" type="HSlider" parent="Control/VBoxContainer/HBoxContainer/LoopSettings/SongBeatCount/Ranges/CenterContainer"] +custom_minimum_size = Vector2(200, 0) +layout_mode = 2 +min_value = 1.0 +max_value = 32.0 +value = 32.0 + +[node name="CenterContainer2" type="CenterContainer" parent="Control/VBoxContainer/HBoxContainer/LoopSettings/SongBeatCount/Ranges"] +layout_mode = 2 + +[node name="SpinBox" type="SpinBox" parent="Control/VBoxContainer/HBoxContainer/LoopSettings/SongBeatCount/Ranges/CenterContainer2"] +layout_mode = 2 +min_value = 1.0 +max_value = 32.0 +value = 32.0 + +[node name="StopCurrButton" type="Button" parent="Control/VBoxContainer/HBoxContainer/LoopSettings"] +layout_mode = 2 +text = "Stop current loop" + +[node name="CancelNextButton" type="Button" parent="Control/VBoxContainer/HBoxContainer/LoopSettings"] +layout_mode = 2 +text = "Cancel next loop" + +[node name="TimeLabels" type="VBoxContainer" parent="Control/VBoxContainer/HBoxContainer"] +layout_mode = 2 + +[node name="GameTimeLabel" type="Label" parent="Control/VBoxContainer/HBoxContainer/TimeLabels"] +layout_mode = 2 +text = "Game Time: 0" + +[node name="AudioTimeLabel" type="Label" parent="Control/VBoxContainer/HBoxContainer/TimeLabels"] +layout_mode = 2 +text = "Audio Time: 0" + +[connection signal="toggled" from="Control/VBoxContainer/HBoxContainer/VBoxContainer/UsePlayScheduled/CheckButton" to="." method="_on_use_play_scheduled_check_button_toggled"] +[connection signal="value_changed" from="Control/VBoxContainer/HBoxContainer/VBoxContainer/MaxFps/CenterContainer/HSlider" to="." method="_on_max_fps_h_slider_value_changed"] +[connection signal="value_changed" from="Control/VBoxContainer/HBoxContainer/VBoxContainer/MaxFps/SpinBox" to="." method="_on_max_fps_spin_box_value_changed"] +[connection signal="value_changed" from="Control/VBoxContainer/HBoxContainer/VBoxContainer/Volume/CenterContainer/HSlider" to="." method="_on_volume_h_slider_value_changed"] +[connection signal="value_changed" from="Control/VBoxContainer/HBoxContainer/LoopSettings/SongBeatCount/Ranges/CenterContainer/HSlider" to="." method="_on_song_beat_count_h_slider_value_changed"] +[connection signal="value_changed" from="Control/VBoxContainer/HBoxContainer/LoopSettings/SongBeatCount/Ranges/CenterContainer2/SpinBox" to="." method="_on_song_beat_count_spin_box_value_changed"] +[connection signal="pressed" from="Control/VBoxContainer/HBoxContainer/LoopSettings/StopCurrButton" to="." method="_on_stop_curr_button_pressed"] +[connection signal="pressed" from="Control/VBoxContainer/HBoxContainer/LoopSettings/CancelNextButton" to="." method="_on_cancel_next_button_pressed"] diff --git a/audio/scheduled_metronome/metronome.gd b/audio/scheduled_metronome/metronome.gd new file mode 100644 index 0000000000..a2372268ac --- /dev/null +++ b/audio/scheduled_metronome/metronome.gd @@ -0,0 +1,30 @@ +extends AudioStreamPlayer + +@export var bpm: float = 130 + +var _running: bool = false +var _start_absolute_time: float = 0 +var _next_tick_time: float = -1 + + +func _process(_delta: float) -> void: + if not _running: + return + + # Play the metronome tick once it is time to play it. The metronome ticks + # are tied to the framerate of the game. + var curr_time := Time.get_ticks_usec() / 1000000.0 + if curr_time >= _next_tick_time: + play() + + # Calculate time for the next tick. + var beat_time := 60 / bpm + var next_tick := ceili((curr_time + 0.001 - _start_absolute_time) / beat_time) + _next_tick_time = _start_absolute_time + next_tick * beat_time + print("playing tick: ", _next_tick_time, " ", next_tick) + + +func start(start_absolute_time: float) -> void: + _running = true + _start_absolute_time = start_absolute_time + _next_tick_time = start_absolute_time diff --git a/audio/scheduled_metronome/metronome.gd.uid b/audio/scheduled_metronome/metronome.gd.uid new file mode 100644 index 0000000000..c5a04f3d5d --- /dev/null +++ b/audio/scheduled_metronome/metronome.gd.uid @@ -0,0 +1 @@ +uid://bwqpovn6r5q7q diff --git a/audio/scheduled_metronome/metronome_scheduled.gd b/audio/scheduled_metronome/metronome_scheduled.gd new file mode 100644 index 0000000000..80daf1484d --- /dev/null +++ b/audio/scheduled_metronome/metronome_scheduled.gd @@ -0,0 +1,28 @@ +extends ScheduledAudioStreamPlayer + +@export var bpm: float = 130 + +var _running: bool = false +var _start_absolute_time: float = 0 +var _scheduled_time: float = -1 + + +func _process(_delta: float) -> void: + if not _running: + return + + # Once the currently scheduled tick has started, begin scheduling the next + # one. + var curr_time := AudioServer.get_absolute_time() + if curr_time > _scheduled_time: + var beat_time := 60 / bpm + var next_tick := ceili((curr_time - _start_absolute_time) / beat_time) + _scheduled_time = _start_absolute_time + next_tick * beat_time + play_scheduled(_scheduled_time) + print("scheduling tick: ", _scheduled_time, " ", next_tick) + + +func start(start_absolute_time: float) -> void: + _running = true + _start_absolute_time = start_absolute_time + _scheduled_time = start_absolute_time - 60 / bpm diff --git a/audio/scheduled_metronome/metronome_scheduled.gd.uid b/audio/scheduled_metronome/metronome_scheduled.gd.uid new file mode 100644 index 0000000000..96171e92fb --- /dev/null +++ b/audio/scheduled_metronome/metronome_scheduled.gd.uid @@ -0,0 +1 @@ +uid://f4crwo4mmo3t diff --git a/audio/scheduled_metronome/project.godot b/audio/scheduled_metronome/project.godot new file mode 100644 index 0000000000..6b1c087bc7 --- /dev/null +++ b/audio/scheduled_metronome/project.godot @@ -0,0 +1,31 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; since the parameters that go here are not all obvious. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters + +config_version=5 + +[application] + +config/name="Scheduled Metronome Demo" +config/description="A demo showcasing scheduled audio. Plays a song on loop with a metronome." +run/main_scene="uid://b3scmm5r6a23q" +config/features=PackedStringArray("4.5", "Forward Plus") +run/max_fps=200 +config/icon="res://icon.svg" + +[audio] + +general/default_playback_type.web=0 + +[debug] + +settings/stdout/print_fps=true +gdscript/warnings/untyped_declaration=1 + +[display] + +window/vsync/vsync_mode=0 diff --git a/audio/scheduled_metronome/screenshots/.gdignore b/audio/scheduled_metronome/screenshots/.gdignore new file mode 100644 index 0000000000..e69de29bb2 diff --git a/audio/scheduled_metronome/screenshots/scheduled-metronome.webp b/audio/scheduled_metronome/screenshots/scheduled-metronome.webp new file mode 100644 index 0000000000..472725ce4d Binary files /dev/null and b/audio/scheduled_metronome/screenshots/scheduled-metronome.webp differ diff --git a/audio/scheduled_metronome/track.ogg b/audio/scheduled_metronome/track.ogg new file mode 100644 index 0000000000..bfcc4802c6 Binary files /dev/null and b/audio/scheduled_metronome/track.ogg differ diff --git a/audio/scheduled_metronome/track.ogg.import b/audio/scheduled_metronome/track.ogg.import new file mode 100644 index 0000000000..92251f9f85 --- /dev/null +++ b/audio/scheduled_metronome/track.ogg.import @@ -0,0 +1,19 @@ +[remap] + +importer="oggvorbisstr" +type="AudioStreamOggVorbis" +uid="uid://c2b3dcgll0ae4" +path="res://.godot/imported/track.ogg-89f08f7c0ab8fb78381fbc8beef25ed1.oggvorbisstr" + +[deps] + +source_file="res://track.ogg" +dest_files=["res://.godot/imported/track.ogg-89f08f7c0ab8fb78381fbc8beef25ed1.oggvorbisstr"] + +[params] + +loop=false +loop_offset=0.0 +bpm=130.0 +beat_count=32 +bar_beats=4