CrossingOver/addons/resonate/sound_manager/sound_manager.gd
2024-03-14 00:53:46 -03:00

493 lines
18 KiB
GDScript

extends Node
## The SoundManager is responsible for all sound events in your game.
##
## It manages pools of 1D, 2D, and 3D audio stream players, which can be used
## for single-shot sound events, or reserved by scripts for repetitive & exclusive use.
## Sound events can contain many variations which will be chosen and played at random.
## Playback can be achieved both sequentially and polyphonically.
##
## @tutorial(View example scenes): https://github.com/hugemenace/resonate/tree/main/examples
const ResonateSettings = preload("../shared/resonate_settings.gd")
var _settings = ResonateSettings.new()
## Emitted only once when the SoundManager has finished setting up and
## is ready to play or instance sound events.
signal loaded
## Emitted every time the SoundManager detects that a SoundBank has
## been added or removed from the scene tree.
signal banks_updated
## Emitted every time one of the player pools is updated.
signal pools_updated
## Emitted whenever [signal SoundManager.loaded], [signal SoundManager.banks_updated],
## or [signal SoundManager.pools_updated] is emitted.
signal updated
## Whether the SoundManager has completed setup and is ready to play or instance sound events.
var has_loaded: bool = false
var _1d_players: Array[PooledAudioStreamPlayer] = []
var _2d_players: Array[PooledAudioStreamPlayer2D] = []
var _3d_players: Array[PooledAudioStreamPlayer3D] = []
var _event_table: Dictionary = {}
var _event_table_hash: int
# ------------------------------------------------------------------------------
# Lifecycle methods
# ------------------------------------------------------------------------------
func _init():
process_mode = Node.PROCESS_MODE_ALWAYS
func _ready() -> void:
_initialise_pool(ProjectSettings.get_setting(
_settings.POOL_1D_SIZE_SETTING_NAME,
_settings.POOL_1D_SIZE_SETTING_DEFAULT),
_create_player_1d)
_initialise_pool(ProjectSettings.get_setting(
_settings.POOL_2D_SIZE_SETTING_NAME,
_settings.POOL_2D_SIZE_SETTING_DEFAULT),
_create_player_2d)
_initialise_pool(ProjectSettings.get_setting(
_settings.POOL_3D_SIZE_SETTING_NAME,
_settings.POOL_3D_SIZE_SETTING_DEFAULT),
_create_player_3d)
_auto_add_events()
var scene_root = get_tree().root.get_tree()
scene_root.node_added.connect(_on_scene_node_added)
scene_root.node_removed.connect(_on_scene_node_removed)
func _process(_p_delta) -> void:
if _event_table_hash != _event_table.hash():
_event_table_hash = _event_table.hash()
banks_updated.emit()
updated.emit()
if has_loaded:
return
has_loaded = true
loaded.emit()
updated.emit()
# ------------------------------------------------------------------------------
# Public methods
# ------------------------------------------------------------------------------
## Returns a new Null player (null object pattern) which mimics a [PooledAudioStreamPlayer],
## allowing you to call methods such as [method PooledAudioStreamPlayer.trigger]
## without the need to wrap the call in a null check.
func null_instance() -> NullPooledAudioStreamPlayer:
return NullPooledAudioStreamPlayer.new()
## Returns a new Null player (null object pattern) which mimics a [PooledAudioStreamPlayer2D],
## allowing you to call methods such as [method PooledAudioStreamPlayer2D.trigger]
## without the need to wrap the call in a null check.
func null_instance_2d() -> NullPooledAudioStreamPlayer2D:
return NullPooledAudioStreamPlayer2D.new()
## Returns a new Null player (null object pattern) which mimics a [PooledAudioStreamPlayer3D],
## allowing you to call methods such as [method PooledAudioStreamPlayer3D.trigger]
## without the need to wrap the call in a null check.
func null_instance_3d() -> NullPooledAudioStreamPlayer3D:
return NullPooledAudioStreamPlayer3D.new()
## Used to determine whether the given [b]p_instance[/b] variable can be instantiated. It will return
## true if the SoundManager hasn't loaded yet, if the instance is already instantiated,
## or if the instance has been instantiated but is currently being released.
func should_skip_instancing(p_instance) -> bool:
if not has_loaded:
return true
if p_instance != null and p_instance.releasing:
return true
if p_instance != null and not p_instance.is_null():
return true
return false
## This a shorthand method used to instantiate a new instance while optionally configuring it
## to be automatically released when the given [b]p_base[/b] is removed from the scene tree.[br][br]
## The [b]p_factory[/b] callable is used to create the instance required. See example below:[br][br]
## [codeblock]
## _instance_note_one = SoundManager.quick_instance(_instance_note_one,
## SoundManager.instance.bind("example", "one"), self)
## [/codeblock]
func quick_instance(p_instance, p_factory: Callable, p_base: Node = null, p_finish_playing: bool = false) -> Variant:
if should_skip_instancing(p_instance):
return
var new_instance = p_factory.call()
if p_base != null:
release_on_exit(p_base, new_instance, p_finish_playing)
return new_instance
## Play a sound event from a SoundBank.
func play(p_bank_label: String, p_event_name: String, p_bus: String = "") -> void:
var instance = _instance_manual(p_bank_label, p_event_name, false, p_bus, false, null)
instance.trigger()
instance.release(true)
## Play a sound event from a SoundBank at a specific [b]Vector2[/b] or [b]Vector3[/b] position.
func play_at_position(p_bank_label: String, p_event_name: String, p_position, p_bus: String = "") -> void:
var instance = _instance_manual(p_bank_label, p_event_name, false, p_bus, false, p_position)
instance.trigger()
instance.release(true)
## Play a sound event from a SoundBank on a [b]Node2D[/b] or [b]Node3D[/b]. This causes the sound to
## synchronise with the Node's global position - causing it to move in 2D or 3D space along with the Node.
func play_on_node(p_bank_label: String, p_event_name: String, p_node, p_bus: String = "") -> void:
var instance = _instance_manual(p_bank_label, p_event_name, false, p_bus, false, p_node)
instance.trigger()
instance.release(true)
## Play a sound event from a SoundBank with the provided pitch and/or volume.
func play_varied(p_bank_label: String, p_event_name: String, p_pitch: float = 1.0, p_volume: float = 0.0, p_bus: String = "") -> void:
var instance = _instance_manual(p_bank_label, p_event_name, false, p_bus, false, null)
instance.trigger_varied(p_pitch, p_volume)
instance.release(true)
## Play a sound event from a SoundBank at a specific [b]Vector2[/b] or [b]Vector3[/b]
## position with the provided pitch and/or volume.
func play_at_position_varied(p_bank_label: String, p_event_name: String, p_position, p_pitch: float = 1.0, p_volume: float = 0.0, p_bus: String = "") -> void:
var instance = _instance_manual(p_bank_label, p_event_name, false, p_bus, false, p_position)
instance.trigger_varied(p_pitch, p_volume)
instance.release(true)
## Play a sound event from a SoundBank on a [b]Node2D[/b] or [b]Node3D[/b] with the provided pitch
## and/or volume. This causes the sound to synchronise with the Node's global position - causing
## it to move in 2D or 3D space along with the Node.
func play_on_node_varied(p_bank_label: String, p_event_name: String, p_node, p_pitch: float = 1.0, p_volume: float = 0.0, p_bus: String = "") -> void:
var instance = _instance_manual(p_bank_label, p_event_name, false, p_bus, false, p_node)
instance.trigger_varied(p_pitch, p_volume)
instance.release(true)
## Returns a reserved [PooledAudioStreamPlayer] for you to use exclusively until it is told to
## [method PooledAudioStreamPlayer.release] or is automatically released when registered
## with [method SoundManager.release_on_exit].
func instance(p_bank_label: String, p_event_name: String, p_bus: String = "") -> Variant:
return _instance_manual(p_bank_label, p_event_name, true, p_bus, false, null)
## Returns a reserved [PooledAudioStreamPlayer2D] or [PooledAudioStreamPlayer3D] (depending on the
## type of [b]p_position[/b]) placed at a specific 2D or 3D position in the world. You will have
## exclusive use of it until it is told to [method PooledAudioStreamPlayer.release] or is automatically
## released when registered with [method SoundManager.release_on_exit].
func instance_at_position(p_bank_label: String, p_event_name: String, p_position, p_bus: String = "") -> Variant:
return _instance_manual(p_bank_label, p_event_name, true, p_bus, false, p_position)
## Returns a reserved [PooledAudioStreamPlayer2D] or [PooledAudioStreamPlayer3D] (depending on the
## type of [b]p_node[/b]) which will synchronise its global position with [b]p_node[/b]. You will have
## exclusive use of it until it is told to [method PooledAudioStreamPlayer.release] or is automatically
## released when registered with [method SoundManager.release_on_exit].
func instance_on_node(p_bank_label: String, p_event_name: String, p_node, p_bus: String = "") -> Variant:
return _instance_manual(p_bank_label, p_event_name, true, p_bus, false, p_node)
## Returns a reserved [PooledAudioStreamPlayer] for you to use exclusively until it is told to
## [method PooledAudioStreamPlayer.release] or is automatically released when registered
## with [method SoundManager.release_on_exit].[br][br]
## [b]Note:[/b] This method will mark the reserved player as polyphonic (able to play
## multiple event variations simultaneously.)
func instance_poly(p_bank_label: String, p_event_name: String, p_bus: String = "") -> Variant:
return _instance_manual(p_bank_label, p_event_name, true, p_bus, true, null)
## Returns a reserved [PooledAudioStreamPlayer2D] or [PooledAudioStreamPlayer3D] (depending on the
## type of [b]p_position[/b]) placed at a specific 2D or 3D position in the world. You will have
## exclusive use of it until it is told to [method PooledAudioStreamPlayer.release] or is automatically
## released when registered with [method SoundManager.release_on_exit].[br][br]
## [b]Note:[/b] This method will mark the reserved player as polyphonic (able to play
## multiple event variations simultaneously.)
func instance_at_position_poly(p_bank_label: String, p_event_name: String, p_position, p_bus: String = "") -> Variant:
return _instance_manual(p_bank_label, p_event_name, true, p_bus, true, p_position)
## Returns a reserved [PooledAudioStreamPlayer2D] or [PooledAudioStreamPlayer3D] (depending on the
## type of [b]p_node[/b]) which will synchronise its global position with [b]p_node[/b]. You will have
## exclusive use of it until it is told to [method PooledAudioStreamPlayer.release] or is automatically
## released when registered with [method SoundManager.release_on_exit].[br][br]
## [b]Note:[/b] This method will mark the reserved player as polyphonic (able to play
## multiple event variations simultaneously.)
func instance_on_node_poly(p_bank_label: String, p_event_name: String, p_node, p_bus: String = "") -> Variant:
return _instance_manual(p_bank_label, p_event_name, true, p_bus, true, p_node)
## Will automatically release the given [b]p_instance[/b] when the provided
## [b]p_base[/b] is removed from the scene tree.
func release_on_exit(p_base: Node, p_instance: Node, p_finish_playing: bool = false) -> void:
if p_instance == null or p_base == null:
return
p_base.tree_exiting.connect(p_instance.release.bind(p_finish_playing))
## Will automatically release the given [b]p_instance[/b] when the provided
## [b]p_base[/b] is removed from the scene tree.[br][br]
## [b]Note:[/b] This method has been deprecated, please use [method SoundManager.release_on_exit] instead.
## @deprecated
func auto_release(p_base: Node, p_instance: Node, p_finish_playing: bool = false) -> Variant:
push_warning("Resonate - auto_release has been deprecated, please use release_on_exit instead.")
if p_instance == null:
return p_instance
release_on_exit(p_base, p_instance, p_finish_playing)
return p_instance
## Manually add a new SoundBank into the event cache.
func add_bank(p_bank: SoundBank) -> void:
_add_bank(p_bank)
## Remove the provided bank from the event cache.
func remove_bank(p_bank_label: String) -> void:
if not _event_table.has(p_bank_label):
return
_event_table.erase(p_bank_label)
## Clear all banks from the event cache.
func clear_banks() -> void:
_event_table.clear()
# ------------------------------------------------------------------------------
# Private methods
# ------------------------------------------------------------------------------
func _on_scene_node_added(p_node: Node) -> void:
if not p_node is SoundBank:
return
_add_bank(p_node)
func _on_scene_node_removed(p_node: Node) -> void:
if not p_node is SoundBank:
return
_remove_bank(p_node)
func _initialise_pool(p_size: int, p_creator_fn: Callable) -> void:
for i in p_size:
p_creator_fn.call_deferred()
func _auto_add_events() -> void:
var sound_banks = ResonateUtils.find_all_nodes(self, "SoundBank")
for sound_bank in sound_banks:
_add_bank(sound_bank)
_event_table_hash = _event_table.hash()
func _add_bank(p_bank: SoundBank) -> void:
if _event_table.has(p_bank.label):
_event_table[p_bank.label]["ref_count"] = \
_event_table[p_bank.label]["ref_count"] + 1
return
_event_table[p_bank.label] = {
"name": p_bank.label,
"bus": p_bank.bus,
"mode": p_bank.mode,
"events": _create_events(p_bank.events),
"ref_count": 1,
}
func _remove_bank(p_bank: SoundBank) -> void:
if not _event_table.has(p_bank.label):
return
if _event_table[p_bank.label]["ref_count"] == 1:
_event_table.erase(p_bank.label)
return
_event_table[p_bank.label]["ref_count"] = \
_event_table[p_bank.label]["ref_count"] - 1
func _create_events(p_events: Array[SoundEventResource]) -> Dictionary:
var events = {}
for event in p_events:
events[event.name] = {
"name": event.name,
"bus": event.bus,
"volume": event.volume,
"pitch": event.pitch,
"streams": event.streams,
}
return events
func _get_bus(p_bank_bus: String, p_event_bus: String) -> String:
if p_event_bus != null and p_event_bus != "":
return p_event_bus
if p_bank_bus != null and p_bank_bus != "":
return p_bank_bus
return ProjectSettings.get_setting(
_settings.SOUND_BANK_BUS_SETTING_NAME,
_settings.SOUND_BANK_BUS_SETTING_DEFAULT)
func _instance_manual(p_bank_label: String, p_event_name: String, p_reserved: bool = false, p_bus: String = "", p_poly: bool = false, p_attachment = null) -> Variant:
if not has_loaded:
push_error("Resonate - The event [%s] on bank [%s] can't be instanced as the SoundManager has not loaded yet. Use the [loaded] signal/event to determine when it is ready." % [p_event_name, p_bank_label])
return _get_null_player(p_attachment)
if not _event_table.has(p_bank_label):
push_error("Resonate - Tried to instance the event [%s] from an unknown bank [%s]." % [p_event_name, p_bank_label])
return _get_null_player(p_attachment)
if not _event_table[p_bank_label]["events"].has(p_event_name):
push_error("Resonate - Tried to instance an unknown event [%s] from the bank [%s]." % [p_event_name, p_bank_label])
return _get_null_player(p_attachment)
var bank = _event_table[p_bank_label] as Dictionary
var event = bank["events"][p_event_name] as Dictionary
if event.streams.size() == 0:
push_error("Resonate - The event [%s] on bank [%s] has no streams, you'll need to add one at minimum." % [p_event_name, p_bank_label])
return _get_null_player(p_attachment)
var player = _get_player(p_attachment)
if player == null:
push_warning("Resonate - The event [%s] on bank [%s] can't be instanced; no pooled players available." % [p_event_name, p_bank_label])
return _get_null_player(p_attachment)
var bus = p_bus if p_bus != "" else _get_bus(bank.bus, event.bus)
player.configure(event.streams, p_reserved, bus, p_poly, event.volume, event.pitch, bank.mode)
player.attach_to(p_attachment)
return player
func _is_player_free(p_player) -> bool:
return not p_player.playing and not p_player.reserved
func _get_player_from_pool(p_pool: Array) -> Variant:
if p_pool.size() == 0:
push_error("Resonate - Player pool has not been initialised. This can occur when calling a [play/instance*] function from [_ready].")
return null
for player in p_pool:
if _is_player_free(player):
return player
push_warning("Resonate - Player pool exhausted, consider increasing the pool size in the project settings (Audio/Manager/Pooling) or releasing unused audio stream players.")
return null
func _get_player_1d() -> PooledAudioStreamPlayer:
return _get_player_from_pool(_1d_players)
func _get_player_2d() -> PooledAudioStreamPlayer2D:
return _get_player_from_pool(_2d_players)
func _get_player_3d() -> PooledAudioStreamPlayer3D:
return _get_player_from_pool(_3d_players)
func _get_player(p_attachment = null) -> Variant:
if ResonateUtils.is_2d_node(p_attachment):
return _get_player_2d()
if ResonateUtils.is_3d_node(p_attachment):
return _get_player_3d()
return _get_player_1d()
func _get_null_player(p_attachment = null) -> Variant:
if ResonateUtils.is_2d_node(p_attachment):
return null_instance_2d()
if ResonateUtils.is_3d_node(p_attachment):
return null_instance_3d()
return null_instance()
func _add_player_to_pool(p_player, p_pool) -> Variant:
add_child(p_player)
p_player.released.connect(_on_player_released.bind(p_player))
p_player.finished.connect(_on_player_finished.bind(p_player))
p_pool.append(p_player)
return p_player
func _create_player_1d() -> PooledAudioStreamPlayer:
return _add_player_to_pool(PooledAudioStreamPlayer.create(), _1d_players)
func _create_player_2d() -> PooledAudioStreamPlayer2D:
return _add_player_to_pool(PooledAudioStreamPlayer2D.create(), _2d_players)
func _create_player_3d() -> PooledAudioStreamPlayer3D:
return _add_player_to_pool(PooledAudioStreamPlayer3D.create(), _3d_players)
func _on_player_released(p_player: Node) -> void:
if p_player.playing:
return
pools_updated.emit()
updated.emit()
func _on_player_finished(p_player: Node) -> void:
if p_player.reserved:
return
pools_updated.emit()
updated.emit()