Go, Go, Godot!
  • 0

Audio Manager to handle the loading of sound effects in bulk

September 29, 2022

Years ago I purchased a game dev bundle on HumbleBundle. Part of that was a sound library called Pro Sound Collection. It’s pretty comprehensive, whether RPG or FPS, there are sounds for a ton of use cases. I might as well use them for something.

Luckily for me, the sound collection is pretty well organized. It is using a fairly decent directory structure and naming convention (e.g. Guns_Weapons/Guns/gun_pistol_shot_01.wav ). Some sounds have up to 10+ variations, and the directory structure is arbitrarily deep. The whole thing is about 2GB of uncompressed audio.

When I wanted to wire them all up to Godot 4’s AudioStreamRandomizer I realized that each sound needed to be added one by one. I could not just drag and drop them onto the component. That would be a nice UX improvement and would help cut down on tedious work. Since what I’m doing is more exploratory in nature, I just want to be able to load sound effects quickly, not spend half a day clicking around in a UI to associate things. To help with that, I wrote a little Audio Manager script.

The goal is to have a script read an entire directory of audio files: load all the streams into memory, and apply some kind of taxonomy to handle them intelligently. For example, all the sounds with sequential numerical suffixes will be loaded into an AudioStreamRandomizer as variations.

Aside from being convenient this also makes switching sounds dynamically easier; At startup, the audio files are loaded as streams and kept track of in a Dictionary. If a sound is requested at runtime, the stream is immediately returned.

This is using the DirAccess and FileAccess classes that Godot 4 beta 2 just added.

# AudioManager Singleton
extends Node

## Tracks all audio files and returns the stream set for a given name
## 
## The goal is to load sets of sound to have variations
##
## The files are named, e.g.: "res://audio/Footsteps/footstep_<set_name>_??.wav"

var streams: Dictionary = {}

@onready
var regex: RegEx = RegEx.new()


## Retrieve an AudioStream
##
func get_stream_set(steam_name: String):
	if streams.has(steam_name):
		return streams.get(steam_name)


func _ready():
	_compile_regex()
	_load_audio(_dir_contents("res://audio/"))

#	# or load them individually
#	var file_list = _dir_contents("res://audio/Footsteps/")
#	file_list.append_array(_dir_contents("res://audio/Animals_Nature_Ambiences/"))
#
#	# recusively loads sub-directories
#	file_list.append_array(_dir_contents("res://audio/Guns_Weapons/"))
#
#	#file_list.append_array(_dir_contents("res://audio/Guns_Weapons/Bullets/"))
#	#file_list.append_array(_dir_contents("res://audio/Guns_Weapons/Guns/"))
#	#file_list.append_array(_dir_contents("res://audio/Guns_Weapons/Guns_Silenced/"))
#	#file_list.append_array(_dir_contents("res://audio/Guns_Weapons/Knife_Sword_Pick/"))
#
#	file_list.append_array(_dir_contents("res://audio/Sci-Fi/"))
#	file_list.append_array(_dir_contents("res://audio/Voice/Human Male A/"))
#	file_list.append_array(_dir_contents("res://audio/Whooshes/"))
#	_load_audio(file_list)


func _dir_contents(path: String) -> Array[String]:
	var file_list : Array[String] = []
	var dir = DirAccess.open(path)
	if dir:
		dir.list_dir_begin()
		var file_name = dir.get_next()
		while file_name != "":
			if dir.current_is_dir():
				# recursively dive in
				file_list.append_array(_dir_contents("%s%s/" % [path, file_name]))
			else:
				if file_name.ends_with(".wav"):
					file_list.push_back(path + file_name)
			file_name = dir.get_next()
	else:
		print_debug("An error occurred when trying to access path: %s." % [path])
	return file_list


func _load_audio(files: Array):
	
	for f in files:
		var asr: AudioStreamRandomizer = _get_stream_set_for_filename(f)
		var stream = AudioStreamWAV.new()

		var file = FileAccess.open(f, FileAccess.READ)
		stream.data = file.get_buffer(file.get_length())
		# file.close()
		
		stream.format = AudioStreamWAV.FORMAT_16_BITS
		stream.mix_rate = 48000
		stream.stereo = true
		
		asr.add_stream(-1, stream)
		if asr.streams_count > 3:
			asr.playback_mode = AudioStreamRandomizer.PLAYBACK_RANDOM_NO_REPEATS
			pass


## Regular Expression that extracts elements from the filename
## E.g. Guns_Weapons/Guns/gun_pistol_shot_01.wav
## becomes:
## - category: gun
## - name: pistol_shot
## - digit: 01
func _compile_regex():
	var result = regex.compile("(?<category>[a-z]+)_(?<name>[0-9a-z_]+)_(?<digit>[0-9a-f]+)\\.wav")
	if result != OK: print_debug("AudioManager cannot compile regex.")


func _get_stream_set_for_filename(filename: String) -> AudioStream:
	# for example: "res://audio/Footsteps/footstep_metal_high_run_02.wav"
	var result = regex.search(filename)
	var stream_name = "_junk"
	if result:
		stream_name = result.get_string("name")
	else:
		# print_debug("Filename %s doesn't match the pattern." % filename)
		pass
	
	if !streams.has(stream_name):
		var rand: AudioStreamRandomizer = AudioStreamRandomizer.new()
		# rand.playback_mode = AudioStreamRandomizer.PLAYBACK_SEQUENTIAL
		rand.playback_mode = AudioStreamRandomizer.PLAYBACK_RANDOM
		rand.random_pitch = 1
		streams[stream_name] = rand
		
	return streams.get(stream_name)

That said, this isn’t production ready. Loading audio files at runtime doesn’t work on arbitrary audio files, because there’s currently no easily accessible way to properly load the audio samples and this is just grabbing the raw file buffer. GDscript would have to parse the wave file header, otherwise, some sounds may start or end with some clicking. At this stage, the script is prototype quality.

developer experiencegodotprogrammingresearch
Posted in Godot.
Share
PreviousProjectiles going through collision objects
NextGodot Engine 4 reaches beta

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Related Posts

  • Godot Game Engine Logo
    June 1, 2023

    Godot Engine 4.1.dev4 is available

    Development snapshot #4 of Godot Engine 4.1 is here. Among many other changes, it fixes a lighting issue related to using Light-only mode in CanvasItemMaterial (#44559). Unfortunately, it also introduced a UX issue with gradient color pickers (#77745), which makes it quite difficult to work with gradients at all. If you use gradients, I recommend …

  • April 1, 2023

    Introducing GodotBuilder: Custom Export Templates built on demand

    Need optimized export templates with PCK encryption support but don’t want to have to set up a build pipeline or download the entire compilation toolchain on your computer? Well, now it is. Fill out the form, checkout, and we’ll email you the download link after the compilation completes. Compilation may take 30 minutes to 3 …

  • February 16, 2024

    Inventory System v1.8.1 available

    A quick update to yesterday’s release with a few fixes:

  • March 20, 2025

    Inventory System 2 Alpha 4 available

    This release finally uses Godot Engine 4.4. It adds the GGCraftingSystem singleton and updates the GGInteractable2DStrategyCrafting class to use it. The crafting editor nodes now have prefixes, which makes it much easier to search for specific recipe or item nodes in larger crafting libraries. Some syntactic sugar was added as well. You can now easily …

    © 2025 GoGoGodot.io. All rights reserved.