First commit 🎉

This commit is contained in:
Tony Bark 2025-07-17 01:49:18 -04:00
commit 43ea213f9b
728 changed files with 37080 additions and 0 deletions

View file

@ -0,0 +1,179 @@
@tool
class_name PopochiuAudioCue
extends Resource
## Used to play audio files with extra properties.
##
## You can set the pitch (with random values), volume, and audio bus, as well as specify whether
## it loops, and whether it is 2D positioned.
## The audio file to play.
@export var audio: AudioStream
## Whether the audio file will loop when played.
@export var loop := false : set = set_loop
## Whether this audio cue uses a 2D position.
@export var is_2d := false
## Whether the audio can be played simultaneously with other instances of itself. Especially useful
## for audio cues set in a loop (where [member loop] is [code]true[/code]).
@export var can_play_simultaneous := true
## The pitch value (in semitones) to use when playing the audio file.
@export var pitch := 0.0
## The volume to use when playing the audio file.
@export var volume := 0.0
## The range of values to use for randomly changing the pitch of the audio file when played.
@export var rnd_pitch := Vector2.ZERO
## The range of values to use to randomly changing the volume of the audio file when played.
@export var rnd_volume := Vector2.ZERO
## Maximum distance from which the audio file is still hearable. This only works if [member is_2d]
## is [code]true[/code].
@export var max_distance := 2000
## The audio bus in which the audio file will be played.
@export var bus := "Master"
#region Public #####################################################################################
## Plays this audio cue with a fade that lasts [param duration] seconds. If [param wait_to_end] is
## set to [code]true[/code], the function will wait for the audio to finish. You can specify the
## starting volume with [param from] and the target volume with [param to], as well as the
## [param position_2d] of the [AudioStreamPlayer] or [AudioStreamPlayer2D] that will play the audio
## file.
func fade(
duration := 1.0, wait_to_end := false, from := -80.0, to := INF, position_2d := Vector2.ZERO
) -> void:
if wait_to_end:
await PopochiuUtils.e.am.play_fade_cue(resource_name, duration, from, to, position_2d, true)
else:
PopochiuUtils.e.am.play_fade_cue(resource_name, duration, from, to, position_2d)
## Plays this audio cue with a fade that lasts [param duration] seconds. If [param wait_to_end] is
## set to [code]true[/code], the function will wait for the audio to finish. You can specify the
## starting volume with [param from] and the target volume with [param to], as well as the
## [param position_2d] of the [AudioStreamPlayer] or [AudioStreamPlayer2D] that will play the audio
## file.[br][br]
## [i]This method is intended to be used inside a [method Popochiu.queue] of instructions.[/i]
func queue_fade(
duration := 1.0, wait_to_end := false, from := -80.0, to := INF, position_2d := Vector2.ZERO
) -> Callable:
return func ():
if wait_to_end:
await fade(duration, wait_to_end, from, to, position_2d)
else:
fade(duration, wait_to_end, from, to, position_2d)
await PopochiuUtils.e.get_tree().process_frame
## Stops the audio cue, with an optional fade effect lasting [param fade_duration] seconds.
func stop(fade_duration := 0.0) -> void:
PopochiuUtils.e.am.stop(resource_name, fade_duration)
## Stops the audio cue, with an optional fade effect lasting [param fade_duration] seconds.[br][br]
## [i]This method is intended to be used inside a [method Popochiu.queue] of instructions.[/i]
func queue_stop(fade_duration := 0.0) -> Callable:
return func ():
stop(fade_duration)
await PopochiuUtils.e.get_tree().process_frame
## Changes the [member AudioStreamPlayer.pitch_scale] of the [AudioStreamPlayer] playing the audio
## file associated with this audio cue to [param pitch]. If the audio was played with a 2D position,
## then [member AudioStreamPlayer2D.pitch_scale] will be affected.
func change_stream_pitch(pitch := 0.0) -> void:
PopochiuUtils.e.am.change_cue_pitch(resource_name, pitch)
## Changes the [member AudioStreamPlayer.volume_db] of the [AudioStreamPlayer] playing the audio
## file associated with this audio cue to [param volume]. If the audio was played with a 2D
## position, then [member AudioStreamPlayer2D.volume_db] will be affected.
func change_stream_volume(volume := 0.0) -> void:
PopochiuUtils.e.am.change_cue_volume(resource_name, volume)
## Returns the value of [member AudioStreamPlayer.pitch_scale] to be applied to the
## [AudioStreamPlayer] playing the audio file associated with this audio cue. If the audio was
## played with a 2D position, then [member AudioStreamPlayer2D.volume_db] will be affected.
func get_pitch_scale() -> float:
var p := PopochiuUtils.a.semitone_to_pitch(pitch)
if rnd_pitch != Vector2.ZERO:
p = _get_rnd_pitch()
return p
## Returns the playback position of this audio cue.
func get_cue_playback_position() -> float:
return PopochiuUtils.e.am.get_cue_playback_position(resource_name)
## Maps [param values] to the properties of this audio cue. This is used by TabAudio when changing
## the script of the audio cue to one of the types: [AudioCueSound] or [AudioCueMusic].
func set_values(values: Dictionary) -> void:
resource_name = values.resource_name
audio = values.audio
loop = values.loop
is_2d = values.is_2d
pitch = values.pitch
volume = values.volume
rnd_pitch = values.rnd_pitch
rnd_volume = values.rnd_volume
max_distance = values.max_distance
bus = values.bus
## Returns the properties of this audio cue as a [Dictionary]. This is used by TabAudio when
## changing the script of the audio cue to one of the types: [AudioCueSound] or [AudioCueMusic].
func get_values() -> Dictionary:
return {
resource_name = resource_name,
audio = audio,
loop = loop,
is_2d = is_2d,
pitch = pitch,
volume = volume,
rnd_pitch = rnd_pitch,
rnd_volume = rnd_volume,
max_distance = max_distance,
bus = bus
}
## Returns [code]true[/code] if playing.
func is_playing() -> bool:
return PopochiuUtils.a.is_playing_cue(resource_name)
#endregion
#region SetGet #####################################################################################
func set_loop(value: bool) -> void:
loop = value
if not audio: return
match audio.get_class():
'AudioStreamOggVorbis', 'AudioStreamMP3':
audio.loop = value
'AudioStreamWAV':
(audio as AudioStreamWAV).loop_mode = (
AudioStreamWAV.LOOP_FORWARD if value else AudioStreamWAV.LOOP_DISABLED
)
notify_property_list_changed()
#endregion
#region Private ####################################################################################
func _get_rnd_pitch() -> float:
randomize()
return PopochiuUtils.a.semitone_to_pitch(pitch + randf_range(rnd_pitch.x, rnd_pitch.y))
func _get_rnd_volume() -> float:
randomize()
return volume + randf_range(rnd_volume.x, rnd_volume.y)
#endregion

View file

@ -0,0 +1 @@
uid://btinlvcono726

View file

@ -0,0 +1,2 @@
extends Resource

View file

@ -0,0 +1 @@
uid://7s3126auywka

View file

@ -0,0 +1,23 @@
@tool
class_name AudioCueMusic
extends PopochiuAudioCue
## A specific type of [PopochiuAudioCue] designed for playing music.
#region Public #####################################################################################
## Plays this audio cue. It can fade for [param fade_duration] seconds, and you can change the track
## starting position in seconds with [param music_position].
func play(fade_duration := 0.0, music_position := 0.0) -> void:
PopochiuUtils.e.am.play_music_cue(resource_name, fade_duration, music_position)
## Plays this audio cue. It can fade for [param fade_duration] seconds, and you can change the track
## starting position in seconds with [param music_position].[br][br]
## [i]This method is intended to be used inside a [method Popochiu.queue] of instructions.[/i]
func queue_play(fade_duration := 0.0, music_position := 0.0) -> Callable:
return func ():
await play(fade_duration, music_position)
await PopochiuUtils.e.get_tree().process_frame
#endregion

View file

@ -0,0 +1 @@
uid://bccg7ywc586ng

View file

@ -0,0 +1,31 @@
@tool
class_name AudioCueSound
extends PopochiuAudioCue
## A specific type of [PopochiuAudioCue] designed for playing sounds.
#region Public #####################################################################################
## Plays this audio cue. If [param wait_to_end] is set to [code]true[/code], the function will pause
## until the audio clip finishes. You can play the file from a specific [param position_2d] in the
## scene if [member PopochiuAudioCue.is_2d] is [code]true[/code].
func play(wait_to_end := false, position_2d := Vector2.ZERO) -> void:
if wait_to_end:
await PopochiuUtils.e.am.play_sound_cue(resource_name, position_2d, true)
else:
PopochiuUtils.e.am.play_sound_cue(resource_name, position_2d)
## Plays this audio cue. If [param wait_to_end] is set to [code]true[/code], the function will pause
## until the audio clip finishes. You can play the file from a specific [param position_2d] in the
## scene if [member PopochiuAudioCue.is_2d] is [code]true[/code].[br][br]
## [i]This method is intended to be used inside a [method Popochiu.queue] of instructions.[/i]
func queue_play(wait_to_end := false, position_2d := Vector2.ZERO) -> Callable:
return func ():
if wait_to_end:
await play(true, position_2d)
else:
play(false, position_2d)
await PopochiuUtils.e.get_tree().process_frame
#endregion

View file

@ -0,0 +1 @@
uid://cqbmt4xruu8eo

View file

@ -0,0 +1,403 @@
class_name PopochiuAudioManager
extends Node
## Handles playing audio using [PopochiuAudioCue]s.
##
## It plays sound effects and music using [AudioStreamPlayer] or [AudioStreamPlayer2D], creating
## these nodes at runtime if needed. By default, it has 6 nodes for positional streams and 5 for
## playing non-positional streams.[br][br]
## The [b]PopochiuAudioManager[/b] is loaded as a child of [Popochiu] when the game starts.
## Used to mark stream players created at runtime that should be [method Node.free] when they are
## no longer needed.
const TEMP_PLAYER := "temporal"
## Specifies the path where the volume configuration for the audio buses used in the game is stored.
const SETTINGS_PATH = "user://audio_settings.save"
## Used to convert the value of the pitch set on [member PopochiuAudioCue.pitch] to the
## corresponding value needed for the [code]pitch_scale[/code] property of the audio stream players.
var twelfth_root_of_two := pow(2, (1.0 / 12))
## Stores the volume values for each of the audio buses used by the game.
var volume_settings := {}
var _mx_cues := {}
var _sfx_cues := {}
var _vo_cues := {}
var _ui_cues := {}
var _active := {}
var _all_in_one := {}
# Serves as a map that stores an AudioStreamPlayer/AudioStreamPlayer2D and the tween used to fade
# its volume
var _fading_sounds := {}
var _dflt_volumes := {}
#region Godot ######################################################################################
func _ready() -> void:
if Engine.is_editor_hint(): return
for bus_idx in range(AudioServer.get_bus_count()):
var bus_name = AudioServer.get_bus_name(bus_idx)
volume_settings[bus_name] = AudioServer.get_bus_volume_db(bus_idx)
for arr in ["mx_cues", "sfx_cues", "vo_cues", "ui_cues"]:
for rp in PopochiuResources.get_data_value("audio", arr, []):
var ac: PopochiuAudioCue = load(rp)
self["_%s" % arr][ac.resource_name] = ac
_all_in_one[ac.resource_name] = ac
_dflt_volumes[ac.resource_name] = ac.volume
#endregion
#region Public #####################################################################################
## Searches for the [PopochiuAudioCue] identified by [param cue_name] among the cues that are NOT
## part of the music group and plays it. You can set a [param position_2d] to play it positionally.
## If [param wait_to_end] is set to [code]true[/code], the function will pause until the audio
## finishes.
func play_sound_cue(cue_name := "", position_2d := Vector2.ZERO, wait_to_end = null) -> Node:
var stream_player: Node = null
if _all_in_one.has(cue_name.to_lower()):
var cue: PopochiuAudioCue = _all_in_one[cue_name.to_lower()]
stream_player = _play(cue, position_2d)
else:
PopochiuUtils.print_error("Sound not found: " + cue_name)
if wait_to_end != null:
await get_tree().process_frame
return null
if wait_to_end == true and stream_player:
await stream_player.finished
elif wait_to_end == false:
await get_tree().process_frame
return stream_player
## Searches for the [PopochiuAudioCue] identified by [param cue_name] among the cues that are part
## of the music group and plays it. It can fade for [param fade_duration] seconds, and you can start
## playing it from a given [param music_position] in seconds.
func play_music_cue(cue_name: String, fade_duration := 0.0, music_position := 0.0) -> Node:
var stream_player: Node = null
if _active.has(cue_name):
return _active[cue_name].players[0]
if _mx_cues.has(cue_name.to_lower()):
var cue: PopochiuAudioCue = _mx_cues[cue_name.to_lower()]
# NOTE: fixes #27 AudioCues were losing the volume set in editor when
# played with a fade
cue.volume = _dflt_volumes[cue_name.to_lower()]
if fade_duration > 0.0:
stream_player = _fade_in(
cue,
Vector2.ZERO,
fade_duration,
-80.0,
cue.volume,
music_position
)
else:
stream_player = _play(cue, Vector2.ZERO, music_position)
else:
PopochiuUtils.print_error("Music not found: " + cue_name)
return stream_player
## Plays the [PopochiuAudioCue] identified by [param cue_name] using a fade that will last
## [param duration] seconds. Specify the starting volume with [param from] and the target volume
## with [param to]. You can play the audio positionally with [param position_2d] and wait for it
## to finish if [param wait_to_end] is set to [code]true[/code].
func play_fade_cue(
cue_name := "",
duration := 1.0,
from := -80.0,
to := INF,
position_2d := Vector2.ZERO,
wait_to_end = null
) -> Node:
var stream_player: Node = null
if _all_in_one.has(cue_name.to_lower()):
var cue: PopochiuAudioCue = _all_in_one[cue_name.to_lower()]
stream_player = _fade_in(cue, position_2d, duration, from, to if to != INF else cue.volume)
else:
PopochiuUtils.print_error("Sound to fade not found " + cue_name)
if wait_to_end != null:
await get_tree().process_frame
return null
if wait_to_end == true and stream_player:
await stream_player.finished
elif wait_to_end == false:
await get_tree().process_frame
return stream_player
## Stops the [PopochiuAudioCue] identified by [param cue_name]. It can use a fade effect that will
## last [param fade_duration] seconds.
func stop(cue_name: String, fade_duration := 0.0) -> void:
if _active.has(cue_name):
var stream_player: Node = (_active[cue_name].players as Array).front()
if is_instance_valid(stream_player):
if fade_duration > 0.0:
_fade_sound(
cue_name, fade_duration, stream_player.volume_db, -80.0
)
else:
stream_player.stop()
# Always emit the signal since it won't be emitted if the audio
# file haven't reach the end yet
stream_player.finished.emit()
else:
_active.erase(cue_name)
## Returns the playback position of the [PopochiuAudioCue] identified by [param cue_name].
func get_cue_playback_position(cue_name: String) -> float:
if not _active.has(cue_name): return -1.0
var stream_player: Node = (_active[cue_name].players as Array).front()
if is_instance_valid(stream_player):
return stream_player.get_playback_position()
return -1.0
## Changes the [code]pitch_scale[/code] of the [PopochiuAudioCue] identified by [param cue_name] to
## the value set (in semitones) in [param pitch].
func change_cue_pitch(cue_name: String, pitch := 0.0) -> void:
if not _active.has(cue_name): return
var stream_player: Node = (_active[cue_name].players as Array).front()
stream_player.set_pitch_scale(_semitone_to_pitch(pitch))
## Changes the [code]volume_db[/code] of the [PopochiuAudioCue] identified by [param cue_name] to
## the value set in [param volume].
func change_cue_volume(cue_name: String, volume := 0.0) -> void:
if not _active.has(cue_name): return
var stream_player: Node = (_active[cue_name].players as Array).front()
stream_player.volume_db = volume
## Sets [param value] as the volume of the audio bus identified with [param bus_name].
func set_bus_volume_db(bus_name: String, value: float) -> void:
if volume_settings.has(bus_name):
volume_settings[bus_name] = value
AudioServer.set_bus_volume_db(
AudioServer.get_bus_index(bus_name), volume_settings[bus_name]
)
save_sound_settings()
## Saves in the file at [constant SETTINGS_PATH] the volume values of all the audio buses.
func save_sound_settings():
var file = FileAccess.open(SETTINGS_PATH, FileAccess.WRITE)
if file == null:
PopochiuUtils.print_error("Error opening file: " + SETTINGS_PATH)
else:
file.store_var(volume_settings)
file.close()
## Loads the volume values stored at [constant SETTINGS_PATH] for all the audio buses.
func load_sound_settings():
var file = FileAccess.open(SETTINGS_PATH, FileAccess.READ)
if file:
volume_settings = file.get_var(true)
file.close()
for bus_idx in range(AudioServer.get_bus_count()):
var bus_name = AudioServer.get_bus_name(bus_idx)
if volume_settings.has(bus_name):
AudioServer.set_bus_volume_db(
AudioServer.get_bus_index(bus_name),
volume_settings[bus_name]
)
else:
volume_settings[bus_name] = AudioServer.get_bus_volume_db(bus_idx)
## Returns [code]true[/code] if the [PopochiuAudioCue] identified by [param cue_name] is playing.
func is_playing_cue(cue_name: String) -> bool:
return get_cue_playback_position(cue_name) > -1
#endregion
#region Private ####################################################################################
# Calculates the [code]pitch_scale[/code] value of [param pitch], which is in semitones.
func _semitone_to_pitch(pitch: float) -> float:
return pow(twelfth_root_of_two, pitch)
# Plays the sound and assigns it to a free AudioStreamPlayer, or creates one if there are no more.
func _play(
cue: PopochiuAudioCue, position := Vector2.ZERO, from_position := 0.0
) -> Node:
var player: Node = null
if cue.is_2d:
player = _get_free_stream($Positional)
if not is_instance_valid(player):
player = AudioStreamPlayer2D.new()
player.set_meta(TEMP_PLAYER, true)
$Active.add_child(player)
(player as AudioStreamPlayer2D).stream = cue.audio
(player as AudioStreamPlayer2D).pitch_scale = cue.get_pitch_scale()
(player as AudioStreamPlayer2D).volume_db = cue.volume
(player as AudioStreamPlayer2D).max_distance = cue.max_distance
(player as AudioStreamPlayer2D).position = position
else:
player = _get_free_stream($Generic)
if not is_instance_valid(player):
player = AudioStreamPlayer.new()
player.set_meta(TEMP_PLAYER, true)
$Active.add_child(player)
(player as AudioStreamPlayer).stream = cue.audio
(player as AudioStreamPlayer).pitch_scale = cue.get_pitch_scale()
(player as AudioStreamPlayer).volume_db = cue.volume
var cue_name: String = cue.resource_name
player.bus = cue.bus
player.play(from_position)
if not player.finished.is_connected(_on_audio_stream_player_finished):
player.finished.connect(_on_audio_stream_player_finished.bind(player, cue_name, 0))
if _active.has(cue_name):
_active[cue_name].players.append(player)
# NOTE: Stop the previous stream player created to play the audio cue
# that is in loop to avoid having more than one sound playing.
if not _active[cue_name].can_play_simultaneous:
stop(cue_name)
else:
_active[cue_name] = {
players = [player],
loop = cue.loop,
can_play_simultaneous = cue.can_play_simultaneous
}
return player
func _get_free_stream(group: Node):
return _reparent(group, $Active, 0)
# Reassigns the [AudioStreamPlayer] to its original group when it finishes so it can be available
# for being used again.
func _on_audio_stream_player_finished(
stream_player: Node, cue_name: String, _debug_idx: int
) -> void:
if stream_player.has_meta(TEMP_PLAYER):
stream_player.queue_free()
elif stream_player is AudioStreamPlayer:
_reparent($Active, $Generic, stream_player.get_index())
else:
_reparent($Active, $Positional, stream_player.get_index())
if _active.has(cue_name):
var players: Array = _active[cue_name].players
for idx in players.size():
if players[idx].get_instance_id() == stream_player.get_instance_id():
players.remove_at(idx)
break
if players.is_empty():
_active.erase(cue_name)
if not stream_player.finished.is_connected(_on_audio_stream_player_finished):
stream_player.finished.connect(_on_audio_stream_player_finished)
func _reparent(source: Node, target: Node, child_idx: int) -> Node:
if not is_instance_valid(source) or source.get_children().is_empty():
return null
var node_to_reparent: Node = source.get_child(child_idx)
if not is_instance_valid(node_to_reparent):
return null
node_to_reparent.reparent(target)
return node_to_reparent
func _fade_in(
cue: PopochiuAudioCue,
position: Vector2,
duration := 1.0,
from := -80.0,
to := 0.0,
from_position := 0.0
) -> Node:
if cue.audio.get_instance_id() in _fading_sounds:
from = _fading_sounds[cue.audio.get_instance_id()].stream.volume_db
var tween: Tween = _fading_sounds[cue.audio.get_instance_id()].tween
# Stop the tween only of the sound that is fading
if is_instance_valid(tween) and tween.is_running():
tween.kill()
_fading_sounds[cue.audio.get_instance_id()].finished.emit()
_fading_sounds.erase(cue.audio.get_instance_id())
cue.volume = from
var stream_player: Node = _play(cue, position, from_position)
if stream_player:
_fade_sound(cue.resource_name, duration, from, to)
else:
cue.volume = to
return stream_player
func _fade_sound(cue_name: String, duration = 1, from = 0, to = 0) -> void:
var stream_player: Node = (_active[cue_name].players as Array).front()
if _fading_sounds.has(stream_player.stream.get_instance_id()):
_fading_sounds[stream_player.stream.get_instance_id()].tween.kill()
var t := create_tween().set_ease(Tween.EASE_IN_OUT)
t.finished.connect(_fadeout_finished.bind(stream_player, t))
t.tween_property(stream_player, "volume_db", to, duration).from(from)
if from > to :
_fading_sounds[stream_player.stream.get_instance_id()] = {
stream = stream_player,
tween = t
}
func _fadeout_finished(stream_player: Node, tween: Tween) -> void:
if stream_player.stream.get_instance_id() in _fading_sounds :
_fading_sounds.erase(stream_player.stream.get_instance_id())
stream_player.stop()
tween.finished.disconnect(_fadeout_finished)
#endregion

View file

@ -0,0 +1 @@
uid://bngbmwj65yxut

View file

@ -0,0 +1,34 @@
[gd_scene load_steps=2 format=3 uid="uid://ciufytcstpkcr"]
[ext_resource type="Script" path="res://addons/popochiu/engine/audio_manager/audio_manager.gd" id="1_3dbpw"]
[node name="AudioManager" type="Node"]
script = ExtResource("1_3dbpw")
[node name="Positional" type="Node" parent="."]
[node name="AudioStreamPlayer2D" type="AudioStreamPlayer2D" parent="Positional"]
[node name="AudioStreamPlayer2D2" type="AudioStreamPlayer2D" parent="Positional"]
[node name="AudioStreamPlayer2D3" type="AudioStreamPlayer2D" parent="Positional"]
[node name="AudioStreamPlayer2D4" type="AudioStreamPlayer2D" parent="Positional"]
[node name="AudioStreamPlayer2D5" type="AudioStreamPlayer2D" parent="Positional"]
[node name="AudioStreamPlayer2D6" type="AudioStreamPlayer2D" parent="Positional"]
[node name="Generic" type="Node" parent="."]
[node name="AudioStreamPlayer" type="AudioStreamPlayer" parent="Generic"]
[node name="AudioStreamPlayer2" type="AudioStreamPlayer" parent="Generic"]
[node name="AudioStreamPlayer3" type="AudioStreamPlayer" parent="Generic"]
[node name="AudioStreamPlayer4" type="AudioStreamPlayer" parent="Generic"]
[node name="AudioStreamPlayer5" type="AudioStreamPlayer" parent="Generic"]
[node name="Active" type="Node" parent="."]