Sound Manager Node?

Godot Version

4.2.2

Question

Hello All,

I think it’s getting to the point where I would like to implement a sound manager. I did this in Unity, years back, and would like to try and do the same in Godot, so, I can keep all the sounds in one place and instead of going through every node that has the play a sound and put in delays the stop the node being destroyed whilst playing a sound. Easy to implement, but, not efficient.

The way I did it in Unity was make a sound manager and put in array of sounds in it and just called the one I wanted to play (I don’t know if that was good practice or not), but, it worked and if the object was destroyed the sound still played no problems. So, I would like to do something similar in Godot, but, I don’t know how to set up an array of “.wav” files in code (I imagine calling them would be similar to Unity) and there doesn’t seem to be a way to set up an array in the AudioStreamer tool.

As always any help/tips or a YouTube tut on the topic would be greatly appreciated!

Regards.

You could use @export or use the paths manually in code. Or a mixture of the two.

extends Node

@export var audios: Array[AudioStream] = [
    preload("res://myaudio.ogg"),
]
1 Like

I just added a MediaManager, which is, essentially, used to play music and sound effects. At this point it only does music, because I’m not there for the effects. But, for those interested, here’s the whole script.

Note that it only contains the very minimum to play the music tracks I own. Play, fade in/out, etc. But I think someone could use that to begin.

The script is a singleton and should be put into the autoload list.

PS: I’m not responsible for the bugs included therein. :sweat_smile:

extends AudioStreamPlayer

var transition_duration := 1
var transition_type := 1
var music_volume = -5

var tracks : Dictionary = \
	{
		"Elizabeth" : "Assets/Music/Elizabeth.ogg",
		"Joana" : "Assets/Music/Joana.ogg",
		"Eloise" : "Assets/Music/Eloise.ogg",
		"Zhomia" : "Assets/Music/Zhomia.ogg",
		"Store" : "Assets/Music/Store.ogg",
		"Main" : "Assets/Music/MainMenu.ogg"
	}

func _init():
	volume_db = music_volume

func play_track(newSong : String):
	fade_in(newSong)
	play()

func fade_out():
	# tween music volume down to -80 (muted)
	var tween_out = create_tween()
	tween_out.tween_property(self, "volume_db", -80, transition_duration)
	Tween.interpolate_value(music_volume, 0, 0, transition_duration, Tween.TRANS_LINEAR, Tween.EASE_OUT)

func fade_out_to(newSong):
	if playing:
		# tween music volume down to -80 (muted)
		var tween_out = create_tween()
		tween_out.tween_property(self, "volume_db", -80, transition_duration)
		Tween.interpolate_value(music_volume, 0, 0, transition_duration, Tween.TRANS_LINEAR, Tween.EASE_OUT)
		tween_out.tween_callback(Callable(self, "done_fade_out").bind(newSong))
	else:
		done_fade_out(newSong)

func fade_in(newSong):
	stream = load(str("res://", tracks.get(newSong)))
	print("Playing : ", str("res://", tracks.get(newSong)), " (fade_in)")
	# tween music volume up to music_volume (normal/defined)
	var tween_in = create_tween()
	tween_in.tween_property(self, "volume_db", music_volume, transition_duration)
	Tween.interpolate_value(-80, 0, 0, transition_duration, Tween.TRANS_LINEAR, Tween.EASE_IN)

func pause_music():
	stream_paused = !stream_paused

func mute():
	volume_db = 0

func unmute():
	volume_db = music_volume

func done_fade_out(newSong : String):
	stop()
	stream = load(str("res://", tracks.get(newSong)))
	print("Playing : ", str("res://", tracks.get(newSong)), " (callback fade_out_to)")
	# tween music volume up to music_volume (normal/defined)
	var tween_in = create_tween()
	tween_in.tween_property(self, "volume_db", music_volume, transition_duration)
	Tween.interpolate_value(-80, 0.5, 0, transition_duration, Tween.TRANS_LINEAR, Tween.EASE_IN)
	unmute()
	play()

2 Likes

Thanks greatly to the both of you. I will start with what @gertkeno suggested as it seems the easiest to implement but as I get more adventurous I will try and add what @dharhix suggested. The ability fade in/out tracks would be exceptional and show true professionalism.

Much regards to both of you.

Just a note here that the comments say: " tween music volume down to 0" which is not right as normal (read full) volume is 0 and muted is -80.

I underline this as, reading it might give the false impression that one would need to bring it UP higher than 0, which would only bring distortion and be very loud.

Nevermind the above. I edited the comments in the post above.

To use my singleton you would only need to fill the dictionary entries with your pathing and file names and call it thus:

[some code somewhere]
MediaManager.play_track("My_Track")
[some other code]

Absolutely nothing else is needed, provided the script is autoloaded.

HTH

1 Like

@gertkeno I tried what you said and have run into an error. That I can’t seem to get to the bottom off and hoping you may be able to help me:

I made a node called SoundFX_manager with the code and added to the game:

extends Node

@export var soundsFX: Array[AudioStream] = [
	preload("res://assets/sounds/hurt.wav"),
	preload("res://assets/sounds/jump.wav"),
]

And then in the player script I put the following:

## access the sound manager
@onready var sound_fx_manager = $"../SoundFX_Manager"

This doesn’t look right to me but I didn’t know what to put:

Then to play the sound I had this later in the code:

	sound_fx_manager.soundsFX[1].play();

And I was getting the error:

Invalid call. Nonexistent function 'play' in base 'AudioStreamWAV'.

And I am really hoping that you might be able to help rectify this error

The audio streams are mearly audio, not a player itself. You’ll have to add a AudioStreamPlayer to the SoundFX_Manager Node, give it a “new AudioStreamPolyphonic”. Then a helper function can play via a polyphonic playback.

extends Node

@export var soundsFX: Array[AudioStream] = [
	preload("res://assets/sounds/hurt.wav"),
	preload("res://assets/sounds/jump.wav"),
]

var audio_stream: AudioStreamPlaybackPolyphonic

func _ready() -> void:
    # has to get the stream playback, documentation is lacking
    audio_stream = $AudioStreamPlayer.get_stream_playback()

func play_index(index: int) -> void:
    audio_stream.play_stream(soundsFX[index])
1 Like

Thankyou so much, I had a feeling that I needed an AudioStreamplayer, but, I couldn’t find any information on how to make it play. I actually had one attached to the node but that was as far as I got before I deleted it.

Thankyou, thankyou, THANKYOU :sunglasses:.

The major trick is “AudioStreamPolyphonic” really good for playing multiple various streams that you wouldn’t use on simpler things (like looping ambience), or animated things.

It might be better to use the animation player for audio clips if you need to sync attack swings with anticipation frames for example. Here I’m using two separate audio streams for a reload animation, not a polyphonic stream, simple drag and drop interface too :slight_smile:

1 Like

@gertkeno, I tried what you suggested and made a change to the player code this:

	#redirect to sfx_manager
	sound_fx_manager.play_index(1);

And added the AudioStreamPlayer back to the SoundFX_Manager along with the code you had and this occurred in the stack trace:

And this occurred in the debugger:

I tried to capture everything by leaving it to get the hover that appeared over the top.

Needless to say this is above my paygrade, way above, so, if it’s not a simple fix, then I will have to go back to my simple solution of when an object is destroyed make it invisible and then make a short delay until the sound effect finishes. So, if it is simple I would like to get it to work, but, only if you won’t get too angry with me for being a simpleton :innocent:.

I do appreciate everything you have done - believe it or not I am learning more about the features of Godot even when things don’t work - actually you learn more WHEN things don’t work. And from your screenshot the animation player looks surprisingly a lot like the one in Unreal :grinning:.

Regards.

Turns out the AudioStreamPlayer needs to have been played once before get_stream_playback works this Github Issue marks it as fixed (for 2D and 3D players) but I believe it’s still hitting you.

Mark autoplay on the AudioStreamPlayer that has the polyphonic playback, and try again. If it still gives the same error, add a frame await to the start

func _ready() -> void:
	# workaround: this bug is supposed to be fixed, need to play then wait a
	# frame for the audio stream "activate" and the stream to be valid
	# https://github.com/godotengine/godot/issues/72048
	await get_tree().process_frame
	weapon_audio_player = $Audio/WeaponFX.get_stream_playback()

Certainly a deep issue, apologies my friend.

1 Like

Still getting the same errors. This is in the too hard basket for me.

I will try and move on to dharhix see if I can get some of that working. That should be fun :stuck_out_tongue_winking_eye:.

Cheers.

This is a SoundManager class based on :https://kidscancode.org/godot_recipes/4.x/audio/audio_manager/index.html.

I use it for in-game sound effects. For background music, I use a different setup on a separate MUSIC audio bus.

extends Node

const NUM_PLAYERS: int = 8
var bus: String = "SFX"

var available : Array = []  # The available players.
var queue : Array = []  # The queue of sounds to play.
var playing : Array = [] # active players i.e playing audio
var volume : float = 0.0

func _ready() -> void:
	# Create the pool of AudioStreamPlayer nodes.
	for i in NUM_PLAYERS:
		var p = AudioStreamPlayer.new()
		add_child(p)
		available.append(p)
		p.finished.connect(_on_stream_finished.bind(p))
		p.bus = bus


func _on_stream_finished(stream: AudioStreamPlayer) -> void:
	# When finished playing a stream, make the player available again.
	available.append(stream)
	playing.erase(stream)


func play(sound_path: String) -> void:
	queue.append(sound_path)


# stop all active players
func stop_all() -> void:
	for i in playing.size():
		playing[i].stop()
		available.append(playing[i])
	playing.clear()


func adjust_volume(vol: float) -> void:
	volume += vol
	for i in playing.size():
		playing[i].set_volume_db(volume)

func _process(_delta) -> void:
	# Play a queued sound if any players are available.
	if queue.size() > 0 and available.size() > 0:
		available[0].stream = load(queue.pop_front())
		available[0].play()
		available[0].set_volume_db(volume)
		playing.append(available.pop_front())

Tried what you suggested, it didn’t work, for some reason, then I went and grabbed the code at the site and it didn’t work either. I set it to autoload all that Jazz. But in the autoload window there was no allowing for a singleton only a global variable.

I tried playing the sound as they said using the “res” prefix in the player script.

The error was Invalid call. Nonexistent function 'empty' in base 'Array'.

at this line:
if not queue.empty() and not available.empty():

So, I must’ve done something pretty wrong in my project to stop this from working, it’s been 3 days with no headway whatsoever. I will have to leave it and revisit it in the far future. I am a little disappointed that none of these solutions worked on my computer, but them the breaks :face_with_raised_eyebrow:.

Thanks to all of you going out of the way to help :sunglasses:.

Regards.

@dharhix I just tried out your code just out of curiosity just to see if it would work, made it autoload, and I wasn’t expecting it to work as there was only a AudioStreamPlayer node and, ummm, it DID work. I really, really wasn’t expecting to work, and for all intents and purposes it is. I double checked the player code to make sure the jump wasn’t being called in that scene, and then triple checked and it wasn’t - it’s your code :sunglasses:.

I can’t say I understand what is going on in your code which is why I was hesitant to try it out, but, it works :sunglasses:.

All I can say is: Thankyou!

1 Like

Ah, ye of little faith! :rofl:

Just happy to help. If it suits your need, that’s plenty for me. :smiley:


Rereading the code I just realized there are a couple “problems.”

Mute:

# Somewhere up top, a new var for the old volume before mute.
var muted_volume : int

func mute():
    muted_volume = volume_db
	volume_db = -80

func unmute():
    volume_db = muted_volume

Honestly I can’t remember off the top of my head if there’s already a mute/unmute method in AudioStreamPlayer. If so, discard those.

1 Like