Initial commit

This commit is contained in:
Bad Manners 2024-03-14 00:53:46 -03:00
commit 7becdd23b6
989 changed files with 28526 additions and 0 deletions

7
addons/resonate/LICENSE Normal file
View file

@ -0,0 +1,7 @@
Copyright 2024 I.A JONES & T. STRATHEARN
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -0,0 +1,78 @@
# Getting started
Godot provides a simple and functional audio engine with all the raw ingredients. However, it leaves it up to you to decide how to organise, manage and orchestrate audio in your projects.
**Resonate** is an *audio manager* designed to fill this gap, providing both convenient and flexible ways to implement audio more easily in Godot.
## Installation
Once you have downloaded Resonate into your project `addons/` directory, open **Project > Project Settings** and go to the **Plugins** tab. Click on the **Enable** checkbox to enable the plugin.
It's good practice to reload your project to ensure freshly installed plugins work correctly. Go to **Project > Reload Current Project**.
Resonate has two core systems: the **SoundManager** and the **MusicManager** which are available as global singletons (Autoload). You can confirm if these are available and enabled under **Project > Project Settings > Autoload**.
The `SoundManager` and `MusicManager` will now be available from any GDScript file. However, Resonate needs to initialise and load properly, so you should be aware of the [script execution order](#script-execution-order).
## Concepts
The following concepts explain how the various components of Resonate work together.
**Sound** (one-shots, sound effects, dialogue) is managed differently compared to **Music.** This is why Resonate comprises of two core systems: the `SoundManager` and `MusicManager`.
### Sound
A **SoundEvent** is any sound that can be triggered, controlled and stopped from code. Anything in your game that produces a sound will need an equivalent SoundEvent. Instead of playing an AudioStream directly, you trigger events in response to gameplay and Resonate takes care of the rest. Events are made up of one or more AudioStreams which are treated as variations to be played at random when the event is triggered, e.g. triggering a "footsteps" event plays one of several pre-recorded variations to add variety and realism to your sound design. You can add as many variations to an event as you need. Using an event name means you have a consistent reference used to trigger sounds, but can easily update the AudioStream files associated with each event independently.
A **SoundBank** is a collection of SoundEvents. As your project grows, SoundBanks help you organise related sound events such as "dialogue" or "impacts" sound effects into groups. You can have as many SoundBanks as you want or need to keep your project organised in a way that suits your game's architecture. Banks also act a little bit like a namespace, e.g. creating separate "player" and "npc" SoundBanks allows you to trigger a "death" dialogue sound event from either bank without name collisions.
### Music
A **MusicTrack** is a piece of music comprised of one or more **Stems**. By default, tracks will fade in/out when played or stopped, and this fade time can be configured. If you play a MusicTrack when another is already playing, the two tracks will be cross-faded so that the new track takes over.
Layers of a MusicTrack are called **Stems**, which typically represent different instruments or mix busses, e.g. "drums", "bass", "melody". This allows for stems to be enabled and disabled independently. By default, stems fade in/out and this fade time can be configured. Care should be taken to ensure Stems are set to loop (see import settings) and that they are either all of the same length or a measure division that enables them to loop in sync with each other. Stems can be used like layers in order to create a dynamic and changing composition. In the context of sound design for games this is sometimes referred to as *vertical composition*. As an example, you could enable a "drums" stem in response to an increase in gameplay tension, then disable it when the tension dissipates.
A **MusicBank** is a collection of MusicTracks. As your project grows, MusicBanks help you organise related music tracks into groups. You can have as many MusicBanks as you want or need to keep your project organised in a way that suits your game's architecture. Banks also act a little bit like a namespace, e.g. creating separate MusicBanks for distinct levels or areas in your game world allows you to name and trigger an "ambient" or "combat" music track from either bank without name collisions. This can help you standardise how you integrate with other game systems, or simply organise audio with a consistent labelling schema.
## Script execution order
Resonate needs to initialise `PooledAudioStreamPlayers` and search the entire scene/node tree for every `SoundBank` and `MusicBank`. This process requires at least one game tick to complete.
Therefore, to immediately trigger sounds or music upon your game's launch, you need to subscribe to the relevant `loaded` signal. The following concepts apply to both the `SoundManager` and `MusicManager`:
```GDScript
func _ready() -> void:
MusicManager.loaded.connect(on_music_manager_loaded)
func on_music_manager_loaded() -> void:
MusicManager.play("boss_fight")
```
You can also perform a safety check to ensure `MusicManager.has_loaded` is true before a function call.
## Scene changes & runtime node creation
Resonate will scan the scene tree for all music and sound banks when your game launches, which it uses internally to create lookup tables. It will also automatically update those tables whenever a node is inserted or removed from the scene tree. If you load a script at runtime attempting to use either the MusicManager or SoundManager, you can leverage the `updated` signal to ensure you're ready to play music or trigger sound events without issues:
```GDScript
var _instance_jump: PooledAudioStreamPlayer = SoundManager.null_instance()
func _ready():
SoundManager.updated.connect(on_sound_manager_updated)
func _input(p_event: InputEvent) -> void:
if p_event.is_action_pressed("jump"):
_instance_jump.trigger()
func on_sound_manager_updated() -> void:
if SoundManager.should_skip_instancing(_instance_jump):
return
_instance_jump = SoundManager.instance("player", "jump")
SoundManager.release_on_exit(self, _instance_jump)
```
## Digging deeper
To understand the music or sound managers in more detail, view examples of setting up banks, or inspect their corresponding APIs, check out the dedicated [MusicManager](music-manager.md) or [SoundManager](sound-manager.md) documentation.

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

View file

@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://decojxomfjk5j"
path="res://.godot/imported/add-music-bank-node.jpg-420caf2d73871ff53dd1cbab494345dc.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/resonate/docs/images/add-music-bank-node.jpg"
dest_files=["res://.godot/imported/add-music-bank-node.jpg-420caf2d73871ff53dd1cbab494345dc.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 676 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

View file

@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dyf0vqo1r1frq"
path="res://.godot/imported/add-sound-bank-node.jpg-a91f65b0a3eac2a6a1735380945abe73.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/resonate/docs/images/add-sound-bank-node.jpg"
dest_files=["res://.godot/imported/add-sound-bank-node.jpg-a91f65b0a3eac2a6a1735380945abe73.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

View file

@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://coy730qpivrbt"
path="res://.godot/imported/music-banks.png-4f6ff10409b2687d87395d8da36bde62.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/resonate/docs/images/music-banks.png"
dest_files=["res://.godot/imported/music-banks.png-4f6ff10409b2687d87395d8da36bde62.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

View file

@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://c33xlcqiwygve"
path="res://.godot/imported/music-manager.png-94b69d2e104f7db933ca8f29270624b8.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/resonate/docs/images/music-manager.png"
dest_files=["res://.godot/imported/music-manager.png-94b69d2e104f7db933ca8f29270624b8.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View file

@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cn3ojlclw1u8i"
path="res://.godot/imported/set-soundbank-label.jpg-f96dc62be961f04105aa7359942e993a.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/resonate/docs/images/set-soundbank-label.jpg"
dest_files=["res://.godot/imported/set-soundbank-label.jpg-f96dc62be961f04105aa7359942e993a.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 8 KiB

View file

@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://b1q8c8jsyx4xs"
path="res://.godot/imported/sound-banks.png-7e9fa46409d6d7a2c4374ffb9307d714.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/resonate/docs/images/sound-banks.png"
dest_files=["res://.godot/imported/sound-banks.png-7e9fa46409d6d7a2c4374ffb9307d714.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 7 KiB

View file

@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://mg8fvg6efg77"
path="res://.godot/imported/sound-manager.png-03f9b96b4f719aeb1dde3ec2d85c4f0f.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/resonate/docs/images/sound-manager.png"
dest_files=["res://.godot/imported/sound-manager.png-03f9b96b4f719aeb1dde3ec2d85c4f0f.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

View file

@ -0,0 +1,58 @@
# MusicManager
## Introduction
The **MusicManager** is responsible for playing music tracks. It does so through a **StemmedMusicStreamPlayer** (SMSP), which extends Godot's **AudioStreamPlayer**. The core feature of SMSPs, as the name suggests, is the management and playback of ***stems***.
![MusicManager](images/music-manager.png)
Stems are music tracks split horizontally. For example, a music track may be split into pad, melody, and drum stems. Each of these stems can be played in isolation or in-sync with any or all of the other stems. This feature (while not required) allows you to craft more dynamic in-game music. For example, while the player is far enough away from a boss, only the pad stem plays. Then when the player gets closer, the melody stem gets added in. Finally, when the player is within the boss's detection distance, the drum stem gets added in. This helps you form a music track that grows in intensity without having to swap music tracks in and out entirely. It's a more organic and seamless transition.
### MusicBanks
The way you configure music is through the use of **MusicBanks**. Each MusicBank contains one or more music tracks, each of which contains one or more stems. Each stem contains one audio stream.
![MusicBanks](images/music-banks.png)
**MusicBanks** are automatically discovered by the **MusicManager** when your game starts, and can be located anywhere in your active scene(s).
### Fading & crossfading
Whenever you start or stop either an entire music track or a single stem, you can provide a (cross)fade time. If you want either of those to start immediately, just provide a (cross)fade time of zero seconds.
## Usage
### Creating MusicBanks
#### Step 1
Add a new MusicBank node to your scene.
![MusicBankNode](images/add-music-bank-node.jpg)
#### Step 2
Give your new MusicBank a label and then create a new MusicTrackResource. A MusicTrackResource represents one track in your music bank. Each track requires a name, which you will use to start it from your script(s).
![AddMusicResource](images/add-music-resource.gif)
#### Step 3
Create as many MusicStemResources as you track requires. Each stem requires a name, which you will use to enable or disable it from your script(s). If you mark a stem as enabled at the resource level, it will automatically be started for you when you play the music track. If often makes sense to have you core stem enabled by default.
![AddMusicStemResource](images/add-music-stem-resources.gif)
### Playing music
To start a new music track, just call the `play` method on the MusicManager with the name of the bank and track you want to play.
```GDScript
MusicManager.play("combat", "boss_fight")
```
To enable or disable stems on the currently playing track, just call `enabled_stem` or `disable_stem`.
```GDScript
MusicManager.enable_stem("melody")
MusicManager.disable_stem("drums")
```

View file

@ -0,0 +1,107 @@
# SoundManager
## Introduction
The **SoundManager** is responsible for triggering sounds. It does so through **PooledAudioStreamPlayers** (PASPs), which extend Godot's native **AudioStreamPlayers** (ASPs). PASPs, like ASPs, support audio playback in 1D, 2D, and 3D space, making them useful for any game sound effect.
![SoundManager](images/sound-manager.png)
Sound events can be configured with multiple variations (audio streams) which are chosen at random when played. This can help create organic-sounding events such as footsteps, gunshots, collisions, etc.
### SoundBanks
The way you configure sound events and their variations is through the use of **SoundBanks**. Each **SoundBank** you create has a name and several associated events, among other configuration options.
![SoundManager](images/sound-banks.png)
**SoundBanks** are automatically discovered and loaded by the **SoundManager** when your game starts. This allows you to co-locate your **SoundBanks** with the entities or systems they belong to.
All registered sound events can be triggered in three ways: uniformly, at a fixed position, or attached to a node. Uniformly triggered sound events always play in 1D space and, therefore, will be heard as if coming from all directions (no stereo or 3D panning). Sound events triggered at a fixed position (Vector2/Vector3) or attached to a node (Node2D/Node3D) will automatically be positioned in 2D or 3D space. They will be heard accordingly through the use of panning.
When you trigger a sound event, the **SoundManager** will retrieve a free PASP from one of its appropriate 1D, 2D, or 3D pools. After the event, the PASP will be freed and returned to the pool, available for the next event. Using pools means you do not have to insert ASPs into your scenes manually. For performance reasons, however, the **SoundManager** will limit how many PASPs it creates in each pool. This limit can be configured under `Audio/Manager/Sound` in your project settings.
When you want to play a particular event consistently, you can request exclusive use of a PASP from the **SoundManager**. When doing so, it will not be automatically returned to its pool when an event has finished playing. It can be manually released if you wish to return it to its pool.
### Polyphony
By default, every PASP, when told to trigger an event, will play the event once. If instructed to trigger the event again while a previous variation is still playing, it will stop playback and immediately begin playing a new random variation. Polyphonic playback can be enabled in situations where this is undesirable, for instance, playing rapid gunshots. When told to trigger, polyphonic playback will start playing a random variation concurrently with all other variations already playing. The maximum number of concurrent variations a PASP can play can be configured under `Audio/Manager/Sound` in your project settings.
## Usage
### Creating SoundBanks
#### Step 1
Add a new **SoundBank** node to your scene.
![SoundBankNode](images/add-sound-bank-node.jpg)
#### Step 2
Set the label for your new **SoundBank**. **SoundBanks** are flexible in that they allow you to group your sounds however you want. The label in this case is the group name. Example labels could be "player", "UI", "gunshots", etc. The name you provide here is what you will use when calling the play or instance functions from your script(s).
![SoundBankNode](images/set-soundbank-label.jpg)
#### Step 3
Create a new **SoundEventResource**. Each **SoundEventResource** is a single **event** in a **SoundBank**. The name you provide here is what you will use when calling the play or instance functions from your script(s).
![SoundBankNode](images/add-sound-event-resource.gif)
#### Step 4
Add as many streams (variations) to the event as you need. These variations, chosen at random, are played when you trigger the event from your script(s).
![SoundBankNode](images/add-sound-event-resource-streams.gif)
You are now ready to trigger the event from your script(s).
### Triggering events
#### Simple
There are two ways to trigger events with the **SoundManager**. The first way is to automatically trigger the event and have the **SoundManager** handle everything for you.
```GDScript
SoundManager.play("player", "footsteps")
```
Using this approach (any **SoundManager** method starting with `play`) will cause the **SoundManager** to pick a free player from the pool, trigger your event on it, and immediately return it to the pool once the sound has finished playing.
#### Advanced
The second way is to manually trigger events. To manually trigger an event, you need to first `instance` a sound event. When a sound event is instanced, the **SoundManager** will return a reserved **PooledAudioStreamPlayer**. You can save a reference to this player and call the `trigger` method on it whenever you want to trigger the reserved event.
`trigger() -> void`
See the following example below:
```GDScript
var instance = SoundManager.instance_poly("player", "footsteps")
instance.trigger()
```
When using an instanced sound event, its your duty to release it back to the pool if you're done using it. This can be achieved by calling the `release` method.
```GDScript
instance.release()
```
However, it's often the case that an instanced sound event will be used indefinitely by the calling script, in which case you do not need to call `release`.
#### Automatic space detection
When calling the `play` or `instance` methods, the **SoundManager** will use a 1D space **PooledAudioStreamPlayer**. If you require an event to be played in 2D or 3D space, you'll need to use one of the extended `play` or `instance` methods (see the API references below.)
#### Polyphonic playback
The `instance` method also offers a further extension which allows you to reserve a **PooledAudioStreamPlayer** in a polyphonic configuration (see the API references below.)
#### Varying pitch and volume
As it's quite common to want to vary the pitch and/or volume of an event, we've added an extended version of the `trigger` method called `trigger_varied`:
`trigger_varied(p_pitch: float = 1.0, p_volume: float = 0.0) -> void`
The `trigger_varied` method works for both polyphonic and non-polyphonic events.

View file

@ -0,0 +1,17 @@
class_name MusicBank
extends Node
## A container used to store & group music tracks in your scene.
## This bank's unique identifier.
@export var label: String
## The bus to use for all tracks played from this bank.[br][br]
## [b]Note:[/b] this will override the bus set in your project settings (Audio/Manager/Music/Bank)
@export var bus: String
## The underlying process mode for all tracks played from this bank.
@export var mode: Node.ProcessMode
## The collection of tracks associated with this bank.
@export var tracks: Array[MusicTrackResource]

View file

@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 16 16">
<g clip-path="url(#a)">
<path fill="url(#b)" d="M14 0H2a2 2 0 0 0-2 2v12c0 1.1.9 2 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2Zm-1.14 10.29a1.81 1.81 0 0 1-1.82 1.79c-1 0-1.82-.82-1.82-1.82a1.83 1.83 0 0 1 2.12-1.79V4.59L6.73 5.92v6.38a2.2 2.2 0 1 1-1.85-2.17V4.15c0-.37.24-.69.59-.79l6.34-1.83a.82.82 0 0 1 1.05.79v7.97Z"/>
</g>
<defs>
<linearGradient id="b" x1="2.9" x2="13.1" y1="16.83" y2="-.83" gradientUnits="userSpaceOnUse">
<stop stop-color="#9F9"/>
<stop offset=".5" stop-color="#E6D767"/>
<stop offset="1" stop-color="#EA6C5E"/>
</linearGradient>
<clipPath id="a">
<path fill="#fff" d="M0 0h16v16H0z"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 758 B

View file

@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bxfmecl088q55"
path="res://.godot/imported/music_bank.svg-4038ad3a11ca952c7a39bede8a3b962e.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/resonate/music_manager/music_bank.svg"
dest_files=["res://.godot/imported/music_bank.svg-4038ad3a11ca952c7a39bede8a3b962e.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View file

@ -0,0 +1,368 @@
extends Node
## The MusicManager is responsible for all music in your game.
##
## It manages the playback of music tracks, which are constructed with one or more
## stems. Each stem can be independently enabled or disabled, allowing for more dynamic
## playback. Music tracks can also be crossfaded when switching from one to another.
##
## @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 MusicManager has finished setting up and
## is ready to play music tracks and enable and disable stems.
signal loaded
## Emitted every time the MusicManager detects that a MusicBank has
## been added or removed from the scene tree.
signal banks_updated
## Emitted whenever [signal MusicManager.loaded] or
## [signal MusicManager.pools_updated] is emitted.
signal updated
## Whether the MusicManager has completed setup and is ready to
## play music tracks and enable and disable stems.
var has_loaded: bool = false
var _music_table: Dictionary = {}
var _music_table_hash: int
var _music_streams: Array[StemmedMusicStreamPlayer] = []
var _volume: float
# ------------------------------------------------------------------------------
# Lifecycle methods
# ------------------------------------------------------------------------------
func _init():
process_mode = Node.PROCESS_MODE_ALWAYS
func _ready() -> void:
_auto_add_music()
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 _music_table_hash != _music_table.hash():
_music_table_hash = _music_table.hash()
banks_updated.emit()
updated.emit()
if has_loaded:
return
has_loaded = true
loaded.emit()
updated.emit()
# ------------------------------------------------------------------------------
# Public methods
# ------------------------------------------------------------------------------
## Play a music track from a SoundBank, and optionally fade-in
## or crossfade over the provided [b]p_crossfade_time[/b].
func play(p_bank_label: String, p_track_name: String, p_crossfade_time: float = 5.0) -> bool:
if not has_loaded:
push_error("Resonate - The music track [%s] on bank [%s] can't be played as the MusicManager has not loaded yet. Use the [loaded] signal/event to determine when it is ready." % [p_track_name, p_bank_label])
return false
if not _music_table.has(p_bank_label):
push_error("Resonate - Tried to play the music track [%s] from an unknown bank [%s]." % [p_track_name, p_bank_label])
return false
if not _music_table[p_bank_label]["tracks"].has(p_track_name):
push_error("Resonate - Tried to play an unknown music track [%s] from the bank [%s]." % [p_track_name, p_bank_label])
return false
var bank = _music_table[p_bank_label] as Dictionary
var track = bank["tracks"][p_track_name] as Dictionary
var stems = track["stems"] as Array
if stems.size() == 0:
push_error("Resonate - The music track [%s] on bank [%s] has no stems, you'll need to add one at minimum." % [p_track_name, p_bank_label])
return false
for stem in stems:
if stem.stream == null:
push_error("Resonate - The stem [%s] on the music track [%s] on bank [%s] does not have an audio stream, you'll need to add one." % [stem.name, p_track_name, p_bank_label])
return false
if not ResonateUtils.is_stream_looped(stem.stream):
push_warning("Resonate - The stem [%s] on the music track [%s] on bank [%s] is not set to loop, which will cause it to work incorrectly." % [stem.name, p_track_name, p_bank_label])
var bus = _get_bus(bank.bus, track.bus)
var player = StemmedMusicStreamPlayer.create(p_bank_label, p_track_name, bus, bank.mode, _volume)
if _music_streams.size() > 0:
for stream in _music_streams:
stream.stop_stems(p_crossfade_time)
_music_streams.append(player)
add_child(player)
player.start_stems(stems, p_crossfade_time)
player.stopped.connect(_on_player_stopped.bind(player))
return true
## Check whether the MusicManager is playing from a specific bank, or any track
## with the given name, or more specifically a certain track from a certain bank.
func is_playing(p_bank_label: String = "", p_track_name: String = "") -> bool:
if not has_loaded:
return false
if _music_streams.size() == 0:
return false
var current_player = _get_current_player()
var is_playing = not current_player.is_stopping
var bank_label = current_player.bank_label
var track_name = current_player.track_name
if p_bank_label == "" and p_track_name == "":
return is_playing
if p_bank_label != "" and p_track_name == "":
return bank_label == p_bank_label and is_playing
if p_bank_label == "" and p_track_name != "":
return track_name == p_track_name and is_playing
return bank_label == p_bank_label and track_name == p_track_name and is_playing
## Stop the playback of all music.
func stop(p_fade_time: float = 5.0) -> void:
if not _is_playing_music():
push_warning("Resonate - Cannot stop the music track as there is no music currently playing.")
return
var current_player = _get_current_player()
current_player.stop_stems(p_fade_time)
## Check whether the MusicManager should skip playing a new track. It will return true if the
## MusicManager has not loaded yet, or if the flag you provide is not [b]false[/b] or [b]null[/b].
func should_skip_playing(p_flag) -> bool:
return not has_loaded or (p_flag != false and p_flag != null)
## Set the volume of the current music track (if playing) and all future tracks to be played.
func set_volume(p_volume: float) -> void:
_volume = p_volume
if not _is_playing_music():
return
var current_player = _get_current_player()
current_player.set_volume(_volume)
## Enable the specified stem on the currently playing music track.
func enable_stem(p_name: String, p_fade_time: float = 2.0) -> void:
_set_stem(p_name, true, p_fade_time)
## Disable the specified stem on the currently playing music track.
func disable_stem(p_name: String, p_fade_time: float = 2.0) -> void:
_set_stem(p_name, false, p_fade_time)
## Set the volume for the specified stem on the currently playing music track.
func set_stem_volume(p_name: String, p_volume: float) -> void:
if not _is_playing_music():
push_warning("Resonate - Cannot set the volume of stem [%s] as there is no music currently playing." % p_name)
return
var current_player = _get_current_player()
current_player.set_stem_volume(p_name, p_volume)
## Get the underlying details of the provided stem for the currently playing music track.
func get_stem_details(p_name: String) -> Variant:
if not _is_playing_music():
push_warning("Resonate - Cannot get the details for stem [%s] as there is no music currently playing." % p_name)
return
var current_player = _get_current_player()
return current_player.get_stem_details(p_name)
## Will automatically stop the provided music track when the provided
## [b]p_base[/b] is removed from the scene tree.
func stop_on_exit(p_base: Node, p_bank_label: String, p_track_name: String, p_fade_time: float = 5.0) -> void:
p_base.tree_exiting.connect(_on_music_player_exiting.bind(p_bank_label, p_track_name, p_fade_time))
## Will automatically stop the provided music track 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 MusicManager.stop_on_exit] instead.
## @deprecated
func auto_stop(p_base: Node, p_bank_label: String, p_track_name: String, p_fade_time: float = 5.0) -> void:
push_warning("Resonate - auto_stop has been deprecated, please use stop_on_exit instead.")
stop_on_exit(p_base, p_bank_label, p_track_name, p_fade_time)
## Manually add a new SoundBank into the music track cache.
func add_bank(p_bank: MusicBank) -> void:
_add_bank(p_bank)
## Remove the provided bank from the music track cache.
func remove_bank(p_bank_label: String) -> void:
if not _music_table.has(p_bank_label):
return
_music_table.erase(p_bank_label)
## Clear all banks from the music track cache.
func clear_banks() -> void:
_music_table.clear()
# ------------------------------------------------------------------------------
# Private methods
# ------------------------------------------------------------------------------
func _on_scene_node_added(p_node: Node) -> void:
if not p_node is MusicBank:
return
_add_bank(p_node)
func _on_scene_node_removed(p_node: Node) -> void:
if not p_node is MusicBank:
return
_remove_bank(p_node)
func _auto_add_music() -> void:
var music_banks = ResonateUtils.find_all_nodes(self, "MusicBank")
for music_bank in music_banks:
_add_bank(music_bank)
_music_table_hash = _music_table.hash()
func _add_bank(p_bank: MusicBank) -> void:
if _music_table.has(p_bank.label):
_music_table[p_bank.label]["ref_count"] = \
_music_table[p_bank.label]["ref_count"] + 1
return
_music_table[p_bank.label] = {
"name": p_bank.label,
"bus": p_bank.bus,
"mode": p_bank.mode,
"tracks": _create_tracks(p_bank.tracks),
"ref_count": 1,
}
func _remove_bank(p_bank: MusicBank) -> void:
if not _music_table.has(p_bank.label):
return
if _music_table[p_bank.label]["ref_count"] == 1:
_music_table.erase(p_bank.label)
return
_music_table[p_bank.label]["ref_count"] = \
_music_table[p_bank.label]["ref_count"] - 1
func _create_tracks(p_tracks: Array[MusicTrackResource]) -> Dictionary:
var tracks = {}
for track in p_tracks:
tracks[track.name] = {
"name": track.name,
"bus": track.bus,
"stems": _create_stems(track.stems),
}
return tracks
func _create_stems(p_stems: Array[MusicStemResource]) -> Array:
var stems = []
for stem in p_stems:
stems.append({
"name": stem.name,
"enabled": stem.enabled,
"volume": stem.volume,
"stream": stem.stream,
})
return stems
func _get_bus(p_bank_bus: String, p_track_bus: String) -> String:
if p_track_bus != null and p_track_bus != "":
return p_track_bus
if p_bank_bus != null and p_bank_bus != "":
return p_bank_bus
return ProjectSettings.get_setting(
_settings.MUSIC_BANK_BUS_SETTING_NAME,
_settings.MUSIC_BANK_BUS_SETTING_DEFAULT)
func _is_playing_music() -> bool:
return _music_streams.size() > 0
func _get_current_player() -> StemmedMusicStreamPlayer:
if _music_streams.size() == 0:
return null
return _music_streams.back() as StemmedMusicStreamPlayer
func _set_stem(p_name: String, p_enabled: bool, p_fade_time: float) -> void:
if not _is_playing_music():
push_warning("Resonate - Cannot toggle the stem [%s] as there is no music currently playing." % p_name)
return
var current_player = _get_current_player()
current_player.toggle_stem(p_name, p_enabled, p_fade_time)
func _on_music_player_exiting(p_bank_label: String, p_track_name: String, p_fade_time: float) -> void:
if not is_playing(p_bank_label, p_track_name):
return
stop(p_fade_time)
func _on_player_stopped(p_player: StemmedMusicStreamPlayer) -> void:
if not _is_playing_music():
return
_music_streams.erase(p_player)
remove_child(p_player)

View file

@ -0,0 +1,16 @@
class_name MusicStemResource
extends Resource
## A container used to store the details of one particular music track's stem.
## This stem's unique identifier within the scope of the track it belongs to.
@export var name: String = ""
## Whether this stem will start playing automatically when the track starts.
@export var enabled: bool = false
## The volume of the stem.
@export_range(-80.0, 6.0, 0.1, "suffix:dB") var volume: float = 0.0
## The audio stream associated with this stem.
@export var stream: AudioStream

View file

@ -0,0 +1,14 @@
class_name MusicTrackResource
extends Resource
## A container used to store the details of a music track and all of its corresponding stems.
## This track's unique identifier within the scope of the bank it belongs to.
@export var name: String = ""
## The bus to use for this particular track.[br][br]
## [b]Note:[/b] this will override the bus set on the bank, or in your project settings (Audio/Manager/Music/Bank)
@export var bus: String = ""
## The collection of stems that make up this track.
@export var stems: Array[MusicStemResource]

View file

@ -0,0 +1,189 @@
class_name StemmedMusicStreamPlayer
extends AudioStreamPlayer
## An extended AudioStreamPlayer capable of managing and playing a
## collection of stems that make up a music track.
## Emitted when this player has completely stopped playing a track.
signal stopped
## True when this player is in the process of shutting down and stopping a track.
var is_stopping: bool
## The label of the bank that this player's track came from.
var bank_label: String
## The name of the track associated with this player.
var track_name: String
const _DISABLED_VOLUME: float = -80
const _START_TRANS: Tween.TransitionType = Tween.TRANS_QUART
const _START_EASE: Tween.EaseType = Tween.EASE_OUT
const _STOP_TRANS: Tween.TransitionType = Tween.TRANS_QUART
const _STOP_EASE: Tween.EaseType = Tween.EASE_IN
var _fade_tween: Tween
var _stems: Dictionary
var _max_volume: float
# ------------------------------------------------------------------------------
# Public methods
# ------------------------------------------------------------------------------
## Create a new player associated with a given bank and track.
static func create(p_bank_label: String, p_track_name: String, p_bus: String, p_mode: Node.ProcessMode, p_max_volume: float) -> StemmedMusicStreamPlayer:
var player = StemmedMusicStreamPlayer.new()
var stream = AudioStreamPolyphonic.new()
player.bank_label = p_bank_label
player.track_name = p_track_name
player.stream = stream
player.process_mode = p_mode
player.bus = p_bus
player.volume_db = _DISABLED_VOLUME
player._max_volume = p_max_volume
player.is_stopping = false
return player
## Start the collection of stems associated with the track on this player.
## This is what fundamentally starts the music track.[br][br]
## [b]Note:[/b] this should only be called once.
func start_stems(p_stems: Array, p_crossfade_time: float) -> void:
if playing:
return
stream.polyphony = p_stems.size()
max_polyphony = p_stems.size()
play()
var playback = get_stream_playback() as AudioStreamPlaybackPolyphonic
for stem in p_stems:
var stream_id = playback.play_stream(stem.stream)
var max_volume = stem.volume
var volume = max_volume if stem.enabled else _DISABLED_VOLUME
playback.set_stream_volume(stream_id, volume)
_stems[stem.name] = {
"name": stem.name,
"enabled": stem.enabled,
"stream_id": stream_id,
"volume": volume,
"max_volume": max_volume,
"tween": null,
}
_fade_tween = create_tween()
_fade_tween \
.tween_property(self, "volume_db", _max_volume, p_crossfade_time) \
.set_trans(_START_TRANS) \
.set_ease(_START_EASE)
## Toggle (enable or disable) the specified stem associated with the track on this player.
func toggle_stem(p_name: String, p_enabled: bool, p_fade_time: float) -> void:
if not _stems.has(p_name):
push_warning("Resonate - Cannot toggle the stem [%s] on music track [%s] from bank [%s] as it does not exist." % [p_name, track_name, bank_label])
return
var playback = get_stream_playback() as AudioStreamPlaybackPolyphonic
var stem = _stems[p_name]
var old_tween = stem.tween as Tween
var new_tween = create_tween()
var target_volume = stem.max_volume if p_enabled else _DISABLED_VOLUME
if old_tween != null:
old_tween.kill()
_stems[p_name]["tween"] = new_tween
_stems[p_name]["enabled"] = p_enabled
var transition = _START_TRANS if p_enabled else _STOP_TRANS
var easing = _START_EASE if p_enabled else _STOP_EASE
new_tween \
.tween_method(_tween_stem_volume.bind(p_name), stem.volume, target_volume, p_fade_time) \
.set_trans(transition) \
.set_ease(easing)
## Set the volume of this player.[br][br]
## [b]Note:[/b] if called when the player is still fading in or out, it will
## immediately cancel the fade and set the volume at specified level.
func set_volume(p_volume: float) -> void:
if _fade_tween != null and _fade_tween.is_running():
_fade_tween.kill()
_max_volume = p_volume
volume_db = p_volume
## Set the volume of a specific stem associated with this track.[br][br]
## [b]Note:[/b] if called when the stem is still fading in or out, it will
## immediately cancel the fade and set the volume at specified level.
func set_stem_volume(p_name: String, p_volume: float) -> void:
var playback = get_stream_playback() as AudioStreamPlaybackPolyphonic
var stem = _stems[p_name]
if stem["tween"] != null and stem["tween"].is_running():
stem["tween"].kill()
playback.set_stream_volume(stem.stream_id, p_volume)
_stems[p_name]["volume"] = p_volume
_stems[p_name]["max_volume"] = p_volume
## This will stop all stems associated with this player, causing it to shut-down and stop.
func stop_stems(p_fade_time: float) -> void:
if is_stopping:
return
is_stopping = true
var tween = create_tween()
tween \
.tween_property(self, "volume_db", _DISABLED_VOLUME, p_fade_time) \
.set_trans(_STOP_TRANS) \
.set_ease(_STOP_EASE)
tween.finished.connect(_on_stop_stems_tween_finished)
## Get the underlying details of the provided stem for the currently playing music track.
func get_stem_details(p_name: String) -> Variant:
if not _stems.has(p_name):
push_warning("Resonate - Cannot get the details for stem [%s] on music track [%s] from bank [%s] as it does not exist." % [p_name, track_name, bank_label])
return null
var stem = _stems[p_name]
return {
"name": stem.name,
"enabled": stem.enabled,
"volume": stem.volume,
}
# ------------------------------------------------------------------------------
# Private methods
# ------------------------------------------------------------------------------
func _tween_stem_volume(p_target_volume: float, p_name: String) -> void:
var playback = get_stream_playback() as AudioStreamPlaybackPolyphonic
var stem = _stems[p_name]
playback.set_stream_volume(stem.stream_id, p_target_volume)
_stems[p_name]["volume"] = p_target_volume
func _on_stop_stems_tween_finished() -> void:
stopped.emit()

View file

@ -0,0 +1,9 @@
[plugin]
name="Resonate"
description=""
author="HugeMenace"
;x-release-please-start-version
version="2.3.4"
;x-release-please-end
script="plugin.gd"

102
addons/resonate/plugin.gd Normal file
View file

@ -0,0 +1,102 @@
@tool
extends EditorPlugin
const ResonateSettings = preload("shared/resonate_settings.gd")
var _settings = ResonateSettings.new()
func _enter_tree():
add_autoload_singleton("SoundManager", "sound_manager/sound_manager.gd")
add_autoload_singleton("MusicManager", "music_manager/music_manager.gd")
add_custom_type("SoundBank", "Node", preload("sound_manager/sound_bank.gd"), preload("sound_manager/sound_bank.svg"))
add_custom_type("MusicBank", "Node", preload("music_manager/music_bank.gd"), preload("music_manager/music_bank.svg"))
add_project_setting(
_settings.SOUND_BANK_BUS_SETTING_NAME,
_settings.SOUND_BANK_BUS_SETTING_DEFAULT,
_settings.SOUND_BANK_BUS_SETTING_ACTUAL,
TYPE_STRING)
add_project_setting(
_settings.POOL_1D_SIZE_SETTING_NAME,
_settings.POOL_1D_SIZE_SETTING_DEFAULT,
_settings.POOL_1D_SIZE_SETTING_ACTUAL,
TYPE_INT, PROPERTY_HINT_RANGE, "1,128")
add_project_setting(
_settings.POOL_2D_SIZE_SETTING_NAME,
_settings.POOL_2D_SIZE_SETTING_DEFAULT,
_settings.POOL_2D_SIZE_SETTING_ACTUAL,
TYPE_INT, PROPERTY_HINT_RANGE, "1,128")
add_project_setting(
_settings.POOL_3D_SIZE_SETTING_NAME,
_settings.POOL_3D_SIZE_SETTING_DEFAULT,
_settings.POOL_3D_SIZE_SETTING_ACTUAL,
TYPE_INT, PROPERTY_HINT_RANGE, "1,128")
add_project_setting(
_settings.MAX_POLYPHONY_SETTING_NAME,
_settings.MAX_POLYPHONY_SETTING_DEFAULT,
_settings.MAX_POLYPHONY_SETTING_ACTUAL,
TYPE_INT, PROPERTY_HINT_RANGE, "1,128")
add_project_setting(
_settings.MUSIC_BANK_BUS_SETTING_NAME,
_settings.MUSIC_BANK_BUS_SETTING_DEFAULT,
_settings.MUSIC_BANK_BUS_SETTING_ACTUAL,
TYPE_STRING)
migrate_old_bus_settings()
func _exit_tree():
remove_autoload_singleton("SoundManager")
remove_autoload_singleton("MusicManager")
remove_custom_type("SoundBank")
remove_custom_type("MusicBank")
func add_project_setting(p_name: String, p_default, p_actual, p_type: int, p_hint: int = PROPERTY_HINT_NONE, p_hint_string: String = ""):
if ProjectSettings.has_setting(p_name):
return
ProjectSettings.set_setting(p_name, p_actual)
ProjectSettings.add_property_info({
"name": p_name,
"type": p_type,
"hint": p_hint,
"hint_string": p_hint_string,
})
ProjectSettings.set_initial_value(p_name, p_default)
var error: int = ProjectSettings.save()
if error:
push_error("Resonate - Encountered error %d when saving project settings." % error)
func migrate_old_bus_settings():
# This migration helps to ensure that users upgrading from an old version of Resonate
# to a version that uses the "*_BANK_BUS_SETTING*" ids won't loose their previous
# audio bus settings. After migration occurs, the old settings are deleted.
if ProjectSettings.has_setting("audio/manager/sound/bank"):
var value = ProjectSettings.get_setting(
"audio/manager/sound/bank",
_settings.SOUND_BANK_BUS_SETTING_ACTUAL)
ProjectSettings.set_setting(_settings.SOUND_BANK_BUS_SETTING_NAME, value)
ProjectSettings.clear("audio/manager/sound/bank")
if ProjectSettings.has_setting("audio/manager/music/bank"):
var value = ProjectSettings.get_setting(
"audio/manager/music/bank",
_settings.MUSIC_BANK_BUS_SETTING_ACTUAL)
ProjectSettings.set_setting(_settings.MUSIC_BANK_BUS_SETTING_NAME, value)
ProjectSettings.clear("audio/manager/music/bank")

View file

@ -0,0 +1,26 @@
extends RefCounted
const SOUND_BANK_BUS_SETTING_NAME = "audio/manager/sound/bus"
const SOUND_BANK_BUS_SETTING_DEFAULT = ""
const SOUND_BANK_BUS_SETTING_ACTUAL = "Sound"
const POOL_1D_SIZE_SETTING_NAME = "audio/manager/sound/pool_1D_size"
const POOL_1D_SIZE_SETTING_DEFAULT = 1
const POOL_1D_SIZE_SETTING_ACTUAL = 16
const POOL_2D_SIZE_SETTING_NAME = "audio/manager/sound/pool_2D_size"
const POOL_2D_SIZE_SETTING_DEFAULT = 1
const POOL_2D_SIZE_SETTING_ACTUAL = 16
const POOL_3D_SIZE_SETTING_NAME = "audio/manager/sound/pool_3D_size"
const POOL_3D_SIZE_SETTING_DEFAULT = 1
const POOL_3D_SIZE_SETTING_ACTUAL = 16
const MAX_POLYPHONY_SETTING_NAME = "audio/manager/sound/max_polyphony"
const MAX_POLYPHONY_SETTING_DEFAULT = 8
const MAX_POLYPHONY_SETTING_ACTUAL = 32
const MUSIC_BANK_BUS_SETTING_NAME = "audio/manager/music/bus"
const MUSIC_BANK_BUS_SETTING_DEFAULT = ""
const MUSIC_BANK_BUS_SETTING_ACTUAL = "Music"

View file

@ -0,0 +1,41 @@
class_name ResonateUtils
extends RefCounted
static func is_stream_looped(p_stream) -> bool:
if p_stream is AudioStreamMP3:
return p_stream.loop
if p_stream is AudioStreamOggVorbis:
return p_stream.loop
if p_stream is AudioStreamWAV:
return p_stream.loop_mode != AudioStreamWAV.LOOP_DISABLED
return false
static func find_all_nodes(p_base: Node, p_type: String) -> Array:
var root_nodes = p_base.get_tree().root.get_children()
var results = []
for node in root_nodes:
results.append_array(node.find_children("*", p_type))
return results
static func is_vector(p_node: Variant) -> bool:
return p_node is Vector2 or p_node is Vector3
static func is_node(p_node: Variant) -> bool:
return p_node is Node2D or p_node is Node3D
static func is_2d_node(p_node: Variant) -> bool:
return p_node is Vector2 or p_node is Node2D
static func is_3d_node(p_node: Variant) -> bool:
return p_node is Vector3 or p_node is Node3D

View file

@ -0,0 +1,38 @@
class_name NullPooledAudioStreamPlayer
extends PooledAudioStreamPlayer
## An extension of PooledAudioStreamPlayer that nerfs all of its public methods.
## Whether this player is a [PooledAudioStreamPlayer], or a Null instance.
func is_null() -> bool:
return true
## A nerfed (does nothing) version of [method PooledAudioStreamPlayer.trigger]
func trigger() -> void:
return
## A nerfed (does nothing) version of [method PooledAudioStreamPlayer.trigger_varied]
func trigger_varied(p_pitch: float = 1.0, p_volume: float = 0.0) -> void:
return
## A nerfed (does nothing) version of [method PooledAudioStreamPlayer.reset_volume]
func reset_volume() -> void:
return
## A nerfed (does nothing) version of [method PooledAudioStreamPlayer.reset_pitch]
func reset_pitch() -> void:
return
## A nerfed (does nothing) version of [method PooledAudioStreamPlayer.reset_all]
func reset_all() -> void:
return
## A nerfed (does nothing) version of [method PooledAudioStreamPlayer.release]
func release(p_finish_playing: bool = false) -> void:
return

View file

@ -0,0 +1,38 @@
class_name NullPooledAudioStreamPlayer2D
extends PooledAudioStreamPlayer2D
## An extension of PooledAudioStreamPlayer2D that nerfs all of its public methods.
## Whether this player is a [PooledAudioStreamPlayer2D], or a Null instance.
func is_null() -> bool:
return true
## A nerfed (does nothing) version of [method PooledAudioStreamPlayer2D.trigger]
func trigger() -> void:
return
## A nerfed (does nothing) version of [method PooledAudioStreamPlayer2D.trigger_varied]
func trigger_varied(p_pitch: float = 1.0, p_volume: float = 0.0) -> void:
return
## A nerfed (does nothing) version of [method PooledAudioStreamPlayer2D.reset_volume]
func reset_volume() -> void:
return
## A nerfed (does nothing) version of [method PooledAudioStreamPlayer2D.reset_pitch]
func reset_pitch() -> void:
return
## A nerfed (does nothing) version of [method PooledAudioStreamPlayer2D.reset_all]
func reset_all() -> void:
return
## A nerfed (does nothing) version of [method PooledAudioStreamPlayer2D.release]
func release(p_finish_playing: bool = false) -> void:
return

View file

@ -0,0 +1,38 @@
class_name NullPooledAudioStreamPlayer3D
extends PooledAudioStreamPlayer3D
## An extension of PooledAudioStreamPlayer3D that nerfs all of its public methods.
## Whether this player is a [PooledAudioStreamPlayer3D], or a Null instance.
func is_null() -> bool:
return true
## A nerfed (does nothing) version of [method PooledAudioStreamPlayer3D.trigger]
func trigger() -> void:
return
## A nerfed (does nothing) version of [method PooledAudioStreamPlayer3D.trigger_varied]
func trigger_varied(p_pitch: float = 1.0, p_volume: float = 0.0) -> void:
return
## A nerfed (does nothing) version of [method PooledAudioStreamPlayer3D.reset_volume]
func reset_volume() -> void:
return
## A nerfed (does nothing) version of [method PooledAudioStreamPlayer3D.reset_pitch]
func reset_pitch() -> void:
return
## A nerfed (does nothing) version of [method PooledAudioStreamPlayer3D.reset_all]
func reset_all() -> void:
return
## A nerfed (does nothing) version of [method PooledAudioStreamPlayer3D.release]
func release(p_finish_playing: bool = false) -> void:
return

View file

@ -0,0 +1,158 @@
class_name PoolEntity
extends RefCounted
## An abstract/static class to house all of the common PooledAudioStreamPlayer* functionality.
const ResonateSettings = preload("../shared/resonate_settings.gd")
enum FollowType {DISABLED, IDLE, PHYSICS}
## Create a new PooledAudioStreamPlayer*.
static func create(p_base) -> Variant:
p_base.process_mode = Node.PROCESS_MODE_ALWAYS
return p_base
## Configure a PooledAudioStreamPlayer*.
static func configure(p_base, p_streams: Array, p_reserved: bool, p_bus: String, p_poly: bool, p_volume: float, p_pitch: float, p_mode: Node.ProcessMode) -> bool:
p_base.streams = p_streams
p_base.poly = p_poly
p_base.bus = p_bus
p_base.process_mode = p_mode
p_base.reserved = p_reserved
p_base.releasing = false
p_base.volume_db = p_volume if not p_poly else 0.0
p_base.pitch_scale = p_pitch if not p_poly else 1.0
p_base.base_volume = p_volume
p_base.base_pitch = p_pitch
p_base.follow_target = null
p_base.follow_type = FollowType.DISABLED
if not p_base.poly:
return false
var _settings = ResonateSettings.new()
var max_polyphony = ProjectSettings.get_setting(
_settings.MAX_POLYPHONY_SETTING_NAME,
_settings.MAX_POLYPHONY_SETTING_DEFAULT)
p_base.stream = AudioStreamPolyphonic.new()
p_base.max_polyphony = max_polyphony
p_base.stream.polyphony = max_polyphony
return true
## Attach a PooledAudioStreamPlayer* to a position or node.
static func attach_to(p_base, p_node: Variant) -> void:
if p_node == null:
return
if ResonateUtils.is_vector(p_node):
p_base.global_position = p_node
if ResonateUtils.is_node(p_node):
p_base.follow_target = p_node
p_base.follow_type = FollowType.IDLE
## Sync a PooledAudioStreamPlayer*'s transform with its target's when applicable.
static func sync_process(p_base) -> void:
if p_base.follow_target == null:
return
if not is_instance_valid(p_base.follow_target):
return
if p_base.follow_type != FollowType.IDLE:
return
p_base.global_position = p_base.follow_target.global_position
## Sync a PooledAudioStreamPlayer*'s transform with its target's
## when applicable during the physics step.
static func sync_physics_process(p_base) -> void:
if p_base.follow_target == null:
return
if not is_instance_valid(p_base.follow_target):
return
if p_base.follow_type != FollowType.PHYSICS:
return
p_base.global_position = p_base.follow_target.global_position
## Trigger a PooledAudioStreamPlayer*.
static func trigger(p_base, p_varied: bool, p_pitch: float, p_volume: float) -> bool:
if p_base.streams.size() == 0:
push_warning("Resonate - The player [%s] does not contain any streams, ensure you're using the SoundManager to instance it correctly." % p_base.name)
return false
var next_stream = p_base.streams.pick_random()
if not p_base.poly and p_varied:
p_base.volume_db = p_volume
p_base.pitch_scale = p_pitch
if not p_base.poly:
p_base.stream = next_stream
return true
var playback = p_base.get_stream_playback() as AudioStreamPlaybackPolyphonic
if p_varied:
playback.play_stream(next_stream, 0, p_volume, p_pitch)
else:
playback.play_stream(next_stream, 0, p_base.base_volume, p_base.base_pitch)
return false
## Reset the volume of a PooledAudioStreamPlayer*.
static func reset_volume(p_base) -> void:
p_base.volume_db = p_base.base_volume
## Reset the pitch of a PooledAudioStreamPlayer*.
static func reset_pitch(p_base) -> void:
p_base.pitch_scale = p_base.base_pitch
## Reset both the volume and pitch of a PooledAudioStreamPlayer*.
static func reset_all(p_base) -> void:
p_base.volume_db = p_base.base_volume
p_base.pitch_scale = p_base.base_pitch
## Release a PooledAudioStreamPlayer* back into the pool.
static func release(p_base, p_finish_playing: bool) -> void:
if p_base.releasing:
return
var has_loops = p_base.streams.any(ResonateUtils.is_stream_looped)
if p_finish_playing and has_loops:
push_warning("Resonate - The player [%s] has looping streams and therefore will never release itself back to the pool (as playback continues indefinitely). It will be forced to stop." % p_base.name)
p_base.stop()
if not p_finish_playing:
p_base.stop()
p_base.reserved = false
p_base.process_mode = Node.PROCESS_MODE_ALWAYS
p_base.releasing = true
p_base.released.emit()
## A callback to release a PooledAudioStreamPlayer* when it finishes playing.
static func finished(p_base) -> void:
if p_base.reserved:
return
p_base.release()

View file

@ -0,0 +1,124 @@
class_name PooledAudioStreamPlayer
extends AudioStreamPlayer
## An extension of AudioStreamPlayer that manages sequential and
## polyphonic playback as part of a pool of players.
## Emitted when this player has been released and should return to the pool.
signal released
## Whether this player has been reserved.
var reserved: bool
## Whether this player is in the process of being released.
var releasing: bool
## Whether this player has been configured to support polyphonic playback.
var poly: bool
## The collection of streams configured on this player.
var streams: Array
## The base/fallback volume of this player.
var base_volume: float
## The base/fallback pitch of this player.
var base_pitch: float
## The target this player should follow in 2D or 3D space.
var follow_target: Node
## When the player should sync its transform when following a target.
var follow_type: PoolEntity.FollowType
# ------------------------------------------------------------------------------
# Lifecycle methods
# ------------------------------------------------------------------------------
func _ready() -> void:
finished.connect(_on_finished)
func _process(_p_delta) -> void:
PoolEntity.sync_process(self)
func _physics_process(_p_delta) -> void:
PoolEntity.sync_physics_process(self)
# ------------------------------------------------------------------------------
# Public methods
# ------------------------------------------------------------------------------
## Returns a new player.
static func create() -> PooledAudioStreamPlayer:
return PoolEntity.create(PooledAudioStreamPlayer.new())
## Whether this player is a [NullPooledAudioStreamPlayer], or real instance.
func is_null() -> bool:
return false
## Configure this player with the given streams and charateristics.
func configure(p_streams: Array, p_reserved: bool, p_bus: String, p_poly: bool, p_volume: float, p_pitch: float, p_mode: Node.ProcessMode) -> void:
var is_polyphonic = PoolEntity.configure(self, p_streams, p_reserved, p_bus, p_poly, p_volume, p_pitch, p_mode)
if is_polyphonic:
super.play()
## Attach this player to a 2D/3D position or node.
func attach_to(p_node: Variant) -> void:
PoolEntity.attach_to(self, p_node)
## Trigger (play) a random variation associated with this player.
func trigger() -> void:
var should_play = PoolEntity.trigger(self, false, 1.0, 0.0)
if should_play:
super.play()
## Trigger (play) a random variation associated with this
## player with the given volume and pitch settings.
func trigger_varied(p_pitch: float = 1.0, p_volume: float = 0.0) -> void:
var should_play = PoolEntity.trigger(self, true, p_pitch, p_volume)
if should_play:
super.play()
## Reset the volume of this player back to the default set in its bank.
func reset_volume() -> void:
PoolEntity.reset_volume(self)
## Reset the pitch of this player back to the default set in its bank.
func reset_pitch() -> void:
PoolEntity.reset_pitch(self)
## Reset both the volume and pitch of this player back to the default set in its bank.
func reset_all() -> void:
PoolEntity.reset_all(self)
## Release this player back to the pool, and optionally
## wait for it to finish playing before doing so.
func release(p_finish_playing: bool = false) -> void:
PoolEntity.release(self, p_finish_playing)
# ------------------------------------------------------------------------------
# Private methods
# ------------------------------------------------------------------------------
func _on_finished() -> void:
PoolEntity.finished(self)

View file

@ -0,0 +1,124 @@
class_name PooledAudioStreamPlayer2D
extends AudioStreamPlayer2D
## An extension of AudioStreamPlayer2D that manages sequential and
## polyphonic playback as part of a pool of players.
## Emitted when this player has been released and should return to the pool.
signal released
## Whether this player has been reserved.
var reserved: bool
## Whether this player is in the process of being released.
var releasing: bool
## Whether this player has been configured to support polyphonic playback.
var poly: bool
## The collection of streams configured on this player.
var streams: Array
## The base/fallback volume of this player.
var base_volume: float
## The base/fallback pitch of this player.
var base_pitch: float
## The target this player should follow in 2D or 3D space.
var follow_target: Node
## When the player should sync its transform when following a target.
var follow_type: PoolEntity.FollowType
# ------------------------------------------------------------------------------
# Lifecycle methods
# ------------------------------------------------------------------------------
func _ready() -> void:
finished.connect(_on_finished)
func _process(_p_delta) -> void:
PoolEntity.sync_process(self)
func _physics_process(_p_delta) -> void:
PoolEntity.sync_physics_process(self)
# ------------------------------------------------------------------------------
# Public methods
# ------------------------------------------------------------------------------
## Returns a new player.
static func create() -> PooledAudioStreamPlayer2D:
return PoolEntity.create(PooledAudioStreamPlayer2D.new())
## Whether this player is a [NullPooledAudioStreamPlayer2D], or real instance.
func is_null() -> bool:
return false
## Configure this player with the given streams and charateristics.
func configure(p_streams: Array, p_reserved: bool, p_bus: String, p_poly: bool, p_volume: float, p_pitch: float, p_mode: Node.ProcessMode) -> void:
var is_polyphonic = PoolEntity.configure(self, p_streams, p_reserved, p_bus, p_poly, p_volume, p_pitch, p_mode)
if is_polyphonic:
super.play()
## Attach this player to a 2D/3D position or node.
func attach_to(p_node: Variant) -> void:
PoolEntity.attach_to(self, p_node)
## Trigger (play) a random variation associated with this player.
func trigger() -> void:
var should_play = PoolEntity.trigger(self, false, 1.0, 0.0)
if should_play:
super.play()
## Trigger (play) a random variation associated with this
## player with the given volume and pitch settings.
func trigger_varied(p_pitch: float = 1.0, p_volume: float = 0.0) -> void:
var should_play = PoolEntity.trigger(self, true, p_pitch, p_volume)
if should_play:
super.play()
## Reset the volume of this player back to the default set in its bank.
func reset_volume() -> void:
PoolEntity.reset_volume(self)
## Reset the pitch of this player back to the default set in its bank.
func reset_pitch() -> void:
PoolEntity.reset_pitch(self)
## Reset both the volume and pitch of this player back to the default set in its bank.
func reset_all() -> void:
PoolEntity.reset_all(self)
## Release this player back to the pool, and optionally
## wait for it to finish playing before doing so.
func release(p_finish_playing: bool = false) -> void:
PoolEntity.release(self, p_finish_playing)
# ------------------------------------------------------------------------------
# Private methods
# ------------------------------------------------------------------------------
func _on_finished() -> void:
PoolEntity.finished(self)

View file

@ -0,0 +1,124 @@
class_name PooledAudioStreamPlayer3D
extends AudioStreamPlayer3D
## An extension of AudioStreamPlayer3D that manages sequential and
## polyphonic playback as part of a pool of players.
## Emitted when this player has been released and should return to the pool.
signal released
## Whether this player has been reserved.
var reserved: bool
## Whether this player is in the process of being released.
var releasing: bool
## Whether this player has been configured to support polyphonic playback.
var poly: bool
## The collection of streams configured on this player.
var streams: Array
## The base/fallback volume of this player.
var base_volume: float
## The base/fallback pitch of this player.
var base_pitch: float
## The target this player should follow in 2D or 3D space.
var follow_target: Node
## When the player should sync its transform when following a target.
var follow_type: PoolEntity.FollowType
# ------------------------------------------------------------------------------
# Lifecycle methods
# ------------------------------------------------------------------------------
func _ready() -> void:
finished.connect(_on_finished)
func _process(_p_delta) -> void:
PoolEntity.sync_process(self)
func _physics_process(_p_delta) -> void:
PoolEntity.sync_physics_process(self)
# ------------------------------------------------------------------------------
# Public methods
# ------------------------------------------------------------------------------
## Returns a new player.
static func create() -> PooledAudioStreamPlayer3D:
return PoolEntity.create(PooledAudioStreamPlayer3D.new())
## Whether this player is a [NullPooledAudioStreamPlayer3D], or real instance.
func is_null() -> bool:
return false
## Configure this player with the given streams and charateristics.
func configure(p_streams: Array, p_reserved: bool, p_bus: String, p_poly: bool, p_volume: float, p_pitch: float, p_mode: Node.ProcessMode) -> void:
var is_polyphonic = PoolEntity.configure(self, p_streams, p_reserved, p_bus, p_poly, p_volume, p_pitch, p_mode)
if is_polyphonic:
super.play()
## Attach this player to a 2D/3D position or node.
func attach_to(p_node: Variant) -> void:
PoolEntity.attach_to(self, p_node)
## Trigger (play) a random variation associated with this player.
func trigger() -> void:
var should_play = PoolEntity.trigger(self, false, 1.0, 0.0)
if should_play:
super.play()
## Trigger (play) a random variation associated with this
## player with the given volume and pitch settings.
func trigger_varied(p_pitch: float = 1.0, p_volume: float = 0.0) -> void:
var should_play = PoolEntity.trigger(self, true, p_pitch, p_volume)
if should_play:
super.play()
## Reset the volume of this player back to the default set in its bank.
func reset_volume() -> void:
PoolEntity.reset_volume(self)
## Reset the pitch of this player back to the default set in its bank.
func reset_pitch() -> void:
PoolEntity.reset_pitch(self)
## Reset both the volume and pitch of this player back to the default set in its bank.
func reset_all() -> void:
PoolEntity.reset_all(self)
## Release this player back to the pool, and optionally
## wait for it to finish playing before doing so.
func release(p_finish_playing: bool = false) -> void:
PoolEntity.release(self, p_finish_playing)
# ------------------------------------------------------------------------------
# Private methods
# ------------------------------------------------------------------------------
func _on_finished() -> void:
PoolEntity.finished(self)

View file

@ -0,0 +1,17 @@
class_name SoundBank
extends Node
## A container used to store & group sound events in your scene.
## This bank's unique identifier.
@export var label: String
## The bus to use for all sound events in this bank.[br][br]
## [b]Note:[/b] this will override the bus set in your project settings (Audio/Manager/Sound/Bank)
@export var bus: String
## The underlying process mode for all sound events in this bank.
@export var mode: Node.ProcessMode
## The collection of sound events associated with this bank.
@export var events: Array[SoundEventResource]

View file

@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 16 16">
<g clip-path="url(#a)">
<path fill="url(#b)" d="M14 0H2a2 2 0 0 0-2 2v12c0 1.1.9 2 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2ZM4.47 11.18a1.19 1.19 0 0 1-1.62.46 1.19 1.19 0 0 1-.47-1.62c.36-.65.55-1.33.55-2.02a4.1 4.1 0 0 0-.55-2.03 1.2 1.2 0 0 1 .47-1.62 1.2 1.2 0 0 1 1.62.46 6.4 6.4 0 0 1 0 6.36v.01Zm3.88 1.37a1.2 1.2 0 0 1-1.62.46 1.2 1.2 0 0 1-.45-1.62 6.91 6.91 0 0 0 0-6.76 1.2 1.2 0 0 1 .45-1.62 1.2 1.2 0 0 1 1.62.46 9.3 9.3 0 0 1 0 9.1v-.02Zm3.85 1.35a1.2 1.2 0 0 1-1.63.45 1.16 1.16 0 0 1-.56-.72c-.08-.31-.05-.63.11-.9a9.58 9.58 0 0 0 0-9.46c-.16-.28-.2-.6-.11-.9.08-.31.28-.56.56-.72a1.2 1.2 0 0 1 1.62.45 12 12 0 0 1 0 11.8h.01Z"/>
</g>
<defs>
<linearGradient id="b" x1="2.9" x2="13.1" y1="16.83" y2="-.83" gradientUnits="userSpaceOnUse">
<stop stop-color="#9F9"/>
<stop offset=".5" stop-color="#E6D767"/>
<stop offset="1" stop-color="#EA6C5E"/>
</linearGradient>
<clipPath id="a">
<path fill="#fff" d="M0 0h16v16H0z"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://ntgipyp6jv34"
path="res://.godot/imported/sound_bank.svg-d1b3f43714c54a9c122e6a364a95c7d8.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/resonate/sound_manager/sound_bank.svg"
dest_files=["res://.godot/imported/sound_bank.svg-d1b3f43714c54a9c122e6a364a95c7d8.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View file

@ -0,0 +1,21 @@
class_name SoundEventResource
extends Resource
## The container used to store the details of a sound event.
## This sound event's unique identifier.
@export var name: String = ""
## The bus to use for all sound events in this bank.[br][br]
## [b]Note:[/b] this will override the bus set in this events sound bank,
## or your project settings (Audio/Manager/Sound/Bank)
@export var bus: String = ""
## The volume of the sound event.
@export_range(-80.0, 6.0, 0.1, "suffix:dB") var volume: float = 0.0
## The pitch of the sound event.
@export var pitch: float = 1.0
## The collection of audio streams (variations) associated with this sound event.
@export var streams: Array[AudioStream]

View file

@ -0,0 +1,493 @@
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()