Mixing System#
This chapter explains where sound is routed, how to adjust groups of sounds together, and how to apply effects to them.
If you are new to Godot-FmodPlayer, think of a Bus like a Godot audio bus: music goes to Music, sound effects go to SFX, and dialogue goes to Voice. Once sounds are separated into buses, you can control an entire category at once.
When is a mixing system needed#
Common needs map to these approaches:
What you want to do |
Recommended approach |
|---|---|
Lower all music |
Play music through the |
Mute sound effects while paused |
Mute the |
Apply filtering to all footsteps |
Create a |
Fine-tune a single currently playing sound |
Get the Channel and control only that playback instance. |
Control a group of sounds together |
Use a Bus or its underlying ChannelGroup. |
Core Concepts#
Bus#
A Bus is a mixing route. You can think of it as a pipeline for a category of sounds.
For example:
Master
├── Music
├── SFX
├── Voice
└── Ambient
All sounds eventually flow into Master. If background music goes to Music and button sounds go to SFX, you can control them separately later.
ChannelGroup#
A ChannelGroup is the lower-level FMOD object used to mix a group of Channel objects. For normal use, prefer FmodAudioBus or the bus layout; access FmodChannelGroup directly only when you need lower-level playback control.
DSP#
A DSP is an audio effect processing unit. Reverb, filtering, delay, compression, and spectrum analysis are all DSPs. When multiple sounds should share the same effect, usually add the effect to a bus instead of adding it to every sound individually.
Send Audio to a Bus#
The simplest way is to set the playback node’s bus:
extends Node
@onready var music := $MusicPlayer
@onready var hit_sfx := $HitPlayer
func _ready():
music.bus = "Music"
hit_sfx.bus = "SFX"
func play_music():
music.play()
func play_hit():
hit_sfx.play()
If you play a Sound directly from code, pass the target ChannelGroup to play_sound():
func play_ui_sound(path: String):
var system := FmodServer.main_system
var ui_bus := system.get_channel_group_by_name("UI")
var sound := system.create_sound_from_file(path)
return system.play_sound(sound, ui_bus, false)
Control Bus Volume, Mute, and Solo#
Volume is usually expressed in decibels (dB):
0.0means the original volume.Negative values make it quieter, such as
-6.0.Positive values make it louder, but be careful about distortion.
func set_music_quiet():
var system := FmodServer.main_system
var music := system.get_channel_group_by_name("Music")
music.volume_db = -12.0
func mute_sfx(muted: bool):
var system := FmodServer.main_system
var sfx := system.get_channel_group_by_name("SFX")
sfx.mute = muted
func solo_voice(enabled: bool):
var system := FmodServer.main_system
var voice := system.get_channel_group_by_name("Voice")
voice.solo = enabled
mute silences this bus. solo lets you hear only this bus, which is useful when debugging dialogue, ambience, or music.
Synchronize with Godot Audio Buses#
Godot-FmodPlayer can synchronize the FMOD bus layout with Godot’s AudioServer bus layout. This means buses such as Music, SFX, and Voice created in Godot’s audio bus panel can also be mapped to the FMOD side.
Usually, you only need to get the current layout through FmodServer.get_audio_bus_layout():
func get_layout():
var layout := FmodServer.get_audio_bus_layout()
return layout
If you modify Godot’s audio bus structure at runtime, you can synchronize the FMOD side when needed:
func sync_buses_from_godot():
var layout := FmodServer.get_audio_bus_layout()
layout.sync_from_audio_server_if_changed()
Synchronization keeps or creates the Master bus, then updates FMOD-side buses, parent-child relationships, volume, mute, solo, bypass state, and supported audio effects according to Godot’s current bus structure.
Note
If your project’s bus structure is mainly maintained in the Godot editor, prefer creating buses in Godot’s audio bus panel and then synchronizing the FMOD layout. Call create_audio_bus() manually only when you need temporary buses at runtime or explicitly want code to manage the bus structure.
Create Your Own Buses#
When the default buses are not enough, you can add buses in Godot’s audio bus panel and synchronize them to FMOD. You can also create new buses directly in code through FmodAudioBusLayout:
func setup_extra_buses():
var layout := FmodServer.get_audio_bus_layout()
layout.create_audio_bus("UI", "Master")
layout.create_audio_bus("Weapons", "SFX")
layout.create_audio_bus("Footsteps", "SFX")
Keep the bus hierarchy simple at first:
Master
├── Music
├── SFX
│ ├── Weapons
│ └── Footsteps
├── Voice
└── UI
If you are not sure whether to create a new bus, ask: will I need to control its volume, mute state, or effects separately later? If the answer is yes, it is a good candidate for its own bus.
Add Effects to a Bus#
Bus effects are useful for processing an entire category of sounds, such as making all indoor footsteps muffled or adding a low-pass effect to music when the pause menu opens.
func add_pause_filter():
var layout := FmodServer.get_audio_bus_layout()
var filter := FmodAudioEffectFilter.new()
filter.cutoff_hz = 1200.0
filter.resonance = 0.2
layout.add_bus_effect("Music", filter)
The filter here is a DSP. If you only want to affect one playback instance, do not add it to the bus; controlling the returned Channel is a better fit.
Fade In and Fade Out#
Bus objects are not Godot nodes, so you cannot bind property animation with tween_property() directly. You can interpolate frame by frame in a loop:
func fade_bus(bus_name: String, target_db: float, duration: float):
var system := FmodServer.main_system
var bus := system.get_channel_group_by_name(bus_name)
if bus == null:
return
var start_db := bus.get_volume_db()
var elapsed := 0.0
while elapsed < duration:
await get_tree().process_frame
elapsed += get_process_delta_time()
var t := clampf(elapsed / duration, 0.0, 1.0)
bus.set_volume_db(lerpf(start_db, target_db, t))
bus.set_volume_db(target_db)
A common use is lowering music while paused:
func on_pause_changed(paused: bool):
if paused:
fade_bus("Music", -10.0, 0.25)
else:
fade_bus("Music", 0.0, 0.25)
Lower Music to Make Room for Dialogue#
This effect is often called ducking: lower the music while dialogue plays, then restore it afterward.
var normal_music_db := 0.0
var ducked_music_db := -12.0
func play_voice_line(path: String):
await fade_bus("Music", ducked_music_db, 0.2)
var system := FmodServer.main_system
var voice_bus := system.get_channel_group_by_name("Voice")
var sound := system.create_sound_from_file(path)
var channel := system.play_sound(sound, voice_bus, false)
while channel != null and channel.is_playing():
await get_tree().process_frame
await fade_bus("Music", normal_music_db, 0.3)
You do not have to use a compressor for this. For most games, directly lowering the music bus is clear enough and easier to understand and debug.
Advanced: When to Look at the Mix Matrix#
A Mix Matrix precisely controls which input channel is sent to which output channel. If you only want to move a sound left or right, prefer FmodChannelControl.set_pan().
Continue to FmodChannelControl.set_mix_matrix() only in cases like these:
You need to manually distribute mono audio to multi-channel output, which is Upmix.
You need to fold multi-channel content such as 5.1 down to stereo, which is Downmix.
You need to swap left and right channels or do special channel routing.
You need to debug the channel order of an audio asset.
A minimal example: swap the left and right channels of a stereo sound.
func swap_stereo(channel: FmodChannel):
var matrix := PackedFloat32Array([
0.0, 1.0,
1.0, 0.0,
])
channel.set_mix_matrix(matrix, 2, 2)
Performance and Troubleshooting#
For mixing-related issues, start with these three checks:
Whether the sound is routed to the correct Bus.
Whether the target bus is muted, soloed, or too quiet.
Whether too many DSP effects are enabled at the same time.
You can view FMOD’s registered performance metrics in Godot’s performance monitors:
func _process(_delta):
var dsp_usage = Performance.get_monitor("FmodCPUUsage/DSP")
var stream_usage = Performance.get_monitor("FmodCPUUsage/Stream")
var channels = FmodServer.main_system.get_channels_playing()
print("DSP: %.2f%% | Stream: %.2f%% | Real: %d | Virtual: %d" % [
dsp_usage,
stream_usage,
channels["real"],
channels["virtual"],
])
Here, Virtual refers to Virtual Channel. It is not necessarily an error; often it simply means FMOD is saving mixing resources for you.
Recommendations#
Start with a simple structure such as
Master,Music,SFX,Voice, andUI.Split buses further only when you need separate control.
When multiple sounds share an effect, add the DSP to the bus.
For temporary control of one sound, prefer the Channel returned by playback.
Do not design a very deep bus tree at the beginning. Being easy to hear and maintain matters more than looking professional.