AudioMaster
September 15, 2022

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.

How to Use
How it Works

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:

  1. Activate the plugin within the Project Settings → Plugins.
  2. In the Project Settings → AutoLoad add a new script, the audiomaster.gd.

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.

Audio Output Volume

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

Audio Stream Player Setup

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.

Audio Playback

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.

Debugging

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.

Cross-fading

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

Mixing

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)

Playback Options

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.

AudioStreamPlayer

OptionDefaultDescription
mix_targetMIX_TARGET_STEREOAssigns the mix_target property of the node. Possible values are MIX_TARGET_STEREO, MIX_TARGET_SURROUND and MIX_TARGET_CENTER
start_from0.0Start the playback from this position (in seconds).

AudioStreamPlayer2D

OptionDefaultDescription
positionVector2(0, 0)Specify the Global position from which the node should be considered when calculating the playback attenuation.
pitch_scale1.0Change the pitch and the tempo of the audio, as a multiplier of the sample rate.
max_distance2000.0Maximum distance from which audio is still hearable.
attenuation1.0Dampens audio over distance with this as an exponent.
area_mask1Areas in which this sound plays.
start_from0.0Start the playback from this position (in seconds).

AudioStreamPlayer3D

OptionDefaultDescription
positionVector3(0, 0, 0)Specify the Global position from which the node should be considered when calculating the playback attenuation.
attenuation_modelATTENUATION_INVERSE_DISTANCEDetermines how the the audio will become quieter with distance. ATTENUATION_INVERSE_DISTANCE, ATTENUATION_INVERSE_SQUARE_DISTANCE, ATTENUATION_LOGARITHMIC or ATTENUATION_DISABLED.
unit_db0.0Base sound level unaffected by dampening, in decibels.
unit_size1.0Factor for the attenuation effect.
max_db3.0Sets the absolute maximum of the sound level, in decibels.
pitch_scale1.0The pitch and the tempo of the audio, as a multiplier of the audio sample's sample rate.
max_distance0.0Sets the distance from which the out_of_range_mode takes effect. Has no effect if set to 0.
out_of_range_modeOUT_OF_RANGE_MIXDecides if audio should pause when source is outside of max_distance range (OUT_OF_RANGE_PAUSE) or continue mixing (OUT_OF_RANGE_MIX).
area_mask1Areas in which this sound plays.
emission_angle_enabledfalseIf true, the audio should be dampened according to the direction of the sound.
emission_angle_degrees45.0The angle in which the audio reaches cameras undampened.
emission_angle_filter_attenuation_db-12.0Dampens audio if camera is outside of emission_angle_degrees and emission_angle_enabled is set by this factor, in decibels.
attenuation_filter_cutoff_hz5000.0Dampens audio above this frequency, in Hz.
attenuation_filter_db-24.0Amount how much the filter affects the loudness, in decibels.
doppler_tracking0Decides in which step the Doppler effect should be calculated, which is DOPPLER_TRACKING_DISABLED, DOPPLER_TRACKING_IDLE_STEP or DOPPLER_TRACKING_PHYSICS_STEP.

Happy forging!