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
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
AudioStreamPlayer3D. By default all created nodes will be of the
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.PlayerTypeenum, which offers
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_modeproperty in all audio stream player nodes of the specified bus name. The possible values are
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
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
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.
|MIX_TARGET_STEREO||Assigns the mix_target property of the node. Possible values are |
|0.0||Start the playback from this position (in seconds).|
|Vector2(0, 0)||Specify the Global position from which the node should be considered when calculating the playback attenuation.|
|1.0||Change the pitch and the tempo of the audio, as a multiplier of the sample rate.|
|2000.0||Maximum distance from which audio is still hearable.|
|1.0||Dampens audio over distance with this as an exponent.|
|1||Areas in which this sound plays.|
|0.0||Start the playback from this position (in seconds).|
|Vector3(0, 0, 0)||Specify the Global position from which the node should be considered when calculating the playback attenuation.|
|ATTENUATION_INVERSE_DISTANCE||Determines how the the audio will become quieter with distance. |
|0.0||Base sound level unaffected by dampening, in decibels.|
|1.0||Factor for the attenuation effect.|
|3.0||Sets the absolute maximum of the sound level, in decibels.|
|1.0||The pitch and the tempo of the audio, as a multiplier of the audio sample's sample rate.|
|0.0||Sets the distance from which the out_of_range_mode takes effect. Has no effect if set to 0.|
|OUT_OF_RANGE_MIX||Decides if audio should pause when source is outside of max_distance range (|
|1||Areas in which this sound plays.|
|false||If true, the audio should be dampened according to the direction of the sound.|
|45.0||The angle in which the audio reaches cameras undampened.|
|-12.0||Dampens audio if camera is outside of emission_angle_degrees and emission_angle_enabled is set by this factor, in decibels.|
|5000.0||Dampens audio above this frequency, in Hz.|
|-24.0||Amount how much the filter affects the loudness, in decibels.|
|0||Decides in which step the Doppler effect should be calculated, which is |
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.
_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
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_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!