混音系统#

本章讲的是“声音最后从哪里出来、怎样一起调音量、怎样一起加效果”。

如果你刚开始使用 Godot-FmodPlayer,可以先把 Bus 理解成 Godot 的音频总线:音乐走 Music,音效走 SFX,对白走 Voice。把声音分到不同总线后,就可以一次性控制一整类声音。

什么时候需要混音系统#

常见需求可以这样对应:

你想做的事

推荐做法

调低所有音乐

把音乐播放到 Music 总线,然后调这个总线的 volume_db

暂停时让音效静音

静音 SFX 总线

对所有脚步声加滤波

建一个 Footsteps 总线,并把效果加到这个总线上

对单个正在播放的声音微调

取得 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 的音频总线面板里创建的 MusicSFXVoice 等总线,也可以被映射到 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()

  • 要把单声道手动分配到多声道输出,也就是 Upmix

  • 要把 5.1 等多声道内容折叠成立体声,也就是 Downmix

  • 要交换左右声道或做特殊声道路由。

  • 要调试某个音频资产的声道顺序。

一个最小示例:把立体声左右声道对调。

func swap_stereo(channel: FmodChannel):
    var matrix := PackedFloat32Array([
        0.0, 1.0,
        1.0, 0.0,
    ])

    channel.set_mix_matrix(matrix, 2, 2)

性能与排查#

混音相关问题可以从三处开始看:

  • 声音是否被送到了正确的 Bus

  • 目标总线是否被静音、独奏或音量过低。

  • 是否有过多 DSP 同时启用。

可以在 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"],
    ])

这里的 VirtualVirtual Channel。它不一定是错误,很多时候只是 FMOD 在帮你节省混音资源。

建议#

  • 先用 MasterMusicSFXVoiceUI 这样的简单结构。

  • 只有需要单独控制时才继续拆分总线。

  • 多个声音共享效果时,把 DSP 加到总线上。

  • 单个声音的临时控制,优先使用播放返回的 Channel

  • 不要一开始就设计很深的总线树;能听清、能维护,比“看起来专业”更重要。