混音系统#
本章讲的是“声音最后从哪里出来、怎样一起调音量、怎样一起加效果”。
如果你刚开始使用 Godot-FmodPlayer,可以先把 Bus 理解成 Godot 的音频总线:音乐走 Music,音效走 SFX,对白走 Voice。把声音分到不同总线后,就可以一次性控制一整类声音。
什么时候需要混音系统#
常见需求可以这样对应:
你想做的事 |
推荐做法 |
|---|---|
调低所有音乐 |
把音乐播放到 |
暂停时让音效静音 |
静音 |
对所有脚步声加滤波 |
建一个 |
对单个正在播放的声音微调 |
取得 Channel,只控制这一次播放 |
对一组声音统一控制 |
使用 Bus 或它底层的 ChannelGroup |
核心概念#
Bus#
Bus 是一条混音路径。你可以把它想成一条“声音分类管道”。
例如:
Master
├── Music
├── SFX
├── Voice
└── Ambient
所有声音最后都会汇入 Master。如果把背景音乐送到 Music,把按钮音效送到 SFX,以后就可以分别控制它们。
ChannelGroup#
ChannelGroup 是 FMOD 底层用于混合一组 Channel 的对象。普通使用时优先操作 FmodAudioBus 或总线布局;只有需要更底层的播放控制时,才直接拿 FmodChannelGroup。
DSP#
DSP 是音频效果处理单元。混响、滤波、延迟、压缩、频谱分析都属于 DSP。多个声音要共享同一个效果时,通常把效果加到总线上,而不是给每个声音单独加。
把声音送到总线#
最简单的方式是设置播放节点的 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()
如果你直接用代码播放 Sound,就把目标 ChannelGroup 传给 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)
控制总线音量、静音和独奏#
音量通常用分贝(dB)表示:
0.0表示原始音量。负数表示变小,例如
-6.0。正数表示变大,但要小心失真。
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 是让这条总线静音。solo 是只听这条总线,常用于调试对白、环境声或音乐。
和 Godot 音频总线同步#
Godot-FmodPlayer 的 FMOD 总线布局会和 Godot 的 AudioServer 总线布局同步。也就是说,你在 Godot 的音频总线面板里创建的 Music、SFX、Voice 等总线,也可以被映射到 FMOD 侧使用。
通常情况下,你只需要通过 FmodServer.get_audio_bus_layout() 取得当前布局:
func get_layout():
var layout := FmodServer.get_audio_bus_layout()
return layout
如果你在运行时修改了 Godot 的音频总线结构,可以让 FMOD 侧按需同步:
func sync_buses_from_godot():
var layout := FmodServer.get_audio_bus_layout()
layout.sync_from_audio_server_if_changed()
同步会保留或创建 Master 总线,并根据 Godot 当前的总线结构更新 FMOD 侧的总线、父子关系、音量、静音、独奏、旁路状态和支持的音频效果。
备注
如果项目的总线结构主要在 Godot 编辑器里维护,推荐优先通过 Godot 音频总线面板创建总线,再让 FMOD 布局同步。只有运行时需要临时总线,或者你明确希望由代码管理总线结构时,再手动调用 create_audio_bus()。
创建自己的总线#
默认总线不够用时,可以通过 Godot 音频总线面板新增总线,然后同步到 FMOD。也可以直接通过 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")
建议总线层级先保持简单:
Master
├── Music
├── SFX
│ ├── Weapons
│ └── Footsteps
├── Voice
└── UI
如果不确定是否要新建总线,先问自己一句:以后是否要单独调它的音量、静音或效果?如果答案是“是”,就适合拆成总线。
给总线添加效果#
总线效果适合处理“一整类声音”。例如所有室内脚步声都变闷,或者暂停菜单打开时让音乐带一点低通。
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)
这里的滤波器就是一个 DSP。如果只想影响某一次播放,不要加到总线上;去控制那一次播放返回的 Channel 会更合适。
淡入淡出#
总线对象不是 Godot 节点,不能直接用 tween_property() 绑属性动画。可以用一个循环逐帧插值:
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)
常见用法是暂停时压低音乐:
func on_pause_changed(paused: bool):
if paused:
fade_bus("Music", -10.0, 0.25)
else:
fade_bus("Music", 0.0, 0.25)
降低音乐给对白让位#
这个效果常被叫作 ducking,意思是对白播放时先把音乐压低,播放结束后再恢复。
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)
这不是必须使用压缩器才能完成。对多数游戏来说,直接调低音乐总线已经足够清楚,也更容易理解和调试。
高级:混音矩阵什么时候需要看#
Mix Matrix 可以精确控制“哪个输入声道送到哪个输出声道”。如果你只是想让声音偏左或偏右,优先用 FmodChannelControl.set_pan()。
只有在这些情况下,才建议继续研究 FmodChannelControl.set_mix_matrix():
一个最小示例:把立体声左右声道对调。
func swap_stereo(channel: FmodChannel):
var matrix := PackedFloat32Array([
0.0, 1.0,
1.0, 0.0,
])
channel.set_mix_matrix(matrix, 2, 2)
性能与排查#
混音相关问题可以从三处开始看:
可以在 Godot 性能监视器中查看 FMOD 注册的性能项:
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"],
])
这里的 Virtual 指 Virtual Channel。它不一定是错误,很多时候只是 FMOD 在帮你节省混音资源。