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-projects
https://github.com/insideout-andrew/godot-audio-manager
https://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_settings.cfg"
const AUDIO_ROOT := "res://audio"

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

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


# ------------------------------------------------
# FILESYSTEM SCANNING
# -------------------------------------------------

func _scan_audio_files(path: String) -> void:
	var dir := DirAccess.open(path)
	if dir == null:
		return

	dir.list_dir_begin()
	var file_name := dir.get_next()

	while file_name != "":
		if file_name.begins_with("."):
			file_name = dir.get_next()
			continue

		var full_path := path.path_join(file_name)

		if dir.current_is_dir():
			_scan_audio_files(full_path)
		else:
			if file_name.get_extension() in ["wav", "ogg", "mp3"]:
				var stream := load(full_path)
				if stream is AudioStream:
					preview_streams[full_path.to_lower()] = stream

		file_name = dir.get_next()

	dir.list_dir_end()
# -------------------------------------------------
# PER-SOUND ACCESSIBILITY
# -------------------------------------------------
#to lower used to avoid any issue with different file systems use of cases
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)


# -------------------------------------------------
# PREVIEW ACCESS
# -------------------------------------------------
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 where for each sound we have:

Its name

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: Control
Set focus to Control in scene tab (CTRL+F8).
Press enter on Control and rename it to AudioLibrary.
Add node GmVBoxContainer 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 VBoxContainer and rename it to Sliders.
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 sliders_container: VBoxContainer = $Sliders
@onready var preview_player := $PreviewPlayer


func _ready() -> void:
	# Create a slider for every discovered sound
	var sound_ids := AudioManager.preview_streams.keys()
	sound_ids.sort()
	for sound_id in sound_ids:
		create_slider_for(sound_id)

func create_slider_for(sound_id: String) -> void:
	# --- Container for one row ---
	var row := HBoxContainer.new()
	row.size_flags_horizontal = Control.SIZE_EXPAND_FILL

	# --- Label ---
	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

	# --- Slider ---
	var slider := HSlider.new()
	slider.accessibility_name = _pretty_name_from_path(sound_id)
	slider.min_value = 0.0
	slider.max_value = 200.0       # Allow boosting
	slider.step = 1
	slider.size_flags_horizontal = Control.SIZE_EXPAND_FILL

	# Set initial value from AudioManager, we want the bar to be whole numbers and this will return a non one, so we will do * 100, 1 is full volume, 2 is double, .5 is half.
	slider.value = AudioManager.get_sound_volume(sound_id) * 100

	# --- Connect slider ---
	slider.value_changed.connect(
		func(value: float) -> void:
			#Doing .01 to do our scale correclty.
			AudioManager.set_sound_volume(sound_id, value *.01)
			preview_player.play_preview(sound_id)
	)
	#when we change focus, stop the preview sounds. 
	slider.focus_exited.connect(
		func() -> void:
			stop_preview()
	)

	# --- Assemble ---
	row.add_child(label)
	row.add_child(slider)
	sliders_container.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://game.tscn")

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 Sliders 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. Making this a scrolling box later might be nice too if you have too many!

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: Node2D
Set 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.gd
Go 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 with
Property 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!