|
| 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 |
0 commit comments