class_name MusicPlayer extends Control ## The bar that plays the songs, with play, restart, progress and volume. Whether a song ## repeats is a quiz setting ([code]Settings.auto_repeat_song[/code]). ## ## The buttons grow a little under the mouse and the play button pops when it switches ## between play and pause, see [ButtonBounce]. ## ## With Short songs on ([member Settings.short_songs]) only a part of each quiz song plays: ## [member Settings.short_song_seconds] from a random point between ## [constant SHORT_START_MIN] and [constant SHORT_START_MAX] of the song. The progress bar, ## the time and seeking all stay within that part, and its end counts as the song ending. signal update_song_list signal play_next_song var play_icon: Texture = preload("res://icons/play_button_icon.svg") var pause_icon: Texture = preload("res://icons/pause_button_icon.svg") var is_changing: bool = false var playback_position: float var stream: AudioStream var song_finished: bool = false ## Where in the song the part played with Short songs may start, as a share of its length. ## Many songs have a quiet intro, so the start is never right at the beginning. const SHORT_START_MIN: float = 0.1 const SHORT_START_MAX: float = 0.6 # The part of the song that plays: its start in seconds, and its length (0: to the end) var _clip_start: float = 0.0 var _clip_length: float = 0.0 @onready var play_button: Button = %PlayButton @onready var restart_button: Button = %RestartButton @onready var audio_player: AudioStreamPlayer = $AudioStreamPlayer @onready var sound_player: AudioStreamPlayer = $AudioSoundPlayer @onready var sound_effect_player: AudioStreamPlayer = $AudioSoundEffectPlayer @onready var progress_slider: HSlider = %MusicPlayerSlider @onready var volume_slider: VolumeSlider = %VolumeSlider @onready var music_time_label: Label = %MusicTimeLabel func _ready() -> void: play_button.pressed.connect(play_or_pause) restart_button.pressed.connect(restart) ButtonBounce.add_to(play_button) ButtonBounce.add_to(restart_button) progress_slider.drag_started.connect(_on_drag_started) progress_slider.drag_ended.connect(_on_drag_ended) audio_player.finished.connect(_on_finished) func _process(_delta: float) -> void: if audio_player.has_stream_playback() && !is_changing && !audio_player.stream_paused \ && audio_player.playing: var position: float = position_in_part() if _clip_length > 0.0 && position >= _clip_length: # The short part has played, which counts as the song ending audio_player.stop() progress_slider.value = _clip_length _on_finished() return if !Settings.hide_beginning || position > Settings.hide_for_seconds: progress_slider.value = position music_time_label.text = format_text(progress_slider.value, part_length()) ## How long the part of the song that plays is: the short part, or the whole song. func part_length() -> float: if _clip_length > 0.0: return _clip_length return stream.get_length() if stream != null else 0.0 ## How far into the part that plays the song is, in seconds. func position_in_part() -> float: return audio_player.get_playback_position() - _clip_start ## Plays only part of [param song_object]'s song when Short songs is on and the song is ## longer than the part. The part's start is picked once per song. func choose_part(song_object: SongObject, song: AudioStream) -> void: _clip_start = 0.0 _clip_length = 0.0 var length: float = song.get_length() if !Settings.short_songs || song_object == null || length <= Settings.short_song_seconds: return if song_object.short_start < 0.0: var latest: float = minf(length * SHORT_START_MAX, length - Settings.short_song_seconds) var earliest: float = minf(length * SHORT_START_MIN, latest) song_object.short_start = randf_range(earliest, latest) _clip_start = minf(song_object.short_start, length - Settings.short_song_seconds) _clip_length = Settings.short_song_seconds ## Whether the play button shows pause, because a song is playing. func shows_playing() -> bool: return play_button.icon == pause_icon # Shows pause on the play button while a song plays, and play otherwise func _show_playing(playing: bool) -> void: var icon: Texture = pause_icon if playing else play_icon if play_button.icon != icon: play_button.icon = icon ButtonBounce.pop(play_button) # The progress bar and time show the length of the part that plays func _show_length() -> void: var length: float = part_length() progress_slider.max_value = round(length) progress_slider.tick_count = 0 if Settings.hide_ticks else round(length / 60) music_time_label.text = format_text(progress_slider.value, length) func format_time(time: float) -> String: var total_seconds: int = roundi(time) @warning_ignore("integer_division") return "%02d:%02d" % [total_seconds / 60, total_seconds % 60] func format_text(part: float, total: float) -> String: if (Settings.hide_beginning && part < Settings.hide_for_seconds) && Settings.hide_length: return "??:?? / ??:??" elif Settings.hide_length: return format_time(part) + " / ??:??" elif Settings.hide_beginning && part < Settings.hide_for_seconds: return "??:?? / " + format_time(total) else: return format_time(part) + " / " + format_time(total) func play_or_pause() -> void: if song_finished: restart() elif audio_player.stream_paused: _show_playing(true) audio_player.stream_paused = false song_finished = false audio_player.seek(playback_position) GameLog.debug("continue") _show_length() else: pause() func pause() -> void: audio_player.stream_paused = true playback_position = audio_player.get_playback_position() _show_playing(false) ## Plays the song, or its short part, from the beginning. func restart() -> void: audio_player.stop() audio_player.stream_paused = false song_finished = false progress_slider.value = 0 playback_position = _clip_start _show_playing(true) audio_player.play(_clip_start) func _on_finished() -> void: _show_playing(false) song_finished = true if !Settings.stop_after_current: play_next_song.emit() if Settings.auto_repeat_song: restart() func _on_drag_started() -> void: is_changing = true func _on_drag_ended(_changed: bool) -> void: audio_player.seek(_clip_start + progress_slider.value) playback_position = _clip_start + progress_slider.value is_changing = false ## Jumps [param new_position] seconds forward, or back when negative, staying within the ## part that plays. func seek(new_position: float) -> void: progress_slider.value += new_position is_changing = true audio_player.seek(_clip_start + progress_slider.value) playback_position = _clip_start + progress_slider.value is_changing = false func change_volume(value: float) -> void: volume_slider.value += value volume_slider.change_volume(volume_slider.value) func _on_point_triggered(point: String) -> void: if point == "first": var value: int = randi_range(0, 10) if value == 0: play_sound(preload("res://sounds/sound1.mp3")) elif value < 5: play_sound(preload("res://sounds/intro_long.mp3")) else: play_sound(preload("res://sounds/intro_short.mp3")) elif point == "match": play_sound(preload("res://sounds/sound0.mp3")) func play_sound_effect(sound_effect_name: AudioStream) -> void: sound_effect_player.stream = sound_effect_name sound_effect_player.play() func play_sound(sound_name: AudioStream) -> void: sound_player.stream = sound_name audio_player.stop() sound_player.play() song_finished = true _show_playing(false) progress_slider.value = 0 func play_song(song: AudioStream) -> void: # Sounds such as the winning song always play whole _clip_start = 0.0 _clip_length = 0.0 audio_player.stream = song audio_player.play() sound_player.stop() song_finished = false _show_playing(true) stream = audio_player.stream progress_slider.value = 0 _show_length() ## Puts song [param song_object_no] in the player without playing it, e.g. the song that ## was playing when the game was closed. Play starts it from the beginning. func load_song_object(song_object_no: int) -> void: var song: AudioStream = Playlist.get_song(song_object_no) if song == null: return audio_player.stream = song stream = song choose_part(Playlist.song_object_array[song_object_no], song) song_finished = true _show_playing(false) progress_slider.value = 0 _show_length() func play_song_object(song_object_no: int) -> void: GameLog.debug("play_song_object") var song: AudioStream = Playlist.get_song(song_object_no) if song == null: GameLog.warning("Song ", song_object_no, " has no file to play") return if audio_player.is_playing(): audio_player.stop() await get_tree().create_timer(0.5).timeout audio_player.stream = song choose_part(Playlist.song_object_array[song_object_no], song) sound_player.stop() audio_player.play(_clip_start) song_finished = false _show_playing(true) # A new song makes the play button pop, also when it was already playing ButtonBounce.pop(play_button) stream = audio_player.stream progress_slider.value = 0 _show_length() Playlist.set_currently_playing_song(song_object_no) Playlist.unset_is_playing() if !Settings.hide_next_track: GameLog.debug("Show answer now!!") Playlist.song_is_answered(song_object_no) Playlist.song_has_played(song_object_no) Playlist.song_is_playing(song_object_no) update_song_list.emit() ## Plays a song to test the sound: one from the server, or a song that comes with the ## game when playing from a music folder. func get_sound_test_song() -> void: if Settings.is_offline(): sound_test_local() return var test_sound: Callable = func test_sound(body: PackedByteArray) -> void: var sound: AudioStream = AudioStreamMP3.new() sound.data = body play_song(sound) MusicServerClient.fetch_sound_test_song(test_sound) # The sound test without a server: a song that comes with the game func sound_test_local() -> void: play_song(preload("res://sounds/01. Opening.mp3"))