Full Code of hugemenace/resonate for AI

main 1675fa359685 cached
79 files
143.7 KB
41.8k tokens
1 requests
Download .txt
Repository: hugemenace/resonate
Branch: main
Commit: 1675fa359685
Files: 79
Total size: 143.7 KB

Directory structure:
gitextract_1afov26k/

├── .gitattributes
├── .github/
│   ├── FUNDING.yml
│   └── workflows/
│       └── main.yml
├── .gitignore
├── .release-please-manifest.json
├── CHANGELOG.md
├── LICENSE
├── README.md
├── addons/
│   └── resonate/
│       ├── LICENSE
│       ├── music_manager/
│       │   ├── music_bank.gd
│       │   ├── music_bank.svg.import
│       │   ├── music_manager.gd
│       │   ├── music_stem_resource.gd
│       │   ├── music_track_resource.gd
│       │   └── stemmed_music_stream_player.gd
│       ├── plugin.cfg
│       ├── plugin.gd
│       ├── shared/
│       │   ├── resonate_settings.gd
│       │   └── resonate_utils.gd
│       └── sound_manager/
│           ├── null_pooled_audio_stream_player.gd
│           ├── null_pooled_audio_stream_player_2d.gd
│           ├── null_pooled_audio_stream_player_3d.gd
│           ├── pool_entity.gd
│           ├── pooled_audio_stream_player.gd
│           ├── pooled_audio_stream_player_2d.gd
│           ├── pooled_audio_stream_player_3d.gd
│           ├── sound_bank.gd
│           ├── sound_bank.svg.import
│           ├── sound_event_resource.gd
│           └── sound_manager.gd
├── audio/
│   ├── music_stems/
│   │   ├── breakbeat_drums_stem.mp3.import
│   │   ├── breakbeat_melody_stem.mp3.import
│   │   ├── breakbeat_pad_stem.mp3.import
│   │   ├── house_bass_pad_stem.mp3.import
│   │   ├── house_drums_stem.mp3.import
│   │   └── house_melody_stem.mp3.import
│   └── sounds/
│       ├── blaster.wav.import
│       ├── note_c5.wav.import
│       ├── note_e5.wav.import
│       └── note_g5.wav.import
├── default_bus_layout.tres
├── docs/
│   ├── getting-started.md
│   ├── images/
│   │   ├── add-music-bank-node.jpg.import
│   │   ├── add-sound-bank-node.jpg.import
│   │   ├── music-banks.png.import
│   │   ├── music-manager.png.import
│   │   ├── set-soundbank-label.jpg.import
│   │   ├── sound-banks.png.import
│   │   └── sound-manager.png.import
│   ├── music-manager.md
│   └── sound-manager.md
├── examples/
│   ├── 2D/
│   │   ├── 2d.gd
│   │   ├── 2d.tscn
│   │   └── moving_target.gd
│   ├── 3D/
│   │   ├── 3d.gd
│   │   ├── 3d.tscn
│   │   └── pivot.gd
│   ├── bulk_instancing/
│   │   ├── bulk_instancing.gd
│   │   └── bulk_instancing.tscn
│   ├── music/
│   │   ├── music.gd
│   │   └── music.tscn
│   ├── polyphonic/
│   │   ├── polyphonic.gd
│   │   └── polyphonic.tscn
│   ├── scene_changes/
│   │   ├── scene_changes.gd
│   │   ├── scene_changes.tscn
│   │   ├── scene_one.gd
│   │   ├── scene_one.tscn
│   │   ├── scene_two.gd
│   │   └── scene_two.tscn
│   └── simple/
│       ├── simple.gd
│       └── simple.tscn
├── icon.svg.import
├── project.godot
├── reasonate-github-header.jpg.import
├── release-please-config.json
├── resonate-logo.png.import
└── shared/
    ├── description_label_settings.tres
    ├── stats_label_settings.tres
    └── title_label_settings.tres

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitattributes
================================================
# Normalize EOL for all files that Git considers text files.
* text=auto eol=lf


================================================
FILE: .github/FUNDING.yml
================================================
patreon: hugemenace


================================================
FILE: .github/workflows/main.yml
================================================
on:
  push:
    branches:
      - main

permissions:
  checks: write
  contents: write
  pull-requests: write

name: Release

jobs:
  release-please:
    runs-on: ubuntu-latest
    steps:
      - name: Create Tag & Changelog
        uses: google-github-actions/release-please-action@v4
        id: release
      - uses: actions/checkout@main
        if: ${{ steps.release.outputs.release_created }}
      - name: Prepare Release Artifact
        if: ${{ steps.release.outputs.release_created }}
        run: |
          mkdir -p resonate-${{ steps.release.outputs.tag_name }}/addons
          cp -r addons/resonate resonate-${{ steps.release.outputs.tag_name }}/addons/
          cp -r docs resonate-${{ steps.release.outputs.tag_name }}/addons/resonate/
      - name: Create Release Artifact
        if: ${{ steps.release.outputs.release_created }}
        uses: thedoctor0/zip-release@0.7.6
        with:
          type: 'zip'
          filename: 'resonate-${{ steps.release.outputs.tag_name }}.zip'
          path: 'resonate-${{ steps.release.outputs.tag_name }}'
      - name: Publish Release
        if: ${{ steps.release.outputs.release_created }}
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: gh release upload ${{ steps.release.outputs.tag_name }} resonate-${{ steps.release.outputs.tag_name }}.zip


================================================
FILE: .gitignore
================================================
# Godot 4+ specific ignores
.godot/


================================================
FILE: .release-please-manifest.json
================================================
{
    ".": "2.4.0"
}


================================================
FILE: CHANGELOG.md
================================================
# Changelog

## [2.4.0](https://github.com/hugemenace/resonate/compare/v2.3.4...v2.4.0) (2024-03-08)


### Features

* add an auto-loop option to the MusicManager's play method to allow non-looping tracks to repeat playback with an integrated crossfade ([f3885c7](https://github.com/hugemenace/resonate/commit/f3885c7263788ec097cf5369768823829dcfdd9d))
* allow polyphonic players to be stopped and synchronise the is_playing state with the underlying polyphonic streams ([b91e062](https://github.com/hugemenace/resonate/commit/b91e06279a45a5b50aa1d84f7389cf9a06f1a718))

## [2.3.4](https://github.com/hugemenace/resonate/compare/v2.3.3...v2.3.4) (2024-02-23)


### Bug Fixes

* fix sound and music manager parse errors on exported projects ([081e753](https://github.com/hugemenace/resonate/commit/081e75382c5a984790051986a5b8da48cb484759))

## [2.3.3](https://github.com/hugemenace/resonate/compare/v2.3.2...v2.3.3) (2024-02-17)


### Bug Fixes

* ensure that the Sound and Music managers ref-count banks to avoid unexpected removal for multi-users ([2fb7223](https://github.com/hugemenace/resonate/commit/2fb72237d8cc217721a8d1fa92d4be5b9202515d))
* fix the Music and Sound manager default bank bus settings and migrate older addon values if present ([9d614db](https://github.com/hugemenace/resonate/commit/9d614db05e4744c885c22f2def05459bfe9ac176))

## [2.3.2](https://github.com/hugemenace/resonate/compare/v2.3.1...v2.3.2) (2024-02-09)


### Bug Fixes

* add missing reset_* methods to the null audio players ([8cd9b7c](https://github.com/hugemenace/resonate/commit/8cd9b7c7c0d68a15532ea0fb5053de1788a2b4cd))
* alter the attachment logic for 2D and 3D players to avoid issues with players being freed before returning to the pool (deprecates auto_release) ([d53d856](https://github.com/hugemenace/resonate/commit/d53d856683b7d15f2b0ef9a55c7ebbf8de57956b))
* replace the remote transform behaviour with an inbuilt follow-target mechanism to avoid audio artifacts ([b741318](https://github.com/hugemenace/resonate/commit/b7413187e8863e47888b7e520adf8c2d863cf23e))

## [2.3.1](https://github.com/hugemenace/resonate/compare/v2.3.0...v2.3.1) (2024-02-07)


### Bug Fixes

* ensure 3D players registered as auto-released aren't lost during scene tree exiting events ([aa0d702](https://github.com/hugemenace/resonate/commit/aa0d702d7c4f8494e9fc50dd98594aabee2b90db))
* fix the project settings registration process when the addon loads ([4b17eec](https://github.com/hugemenace/resonate/commit/4b17eecbb3998c5c8d8c0f8fc8f781f5d7ffd66c))

## [2.3.0](https://github.com/hugemenace/resonate/compare/v2.2.0...v2.3.0) (2024-02-06)


### Features

* add a volume variable to sound events (bank-configured) ([6b3d66d](https://github.com/hugemenace/resonate/commit/6b3d66d97bb4258e1f6f50b226604b548e682571))
* add enhanced return types, signals, and helper methods to the music manager ([e621269](https://github.com/hugemenace/resonate/commit/e6212692baaf777bab0391c8ab7fddb876cc8117))
* add null object pattern to the sound manager for instances and some additional helper methods and events for implementation ([df8eb86](https://github.com/hugemenace/resonate/commit/df8eb86ee2d979ed220388d19ed57e21730005e0))
* add pitch variable to sound event resources and fix pitch and volume behaviour across audio stream players ([5918f8e](https://github.com/hugemenace/resonate/commit/5918f8eef87a2b14c4d71fdda4911ecc90a1b8a9))
* add play_varied, play_at_position_varied, and play_on_node_varied methods to the sound manager ([3f2ec86](https://github.com/hugemenace/resonate/commit/3f2ec86ac70c414039c274a21cc6c84d157894ea))
* add reset_volume, reset_pitch, and reset_all methods to pooled audio stream players ([07ec5ef](https://github.com/hugemenace/resonate/commit/07ec5efb70742ee874641d048ab3616f368e8dc4))
* add the quick_instance method to the sound manager for bulk instances, or for shorter registration calls ([03f2515](https://github.com/hugemenace/resonate/commit/03f2515a84c9b19395ffe118d25132dbc9f2663d))


### Bug Fixes

* ensure that pooled audio stream players return the player volume back to the base volume when calling trigger after trigger_varied, and that trigger_varied isn't affected by the base volume on polyphonic playback ([f53f709](https://github.com/hugemenace/resonate/commit/f53f709e314ef54f537736b903290492acb94ed8))
* fix the "signal already connected" error in multiple single-script auto_released instances and ensure the connection is deferred ([f7b2553](https://github.com/hugemenace/resonate/commit/f7b25535f8b8bfc0d40330bfb2ea7307330b69e5))
* fix the error handling & messages for missing streams or players in the sound manager ([9e4d27d](https://github.com/hugemenace/resonate/commit/9e4d27d60c11cff8e2c0f4088c2170c0c598e775))
* fix the volume adjustment in the trigger_varied method on non-polyphonic pooled audio stream players ([1ed5ebd](https://github.com/hugemenace/resonate/commit/1ed5ebd23d657f66b487fae326b6170d448254f4))
* increase the volume step resolution on music stem resources to allow for fractional dBs ([51cf761](https://github.com/hugemenace/resonate/commit/51cf76164dbad187179501da3c39f072d6f67a66))
* push a warning when instanced sound events that contain looping variations are released with "finish playing" - these will be forced to stop playback ([fdce4d2](https://github.com/hugemenace/resonate/commit/fdce4d2764a8ffd2c617064a5a352bdb6484e6bd))

## [2.2.0](https://github.com/hugemenace/resonate/compare/v2.1.0...v2.2.0) (2024-02-01)


### Features

* add a warning for when a music track contains non-looping stems ([3577b09](https://github.com/hugemenace/resonate/commit/3577b09a8fa80547891ddc97916319a112b5d90b))
* add auto_release and auto_stop convenience methods to the sound and music managers ([5640a57](https://github.com/hugemenace/resonate/commit/5640a57725bedd0098cbf08337a82c71c52d180f))


### Bug Fixes

* ensure that pooled audio stream players immediately stop playback (by default) on release ([3cf44cd](https://github.com/hugemenace/resonate/commit/3cf44cd075035d2879e9c1ec722cc0741217ee19))
* ensure the pool entity release method immediately stops the playback of looping stems even if finish_playing is set ([8014e20](https://github.com/hugemenace/resonate/commit/8014e205ba32b4dcdd9fdf1484c1c7423b79348a))

## [2.1.0](https://github.com/hugemenace/resonate/compare/v2.0.0...v2.1.0) (2024-01-31)


### Features

* add automatic detection of new sound and music banks during scene changes & runtime node insertion/deletion ([f964908](https://github.com/hugemenace/resonate/commit/f964908558b95adc18c0f3c92365f7eb37e664ae))
* enhance the is_playing method on the MusicManager to allow bank and track scoping ([90eb8d3](https://github.com/hugemenace/resonate/commit/90eb8d317203fc260f79a45e11ec07e4ff8fca98))

## [2.0.0](https://github.com/hugemenace/resonate/compare/v1.0.2...v2.0.0) (2024-01-30)


### ⚠ BREAKING CHANGES

* rename MusicResource to MusicTrackResource

### Features

* add a bus override option to MusicBanks and MusicTrackResources ([61ede7e](https://github.com/hugemenace/resonate/commit/61ede7ec1574f22fc84779427282b802a9523682))
* add global MusicManager and individual stem volume management ([02696cd](https://github.com/hugemenace/resonate/commit/02696cdade253a2d5374f73229f24624c59f0e0b))
* add labels to MusicBanks to allow the grouping/scoping of music tracks ([1192122](https://github.com/hugemenace/resonate/commit/1192122e5d5673184d3a820238154824cbe0c093))
* expose the MusicManager's loaded status via a has_loaded public variable ([4145ac0](https://github.com/hugemenace/resonate/commit/4145ac0cb7dc8b5767de0d9a9628c79991bf119c))


### Bug Fixes

* rename MusicResource to MusicTrackResource ([8ba7355](https://github.com/hugemenace/resonate/commit/8ba73556b28ef5b830e75c53d2b50c6dd9f3697e))
* update the build script to include a top-level name & version directory in the release .zip and also bundle-in a copy of the documentation ([8db1ae4](https://github.com/hugemenace/resonate/commit/8db1ae4fc04f11364cc556aff095dfe92a3e5431))

## [1.0.2](https://github.com/hugemenace/resonate/compare/v1.0.1...v1.0.2) (2024-01-29)


### Bug Fixes

* remove the extraneous pool_updated signal and PoolType enum from the SoundManager ([2066868](https://github.com/hugemenace/resonate/commit/2066868ff96f831d83624761addefd88af7e92b8))
* remove the SoundManager type references from the pooled audio stream players to avoid errors when the addon is disabled ([c3e3cd9](https://github.com/hugemenace/resonate/commit/c3e3cd91a3098d59eb2e24dd225a9e8121f94d02))

## [1.0.1](https://github.com/hugemenace/resonate/compare/v1.0.0...v1.0.1) (2024-01-28)


### Bug Fixes

* ensure the release zip includes the addons directory ([fc543d3](https://github.com/hugemenace/resonate/commit/fc543d33fde886c467fe071ee48e23371deda408))

## [1.0.0](https://github.com/hugemenace/resonate/compare/v0.1.0...v1.0.0) (2024-01-28)


### Features

* add a pool_updated event to the SoundManager ([0ada03c](https://github.com/hugemenace/resonate/commit/0ada03c4bb5805bcf9274210143cd060996626fc))
* add music track crossfading, stem toggle fading, and update the example music scene ([d3b180f](https://github.com/hugemenace/resonate/commit/d3b180fd08ee52144f4c496fbbb863e05c80d42f))
* add pool stats and tidy up the simple example scene ([03c7b4c](https://github.com/hugemenace/resonate/commit/03c7b4c174fc53e7e5fe77e6aebe189b6b301ae1))
* add process modes to music and sound banks ([d3f6142](https://github.com/hugemenace/resonate/commit/d3f61421f6d142c38c334b5add041ba63ad03403))
* add trigger_varied to pooled* audio streams to allow simplified pitch and volume control for reserved instances ([ee0bc2b](https://github.com/hugemenace/resonate/commit/ee0bc2b7337dda290ebadefcc79d317ee0dcd040))
* complete the 2D example scene ([a6a55fa](https://github.com/hugemenace/resonate/commit/a6a55fa04802d2c7f409fe3c6a3136b08ff00d04))
* complete the 3D example scene ([2a93dc7](https://github.com/hugemenace/resonate/commit/2a93dc7bcc94e5e06d98ab8f9857ff243176271c))
* complete the example music scene ([0e9fb4c](https://github.com/hugemenace/resonate/commit/0e9fb4c964307f54af5cf387f4141ce7d9b76d8a))
* complete the polyphonic example scene ([50cfcd2](https://github.com/hugemenace/resonate/commit/50cfcd2bf196d2a6c232a449cc615327c92b71a3))
* complete the simple example scene ([28a07c6](https://github.com/hugemenace/resonate/commit/28a07c68a50e42bc7415eaabe055f4d30149e6c0))


### Bug Fixes

* change the SoundManager's pool_type enum name to PoolType ([1705d10](https://github.com/hugemenace/resonate/commit/1705d10a03ebefb637802f3b3bfc47619db6e855))
* change the SoundManager's private _loaded member variable to a public has_loaded ([25477e3](https://github.com/hugemenace/resonate/commit/25477e3741f72e0036bdea641de70e987a624435))
* ensure that music is played through the bus configured in the project settings ([ed9b4c7](https://github.com/hugemenace/resonate/commit/ed9b4c7a9f64f3c64662aa045da445836d1fb9a2))
* fix the music example scene and add an is_playing function to the music manager ([9289a40](https://github.com/hugemenace/resonate/commit/9289a407c026dc638fb8dbbc9567d06e7f6db663))
* remove the reserved parameter from the pooled audio stream player's configure functions and ensure play* functions are set to auto-release when triggered ([822d6ee](https://github.com/hugemenace/resonate/commit/822d6eef439a8347c1dc4d82cc60f29632121ed0))


### Miscellaneous Chores

* release 1.0.0 ([c3945bc](https://github.com/hugemenace/resonate/commit/c3945bc7f93c124ca2a8900b71265deae813ae55))


================================================
FILE: LICENSE
================================================
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.

================================================
FILE: README.md
================================================
<img src='reasonate-github-header.jpg' width='100%'>

# ***Resonate***

> [!IMPORTANT]
> This project was originally developed and maintained by Huge Menace. **As of Jan 2026, it is officially maintained by Widgit Gaming**. Future updates, issues, and discussions are hosted at 👉 [gitlab.com/widgitgaming/godot/resonate](https://gitlab.com/widgitgaming/godot/resonate) — Thank you to the community for your support!

An all-in-one sound and music management addon for the Godot game engine (see compatibility).

## Features
- Pooled audio stream players.
- Automatic 2D and 3D space detection.
- Polyphonic playback.
- Stemmed music tracks.
- Music crossfading.

## TL;DR

Resonate has two core systems: the **SoundManager** and the **MusicManager**.

The **SoundManager** automatically pools and orchestrates **AudioStreamPlayers** for you and gives you control over the players when needed. 

The **MusicManager** composes music tracks built from ***stems*** and supports the (cross)fading of tracks or stems out of the box.

## Docs

- [Getting started](docs/getting-started.md)
- [SoundManager documentation](docs/sound-manager.md)
- [MusicManager documentation](docs/music-manager.md)

## Examples

This repo is a Godot project you can clone and run.

Inside the `examples/` folder are scenes demonstrating all the Sound and Music Manager's features.

## Getting the addon

You have a few different options:

- You can download Resonate from the [Godot Asset Library](https://godotengine.org/asset-library/asset/2546) (or from within the editor). 
- You can grab the latest version from the [Github releases page](https://github.com/hugemenace/resonate/releases).
- You can also clone or download this repo, and extract the `addons/resonate` directory into the root folder of your Godot project.
- You can grab the addon from [Gumroad](https://hugemenace.gumroad.com/l/resonate-godot-addon) (if you'd like to financially support this project).

## License

This project is provided ***free for personal and commercial use*** under the [MIT license](LICENSE) ❤


================================================
FILE: addons/resonate/LICENSE
================================================
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.

================================================
FILE: addons/resonate/music_manager/music_bank.gd
================================================
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]


================================================
FILE: addons/resonate/music_manager/music_bank.svg.import
================================================
[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


================================================
FILE: addons/resonate/music_manager/music_manager.gd
================================================
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]. If [b]p_auto_loop[/b] is set, the track 
## will be automatically played again once its longest stem is about to finish playing,
## taking into account the amount of time required to crossfade into the next loop.
func play(p_bank_label: String, p_track_name: String, p_crossfade_time: float = 5.0, p_auto_loop: bool = false) -> 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 p_auto_loop and 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, p_auto_loop)
	
	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))
	
	if p_auto_loop:
		player.auto_loop_completed.connect(_on_auto_loop_completed, CONNECT_REFERENCE_COUNTED)
	
	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)


func _on_auto_loop_completed(p_bank_label: String, p_track_name: String, p_crossfade_time: float) -> void:
	play(p_bank_label, p_track_name, p_crossfade_time, true)


================================================
FILE: addons/resonate/music_manager/music_stem_resource.gd
================================================
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


================================================
FILE: addons/resonate/music_manager/music_track_resource.gd
================================================
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]


================================================
FILE: addons/resonate/music_manager/stemmed_music_stream_player.gd
================================================
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

## Emitted when this player has finished the current auto-loop and should begin playback again.
signal auto_loop_completed(p_bank_label: String, p_track_name: String, p_crossfade_time: float)

## 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 _stem_table: Dictionary
var _max_volume: float
var _crossfade_time: float
var _auto_loop: bool
var _track_length: float
var _current_play_time: float
var _loop_endpoint_reached_emitted: bool


# ------------------------------------------------------------------------------
# Public methods
# ------------------------------------------------------------------------------


func _process(p_delta):
	if is_stopping:
		return
	
	if not _auto_loop:
		return
	
	if _loop_endpoint_reached_emitted:
		return
	
	_current_play_time += p_delta
	if _current_play_time >= (_track_length - _crossfade_time):
		auto_loop_completed.emit(bank_label, track_name, _crossfade_time)
		_loop_endpoint_reached_emitted = true


# ------------------------------------------------------------------------------
# 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, p_auto_loop: bool) -> 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.is_stopping = false
	player._max_volume = p_max_volume
	player._auto_loop = p_auto_loop
	
	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
	var max_length = 0
	
	for stem in p_stems:
		var stream = stem.stream as AudioStream
		var length = stream.get_length()
		if length > max_length:
			max_length = length
		
		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)
		
		_stem_table[stem.name] = {
			"name": stem.name,
			"enabled": stem.enabled,
			"stream_id": stream_id,
			"volume": volume,
			"max_volume": max_volume,
			"tween": null,
		}
	
	_track_length = max_length
	_crossfade_time = p_crossfade_time
	
	_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 _stem_table.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 = _stem_table[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()
	
	_stem_table[p_name]["tween"] = new_tween
	_stem_table[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 = _stem_table[p_name]
	
	if stem["tween"] != null and stem["tween"].is_running():
		stem["tween"].kill()
	
	playback.set_stream_volume(stem.stream_id, p_volume)
	
	_stem_table[p_name]["volume"] = p_volume
	_stem_table[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 _stem_table.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 = _stem_table[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 = _stem_table[p_name]
	
	playback.set_stream_volume(stem.stream_id, p_target_volume)
	
	_stem_table[p_name]["volume"] = p_target_volume


func _on_stop_stems_tween_finished() -> void:
	stopped.emit()


================================================
FILE: addons/resonate/plugin.cfg
================================================
[plugin]

name="Resonate"
description=""
author="HugeMenace"
;x-release-please-start-version
version="2.4.0"
;x-release-please-end
script="plugin.gd"


================================================
FILE: addons/resonate/plugin.gd
================================================
@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")
		


================================================
FILE: addons/resonate/shared/resonate_settings.gd
================================================
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"


================================================
FILE: addons/resonate/shared/resonate_utils.gd
================================================
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


================================================
FILE: addons/resonate/sound_manager/null_pooled_audio_stream_player.gd
================================================
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


================================================
FILE: addons/resonate/sound_manager/null_pooled_audio_stream_player_2d.gd
================================================
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


================================================
FILE: addons/resonate/sound_manager/null_pooled_audio_stream_player_3d.gd
================================================
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


================================================
FILE: addons/resonate/sound_manager/pool_entity.gd
================================================
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) -> void:
	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
	
	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
	

## 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


## Stop the underlying stream player if not polyphonic streams are currently playing.
static func update_poly_playback_state(p_base) -> void:
	if not p_base.poly:
		return
	
	if not p_base.playing:
		return
	
	var playback = p_base.get_stream_playback() as AudioStreamPlaybackPolyphonic
	
	if playback == null:
		p_base.stop()
		return
	
	for stream_id in p_base.poly_stream_ids:
		if playback.is_stream_playing(stream_id):
			return
	
	p_base.poly_stream_ids.clear()
	p_base.stop()
	

## 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) -> void:
	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
		
	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
		p_base.play()
		return
	
	if not p_base.playing:
		p_base.play()
	
	var playback = p_base.get_stream_playback() as AudioStreamPlaybackPolyphonic
	var stream_volume = p_volume if p_varied else p_base.base_volume
	var stream_pitch = p_pitch if p_varied else p_base.base_pitch
	
	var stream_id = playback.play_stream(next_stream, 0, stream_volume, stream_pitch)
	
	if stream_id != AudioStreamPlaybackPolyphonic.INVALID_ID:
		p_base.poly_stream_ids.append(stream_id)


## 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()


================================================
FILE: addons/resonate/sound_manager/pooled_audio_stream_player.gd
================================================
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

## A collection of all active polyphonic stream IDs.
var poly_stream_ids: Array

## 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)
	PoolEntity.update_poly_playback_state(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:
	PoolEntity.configure(self, p_streams, p_reserved, p_bus, p_poly, p_volume, p_pitch, p_mode)


## 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:
	PoolEntity.trigger(self, false, 1.0, 0.0)


## 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:
	PoolEntity.trigger(self, true, p_pitch, p_volume)


## 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)


================================================
FILE: addons/resonate/sound_manager/pooled_audio_stream_player_2d.gd
================================================
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

## A collection of all active polyphonic stream IDs.
var poly_stream_ids: Array

## 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)
	PoolEntity.update_poly_playback_state(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:
	PoolEntity.configure(self, p_streams, p_reserved, p_bus, p_poly, p_volume, p_pitch, p_mode)
	

## 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:
	PoolEntity.trigger(self, false, 1.0, 0.0)
	

## 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:
	PoolEntity.trigger(self, true, p_pitch, p_volume)


## 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)


================================================
FILE: addons/resonate/sound_manager/pooled_audio_stream_player_3d.gd
================================================
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

## A collection of all active polyphonic stream IDs.
var poly_stream_ids: Array

## 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)
	PoolEntity.update_poly_playback_state(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:
	PoolEntity.configure(self, p_streams, p_reserved, p_bus, p_poly, p_volume, p_pitch, p_mode)


## 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:
	PoolEntity.trigger(self, false, 1.0, 0.0)


## 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:
	PoolEntity.trigger(self, true, p_pitch, p_volume)


## 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)


================================================
FILE: addons/resonate/sound_manager/sound_bank.gd
================================================
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]


================================================
FILE: addons/resonate/sound_manager/sound_bank.svg.import
================================================
[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


================================================
FILE: addons/resonate/sound_manager/sound_event_resource.gd
================================================
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]


================================================
FILE: addons/resonate/sound_manager/sound_manager.gd
================================================
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()


================================================
FILE: audio/music_stems/breakbeat_drums_stem.mp3.import
================================================
[remap]

importer="mp3"
type="AudioStreamMP3"
uid="uid://clkjvyfm227dt"
path="res://.godot/imported/breakbeat_drums_stem.mp3-20b1452215af88bd5c19cbdc0120040c.mp3str"

[deps]

source_file="res://audio/music_stems/breakbeat_drums_stem.mp3"
dest_files=["res://.godot/imported/breakbeat_drums_stem.mp3-20b1452215af88bd5c19cbdc0120040c.mp3str"]

[params]

loop=true
loop_offset=0
bpm=140.0
beat_count=0
bar_beats=4


================================================
FILE: audio/music_stems/breakbeat_melody_stem.mp3.import
================================================
[remap]

importer="mp3"
type="AudioStreamMP3"
uid="uid://ba8e5llasqjhk"
path="res://.godot/imported/breakbeat_melody_stem.mp3-56160c730f32e58f1cb8c6befe129e69.mp3str"

[deps]

source_file="res://audio/music_stems/breakbeat_melody_stem.mp3"
dest_files=["res://.godot/imported/breakbeat_melody_stem.mp3-56160c730f32e58f1cb8c6befe129e69.mp3str"]

[params]

loop=true
loop_offset=0
bpm=140.0
beat_count=0
bar_beats=4


================================================
FILE: audio/music_stems/breakbeat_pad_stem.mp3.import
================================================
[remap]

importer="mp3"
type="AudioStreamMP3"
uid="uid://sjkaeg8p0qig"
path="res://.godot/imported/breakbeat_pad_stem.mp3-e58ead83629b9866131aff536e307866.mp3str"

[deps]

source_file="res://audio/music_stems/breakbeat_pad_stem.mp3"
dest_files=["res://.godot/imported/breakbeat_pad_stem.mp3-e58ead83629b9866131aff536e307866.mp3str"]

[params]

loop=true
loop_offset=0
bpm=140.0
beat_count=0
bar_beats=4


================================================
FILE: audio/music_stems/house_bass_pad_stem.mp3.import
================================================
[remap]

importer="mp3"
type="AudioStreamMP3"
uid="uid://bhbxnxeuk62q0"
path="res://.godot/imported/house_bass_pad_stem.mp3-bf6488bffc812b1b3a0bdcc8582523e2.mp3str"

[deps]

source_file="res://audio/music_stems/house_bass_pad_stem.mp3"
dest_files=["res://.godot/imported/house_bass_pad_stem.mp3-bf6488bffc812b1b3a0bdcc8582523e2.mp3str"]

[params]

loop=true
loop_offset=0
bpm=0
beat_count=0
bar_beats=4


================================================
FILE: audio/music_stems/house_drums_stem.mp3.import
================================================
[remap]

importer="mp3"
type="AudioStreamMP3"
uid="uid://bcv4ev20vqxx2"
path="res://.godot/imported/house_drums_stem.mp3-44722d3fa5eaed738862780bc6f9a2c4.mp3str"

[deps]

source_file="res://audio/music_stems/house_drums_stem.mp3"
dest_files=["res://.godot/imported/house_drums_stem.mp3-44722d3fa5eaed738862780bc6f9a2c4.mp3str"]

[params]

loop=true
loop_offset=0
bpm=0
beat_count=0
bar_beats=4


================================================
FILE: audio/music_stems/house_melody_stem.mp3.import
================================================
[remap]

importer="mp3"
type="AudioStreamMP3"
uid="uid://dq5s5n0folm21"
path="res://.godot/imported/house_melody_stem.mp3-a8a250926da00e8e86b09a739ce09e4b.mp3str"

[deps]

source_file="res://audio/music_stems/house_melody_stem.mp3"
dest_files=["res://.godot/imported/house_melody_stem.mp3-a8a250926da00e8e86b09a739ce09e4b.mp3str"]

[params]

loop=true
loop_offset=0
bpm=0
beat_count=0
bar_beats=4


================================================
FILE: audio/sounds/blaster.wav.import
================================================
[remap]

importer="wav"
type="AudioStreamWAV"
uid="uid://cy4r18lxb4rbh"
path="res://.godot/imported/blaster.wav-efe4f879684d64f1eb83adf3227d91ab.sample"

[deps]

source_file="res://audio/sounds/blaster.wav"
dest_files=["res://.godot/imported/blaster.wav-efe4f879684d64f1eb83adf3227d91ab.sample"]

[params]

force/8_bit=false
force/mono=true
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=0


================================================
FILE: audio/sounds/note_c5.wav.import
================================================
[remap]

importer="wav"
type="AudioStreamWAV"
uid="uid://de550fklyhu88"
path="res://.godot/imported/note_c5.wav-0c5ae09afd8333f8d738cc036453e061.sample"

[deps]

source_file="res://audio/sounds/note_c5.wav"
dest_files=["res://.godot/imported/note_c5.wav-0c5ae09afd8333f8d738cc036453e061.sample"]

[params]

force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=0


================================================
FILE: audio/sounds/note_e5.wav.import
================================================
[remap]

importer="wav"
type="AudioStreamWAV"
uid="uid://b35ao3bdtstka"
path="res://.godot/imported/note_e5.wav-297f657b0896a529a466947ad314187e.sample"

[deps]

source_file="res://audio/sounds/note_e5.wav"
dest_files=["res://.godot/imported/note_e5.wav-297f657b0896a529a466947ad314187e.sample"]

[params]

force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=0


================================================
FILE: audio/sounds/note_g5.wav.import
================================================
[remap]

importer="wav"
type="AudioStreamWAV"
uid="uid://bmg3dvylup5k"
path="res://.godot/imported/note_g5.wav-d7fdc8c79a744d03c9a83d9ffeced1bc.sample"

[deps]

source_file="res://audio/sounds/note_g5.wav"
dest_files=["res://.godot/imported/note_g5.wav-d7fdc8c79a744d03c9a83d9ffeced1bc.sample"]

[params]

force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=0


================================================
FILE: default_bus_layout.tres
================================================
[gd_resource type="AudioBusLayout" format=3 uid="uid://3vigst7csyr8"]

[resource]
bus/0/volume_db = -2.0
bus/1/name = &"Music"
bus/1/solo = false
bus/1/mute = false
bus/1/bypass_fx = false
bus/1/volume_db = 0.0
bus/1/send = &"Master"
bus/2/name = &"Sound"
bus/2/solo = false
bus/2/mute = false
bus/2/bypass_fx = false
bus/2/volume_db = 0.0
bus/2/send = &"Master"


================================================
FILE: docs/getting-started.md
================================================
# 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.


================================================
FILE: docs/images/add-music-bank-node.jpg.import
================================================
[remap]

importer="texture"
type="CompressedTexture2D"
uid="uid://decojxomfjk5j"
path="res://.godot/imported/add-music-bank-node.jpg-e8c51bb61324a44c5e2f517c657b487e.ctex"
metadata={
"vram_texture": false
}

[deps]

source_file="res://docs/images/add-music-bank-node.jpg"
dest_files=["res://.godot/imported/add-music-bank-node.jpg-e8c51bb61324a44c5e2f517c657b487e.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


================================================
FILE: docs/images/add-sound-bank-node.jpg.import
================================================
[remap]

importer="texture"
type="CompressedTexture2D"
uid="uid://dyf0vqo1r1frq"
path="res://.godot/imported/add-sound-bank-node.jpg-b2f670516538f11b95735e6253306141.ctex"
metadata={
"vram_texture": false
}

[deps]

source_file="res://docs/images/add-sound-bank-node.jpg"
dest_files=["res://.godot/imported/add-sound-bank-node.jpg-b2f670516538f11b95735e6253306141.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


================================================
FILE: docs/images/music-banks.png.import
================================================
[remap]

importer="texture"
type="CompressedTexture2D"
uid="uid://coy730qpivrbt"
path="res://.godot/imported/music-banks.png-ccdadc82979cc50e9bc6e13c7765c483.ctex"
metadata={
"vram_texture": false
}

[deps]

source_file="res://docs/images/music-banks.png"
dest_files=["res://.godot/imported/music-banks.png-ccdadc82979cc50e9bc6e13c7765c483.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


================================================
FILE: docs/images/music-manager.png.import
================================================
[remap]

importer="texture"
type="CompressedTexture2D"
uid="uid://c33xlcqiwygve"
path="res://.godot/imported/music-manager.png-9e8dfcf14c60a02ae344f87e1e3c81b9.ctex"
metadata={
"vram_texture": false
}

[deps]

source_file="res://docs/images/music-manager.png"
dest_files=["res://.godot/imported/music-manager.png-9e8dfcf14c60a02ae344f87e1e3c81b9.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


================================================
FILE: docs/images/set-soundbank-label.jpg.import
================================================
[remap]

importer="texture"
type="CompressedTexture2D"
uid="uid://cn3ojlclw1u8i"
path="res://.godot/imported/set-soundbank-label.jpg-23229b0a77b1ed27b07cc2145c4d2e18.ctex"
metadata={
"vram_texture": false
}

[deps]

source_file="res://docs/images/set-soundbank-label.jpg"
dest_files=["res://.godot/imported/set-soundbank-label.jpg-23229b0a77b1ed27b07cc2145c4d2e18.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


================================================
FILE: docs/images/sound-banks.png.import
================================================
[remap]

importer="texture"
type="CompressedTexture2D"
uid="uid://b1q8c8jsyx4xs"
path="res://.godot/imported/sound-banks.png-e6053c754f48df4c35b1ce7bfd0c02f7.ctex"
metadata={
"vram_texture": false
}

[deps]

source_file="res://docs/images/sound-banks.png"
dest_files=["res://.godot/imported/sound-banks.png-e6053c754f48df4c35b1ce7bfd0c02f7.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


================================================
FILE: docs/images/sound-manager.png.import
================================================
[remap]

importer="texture"
type="CompressedTexture2D"
uid="uid://mg8fvg6efg77"
path="res://.godot/imported/sound-manager.png-4662b0febe7313414142e7d2003ba9f9.ctex"
metadata={
"vram_texture": false
}

[deps]

source_file="res://docs/images/sound-manager.png"
dest_files=["res://.godot/imported/sound-manager.png-4662b0febe7313414142e7d2003ba9f9.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


================================================
FILE: docs/music-manager.md
================================================
# 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")
```


================================================
FILE: docs/sound-manager.md
================================================
# 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.


================================================
FILE: examples/2D/2d.gd
================================================
extends Node2D


# For reference, it's worth taking a moment to inspect the SoundBank attached to 
# this example scene. SoundBanks hold the configuration for all of your sound 
# events and the variations (AudioStreams) associated with the event.


@onready var moving_target = $MovingTarget

# We instantiate a Null PooledAudioStreamPlayer* here, which removes the need to
# perform null checks before calling methods such as trigger() in your scripts.
var _instance: PooledAudioStreamPlayer2D = SoundManager.null_instance_2d()


func _ready() -> void:
	# As the SoundManager requires some preparation when the game loads, we need 
	# to hook into one or more of its lifecycle events before trying to play or 
	# instance an audio event. In this example, we've use the `updated` event 
	# as it's fired whenever any part of the SoundManager's state updates.
	SoundManager.updated.connect(on_sound_manager_updated)
	

func on_sound_manager_updated() -> void:
	# The method call below is an inbuilt guard-clause that'll help us avoid 
	# instancing an audio event when the SoundManager has not loaded, or when 
	# we've already replaced our Null instance with a real one further down.
	if SoundManager.should_skip_instancing(_instance):
		return
		
	_instance = SoundManager.instance_on_node("2d", "drums", moving_target)
	_instance.trigger()


================================================
FILE: examples/2D/2d.tscn
================================================
[gd_scene load_steps=10 format=3 uid="uid://coebcgearnost"]

[ext_resource type="Script" path="res://examples/2d/2d.gd" id="1_3xxt8"]
[ext_resource type="Script" path="res://addons/resonate/sound_manager/sound_bank.gd" id="2_8j415"]
[ext_resource type="Script" path="res://addons/resonate/sound_manager/sound_event_resource.gd" id="3_cnd76"]
[ext_resource type="AudioStream" uid="uid://bcv4ev20vqxx2" path="res://audio/music_stems/house_drums_stem.mp3" id="4_q31s7"]
[ext_resource type="LabelSettings" uid="uid://tysg4xge4hsu" path="res://shared/title_label_settings.tres" id="7_wknu3"]
[ext_resource type="LabelSettings" uid="uid://bldprl7vev3uv" path="res://shared/description_label_settings.tres" id="8_h0mrm"]
[ext_resource type="Script" path="res://examples/2d/moving_target.gd" id="9_kf013"]

[sub_resource type="Resource" id="Resource_5hea7"]
script = ExtResource("3_cnd76")
name = "drums"
bus = ""
volume = 0.0
pitch = 1.0
streams = Array[AudioStream]([ExtResource("4_q31s7")])

[sub_resource type="BoxMesh" id="BoxMesh_rk1yn"]
size = Vector3(100, 100, 100)

[node name="2D" type="Node2D"]
script = ExtResource("1_3xxt8")

[node name="SoundBank" type="Node" parent="."]
script = ExtResource("2_8j415")
label = "2d"
events = Array[ExtResource("3_cnd76")]([SubResource("Resource_5hea7")])

[node name="Title" type="Label" parent="."]
offset_left = 100.0
offset_top = 50.0
offset_right = 208.0
offset_bottom = 103.0
text = "2d"
label_settings = ExtResource("7_wknu3")
uppercase = true

[node name="Description" type="Label" parent="."]
offset_left = 100.0
offset_top = 125.0
offset_right = 824.0
offset_bottom = 185.0
text = "This example uses the \"drums\" sound event (see the SoundBank.)
It has been attached to the square below, causing the sound to move along with it in 2D space."
label_settings = ExtResource("8_h0mrm")

[node name="MovingTarget" type="MeshInstance2D" parent="."]
position = Vector2(-100, 600)
scale = Vector2(1.117, 1)
mesh = SubResource("BoxMesh_rk1yn")
script = ExtResource("9_kf013")

[node name="AudioListener2D" type="AudioListener2D" parent="."]
position = Vector2(600, 400)
current = true


================================================
FILE: examples/2D/moving_target.gd
================================================
extends MeshInstance2D


var _time: float


func _process(p_delta) -> void:
	_time += p_delta
	
	var weight = (1.0 + sin(_time)) / 2.0
	var x_position = lerpf(-100.0, 1300.0, weight)
	
	global_position.x = x_position


================================================
FILE: examples/3D/3d.gd
================================================
extends Node3D


# For reference, it's worth taking a moment to inspect the SoundBank attached to 
# this example scene. SoundBanks hold the configuration for all of your sound 
# events and the variations (AudioStreams) associated with the event.


@onready var moving_target = $Pivot/MovingTarget

# We instantiate a Null PooledAudioStreamPlayer* here, which removes the need to
# perform null checks before calling methods such as trigger() in your scripts.
var _instance: PooledAudioStreamPlayer3D = SoundManager.null_instance_3d()


func _ready() -> void:
	# As the SoundManager requires some preparation when the game loads, we need 
	# to hook into one or more of its lifecycle events before trying to play or 
	# instance an audio event. In this example, we've use the `updated` event 
	# as it's fired whenever any part of the SoundManager's state updates.
	SoundManager.updated.connect(on_sound_manager_updated)
	

func on_sound_manager_updated() -> void:
	# The method call below is an inbuilt guard-clause that'll help us avoid 
	# instancing an audio event when the SoundManager has not loaded, or when 
	# we've already replaced our Null instance with a real one further down.
	if SoundManager.should_skip_instancing(_instance):
		return
		
	_instance = SoundManager.instance_on_node("3d", "drums", moving_target)
	_instance.trigger()


================================================
FILE: examples/3D/3d.tscn
================================================
[gd_scene load_steps=11 format=3 uid="uid://3kswph4l3ba6"]

[ext_resource type="Script" path="res://examples/3d/3d.gd" id="1_4y185"]
[ext_resource type="Script" path="res://addons/resonate/sound_manager/sound_bank.gd" id="2_xngck"]
[ext_resource type="Script" path="res://addons/resonate/sound_manager/sound_event_resource.gd" id="3_3oot1"]
[ext_resource type="AudioStream" uid="uid://bcv4ev20vqxx2" path="res://audio/music_stems/house_drums_stem.mp3" id="4_lnff3"]
[ext_resource type="LabelSettings" uid="uid://tysg4xge4hsu" path="res://shared/title_label_settings.tres" id="7_ythoj"]
[ext_resource type="LabelSettings" uid="uid://bldprl7vev3uv" path="res://shared/description_label_settings.tres" id="8_6qcxl"]
[ext_resource type="Script" path="res://examples/3d/pivot.gd" id="9_leetf"]

[sub_resource type="Resource" id="Resource_5hea7"]
script = ExtResource("3_3oot1")
name = "drums"
bus = ""
volume = 0.0
pitch = 1.0
streams = Array[AudioStream]([ExtResource("4_lnff3")])

[sub_resource type="CapsuleMesh" id="CapsuleMesh_ajqyv"]

[sub_resource type="BoxMesh" id="BoxMesh_aceq4"]

[node name="3D" type="Node3D"]
script = ExtResource("1_4y185")

[node name="SoundBank" type="Node" parent="."]
script = ExtResource("2_xngck")
label = "3d"
events = Array[ExtResource("3_3oot1")]([SubResource("Resource_5hea7")])

[node name="Title" type="Label" parent="."]
offset_left = 100.0
offset_top = 50.0
offset_right = 208.0
offset_bottom = 103.0
text = "3d"
label_settings = ExtResource("7_ythoj")
uppercase = true

[node name="Description" type="Label" parent="."]
offset_left = 100.0
offset_top = 125.0
offset_right = 824.0
offset_bottom = 185.0
text = "This example uses the \"drums\" sound event (see the SoundBank.)
It has been attached to the box below, causing the sound to move along with it in 3D space.
A listener has been placed on the capsule mesh."
label_settings = ExtResource("8_6qcxl")

[node name="AudioListener3D" type="AudioListener3D" parent="."]

[node name="ListenerMesh" type="MeshInstance3D" parent="AudioListener3D"]
mesh = SubResource("CapsuleMesh_ajqyv")

[node name="Pivot" type="Node3D" parent="."]
script = ExtResource("9_leetf")

[node name="MovingTarget" type="MeshInstance3D" parent="Pivot"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, -7)
mesh = SubResource("BoxMesh_aceq4")
skeleton = NodePath("../..")

[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 0.866025, 0.5, 0, -0.5, 0.866025, 0, 19, 27)

[node name="Camera3D" type="Camera3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 0.707107, 0.707107, 0, -0.707107, 0.707107, 0, 10, 12)


================================================
FILE: examples/3D/pivot.gd
================================================
extends Node3D


func _process(p_delta):
	rotate_y(TAU * p_delta * 0.1)


================================================
FILE: examples/bulk_instancing/bulk_instancing.gd
================================================
extends Node2D


# For reference, it's worth taking a moment to inspect the SoundBank attached to 
# this example scene. SoundBanks hold the configuration for all of your sound 
# events and the variations (AudioStreams) associated with the event.


var _instance_note_one: PooledAudioStreamPlayer = SoundManager.null_instance()
var _instance_note_two: PooledAudioStreamPlayer = SoundManager.null_instance()
var _instance_note_three: PooledAudioStreamPlayer = SoundManager.null_instance()


func _ready() -> void:
	# As the SoundManager requires some preparation when the game loads, we need 
	# to hook into one or more of its lifecycle events before trying to play or 
	# instance an audio event. In this example, we've use the `updated` event 
	# as it's fired whenever any part of the SoundManager's state updates.
	SoundManager.updated.connect(on_sound_manager_updated)
	

func _input(p_event: InputEvent) -> void:
	if p_event.is_action_pressed("one"):
		_instance_note_one.trigger()
	
	if p_event.is_action_pressed("two"):
		_instance_note_two.trigger()
		
	if p_event.is_action_pressed("three"):
		_instance_note_three.trigger()


func on_sound_manager_updated() -> void:
	# We'll use the quick_instance method here as it'll take care of a number of
	# repetitive steps that would otherwise be necessary to set up all three 
	# instances. It also ensures that each instance is only registered once.
	
	_instance_note_one = SoundManager.quick_instance(_instance_note_one,
			SoundManager.instance.bind("bulk", "one"), self)
			
	_instance_note_two = SoundManager.quick_instance(_instance_note_two,
			SoundManager.instance.bind("bulk", "two"), self)
			
	_instance_note_three = SoundManager.quick_instance(_instance_note_three,
			SoundManager.instance.bind("bulk", "three"), self)


================================================
FILE: examples/bulk_instancing/bulk_instancing.tscn
================================================
[gd_scene load_steps=12 format=3 uid="uid://dgmta52w7wn21"]

[ext_resource type="Script" path="res://examples/bulk_instancing/bulk_instancing.gd" id="1_0yybq"]
[ext_resource type="Script" path="res://addons/resonate/sound_manager/sound_bank.gd" id="2_r0ory"]
[ext_resource type="Script" path="res://addons/resonate/sound_manager/sound_event_resource.gd" id="3_hf22l"]
[ext_resource type="AudioStream" uid="uid://de550fklyhu88" path="res://audio/sounds/note_c5.wav" id="4_uqepo"]
[ext_resource type="AudioStream" uid="uid://b35ao3bdtstka" path="res://audio/sounds/note_e5.wav" id="5_w24x2"]
[ext_resource type="AudioStream" uid="uid://bmg3dvylup5k" path="res://audio/sounds/note_g5.wav" id="6_1thix"]
[ext_resource type="LabelSettings" uid="uid://tysg4xge4hsu" path="res://shared/title_label_settings.tres" id="7_mpcft"]
[ext_resource type="LabelSettings" uid="uid://bldprl7vev3uv" path="res://shared/description_label_settings.tres" id="8_3ggq1"]

[sub_resource type="Resource" id="Resource_nfhjk"]
script = ExtResource("3_hf22l")
name = "one"
bus = ""
volume = 0.0
pitch = 1.0
streams = Array[AudioStream]([ExtResource("4_uqepo")])

[sub_resource type="Resource" id="Resource_5d8ei"]
script = ExtResource("3_hf22l")
name = "two"
bus = ""
volume = 0.0
pitch = 1.0
streams = Array[AudioStream]([ExtResource("5_w24x2")])

[sub_resource type="Resource" id="Resource_8ke84"]
script = ExtResource("3_hf22l")
name = "three"
bus = ""
volume = 0.0
pitch = 1.0
streams = Array[AudioStream]([ExtResource("6_1thix")])

[node name="BulkInstancing" type="Node2D"]
script = ExtResource("1_0yybq")

[node name="SoundBank" type="Node" parent="."]
script = ExtResource("2_r0ory")
label = "bulk"
events = Array[ExtResource("3_hf22l")]([SubResource("Resource_nfhjk"), SubResource("Resource_5d8ei"), SubResource("Resource_8ke84")])

[node name="Title" type="Label" parent="."]
offset_left = 100.0
offset_top = 50.0
offset_right = 208.0
offset_bottom = 103.0
text = "Bulk Instancing"
label_settings = ExtResource("7_mpcft")
uppercase = true

[node name="Description" type="Label" parent="."]
offset_left = 100.0
offset_top = 125.0
offset_right = 824.0
offset_bottom = 185.0
text = "This example instances 3 sound events from a single bank.
Press \"1\" on your keyboard to trigger the first event.
Press \"2\" on your keyboard to trigger the second event.
Press \"3\" on your keyboard to trigger the third event.
"
label_settings = ExtResource("8_3ggq1")


================================================
FILE: examples/music/music.gd
================================================
extends Node2D


# For reference, it's worth taking a moment to inspect the MusicBank attached to 
# this example scene. MusicBanks hold the configuration for all of your music 
# tracks and the stems (MusicStemResources) associated with the music track.


@onready var stem_details = $StemDetails

const _TRACKS: Array[String] = ["house", "breakbeat"]

var _is_playing: bool
var _track_number: int = 0
var _track_name: String = _TRACKS[_track_number]


func _ready() -> void:
	# As the MusicManager requires some preparation when the game loads, we need 
	# to hook into one or more of its lifecycle events before trying to play a
	# music track and/or stems. In this example, we've use the `updated` event 
	# as it's fired whenever any part of the MusicManager's state updates.
	MusicManager.updated.connect(on_music_manager_updated)


func _input(p_event: InputEvent) -> void:
	if p_event.is_action_pressed("one"):
		MusicManager.enable_stem("melody")
		
	if p_event.is_action_released("one"):
		MusicManager.disable_stem("melody")
		
	if p_event.is_action_pressed("two"):
		MusicManager.enable_stem("drums")
		
	if p_event.is_action_released("two"):
		MusicManager.disable_stem("drums")
		
	if p_event.is_action_pressed("three"):
		_track_number = _track_number + 1 if (_track_number + 1) < _TRACKS.size() else 0
		_track_name = _TRACKS[_track_number]
		
		MusicManager.play("instrumental", _track_name)
				
	if p_event.is_action_pressed("four"):
		MusicManager.stop()


func _process(_p_delta):
	if not MusicManager.is_playing():
		stem_details.text = "No music playing."
		return
		
	var melody_stem = MusicManager.get_stem_details("melody")
	var drums_stem = MusicManager.get_stem_details("drums")
	
	if melody_stem == null or drums_stem == null:
		stem_details.text = "Stem details unavailable."
		return
	
	stem_details.text = """Current track: %s
	Melody stem:
	 - Volume: %ddB
	 - Enabled: %s
	Drums stem:
	 - Volume: %ddB
	 - Enabled: %s
	""" % [_track_name, melody_stem.volume, melody_stem.enabled, drums_stem.volume, drums_stem.enabled]


func on_music_manager_updated() -> void:
	# The method call below is an inbuilt guard-clause that'll help us avoid playing 
	# a music track and/or stems when the MusicManager has not loaded, or when we've 
	# already set the `_is_playing` variable to true (returned by the play method).
	if MusicManager.should_skip_playing(_is_playing):
		return
		
	_is_playing = MusicManager.play("instrumental", _track_name)


================================================
FILE: examples/music/music.tscn
================================================
[gd_scene load_steps=22 format=3 uid="uid://jw5kbo20k3hh"]

[ext_resource type="Script" path="res://examples/music/music.gd" id="1_b17ub"]
[ext_resource type="Script" path="res://addons/resonate/music_manager/music_bank.gd" id="2_cql1n"]
[ext_resource type="Script" path="res://addons/resonate/music_manager/music_track_resource.gd" id="3_o75hm"]
[ext_resource type="Script" path="res://addons/resonate/music_manager/music_stem_resource.gd" id="4_ksop8"]
[ext_resource type="LabelSettings" uid="uid://bldprl7vev3uv" path="res://shared/description_label_settings.tres" id="5_cg2qn"]
[ext_resource type="LabelSettings" uid="uid://tysg4xge4hsu" path="res://shared/title_label_settings.tres" id="5_vbmlt"]
[ext_resource type="AudioStream" uid="uid://sjkaeg8p0qig" path="res://audio/music_stems/breakbeat_pad_stem.mp3" id="5_wctds"]
[ext_resource type="AudioStream" uid="uid://ba8e5llasqjhk" path="res://audio/music_stems/breakbeat_melody_stem.mp3" id="6_hgf2k"]
[ext_resource type="AudioStream" uid="uid://clkjvyfm227dt" path="res://audio/music_stems/breakbeat_drums_stem.mp3" id="7_57fs0"]
[ext_resource type="AudioStream" uid="uid://bhbxnxeuk62q0" path="res://audio/music_stems/house_bass_pad_stem.mp3" id="8_hxbnm"]
[ext_resource type="AudioStream" uid="uid://dq5s5n0folm21" path="res://audio/music_stems/house_melody_stem.mp3" id="9_3rpxp"]
[ext_resource type="AudioStream" uid="uid://bcv4ev20vqxx2" path="res://audio/music_stems/house_drums_stem.mp3" id="10_13th8"]
[ext_resource type="LabelSettings" uid="uid://bnsew6gimofj5" path="res://shared/stats_label_settings.tres" id="13_787wl"]

[sub_resource type="Resource" id="Resource_ognhn"]
script = ExtResource("4_ksop8")
name = "pad"
enabled = true
volume = 0.0
stream = ExtResource("5_wctds")

[sub_resource type="Resource" id="Resource_4apoh"]
script = ExtResource("4_ksop8")
name = "melody"
enabled = false
volume = 0.0
stream = ExtResource("6_hgf2k")

[sub_resource type="Resource" id="Resource_x14g1"]
script = ExtResource("4_ksop8")
name = "drums"
enabled = false
volume = 0.0
stream = ExtResource("7_57fs0")

[sub_resource type="Resource" id="Resource_1ncxf"]
script = ExtResource("3_o75hm")
name = "breakbeat"
bus = ""
stems = Array[ExtResource("4_ksop8")]([SubResource("Resource_ognhn"), SubResource("Resource_4apoh"), SubResource("Resource_x14g1")])

[sub_resource type="Resource" id="Resource_ee683"]
script = ExtResource("4_ksop8")
name = "pad"
enabled = true
volume = 0.0
stream = ExtResource("8_hxbnm")

[sub_resource type="Resource" id="Resource_64pmf"]
script = ExtResource("4_ksop8")
name = "melody"
enabled = false
volume = 0.0
stream = ExtResource("9_3rpxp")

[sub_resource type="Resource" id="Resource_slh24"]
script = ExtResource("4_ksop8")
name = "drums"
enabled = false
volume = 0.0
stream = ExtResource("10_13th8")

[sub_resource type="Resource" id="Resource_pmaoq"]
script = ExtResource("3_o75hm")
name = "house"
bus = ""
stems = Array[ExtResource("4_ksop8")]([SubResource("Resource_ee683"), SubResource("Resource_64pmf"), SubResource("Resource_slh24")])

[node name="Music" type="Node2D"]
script = ExtResource("1_b17ub")

[node name="MusicBank" type="Node" parent="."]
script = ExtResource("2_cql1n")
label = "instrumental"
tracks = Array[ExtResource("3_o75hm")]([SubResource("Resource_1ncxf"), SubResource("Resource_pmaoq")])

[node name="Title" type="Label" parent="."]
offset_left = 100.0
offset_top = 50.0
offset_right = 208.0
offset_bottom = 103.0
text = "Music"
label_settings = ExtResource("5_vbmlt")
uppercase = true

[node name="Description" type="Label" parent="."]
offset_left = 100.0
offset_top = 125.0
offset_right = 824.0
offset_bottom = 185.0
text = "This example automatically plays the \"exploration\" music track (see MusicBank.)
Hold down \"1\" on your keyboard to enable the \"melody\" stem.
Hold down \"2\" on your keyboard to enable the \"drums\" stem.
Press \"3\" on your keyboard to start the next track.
Press \"4\" on your keyboard to stop the current track."
label_settings = ExtResource("5_cg2qn")

[node name="StemDetails" type="Label" parent="."]
offset_left = 100.0
offset_top = 315.0
offset_right = 824.0
offset_bottom = 375.0
text = "{ stem details }"
label_settings = ExtResource("13_787wl")


================================================
FILE: examples/polyphonic/polyphonic.gd
================================================
extends Node2D


# For reference, it's worth taking a moment to inspect the SoundBank attached to 
# this example scene. SoundBanks hold the configuration for all of your sound 
# events and the variations (AudioStreams) associated with the event.


# We instantiate a Null PooledAudioStreamPlayer* here, which removes the need to
# perform null checks before calling methods such as trigger() in your scripts.
var _instance: PooledAudioStreamPlayer = SoundManager.null_instance()


func _ready() -> void:
	# As the SoundManager requires some preparation when the game loads, we need 
	# to hook into one or more of its lifecycle events before trying to play or 
	# instance an audio event. In this example, we've use the `updated` event 
	# as it's fired whenever any part of the SoundManager's state updates.
	SoundManager.updated.connect(on_sound_manager_updated)
	

func _input(p_event: InputEvent) -> void:
	if p_event.is_action_pressed("one"):
		_instance.trigger()
		
	if p_event.is_action_pressed("two"):
		_instance.trigger_varied(randf_range(0.9, 1.2), randf_range(-2.0, 0.0))


func on_sound_manager_updated() -> void:
	# The method call below is an inbuilt guard-clause that'll help us avoid 
	# instancing an audio event when the SoundManager has not loaded, or when 
	# we've already replaced our Null instance with a real one further down.
	if SoundManager.should_skip_instancing(_instance):
		return
	
	_instance = SoundManager.instance_poly("polyphonic", "blaster")


================================================
FILE: examples/polyphonic/polyphonic.tscn
================================================
[gd_scene load_steps=8 format=3 uid="uid://ys1gdvqjju2q"]

[ext_resource type="Script" path="res://examples/polyphonic/polyphonic.gd" id="1_apacb"]
[ext_resource type="Script" path="res://addons/resonate/sound_manager/sound_bank.gd" id="2_pol04"]
[ext_resource type="Script" path="res://addons/resonate/sound_manager/sound_event_resource.gd" id="3_s5fje"]
[ext_resource type="AudioStream" uid="uid://cy4r18lxb4rbh" path="res://audio/sounds/blaster.wav" id="4_pis6r"]
[ext_resource type="LabelSettings" uid="uid://tysg4xge4hsu" path="res://shared/title_label_settings.tres" id="7_v2tg6"]
[ext_resource type="LabelSettings" uid="uid://bldprl7vev3uv" path="res://shared/description_label_settings.tres" id="8_qupom"]

[sub_resource type="Resource" id="Resource_hqydm"]
script = ExtResource("3_s5fje")
name = "blaster"
bus = ""
volume = 0.0
pitch = 1.0
streams = Array[AudioStream]([ExtResource("4_pis6r")])

[node name="Polyphonic" type="Node2D"]
script = ExtResource("1_apacb")

[node name="SoundBank" type="Node" parent="."]
script = ExtResource("2_pol04")
label = "polyphonic"
events = Array[ExtResource("3_s5fje")]([SubResource("Resource_hqydm")])

[node name="Title" type="Label" parent="."]
offset_left = 100.0
offset_top = 50.0
offset_right = 208.0
offset_bottom = 103.0
text = "Polyphonic"
label_settings = ExtResource("7_v2tg6")
uppercase = true

[node name="Description" type="Label" parent="."]
offset_left = 100.0
offset_top = 125.0
offset_right = 824.0
offset_bottom = 185.0
text = "This example uses the \"blaster\" sound event (see SoundBank.)
Press \"1\" on your keyboard to trigger the event.
Press \"2\" on you keyboard to trigger the event with slight pitch and volume variations."
label_settings = ExtResource("8_qupom")


================================================
FILE: examples/scene_changes/scene_changes.gd
================================================
extends Node2D


# This script is simply glue code for the two other example scenes in this folder.
# It's worth inspecting scene_one and scene_two to learn how to handle runtime 
# scene insertion and deletion while utilising the Sound and Music Managers.


@onready var scene_details = $SceneDetails

const _SCENE_NAMES: Array[String] = ["scene_one", "scene_two"]
const _SCENES: Dictionary = {
	"scene_one": preload("res://examples/scene_changes/scene_one.tscn"),
	"scene_two": preload("res://examples/scene_changes/scene_two.tscn"),
}

var _current_scene: int = 0
var _current_scene_ref: Node


func _ready() -> void:
	load_scene(_current_scene)
	

func _process(_p_delta) -> void:
	scene_details.text = "Current scene: %s" % _SCENE_NAMES[_current_scene]


func _input(p_event) -> void:
	if p_event.is_action_pressed("one"):
		_current_scene = _current_scene + 1 if (_current_scene + 1) < _SCENE_NAMES.size() else 0
		load_scene(_current_scene)


func load_scene(p_index: int) -> void:
	if _current_scene_ref != null:
		remove_child(_current_scene_ref)
	
	var scene_name = _SCENE_NAMES[p_index]
	_current_scene_ref = _SCENES[scene_name].instantiate()
	
	add_child(_current_scene_ref)


================================================
FILE: examples/scene_changes/scene_changes.tscn
================================================
[gd_scene load_steps=5 format=3 uid="uid://bcqg6ao82u1by"]

[ext_resource type="Script" path="res://examples/scene_changes/scene_changes.gd" id="1_p51s5"]
[ext_resource type="LabelSettings" uid="uid://tysg4xge4hsu" path="res://shared/title_label_settings.tres" id="2_rh3pj"]
[ext_resource type="LabelSettings" uid="uid://bldprl7vev3uv" path="res://shared/description_label_settings.tres" id="3_k4a5g"]
[ext_resource type="LabelSettings" uid="uid://bnsew6gimofj5" path="res://shared/stats_label_settings.tres" id="4_gpwbw"]

[node name="SceneChanges" type="Node2D"]
script = ExtResource("1_p51s5")

[node name="Title" type="Label" parent="."]
offset_left = 100.0
offset_top = 50.0
offset_right = 208.0
offset_bottom = 103.0
text = "Scene Changes"
label_settings = ExtResource("2_rh3pj")
uppercase = true

[node name="Description" type="Label" parent="."]
offset_left = 100.0
offset_top = 125.0
offset_right = 824.0
offset_bottom = 185.0
text = "This example loads different scenes at runtime to demonstrate automatic bank detection.
Press \"1\" on your keyboard to cycle scenes."
label_settings = ExtResource("3_k4a5g")

[node name="SceneDetails" type="Label" parent="."]
offset_left = 100.0
offset_top = 215.0
offset_right = 824.0
offset_bottom = 275.0
text = "{ scene details }"
label_settings = ExtResource("4_gpwbw")


================================================
FILE: examples/scene_changes/scene_one.gd
================================================
extends Node2D


# For reference, it's worth taking a moment to inspect the MusicBank attached to 
# this example scene. MusicBanks hold the configuration for all of your music 
# tracks and the stems (MusicStemResources) associated with the music track.


@onready var timer: Timer = $Timer

var _is_playing: bool


func _ready():
	# As the MusicManager requires some preparation when the game loads, we need 
	# to hook into one or more of its lifecycle events before trying to play a
	# music track and/or stems. In this example, we've use the `updated` event 
	# as it's fired whenever any part of the MusicManager's state updates.
	MusicManager.updated.connect(on_music_manager_updated)


func on_music_manager_updated() -> void:
	# The method call below is an inbuilt guard-clause that'll help us avoid playing 
	# a music track and/or stems when the MusicManager has not loaded, or when we've 
	# already set the `_is_playing` variable to true (returned by the play method).
	if MusicManager.should_skip_playing(_is_playing):
		return
	
	_is_playing = MusicManager.play("scene_one", "track_a")
	
	# If you know that a scene will be inserted or removed at runtime, and you
	# don't want to hook into lifecycle events to ensure your music tracks
	# stop playing, you can instruct the MusicManager to do it for you.
	MusicManager.stop_on_exit(self, "scene_one", "track_a")


================================================
FILE: examples/scene_changes/scene_one.tscn
================================================
[gd_scene load_steps=8 format=3 uid="uid://qfk8ucywmppo"]

[ext_resource type="Script" path="res://examples/scene_changes/scene_one.gd" id="1_ac5rj"]
[ext_resource type="Script" path="res://addons/resonate/music_manager/music_bank.gd" id="4_fi4fr"]
[ext_resource type="Script" path="res://addons/resonate/music_manager/music_track_resource.gd" id="5_wtd24"]
[ext_resource type="Script" path="res://addons/resonate/music_manager/music_stem_resource.gd" id="6_4afm1"]
[ext_resource type="AudioStream" uid="uid://bcv4ev20vqxx2" path="res://audio/music_stems/house_drums_stem.mp3" id="8_qa3at"]

[sub_resource type="Resource" id="Resource_6vob4"]
script = ExtResource("6_4afm1")
name = "stem_a"
enabled = true
volume = 0.0
stream = ExtResource("8_qa3at")

[sub_resource type="Resource" id="Resource_q7ce0"]
script = ExtResource("5_wtd24")
name = "track_a"
bus = ""
stems = Array[ExtResource("6_4afm1")]([SubResource("Resource_6vob4")])

[node name="SceneOne" type="Node2D"]
script = ExtResource("1_ac5rj")

[node name="MusicBank" type="Node" parent="."]
script = ExtResource("4_fi4fr")
label = "scene_one"
tracks = Array[ExtResource("5_wtd24")]([SubResource("Resource_q7ce0")])

[node name="Timer" type="Timer" parent="."]
autostart = true


================================================
FILE: examples/scene_changes/scene_two.gd
================================================
extends Node2D


# For reference, it's worth taking a moment to inspect the SoundBank attached to 
# this example scene. SoundBanks hold the configuration for all of your sound 
# events and the variations (AudioStreams) associated with the event.


@onready var timer: Timer = $Timer

# We instantiate a Null PooledAudioStreamPlayer* here, which removes the need to
# perform null checks before calling methods such as trigger() in your scripts.
var _instance: PooledAudioStreamPlayer = SoundManager.null_instance()


func _ready():
	# As the SoundManager requires some preparation when the game loads, we need 
	# to hook into one or more of its lifecycle events before trying to play or 
	# instance an audio event. In this example, we've use the `updated` event 
	# as it's fired whenever any part of the SoundManager's state updates.
	SoundManager.updated.connect(on_sound_manager_updated)
	
	timer.timeout.connect(on_timer_timeout)


func on_sound_manager_updated() -> void:
	# The method call below is an inbuilt guard-clause that'll help us avoid 
	# instancing an audio event when the SoundManager has not loaded, or when 
	# we've already replaced our Null instance with a real one further down.
	if SoundManager.should_skip_instancing(_instance):
		return

	_instance = SoundManager.instance("scene_two", "note")

	# If you know that a scene will be inserted or removed at runtime, and you
	# don't want to hook into lifecycle events to ensure your instances are
	# being released, you can instruct the SoundManager to do it for you.
	SoundManager.release_on_exit(self, _instance, true)


func on_timer_timeout():
	_instance.trigger()


================================================
FILE: examples/scene_changes/scene_two.tscn
================================================
[gd_scene load_steps=8 format=3 uid="uid://cyme2yjik1ioe"]

[ext_resource type="Script" path="res://addons/resonate/sound_manager/sound_bank.gd" id="1_cdph3"]
[ext_resource type="Script" path="res://examples/scene_changes/scene_two.gd" id="1_fw31x"]
[ext_resource type="Script" path="res://addons/resonate/sound_manager/sound_event_resource.gd" id="2_la3fn"]
[ext_resource type="AudioStream" uid="uid://de550fklyhu88" path="res://audio/sounds/note_c5.wav" id="3_ll4y0"]
[ext_resource type="AudioStream" uid="uid://b35ao3bdtstka" path="res://audio/sounds/note_e5.wav" id="5_a7sct"]
[ext_resource type="AudioStream" uid="uid://bmg3dvylup5k" path="res://audio/sounds/note_g5.wav" id="6_37wm4"]

[sub_resource type="Resource" id="Resource_tk6xx"]
script = ExtResource("2_la3fn")
name = "note"
bus = ""
volume = 0.0
pitch = 1.0
streams = Array[AudioStream]([ExtResource("3_ll4y0"), ExtResource("5_a7sct"), ExtResource("6_37wm4")])

[node name="SceneTwo" type="Node2D"]
script = ExtResource("1_fw31x")

[node name="SoundBank" type="Node" parent="."]
script = ExtResource("1_cdph3")
label = "scene_two"
events = Array[ExtResource("2_la3fn")]([SubResource("Resource_tk6xx")])

[node name="Timer" type="Timer" parent="."]
autostart = true


================================================
FILE: examples/simple/simple.gd
================================================
extends Node2D


# For reference, it's worth taking a moment to inspect the SoundBank attached to 
# this example scene. SoundBanks hold the configuration for all of your sound 
# events and the variations (AudioStreams) associated with the event.


func _input(p_event: InputEvent) -> void:
	if p_event.is_action_pressed("one"):
		SoundManager.play("simple", "note")
		
	if p_event.is_action_pressed("two"):
		SoundManager.play_varied("simple", "note", randf_range(0.8, 1.2), randf_range(-8, 0))


================================================
FILE: examples/simple/simple.tscn
================================================
[gd_scene load_steps=10 format=3 uid="uid://dfy6ygeu8pya5"]

[ext_resource type="Script" path="res://examples/simple/simple.gd" id="1_fvr5o"]
[ext_resource type="LabelSettings" uid="uid://tysg4xge4hsu" path="res://shared/title_label_settings.tres" id="2_6x4oj"]
[ext_resource type="LabelSettings" uid="uid://bldprl7vev3uv" path="res://shared/description_label_settings.tres" id="3_ljjap"]
[ext_resource type="Script" path="res://addons/resonate/sound_manager/sound_event_resource.gd" id="3_qyv4q"]
[ext_resource type="Script" path="res://addons/resonate/sound_manager/sound_bank.gd" id="4_cwmja"]
[ext_resource type="AudioStream" uid="uid://de550fklyhu88" path="res://audio/sounds/note_c5.wav" id="4_it7rc"]
[ext_resource type="AudioStream" uid="uid://b35ao3bdtstka" path="res://audio/sounds/note_e5.wav" id="5_vxpmp"]
[ext_resource type="AudioStream" uid="uid://bmg3dvylup5k" path="res://audio/sounds/note_g5.wav" id="6_kifph"]

[sub_resource type="Resource" id="Resource_1ir4x"]
script = ExtResource("3_qyv4q")
name = "note"
bus = ""
volume = 0.0
pitch = 1.0
streams = Array[AudioStream]([ExtResource("4_it7rc"), ExtResource("5_vxpmp"), ExtResource("6_kifph")])

[node name="Simple" type="Node2D"]
script = ExtResource("1_fvr5o")

[node name="SoundBank" type="Node" parent="."]
script = ExtResource("4_cwmja")
label = "simple"
events = Array[ExtResource("3_qyv4q")]([SubResource("Resource_1ir4x")])

[node name="Title" type="Label" parent="."]
offset_left = 100.0
offset_top = 50.0
offset_right = 208.0
offset_bottom = 103.0
text = "Simple"
label_settings = ExtResource("2_6x4oj")
uppercase = true

[node name="Description" type="Label" parent="."]
offset_left = 100.0
offset_top = 125.0
offset_right = 824.0
offset_bottom = 185.0
text = "This example uses the \"note\" sound event (see SoundBank.)
Press \"1\" on your keyboard to trigger the event.
Press \"2\" on your keyboard to trigger the event with varied pitch and volume.
Each time an event is triggered, one of its variations are chosen randomly."
label_settings = ExtResource("3_ljjap")


================================================
FILE: icon.svg.import
================================================
[remap]

importer="texture"
type="CompressedTexture2D"
uid="uid://dxujf3r8cbmga"
path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"
metadata={
"vram_texture": false
}

[deps]

source_file="res://icon.svg"
dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.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


================================================
FILE: project.godot
================================================
; Engine configuration file.
; It's best edited using the editor UI and not directly,
; since the parameters that go here are not all obvious.
;
; Format:
;   [section] ; section goes between []
;   param=value ; assign values to parameters

config_version=5

[application]

config/name="Resonate"
config/features=PackedStringArray("4.2", "Forward Plus")
config/icon="res://icon.svg"

[audio]

manager/sound/pool_1D_size=16
manager/sound/pool_2D_size=16
manager/sound/pool_3D_size=16
manager/sound/max_polyphony=32
manager/sound/bus="Sound"
manager/music/bus="Music"

[autoload]

SoundManager="*res://addons/resonate/sound_manager/sound_manager.gd"
MusicManager="*res://addons/resonate/music_manager/music_manager.gd"

[display]

window/size/viewport_width=1200
window/size/viewport_height=800

[editor_plugins]

enabled=PackedStringArray("res://addons/resonate/plugin.cfg")

[file_customization]

folder_colors={
"res://addons/resonate/": "orange"
}

[input]

one={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":49,"key_label":0,"unicode":49,"echo":false,"script":null)
]
}
two={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":50,"key_label":0,"unicode":50,"echo":false,"script":null)
]
}
three={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":51,"key_label":0,"unicode":51,"echo":false,"script":null)
]
}
four={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":52,"key_label":0,"unicode":52,"echo":false,"script":null)
]
}

[rendering]

environment/defaults/default_clear_color=Color(0.0588235, 0.0588235, 0.0588235, 1)


================================================
FILE: reasonate-github-header.jpg.import
================================================
[remap]

importer="texture"
type="CompressedTexture2D"
uid="uid://bb75idsxlhjl7"
path="res://.godot/imported/reasonate-github-header.jpg-02c12c49b558975276fb580939e5b7bd.ctex"
metadata={
"vram_texture": false
}

[deps]

source_file="res://reasonate-github-header.jpg"
dest_files=["res://.godot/imported/reasonate-github-header.jpg-02c12c49b558975276fb580939e5b7bd.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


================================================
FILE: release-please-config.json
================================================
{
    "packages": {
        ".": {
            "release-type": "simple",
            "extra-files": [
                "addons/resonate/plugin.cfg"
            ]
        }
    }
}


================================================
FILE: resonate-logo.png.import
================================================
[remap]

importer="texture"
type="CompressedTexture2D"
uid="uid://dughwn5cisraq"
path="res://.godot/imported/resonate-logo.png-6a8a4a98c9457c7843a50699ddface48.ctex"
metadata={
"vram_texture": false
}

[deps]

source_file="res://resonate-logo.png"
dest_files=["res://.godot/imported/resonate-logo.png-6a8a4a98c9457c7843a50699ddface48.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


================================================
FILE: shared/description_label_settings.tres
================================================
[gd_resource type="LabelSettings" format=3 uid="uid://bldprl7vev3uv"]

[resource]
line_spacing = 8.0
font_size = 18


================================================
FILE: shared/stats_label_settings.tres
================================================
[gd_resource type="LabelSettings" format=3 uid="uid://bnsew6gimofj5"]

[resource]
line_spacing = 8.0
font_color = Color(0.117647, 0.8, 0.858824, 1)


================================================
FILE: shared/title_label_settings.tres
================================================
[gd_resource type="LabelSettings" format=3 uid="uid://tysg4xge4hsu"]

[resource]
font_size = 38
font_color = Color(0.92549, 0.490196, 0, 1)
Download .txt
gitextract_1afov26k/

├── .gitattributes
├── .github/
│   ├── FUNDING.yml
│   └── workflows/
│       └── main.yml
├── .gitignore
├── .release-please-manifest.json
├── CHANGELOG.md
├── LICENSE
├── README.md
├── addons/
│   └── resonate/
│       ├── LICENSE
│       ├── music_manager/
│       │   ├── music_bank.gd
│       │   ├── music_bank.svg.import
│       │   ├── music_manager.gd
│       │   ├── music_stem_resource.gd
│       │   ├── music_track_resource.gd
│       │   └── stemmed_music_stream_player.gd
│       ├── plugin.cfg
│       ├── plugin.gd
│       ├── shared/
│       │   ├── resonate_settings.gd
│       │   └── resonate_utils.gd
│       └── sound_manager/
│           ├── null_pooled_audio_stream_player.gd
│           ├── null_pooled_audio_stream_player_2d.gd
│           ├── null_pooled_audio_stream_player_3d.gd
│           ├── pool_entity.gd
│           ├── pooled_audio_stream_player.gd
│           ├── pooled_audio_stream_player_2d.gd
│           ├── pooled_audio_stream_player_3d.gd
│           ├── sound_bank.gd
│           ├── sound_bank.svg.import
│           ├── sound_event_resource.gd
│           └── sound_manager.gd
├── audio/
│   ├── music_stems/
│   │   ├── breakbeat_drums_stem.mp3.import
│   │   ├── breakbeat_melody_stem.mp3.import
│   │   ├── breakbeat_pad_stem.mp3.import
│   │   ├── house_bass_pad_stem.mp3.import
│   │   ├── house_drums_stem.mp3.import
│   │   └── house_melody_stem.mp3.import
│   └── sounds/
│       ├── blaster.wav.import
│       ├── note_c5.wav.import
│       ├── note_e5.wav.import
│       └── note_g5.wav.import
├── default_bus_layout.tres
├── docs/
│   ├── getting-started.md
│   ├── images/
│   │   ├── add-music-bank-node.jpg.import
│   │   ├── add-sound-bank-node.jpg.import
│   │   ├── music-banks.png.import
│   │   ├── music-manager.png.import
│   │   ├── set-soundbank-label.jpg.import
│   │   ├── sound-banks.png.import
│   │   └── sound-manager.png.import
│   ├── music-manager.md
│   └── sound-manager.md
├── examples/
│   ├── 2D/
│   │   ├── 2d.gd
│   │   ├── 2d.tscn
│   │   └── moving_target.gd
│   ├── 3D/
│   │   ├── 3d.gd
│   │   ├── 3d.tscn
│   │   └── pivot.gd
│   ├── bulk_instancing/
│   │   ├── bulk_instancing.gd
│   │   └── bulk_instancing.tscn
│   ├── music/
│   │   ├── music.gd
│   │   └── music.tscn
│   ├── polyphonic/
│   │   ├── polyphonic.gd
│   │   └── polyphonic.tscn
│   ├── scene_changes/
│   │   ├── scene_changes.gd
│   │   ├── scene_changes.tscn
│   │   ├── scene_one.gd
│   │   ├── scene_one.tscn
│   │   ├── scene_two.gd
│   │   └── scene_two.tscn
│   └── simple/
│       ├── simple.gd
│       └── simple.tscn
├── icon.svg.import
├── project.godot
├── reasonate-github-header.jpg.import
├── release-please-config.json
├── resonate-logo.png.import
└── shared/
    ├── description_label_settings.tres
    ├── stats_label_settings.tres
    └── title_label_settings.tres
Condensed preview — 79 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (160K chars).
[
  {
    "path": ".gitattributes",
    "chars": 80,
    "preview": "# Normalize EOL for all files that Git considers text files.\n* text=auto eol=lf\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "chars": 20,
    "preview": "patreon: hugemenace\n"
  },
  {
    "path": ".github/workflows/main.yml",
    "chars": 1339,
    "preview": "on:\n  push:\n    branches:\n      - main\n\npermissions:\n  checks: write\n  contents: write\n  pull-requests: write\n\nname: Rel"
  },
  {
    "path": ".gitignore",
    "chars": 36,
    "preview": "# Godot 4+ specific ignores\n.godot/\n"
  },
  {
    "path": ".release-please-manifest.json",
    "chars": 21,
    "preview": "{\n    \".\": \"2.4.0\"\n}\n"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 11556,
    "preview": "# Changelog\n\n## [2.4.0](https://github.com/hugemenace/resonate/compare/v2.3.4...v2.4.0) (2024-03-08)\n\n\n### Features\n\n* a"
  },
  {
    "path": "LICENSE",
    "chars": 1064,
    "preview": "Copyright 2024 I.A JONES & T. STRATHEARN\n\nPermission is hereby granted, free of charge, to any person obtaining a copy o"
  },
  {
    "path": "README.md",
    "chars": 2065,
    "preview": "<img src='reasonate-github-header.jpg' width='100%'>\n\n# ***Resonate***\n\n> [!IMPORTANT]\n> This project was originally dev"
  },
  {
    "path": "addons/resonate/LICENSE",
    "chars": 1064,
    "preview": "Copyright 2024 I.A JONES & T. STRATHEARN\n\nPermission is hereby granted, free of charge, to any person obtaining a copy o"
  },
  {
    "path": "addons/resonate/music_manager/music_bank.gd",
    "chars": 556,
    "preview": "class_name MusicBank\nextends Node\n## A container used to store & group music tracks in your scene.\n\n\n## This bank's uniq"
  },
  {
    "path": "addons/resonate/music_manager/music_bank.svg.import",
    "chars": 891,
    "preview": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://bxfmecl088q55\"\npath=\"res://.godot/imported/music_bank."
  },
  {
    "path": "addons/resonate/music_manager/music_manager.gd",
    "chars": 11865,
    "preview": "extends Node\n## The MusicManager is responsible for all music in your game.\n##\n## It manages the playback of music track"
  },
  {
    "path": "addons/resonate/music_manager/music_stem_resource.gd",
    "chars": 523,
    "preview": "class_name MusicStemResource\nextends Resource\n## A container used to store the details of one particular music track's s"
  },
  {
    "path": "addons/resonate/music_manager/music_track_resource.gd",
    "chars": 546,
    "preview": "class_name MusicTrackResource\nextends Resource\n## A container used to store the details of a music track and all of its "
  },
  {
    "path": "addons/resonate/music_manager/stemmed_music_stream_player.gd",
    "chars": 7148,
    "preview": "class_name StemmedMusicStreamPlayer\nextends AudioStreamPlayer\n## An extended AudioStreamPlayer capable of managing and p"
  },
  {
    "path": "addons/resonate/plugin.cfg",
    "chars": 150,
    "preview": "[plugin]\n\nname=\"Resonate\"\ndescription=\"\"\nauthor=\"HugeMenace\"\n;x-release-please-start-version\nversion=\"2.4.0\"\n;x-release-"
  },
  {
    "path": "addons/resonate/plugin.gd",
    "chars": 3319,
    "preview": "@tool\nextends EditorPlugin\n\n\nconst ResonateSettings = preload(\"shared/resonate_settings.gd\")\nvar _settings = ResonateSet"
  },
  {
    "path": "addons/resonate/shared/resonate_settings.gd",
    "chars": 918,
    "preview": "extends RefCounted\n\n\nconst SOUND_BANK_BUS_SETTING_NAME = \"audio/manager/sound/bus\"\nconst SOUND_BANK_BUS_SETTING_DEFAULT "
  },
  {
    "path": "addons/resonate/shared/resonate_utils.gd",
    "chars": 956,
    "preview": "class_name ResonateUtils\nextends RefCounted\n\n\nstatic func is_stream_looped(p_stream) -> bool:\n\tif p_stream is AudioStrea"
  },
  {
    "path": "addons/resonate/sound_manager/null_pooled_audio_stream_player.gd",
    "chars": 1055,
    "preview": "class_name NullPooledAudioStreamPlayer\nextends PooledAudioStreamPlayer\n## An extension of PooledAudioStreamPlayer that n"
  },
  {
    "path": "addons/resonate/sound_manager/null_pooled_audio_stream_player_2d.gd",
    "chars": 1075,
    "preview": "class_name NullPooledAudioStreamPlayer2D\nextends PooledAudioStreamPlayer2D\n## An extension of PooledAudioStreamPlayer2D "
  },
  {
    "path": "addons/resonate/sound_manager/null_pooled_audio_stream_player_3d.gd",
    "chars": 1075,
    "preview": "class_name NullPooledAudioStreamPlayer3D\nextends PooledAudioStreamPlayer3D\n## An extension of PooledAudioStreamPlayer3D "
  },
  {
    "path": "addons/resonate/sound_manager/pool_entity.gd",
    "chars": 5143,
    "preview": "class_name PoolEntity\nextends RefCounted\n## An abstract/static class to house all of the common PooledAudioStreamPlayer*"
  },
  {
    "path": "addons/resonate/sound_manager/pooled_audio_stream_player.gd",
    "chars": 3530,
    "preview": "class_name PooledAudioStreamPlayer\nextends AudioStreamPlayer\n## An extension of AudioStreamPlayer that manages sequentia"
  },
  {
    "path": "addons/resonate/sound_manager/pooled_audio_stream_player_2d.gd",
    "chars": 3544,
    "preview": "class_name PooledAudioStreamPlayer2D\nextends AudioStreamPlayer2D\n## An extension of AudioStreamPlayer2D that manages seq"
  },
  {
    "path": "addons/resonate/sound_manager/pooled_audio_stream_player_3d.gd",
    "chars": 3542,
    "preview": "class_name PooledAudioStreamPlayer3D\nextends AudioStreamPlayer3D\n## An extension of AudioStreamPlayer3D that manages seq"
  },
  {
    "path": "addons/resonate/sound_manager/sound_bank.gd",
    "chars": 556,
    "preview": "class_name SoundBank\nextends Node\n## A container used to store & group sound events in your scene.\n\n\n## This bank's uniq"
  },
  {
    "path": "addons/resonate/sound_manager/sound_bank.svg.import",
    "chars": 890,
    "preview": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://ntgipyp6jv34\"\npath=\"res://.godot/imported/sound_bank.s"
  },
  {
    "path": "addons/resonate/sound_manager/sound_event_resource.gd",
    "chars": 693,
    "preview": "class_name SoundEventResource\nextends Resource\n## The container used to store the details of a sound event.\n\n\n## This so"
  },
  {
    "path": "addons/resonate/sound_manager/sound_manager.gd",
    "chars": 18246,
    "preview": "extends Node\n## The SoundManager is responsible for all sound events in your game.\n##\n## It manages pools of 1D, 2D, and"
  },
  {
    "path": "audio/music_stems/breakbeat_drums_stem.mp3.import",
    "chars": 410,
    "preview": "[remap]\n\nimporter=\"mp3\"\ntype=\"AudioStreamMP3\"\nuid=\"uid://clkjvyfm227dt\"\npath=\"res://.godot/imported/breakbeat_drums_stem"
  },
  {
    "path": "audio/music_stems/breakbeat_melody_stem.mp3.import",
    "chars": 413,
    "preview": "[remap]\n\nimporter=\"mp3\"\ntype=\"AudioStreamMP3\"\nuid=\"uid://ba8e5llasqjhk\"\npath=\"res://.godot/imported/breakbeat_melody_ste"
  },
  {
    "path": "audio/music_stems/breakbeat_pad_stem.mp3.import",
    "chars": 403,
    "preview": "[remap]\n\nimporter=\"mp3\"\ntype=\"AudioStreamMP3\"\nuid=\"uid://sjkaeg8p0qig\"\npath=\"res://.godot/imported/breakbeat_pad_stem.mp"
  },
  {
    "path": "audio/music_stems/house_bass_pad_stem.mp3.import",
    "chars": 403,
    "preview": "[remap]\n\nimporter=\"mp3\"\ntype=\"AudioStreamMP3\"\nuid=\"uid://bhbxnxeuk62q0\"\npath=\"res://.godot/imported/house_bass_pad_stem."
  },
  {
    "path": "audio/music_stems/house_drums_stem.mp3.import",
    "chars": 394,
    "preview": "[remap]\n\nimporter=\"mp3\"\ntype=\"AudioStreamMP3\"\nuid=\"uid://bcv4ev20vqxx2\"\npath=\"res://.godot/imported/house_drums_stem.mp3"
  },
  {
    "path": "audio/music_stems/house_melody_stem.mp3.import",
    "chars": 397,
    "preview": "[remap]\n\nimporter=\"mp3\"\ntype=\"AudioStreamMP3\"\nuid=\"uid://dq5s5n0folm21\"\npath=\"res://.godot/imported/house_melody_stem.mp"
  },
  {
    "path": "audio/sounds/blaster.wav.import",
    "chars": 491,
    "preview": "[remap]\n\nimporter=\"wav\"\ntype=\"AudioStreamWAV\"\nuid=\"uid://cy4r18lxb4rbh\"\npath=\"res://.godot/imported/blaster.wav-efe4f879"
  },
  {
    "path": "audio/sounds/note_c5.wav.import",
    "chars": 492,
    "preview": "[remap]\n\nimporter=\"wav\"\ntype=\"AudioStreamWAV\"\nuid=\"uid://de550fklyhu88\"\npath=\"res://.godot/imported/note_c5.wav-0c5ae09a"
  },
  {
    "path": "audio/sounds/note_e5.wav.import",
    "chars": 492,
    "preview": "[remap]\n\nimporter=\"wav\"\ntype=\"AudioStreamWAV\"\nuid=\"uid://b35ao3bdtstka\"\npath=\"res://.godot/imported/note_e5.wav-297f657b"
  },
  {
    "path": "audio/sounds/note_g5.wav.import",
    "chars": 491,
    "preview": "[remap]\n\nimporter=\"wav\"\ntype=\"AudioStreamWAV\"\nuid=\"uid://bmg3dvylup5k\"\npath=\"res://.godot/imported/note_g5.wav-d7fdc8c79"
  },
  {
    "path": "default_bus_layout.tres",
    "chars": 363,
    "preview": "[gd_resource type=\"AudioBusLayout\" format=3 uid=\"uid://3vigst7csyr8\"]\n\n[resource]\nbus/0/volume_db = -2.0\nbus/1/name = &\""
  },
  {
    "path": "docs/getting-started.md",
    "chars": 6423,
    "preview": "# Getting started\n\nGodot provides a simple and functional audio engine with all the raw ingredients. However, it leaves "
  },
  {
    "path": "docs/images/add-music-bank-node.jpg.import",
    "chars": 803,
    "preview": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://decojxomfjk5j\"\npath=\"res://.godot/imported/add-music-b"
  },
  {
    "path": "docs/images/add-sound-bank-node.jpg.import",
    "chars": 803,
    "preview": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://dyf0vqo1r1frq\"\npath=\"res://.godot/imported/add-sound-b"
  },
  {
    "path": "docs/images/music-banks.png.import",
    "chars": 779,
    "preview": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://coy730qpivrbt\"\npath=\"res://.godot/imported/music-banks"
  },
  {
    "path": "docs/images/music-manager.png.import",
    "chars": 785,
    "preview": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://c33xlcqiwygve\"\npath=\"res://.godot/imported/music-manag"
  },
  {
    "path": "docs/images/set-soundbank-label.jpg.import",
    "chars": 803,
    "preview": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://cn3ojlclw1u8i\"\npath=\"res://.godot/imported/set-soundba"
  },
  {
    "path": "docs/images/sound-banks.png.import",
    "chars": 779,
    "preview": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://b1q8c8jsyx4xs\"\npath=\"res://.godot/imported/sound-banks"
  },
  {
    "path": "docs/images/sound-manager.png.import",
    "chars": 784,
    "preview": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://mg8fvg6efg77\"\npath=\"res://.godot/imported/sound-manage"
  },
  {
    "path": "docs/music-manager.md",
    "chars": 2903,
    "preview": "# MusicManager\n\n## Introduction\n\nThe **MusicManager** is responsible for playing music tracks. It does so through a **St"
  },
  {
    "path": "docs/sound-manager.md",
    "chars": 6285,
    "preview": "# SoundManager\n\n## Introduction\n\nThe **SoundManager** is responsible for triggering sounds. It does so through **PooledA"
  },
  {
    "path": "examples/2D/2d.gd",
    "chars": 1343,
    "preview": "extends Node2D\n\n\n# For reference, it's worth taking a moment to inspect the SoundBank attached to \n# this example scene."
  },
  {
    "path": "examples/2D/2d.tscn",
    "chars": 2126,
    "preview": "[gd_scene load_steps=10 format=3 uid=\"uid://coebcgearnost\"]\n\n[ext_resource type=\"Script\" path=\"res://examples/2d/2d.gd\" "
  },
  {
    "path": "examples/2D/moving_target.gd",
    "chars": 217,
    "preview": "extends MeshInstance2D\n\n\nvar _time: float\n\n\nfunc _process(p_delta) -> void:\n\t_time += p_delta\n\t\n\tvar weight = (1.0 + sin"
  },
  {
    "path": "examples/3D/3d.gd",
    "chars": 1349,
    "preview": "extends Node3D\n\n\n# For reference, it's worth taking a moment to inspect the SoundBank attached to \n# this example scene."
  },
  {
    "path": "examples/3D/3d.tscn",
    "chars": 2639,
    "preview": "[gd_scene load_steps=11 format=3 uid=\"uid://3kswph4l3ba6\"]\n\n[ext_resource type=\"Script\" path=\"res://examples/3d/3d.gd\" i"
  },
  {
    "path": "examples/3D/pivot.gd",
    "chars": 72,
    "preview": "extends Node3D\n\n\nfunc _process(p_delta):\n\trotate_y(TAU * p_delta * 0.1)\n"
  },
  {
    "path": "examples/bulk_instancing/bulk_instancing.gd",
    "chars": 1788,
    "preview": "extends Node2D\n\n\n# For reference, it's worth taking a moment to inspect the SoundBank attached to \n# this example scene."
  },
  {
    "path": "examples/bulk_instancing/bulk_instancing.tscn",
    "chars": 2433,
    "preview": "[gd_scene load_steps=12 format=3 uid=\"uid://dgmta52w7wn21\"]\n\n[ext_resource type=\"Script\" path=\"res://examples/bulk_insta"
  },
  {
    "path": "examples/music/music.gd",
    "chars": 2468,
    "preview": "extends Node2D\n\n\n# For reference, it's worth taking a moment to inspect the MusicBank attached to \n# this example scene."
  },
  {
    "path": "examples/music/music.tscn",
    "chars": 4204,
    "preview": "[gd_scene load_steps=22 format=3 uid=\"uid://jw5kbo20k3hh\"]\n\n[ext_resource type=\"Script\" path=\"res://examples/music/music"
  },
  {
    "path": "examples/polyphonic/polyphonic.gd",
    "chars": 1483,
    "preview": "extends Node2D\n\n\n# For reference, it's worth taking a moment to inspect the SoundBank attached to \n# this example scene."
  },
  {
    "path": "examples/polyphonic/polyphonic.tscn",
    "chars": 1738,
    "preview": "[gd_scene load_steps=8 format=3 uid=\"uid://ys1gdvqjju2q\"]\n\n[ext_resource type=\"Script\" path=\"res://examples/polyphonic/p"
  },
  {
    "path": "examples/scene_changes/scene_changes.gd",
    "chars": 1187,
    "preview": "extends Node2D\n\n\n# This script is simply glue code for the two other example scenes in this folder.\n# It's worth inspect"
  },
  {
    "path": "examples/scene_changes/scene_changes.tscn",
    "chars": 1320,
    "preview": "[gd_scene load_steps=5 format=3 uid=\"uid://bcqg6ao82u1by\"]\n\n[ext_resource type=\"Script\" path=\"res://examples/scene_chang"
  },
  {
    "path": "examples/scene_changes/scene_one.gd",
    "chars": 1377,
    "preview": "extends Node2D\n\n\n# For reference, it's worth taking a moment to inspect the MusicBank attached to \n# this example scene."
  },
  {
    "path": "examples/scene_changes/scene_one.tscn",
    "chars": 1236,
    "preview": "[gd_scene load_steps=8 format=3 uid=\"uid://qfk8ucywmppo\"]\n\n[ext_resource type=\"Script\" path=\"res://examples/scene_change"
  },
  {
    "path": "examples/scene_changes/scene_two.gd",
    "chars": 1646,
    "preview": "extends Node2D\n\n\n# For reference, it's worth taking a moment to inspect the SoundBank attached to \n# this example scene."
  },
  {
    "path": "examples/scene_changes/scene_two.tscn",
    "chars": 1230,
    "preview": "[gd_scene load_steps=8 format=3 uid=\"uid://cyme2yjik1ioe\"]\n\n[ext_resource type=\"Script\" path=\"res://addons/resonate/soun"
  },
  {
    "path": "examples/simple/simple.gd",
    "chars": 497,
    "preview": "extends Node2D\n\n\n# For reference, it's worth taking a moment to inspect the SoundBank attached to \n# this example scene."
  },
  {
    "path": "examples/simple/simple.tscn",
    "chars": 2049,
    "preview": "[gd_scene load_steps=10 format=3 uid=\"uid://dfy6ygeu8pya5\"]\n\n[ext_resource type=\"Script\" path=\"res://examples/simple/sim"
  },
  {
    "path": "icon.svg.import",
    "chars": 843,
    "preview": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://dxujf3r8cbmga\"\npath=\"res://.godot/imported/icon.svg-21"
  },
  {
    "path": "project.godot",
    "chars": 2361,
    "preview": "; Engine configuration file.\n; It's best edited using the editor UI and not directly,\n; since the parameters that go her"
  },
  {
    "path": "reasonate-github-header.jpg.import",
    "chars": 803,
    "preview": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://bb75idsxlhjl7\"\npath=\"res://.godot/imported/reasonate-g"
  },
  {
    "path": "release-please-config.json",
    "chars": 179,
    "preview": "{\n    \"packages\": {\n        \".\": {\n            \"release-type\": \"simple\",\n            \"extra-files\": [\n                \"a"
  },
  {
    "path": "resonate-logo.png.import",
    "chars": 773,
    "preview": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://dughwn5cisraq\"\npath=\"res://.godot/imported/resonate-lo"
  },
  {
    "path": "shared/description_label_settings.tres",
    "chars": 116,
    "preview": "[gd_resource type=\"LabelSettings\" format=3 uid=\"uid://bldprl7vev3uv\"]\n\n[resource]\nline_spacing = 8.0\nfont_size = 18\n"
  },
  {
    "path": "shared/stats_label_settings.tres",
    "chars": 148,
    "preview": "[gd_resource type=\"LabelSettings\" format=3 uid=\"uid://bnsew6gimofj5\"]\n\n[resource]\nline_spacing = 8.0\nfont_color = Color("
  },
  {
    "path": "shared/title_label_settings.tres",
    "chars": 140,
    "preview": "[gd_resource type=\"LabelSettings\" format=3 uid=\"uid://tysg4xge4hsu\"]\n\n[resource]\nfont_size = 38\nfont_color = Color(0.925"
  }
]

About this extraction

This page contains the full source code of the hugemenace/resonate GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 79 files (143.7 KB), approximately 41.8k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!