Skip to content

Building a sound library


Creating an automatic sound library!

Published: Sat Jan 24 2026 22:53:15 GMT+0000 (Coordinated Universal Time)

Introduction:

For this tutorial we will be setting up a sound library that will automatically manage all audio files in a specific folder, allow users to change the volume in game of each one, and save the audio changes!

To to this we are going to use:

A global script to be our Audio Manager

A generic script that we attach to any stream player you want to be impacted by the stream player. If you want every audio player to be impacted by it, then you can make it in an instance and just us the instanced audio player instead of the node.

Then we will set up a UI stream with audio player that builds out the menu

Then finally, we'll make a small test scene we can go to so we can confirm our settings are saving.

References:

This uses an Audio Manager implementation that I modified.

This is a common technique and parts of my implementation will resemble things from the following:https://github.com/godotengine/godot-demo-projectshttps://github.com/insideout-andrew/godot-audio-managerhttps://kidscancode.org/godot_recipes/4.x/audio/https://github.com/GDQuest/godot-design-patterns

Enjoy my Frankenstein creation!

Creating Generic Scripts

Description

The first thing we will do is create a Global script, creating a singleton. With this we can fill an array with audio values that every other game object always has access to. So we will create a script and attach it in program settings.

We'll then create a script we won't attach to anywhere for a bit, but we'll want it handy.

Instructions

First, open a blank project with the GMAP Hotkeys enabled. Importing the template is fastest method and outlined in first tutorial.

Ctrl+F3 to open the script editor

Ctrl+N to open the new script dialogue

Name it "AudioManager.gd" then accept. All other defaults are fine.

Copy the below script into this window

extends Node

const SETTINGS_PATH := "user://audio_setting.cfg"
const AUDIO_ROOT := "res://sounds" #This is the folder we will search. Change this to the root folder.


# DATA
var sound_overrides := {}   # sound_id -> linear volume
var preview_streams := {}  # sound_id -> AudioStream

func _ready() -> void:
	_scan_audio_files(AUDIO_ROOT)
	load_settings()


#Search the AUDIO_ROOT for all audio files, it looks for .wav, .ogg, and .mp3
func _scan_audio_files(path: String) -> void:
	var entries := ResourceLoader.list_directory(path)

	for entry in entries:
		if entry.begins_with("."):
			continue

		var full_path := path.path_join(entry)

		# Try to list it as a directory
		var sub_entries := ResourceLoader.list_directory(full_path)
		if sub_entries.size() > 0:
			# It is a directory
			_scan_audio_files(full_path)
		else:
			# It is a file
			if entry.get_extension().to_lower() in ["wav", "ogg", "mp3"]:
				var stream := load(full_path)
				if stream is AudioStream:
					preview_streams[full_path.to_lower()] = stream
#Getter/setters for volume for each sounds
func set_sound_volume(sound_id: String, linear: float) -> void:
	sound_overrides[sound_id.to_lower()] = linear
	
func get_sound_volume(sound_id: String) -> float:
	return sound_overrides.get(sound_id.to_lower(), 1.0)

# Previewer code
func get_preview_stream(sound_id: String) -> AudioStream:
	return preview_streams.get(sound_id.to_lower())
	
# SAVE / LOAD
func save_settings() -> void:
	var cfg := ConfigFile.new()
	
	for sound_id in sound_overrides:
		cfg.set_value("sounds", sound_id.to_lower(), sound_overrides[sound_id.to_lower()])

	cfg.save(SETTINGS_PATH)
	
func load_settings() -> void:
	var cfg := ConfigFile.new()
	var err := cfg.load(SETTINGS_PATH)

	if err != OK:
		return  # First run, nosthing saved yet
		
	# Load per-sound volumes
	if cfg.has_section("sounds"):
		for sound_id in cfg.get_section_keys("sounds"):
			sound_overrides[sound_id.to_lower()] = cfg.get_value("sounds", sound_id.to_lower(), 1.0)

Ctrl+N to open the new script dialogue

Create a script called "AccessibleAudioPlayer.gd"

Paste this code into it

extends AudioStreamPlayer

@export var base_volume: float = 1.0
var sound_id: String

func _ready() -> void:
	if stream == null:
		push_warning("AudioStreamPlayer has no stream assigned.")
		return
	sound_id = stream.resource_path
	apply_accessibility_volume()


func apply_accessibility_volume() -> void:
	var accessibility := AudioManager.get_sound_volume(sound_id)
	volume_db = linear_to_db(base_volume * accessibility)

We now need to make our first script a Global script.

Ctrl+F10 to go to the menu bar

Press right twice, then press down once to get to project settings, press enter.

Press shift tab to get to the menu tabs

Press right along the menu tabs until you get to "Globals". Focus may jump into body of a menu before then, so press SHIFT+TAB to get back to tabs if it does.

Warning: There are some "Input capture" sections in the settings. If you run into them, ESC key will change your focus. Tab/Arrow keys will be captured and not processed.

Press down twice, then enter. This should open up a file selection menu. Navigate and find AudioManager.GD

After it's been selected, press right twice, then enter, to add your global. That script is now initialized on execution, and accessible from anywhere in the game.

Our Sound Library

Description:

With our global script set up, we now need a menu which will have:

A tab for each folder inside of our Audio Root.

Name of the sound

Ability to listen to it

Current volume

Ability to change the volume

Thanks to our singleton, that'll actually be pretty easy regardless of how many sounds we have.

Instructions:

Create a new scene. CTRL+N, CTRL+A, then select: ControlSet focus to Control in scene tab (CTRL+F8).Press enter on Control and rename it to AudioLibrary.Add node TabContainer as child of AudioLibrary(Control Node) CTRL+A to open node create menu.Set focus to AudioLibrary in scene tab (CTRL+F8).Add node AudioStreamPlayer as child of AudioLibrary(Control Node) CTRL+A to open node create menu.Press enter on TabContainer and rename it to Tabs.Set focus to AudioStreamPlayer in scene tab (CTRL+F8).Press enter on AudioStreamPlayer and rename it to PreviewPlayer.Set focus to AudioLibrary in scene tab (CTRL+F8).Attach a script to AudioLibrary(CTRL+Equal Sign)

extends Control


@onready var preview_player := $PreviewPlayer
@onready var tabs: TabContainer = $Tabs
var category_containers := {}

func _ready() -> void:
	var sound_ids = AudioManager.preview_streams.keys()
	sound_ids.sort()

	for sound_id in sound_ids:
		var category := _get_sound_category(sound_id)
		var container := _get_or_create_tab(category)
		create_slider_for(sound_id, container)
	tabs.get_tab_bar().grab_focus()

func create_slider_for(sound_id: String, parent: VBoxContainer) -> void:
	var row := HBoxContainer.new()
	row.size_flags_horizontal = Control.SIZE_EXPAND_FILL

	var label := Label.new()
	label.text = _pretty_name_from_path(sound_id)
	label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
	label.custom_minimum_size.x = 200

	var slider := HSlider.new()
	slider.accessibility_name = label.text
	slider.min_value = 0.0
	slider.max_value = 200.0
	slider.step = 1
	slider.size_flags_horizontal = Control.SIZE_EXPAND_FILL
	slider.value = AudioManager.get_sound_volume(sound_id) * 100

	slider.value_changed.connect(func(value: float) -> void:
		AudioManager.set_sound_volume(sound_id, value * 0.01)
		preview_player.play_preview(sound_id)
	)

	slider.focus_exited.connect(func() -> void:
		stop_preview()
	)

	row.add_child(label)
	row.add_child(slider)
	parent.add_child(row)
	
func _pretty_name_from_path(path: String) -> String:
	var sound_name := path.get_file().get_basename()
	sound_name = sound_name.replace("_", " ")
	return sound_name.capitalize()
	
#When a slider loses focus, we should stop playing the sound, and save settings.
func stop_preview():
	preview_player.stop()
	AudioManager.save_settings()

func _unhandled_input(event):
	if event is InputEventKey:
		#when escapse is pressed, stop the preview and save the settings, then go to another room!
		if event.pressed and event.keycode == KEY_ESCAPE:
			stop_preview()
			get_tree().change_scene_to_file("res://rooms/main_screen.tscn")
			
func _get_sound_category(path: String) -> String:
	var parts := path.replace("res://sounds/", "").split("/")
	return parts[0] if parts.size() > 1 else "Other"

func _get_or_create_tab(category: String) -> VBoxContainer:
	if category_containers.has(category):
		return category_containers[category]

	# Root node for the tab
	var scroll := ScrollContainer.new()
	scroll.name = category.capitalize()
	scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
	scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL

	var vbox := VBoxContainer.new()
	vbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL
	vbox.size_flags_vertical = Control.SIZE_EXPAND_FILL

	scroll.add_child(vbox)
	tabs.add_child(scroll)

	category_containers[category] = vbox
	return vbox

Ctrl+S, then Enter to save it with default name.

Set focus to PreviewPlayer in scene tab (CTRL+F8).Attach a script to PreviewPlayer(CTRL+Equal Sign)

extends AudioStreamPlayer

func play_preview(sound_id: String, base_volume: float = 1.0) -> void:
	var input_stream := AudioManager.get_preview_stream(sound_id)
	if input_stream == null:
		return

	if playing:
		stop()

	stream = input_stream
	volume_db = linear_to_db(
		base_volume * AudioManager.get_sound_volume(sound_id)
	)
	play()

Set focus to Tabs in scene tab (CTRL+F8).Go to inspector, ctrl+f6 to get to inspector tab, ctrl+U to inspector categories.Property size set to (1280.0, 1280.0) Just for any sighted players. Putting this a scrolling box later might be nice too if you have too many in one folder!

Now in the directory you created your game project, create a folder named "audio" and add some sample sounds to it. This is easiest done outside of Godot. When you come back to Godot it should refresh and have your audio imported now!

Press F5 to test your project. Choose current scene as default if it asks, and press enter if you need to save!

You should be able to go up and down using the arrow keys, then left and right changes the volumes and after a small delay you will hear the game at the new volume.

Test room

Description:

We're going to just create a a scene with nothing but a player setup like how an actual player would be set up, to demonstrate how now that it works the volume will now save from scene to scene and game to game!

Instructions:

Create a new scene. CTRL+N, CTRL+A, then select: Node2DSet focus to Node2D in scene tab (CTRL+F8).Press enter on Node2D and rename it to Game.Add node AudioStreamPlayer as child of Game(Node2D Node) CTRL+A to open node create menu.Press enter on AudioStreamPlayer and rename it to Music.Attach a script to Music(CTRL+Equal Sign)

Select the script AccessibleAudioPlayer.gdGo to inspector, ctrl+f6 to get to inspector tab, ctrl+U to inspector categories.Property stream then select the name of sound file you want to test withProperty autoplay set to true

Test!

Press F5 and then adjust the volume using your sliders of the sound that you just selected.

Press ESC to go to the "game" room.

You should now hear your sound at the new volume, and the changes will save!

Going forward:

Some things to add if you want to use this in project could be:

Creating a scene that is an audio player and has the script attached and using that instance whenever needed.

If you're going to use it for 2D audio, can use those nodes.

Maybe the name isn't enough information, so can add a library where for certain sounds you can add more details on what the sound means!

Set it up so you have different major categories.