Setting Godot 4 audio bus volume from a slider

Wire a 0-to-1 slider to a Godot 4 audio bus volume setting: linear_to_db, set_bus_volume_db, muting at zero, and saving the level. From a real addon.

A player pulls the Music slider from full down to about a quarter and expects the track to drop to roughly a quarter of its loudness. Instead it barely moves for most of the travel, then falls off a cliff near the bottom. Maybe it does the opposite, depending on which value you fed where. The slider reads 0.0 to 1.0, a Godot 4 audio bus wants its volume in decibels, and the mismatch between those two number lines is the whole problem this post is about.

A Godot audio bus stores its volume in dB. 0 dB is unity, no change. Negative numbers are quieter and positive numbers are louder, on a logarithmic scale, because that is how loudness actually registers to a human ear. A linear slider from 0 to 1 is not that scale. If you take the slider's value and drop it straight into set_bus_volume_db, you are telling the bus to sit between 0 dB and 1 dB across the entire range of the control. That is a band about one decibel wide. The slider does almost nothing for its whole length, and everything you hear happens in the last hair of travel. This is the single most common way people wire audio settings, and it is wrong in a way that sounds like the slider is broken.

The one function that fixes it

Godot ships the conversion you need as a global: linear_to_db. It takes a linear amplitude from 0 to 1 and returns the dB value that produces that amplitude. Feed it 1.0 and you get 0 dB. Feed it 0.5 and you get about -6 dB, which is the half-loudness point, and that is the number the bus wants. So the real line, the one doing the actual work, is this:

AudioServer.set_bus_volume_db(bus_idx, linear_to_db(vol))

That is it. vol is the raw slider value and bus_idx is the integer index of the bus you are controlling, with linear_to_db bridging the gap between them. Everything else in a settings menu is bookkeeping around that one call: locating the right bus, handling the zero case, then writing the value to disk so it survives a restart.

Finding the audio bus by name

set_bus_volume_db wants an index, not a name, and indices are brittle. Bus 0 is always Master, but Music landing on bus 1 and Sfx on bus 2 depends entirely on the order you added them in the audio panel. Hard-code the index and someone reorders the layout six months later and every slider controls the wrong channel. So resolve by name at the point of use. Here is how the Settings addon does it, in the worker that applies a single audio setting:

func _apply_audio_one(bus_key: String) -> void:
    var bus_idx := AudioServer.get_bus_index(bus_key.capitalize())
    if bus_idx < 0:
        # Fall back to lowercased name.
        bus_idx = AudioServer.get_bus_index(bus_key)
    if bus_idx < 0:
        # Surface this loudly -- silent -1 means a bus the developer
        # renamed or never created, and audio sliders quietly do nothing.
        push_warning("Settings: no audio bus named '%s' or '%s'; '%s' slider will be inert." % [bus_key.capitalize(), bus_key, bus_key])
        return
    var vol: float = float(get_value("audio.%s" % bus_key, 1.0))
    if vol <= 0.0001:
        AudioServer.set_bus_mute(bus_idx, true)
    else:
        AudioServer.set_bus_mute(bus_idx, false)
        AudioServer.set_bus_volume_db(bus_idx, linear_to_db(vol))

The bus_key here is a short lowercase token like "master" or "sfx". The stored setting lives under audio.master, audio.sfx, and so on, so the key and the bus name stay coupled by convention rather than by a second lookup table you have to keep in sync. get_value pulls the last saved level, defaulting to 1.0 (full) when nothing has been saved yet. get_bus_index returns -1 for a name it does not recognize, and that -1 is quiet. Nothing throws. The slider just moves and no sound changes, which is a miserable bug to chase, so the addon turns that -1 into a push_warning naming both spellings it tried.

Zero is a mute, not a volume

Notice the code never calls linear_to_db(0.0). It checks for zero first and mutes the bus instead. The reason is that linear_to_db(0.0) returns negative infinity, because silence is infinitely far down a logarithmic scale. Some Godot versions clamp it and some hand you -inf straight through, and neither is something you want flowing into set_bus_volume_db and getting serialized into a save file. So the addon draws a floor. Anything at or below 0.0001 counts as off, and it flips set_bus_mute(bus_idx, true) rather than trying to express silence as a volume. Above the floor, it unmutes and sets the real level. That unmute on the else branch matters. Drag to zero, then drag back up, and without the explicit set_bus_mute(bus_idx, false) the bus stays muted and the slider lies to you.

The slider end

On the UI side there is nothing clever, and that is on purpose. The slider is a plain HSlider with a 0-to-1 range, and it hands its value to the Settings autoload the moment it changes. The autoload does the conversion and the persistence. The control just reports a number between 0 and 1.

var slider := HSlider.new()
slider.min_value = 0.0
slider.max_value = 1.0
slider.step = 0.01
slider.value = float(settings.get_value(setting_key, 1.0))

slider.value_changed.connect(func(v: float):
    settings.set_value(setting_key, v)
    value_label.text = "%d%%" % int(v * 100.0))

The initial value is read back from saved settings, so a menu opened after a restart shows where the player actually left the slider, not a default. The percentage label is display only. It takes the same linear 0-to-1 value and multiplies by 100, which is honest here precisely because the slider is linear. If you tried to label the dB value as a percentage you would be back in the trap from the top of this post, showing "50%" at a spot that is nowhere near half as loud.

Saving, and why the conversion never touches disk

set_value does two jobs. It applies the change live by routing back through _apply_audio_one, and it writes the settings dictionary to user://settings.json. What gets saved is the linear slider value from 0 to 1, not the dB. This is the right call for two reasons. The stored number stays human-readable and matches what the slider shows, and the linear-to-dB conversion runs fresh every time settings are applied on load. Keep the raw player-facing value as the source of truth and derive dB from it at the last possible moment. If you ever store the dB instead, you have baked one representation into your save format and made the linear slider a second-class citizen that has to reverse the conversion every time it opens.

To rebuild a rough version yourself: make a bus per key with names that match, add a 0-to-1 slider for each, and on value_changed look up the bus index by name. Below a small epsilon, mute the bus. Otherwise call set_bus_volume_db(idx, linear_to_db(value)) and persist the linear value. Apply the same logic once at startup so saved levels take effect before the player hears anything. The conversion is one function. The care is in the zero case and the bus-name match.

Or use mine

Settings + Menus, free on GitHub

All Godot posts · Home