Short songs (#30): a quiz settings switch and a Seconds per song row (30 by default, 10-120 in steps of 5), saved with the quiz settings. Only part of each quiz song plays, from a random point 10-60% into it, picked once per song so restart and repeat play the same part. The progress bar, time and seeking stay within the part, a hidden beginning counts inside it, and its end counts as the song ending (repeat, stop or go to the next song as set). Random songs (#51): the low-played switch in Settings is now a Normal / Low played / Classic choice, stored as Settings.random_mode and not saved. fetch_random_song(mode) picks /music/rand, /music/rand/low or /music/rand/classic. Closes #30 Closes #51 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
302 lines
9.7 KiB
GDScript
302 lines
9.7 KiB
GDScript
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"))
|