-->
Audio playback in Godot is relatively easy. Basically we add an AudioStreamPlayer
node, assign an audio resource then, when desired, we call the play()
function within the node instance. Yet, often we need to take care about the node lifetime.
As an example, consider a scene meant to be used a pickup. In this scene an audio stream player node is added in order to playback a sound effect whenever the player picks that object. Within the code we call the play()
function at that moment then destroy the pickup instance. The playback, if it starts at all, will most likely not finish. The reason is that the destruction of the pickup object also removes the stream player node from the tree.
And how about fade-in, fade-out or even cross fading effects? While those are not exactly hard to code, depending on how the audio stream player nodes are distributed around the project it might become a little problematic to perform this kind of fading.
So, this addon's goal is aimed at solving those problems.
Before delving into how to use this addon, there is one important thing I wanted to mention, which I have noticed while testing the system. This is unrelated to the script in this addon. Using AudioStreamPlayer2D
and AudioStreamPlayer3D
to playback OGG files resulted in random "pops" instead of the desired sound effect, while sometimes the "pop" came slightly after starting the playback of the sound effect. I didn't notice any problems with "WAV" playback on those two types of nodes. With the AudioStreamPlayer
I didn't notice any problems with either OGG or WAV.
This addon contains a Node that is designed to be automatically loaded. That is, it must be part of the Autoload scripts. For this, there are two options:
When you activate the plugin it automatically adds the audiomaster.gd into the auto load list, assigning the name AudioMaster
. If you choose to manually add this script into the auto load then you have slightly more control over the process. In the end there will be no difference in how you use this addon. Just know that all examples shown here will use the default AudioMaster
name to access the script's functionality.
Once the script is turned into a "singleton" its functionality can be accessed through any other script in the project.
One important aspect you must know about all operations done by this plugin is that you must provide a Bus name. Indeed, during this plugin's node initialization it will retrieve from the AudioServer
all configured buses. Internally this will create a map from bus name into the corresponding bus index.
That said, the audio stream player nodes necessary to perform the playback are handled by the AudioMaster
script. To be more specific, each bus can contain a number of nodes associated with. And each bus can have a different setup that will be applied into the corresponding stream player nodes.
Because all the interaction with this addon is done through script, I will list all available functions and what they do.
Normally to change the audio output volume we simply call a function within the AudioServer
object. However it requires the value to be in decibels, which is not a linear scale. This might become problematic when we want to provide sliders to control the volume of the audio. In the AudioMaster
script there are two functions to deal with the volume but using percent values, in the range [0..1]. Much like the builtin functions, the volume configuration is done per bus. The functions are:
get_bus_volume_percent(bus: String)
: Retrieve the output volume of the specified bus name. The returned value will be in "percent" (linear) rather than decibels.set_bus_volume_percent(bus: String, newvol: float)
: Assigns a new output volume into the specified bus name. The new volume must be in range [0..1] (a percent).Godot offers three different types of audio stream player nodes, which are AudioStreamPlayer
, AudioStreamPlayer2D
and AudioStreamPlayer3D
. By default all created nodes will be of the AudioStreamPlayer
.
When requesting to playback an audio, the script will automatically pick an available player node. However, if there isn't any available, a new one will be created. Obviously a maximum amount of those nodes can be set, and by default it's 32. It's also possible to pre-allocate those nodes beforehand, which might be very useful during the game/application initialization. Depending on the use case it might even be necessary (as will be seen shortly).
Finally, still related to the stream player node setup, there is the pause mode. By default when the game pauses, all stream player nodes will also pause the playback. If the UI emits sound and a bus is created specifically for this, then it would probably be desired to change the pause mode of the nodes associated with the "UI bus". This can be done.
set_maximum_players(bus: String, val: int)
: Assigns the maximum amount of audio stream player nodes that can be created associated with the specified bus name. By default this is 32.set_player_type(bus: String, ptype: int)
: Assigns the type of audio stream player that will be created within the specified bus name. The type is an option from the AudioMaster.PlayerType
enum, which offers PlayerNormal
, Player2D
and Player3D
.allocate_players(bus: String, num: int)
: Create num audio stream players associated with the specified bus name. Note that this will not bypass the maximum amount of players set for the bus.set_player_pause_mode(bus: String, mode: int)
: Assigns the pause_mode
property in all audio stream player nodes of the specified bus name. The possible values are PAUSE_MODE_INHERIT
, PAUSE_MODE_PROCESS
and PAUSE_MODE_STOP
.As mentioned, when requesting to playback an audio, an available stream player node will be automatically picked by the AudioMaster
script. Yet it's possible to forcefully specify a node index, provided it already exists. This is somewhat required to perform the fading effects. Knowing the index of the node also allows some query to be done, like asking if a specific stream player is currently playing something, the current position of the playback and so on.
In the play and stop functions there is a fade_time parameter. This is meant to specify, in seconds, the amount of time take to fade-in and fade-out, respectively. Please note that it's not possible to use this effect with the AudioStreamPlayer3D
nodes and the AudioMaster
script will push a warning message every time there is an attempt to use the fading effect within nodes of this type.
is_playing(bus: String, player_index: int)
: Returns true if an audio stream player at player_index associated with the given bus name is currently playing an audio.play_audio(bus: String, audio: AudioStream, index: int, fade_time: float, extra: Dictionary)
: Plays the incoming audio stream within the specified audio stream player node index that is associated with the given bus name. A "fade-in" effect can be used if fade_time is a positive value. The extra can be used to provide additional settings.load_and_play(bus: String, file_path: String, index: int, fade_time: float, extra: Dictionary)
: Loads an audio stream from the specified file_path then plays it. All of the other parameters are the same of play_audio()
.stop(bus: String, index: int, fade_time: float)
: Stops the playback of the audio stream node at index that is associated with the given bus name. The fade_time parameter can be used to specify a fade-out effect, in seconds.stop_all_in_bus(bus: String, fade_time: float)
: Stops the playback of all audio stream player nodes associated with the given bus name. A fade-out effect can be used by specifying a positive value in fade_time, which expects the amount to be in seconds.stop_all(fade_time: float)
: Stops the playback of all audio stream player nodes associated with all buses. And the fade-out effect can be used by specifying a positive value, in seconds.get_playback_position(bus: String, player_index: int)
: Retrieves in which position the playback of the audio stream player at player_index node associated with the bus name is currently in. The value is in seconds.set_playback_position(bus: String, player_index: int, new_position: float)
: Seek into the specified position in seconds, effectively allowing to jump into a section of the playback of the player_index node at the corresponding bus_name.get_available_player_index(bus: String)
: Get the index of an available audio stream player node. If -1 is returned then there isn't any available.Before moving on, I believe the play_audio()
requires bit more explanation. The index parameter can be negative (default), in which case the script will automatically take any available stream player node. Note that in this case it wont be possible to retrieve which one was used. This is a deliberate design choice just so the play_audio()
function doesn't return any value. The reason? Prevent script warnings every time we call this function without using its return value.
The extra parameter is, by default, an empty dictionary. This is used to provide optional additional settings to be applied into the node for its playback. The list of possible entries will be shown near the end of this How to tab.
When the playback finishes the AudioMaster
node emits a signal, playback_finished. The function will receive two parameters, the name of the bus and the index of the stream player node associated with that bus.
There is one function that can be used in order to retrieve some debugging information. It basically returns an array containing a dictionary for each audio bus.
get_debug_info()
: Returns an array containing dictionaries with information about each "bus".The contents of each dictionary are:
bus
: The name of the bus.player_count
: Total amount of audio stream player nodes that have been created.playing
: How many of those nodes are currently playing.available
: How many of those nodes are not playing anything (available for playback).type
: A string that is either Normal, 2D or 3D.In order to cross-fade, we need two audio stream player nodes. One that is already playing and should fade-out. The other should start playing a new stream while performing a fade-in effect. This is one of the reasons the play_audio()
function allows to "forcefully" use a specified stream player node, by its index.
When this effect is desired, it becomes rather important to hold somewhere in the code which stream player is currently playing. One possible strategy here is to allocate 2 stream players and create a variable to hold which one is currently active. When it's time to cross-fade, stop the "currently active" specifying some fade time. After that start the playback of the new audio file using the other stream player, also specifying some fade-in time, remembering to update the "currently active" variable. The snippet bellow depicts this:
func cross_fade(to_audio: AudioStream) -> void:
# Fade-out currently active audio - 1.5 seconds
AudioMaster.stop("Music", currently_active, 1.5)
# Newly active player
currently_active = 1 if currently_active == 0 else 0
# Fade-in the new audio
AudioMaster.play_audio("Music", to_audio, currently_active, 1.5)
Remember, the stream player nodes must exist in order for the index specification to correctly work. So during the initialization of the game/application call allocate_players()
function.
Yet, the above script works if you are sure there is no need to cross-fade audios in intervals smaller than the 1.5 seconds (or any other specified time). The thing is, while fading out the audio stream player is still in use. So a different approach is necessary if there is any chance the cross-fading will be required in smaller intervals. An example of such case is a button to alternate between audio tracks. There is no way to predict how often the user will press that. While it would be possible to disable the button while the effect doesn't end, there is an alternative is to query for the index of a free stream player from the AudioMaster
, like in the snippet bellow (this is what was used in the demo project):
func cross_fade(to_audio: AudioStream) -> void:
# Query newly active audio index
var new_active: int = AudioMaster.get_available_player_index("Music")
# Fade-out currently active audio
AudioMaster.stop("Music", currently_active, 1.5)
# Fade-in the new audio
AudioMaster.play_audio("Music", to_audio, new_active, 1.5)
# Update currently active
current_active = new_active
In this case, when first opening the level the currently_active
should be initialized with an invalid index, such as a negative value. Then, when requesting to playback something for the first time, it should be initialized with the value returned from the AudioMaster.get_available_player_index()
function. As an example, the snippet bellow performs a few checks before playing an audio track. If the currently_active
is negative it simply starts the playback, otherwise it cross-fades:
func play_track(audio: AudioStream) -> void:
if (currently_active == -1):
currently_active = AudioMaster.get_available_player_index()
# If desired a fade-in time can be used
AudioMaster.play_audio("Music", audio, currently_active)
else:
cross_fade(audio)
Obviously that whenever the playback of the audio finishes the variable has to be reset back to an invalid index. For that it's necessary to listen to the playback_finished signal of the AudioMaster
object. In that function we must ensure the finished track is from the correct corresponding audio bus and the index is the same of the currently_active
variable. Something like this:
...
# Somewhere in the script connect the function to handle the playback finished signal
AudioMaster.connect("playback_finished", self, "on_playback_finished")
func on_playback_finished(bus_name: String, index: int) -> void:
if (bus_name == "Music" && index == currently_active):
currently_active = -1
As you probably gathered from the description of the functions, it's possible to mix various audio tracks together in order to create music. All that is required is that each audio stream uses a known specified player node index. In one of my projects I had 8 audio files, each one with an instrument. Then at the beginning of the level all 8 started playing. As times passes I stopped one of them until at the end there was only one playing. The snippet bellow depicts it in a very simplified way:
func play_mix() -> void:
for i in 8:
AudioMaster.play_audio("Music", audio_stream[i], i)
func remove_from_mix(index: int) -> void:
AudioMaster.stop("Music", index)
Each of the AudioStreamPlayer*
nodes in Godot offers a number of additional settings that can be used in order to change how the playback will occur. Bellow are the additional options for each node type. In all tables, the Default column specifies which value will be used in case the option is not given at all. In all cases the option is given as the key in the extra Dictionary.
Option | Default | Description |
---|---|---|
mix_target | MIX_TARGET_STEREO | Assigns the mix_target property of the node. Possible values are MIX_TARGET_STEREO , MIX_TARGET_SURROUND and MIX_TARGET_CENTER |
start_from | 0.0 | Start the playback from this position (in seconds). |
Option | Default | Description |
---|---|---|
position | Vector2(0, 0) | Specify the Global position from which the node should be considered when calculating the playback attenuation. |
pitch_scale | 1.0 | Change the pitch and the tempo of the audio, as a multiplier of the sample rate. |
max_distance | 2000.0 | Maximum distance from which audio is still hearable. |
attenuation | 1.0 | Dampens audio over distance with this as an exponent. |
area_mask | 1 | Areas in which this sound plays. |
start_from | 0.0 | Start the playback from this position (in seconds). |
Option | Default | Description |
---|---|---|
position | Vector3(0, 0, 0) | Specify the Global position from which the node should be considered when calculating the playback attenuation. |
attenuation_model | ATTENUATION_INVERSE_DISTANCE | Determines how the the audio will become quieter with distance. ATTENUATION_INVERSE_DISTANCE , ATTENUATION_INVERSE_SQUARE_DISTANCE , ATTENUATION_LOGARITHMIC or ATTENUATION_DISABLED . |
unit_db | 0.0 | Base sound level unaffected by dampening, in decibels. |
unit_size | 1.0 | Factor for the attenuation effect. |
max_db | 3.0 | Sets the absolute maximum of the sound level, in decibels. |
pitch_scale | 1.0 | The pitch and the tempo of the audio, as a multiplier of the audio sample's sample rate. |
max_distance | 0.0 | Sets the distance from which the out_of_range_mode takes effect. Has no effect if set to 0. |
out_of_range_mode | OUT_OF_RANGE_MIX | Decides if audio should pause when source is outside of max_distance range (OUT_OF_RANGE_PAUSE ) or continue mixing (OUT_OF_RANGE_MIX ). |
area_mask | 1 | Areas in which this sound plays. |
emission_angle_enabled | false | If true, the audio should be dampened according to the direction of the sound. |
emission_angle_degrees | 45.0 | The angle in which the audio reaches cameras undampened. |
emission_angle_filter_attenuation_db | -12.0 | Dampens audio if camera is outside of emission_angle_degrees and emission_angle_enabled is set by this factor, in decibels. |
attenuation_filter_cutoff_hz | 5000.0 | Dampens audio above this frequency, in Hz. |
attenuation_filter_db | -24.0 | Amount how much the filter affects the loudness, in decibels. |
doppler_tracking | 0 | Decides in which step the Doppler effect should be calculated, which is DOPPLER_TRACKING_DISABLED , DOPPLER_TRACKING_IDLE_STEP or DOPPLER_TRACKING_PHYSICS_STEP . |
The problem this addon (attempts) to solve is precisely the node lifetime when audio must be played. Because auto-load scripts are indeed nodes, and ones that remain "alive" during the entire time the game is running, regardless of which scene is currently active, one frequently used solution is to use an auto-load script to handle audio playback. And so, this is how this addon performs this task.
When this node is initialized it first queries the AudioServer
singleton for all configured audio buses. For each one it will generate an instance of a class defined in the playerdata.gd. This class is not meant to be directly used so I didn't document any of it in the How to tab. That said, in an internal map (Dictionary) an entry is created, keyed by the bus name, while holding the instance of the PlayerData
class. Also, for each bus, a generic Node
is created and attached into the AudioMaster
itself. This is done purely for organization purposes. If you launch the game and check the "Remote" tree view then you see that the AudioMaster
node will have a child node for each bus, having the name of that bus in question.
When an audio stream player node is created, it will be added into the corresponding "bus node" and will have the Player_[index] pattern as its name. Most of the node setup can be found in the create()
function within the playerdata.gd script. Note that this function receives one object as argument. This is meant to be the AudioMaster
itself, which will handle the finished signal of the stream player node. Because GDScript does not support this kind of cross reference, I used the Object
class as type.
One important thing that happens when the new audio stream player node is created is updating one inner Dictionary named _available
. This is actually used as a set rather than a map. The idea here is that if an index is inside the _available
container then the corresponding stream player is available for playback. So, when a node is used its index is removed from the set and added again when the playback finishes. Originally this was an array used as a queue. However upkeep became rather problematic when dealing with some other more advanced usage, like forcefully specifying the index to be played, the stop()
function and so on.
One consequence of using a dictionary instead of an array to hold the list of player nodes that are available is in how to obtain one that is free. In an array working as a queue it becomes as simple as calling array.front()
. In the dictionary we have to first obtain the list of keys, which is returned as an array from the get_keys()
function. Because the number of stream player nodes is very unlikely to become big, calling this function might not be too problematic in terms of performance.
In the _process()
function of the AudioMaster class there is some code to perform "over time effects". As an example, when fading the audio, what it actually does is gradually change the output volume playback of the corresponding stream player node. This processing occurs by iterating instances of the inner _Processor
class. When the effect ends the object is removed from the array (_to_process
). Once this array is empty, then the _process()
function is disabled, which might save a little bit of processing power.
I mentioned that in the create()
it connects a function to handle the finished signal of the audio stream player node. This function is the _playback_finished()
. Note that in the connection two bindings are created as payloads to the event handler. The first one is the instance of the PlayerData
itself. The second is the index to the stream player node. With this information it becomes possible to request the PlayerData
object to "release" the player node, that is, make it available again within the _available
container. Besides that, it provides information in order to query the name of the bus just so the playback_finished signal can be emitted within the AudioMaster
node.
In the play_audio()
function, one of the arguments is a Dictionary
. It has been used in order to provide additional settings to the playback itself. The option to use a dictionary for this kind of setup is because each type of stream player node offers a different number of options. Using a dictionary here makes it rather easy to contemplate all three node types while also making every single one of the available settings to be completely optional. In order to not clutter the body of the play_audio()
with this setup, two different functions have been created, _setup_player2d()
and _setup_player3d()
. Those two functions take the stream player node and the dictionary containing the settings. Then those are applied into the node.
Because the "normal" player node only offers one additional setting, there was no need to have a separate function just to perform the additional setup.
That's basically it for this addon!
Happy forging!