Skip to content

回声的归处

背景:脚步声和环境音效共用一个全局音量,调一个,全改。ADR-088 决策引入 audio-bus 音效总线。 过程:AudioBus 类 → 音量分组 → 全局音量控制 → 混音器 UI。


一、同一个音量

脚步声和环境音效共用一个全局变量。

Sisyphus 在 audio.ts 里找到一个叫 volume 的数字。它同时控制脚步声、风声、音乐。把它调小,脚步声小了,风声也小了,音乐也小了。把它调大,三个一起大。

这是一个合理的设计——如果联邦只有一种声音。

但它不是一。它有三种:角色走路时的脚步声,环境里的风声和水声,还有背景音乐。它们应该各自有各自的音量,然后合在一起,交给一个总开关。

但代码里没有这个结构。只有一个数字。


二、总线

Sisyphus 在 src/core/audio-bus.ts 里新建了一个类:AudioBus

它的结构不复杂——一个 Map,把 channel 按名字存起来。每个 channel 属于一个组:sfxambientmusic

typescript
class AudioBus {
    private channels = new Map<string, AudioChannel>();
}

每个 AudioChannel 内部是一个 GainNode。Web Audio API 的音量控制节点,所有音效通过它输出到扬声器。

组之间互不干扰。调 sfx 组的音量,ambientmusic 不受影响。

三个组之上还有一个 master gain。所有组的输出先汇聚到 master,再通过 master 输出。调 master,所有组一起变。

Sisyphus 把脚步声归到 sfx,风声归到 ambient,音乐归到 music

分组之后,代码里出现了一个新概念:组音量。audioBus.getGroupVolume('sfx') 返回音效组的当前音量。audioBus.setGroupVolume('sfx', 0.7) 把音效组调到 70%。

这是一个很小的改动,但它改变了声音在联邦里的组织方式。


三、脚步声

脚步声是 audio-bus 的第一个使用者。

perception-balance.ts 里,每帧检测脚是否落地。落地时触发一段音效。

typescript
function _onFootLanding(side: 'left' | 'right'): void {
    const sound = footstepSounds[side][
        Math.floor(Math.random() * footstepSounds[side].length)
    ];
    const channel = audioBus.getChannel(`footstep-${side}`, 'sfx');
    channel.play(sound);
}

每个角色的脚步声是独立的 channel,但它们都属于 sfx 组。用户调 sfx 音量,所有角色的脚步声一起变。

这解决了两个问题。第一,脚步声不再影响风声和音乐。第二,多个角色同时走路时,脚步声不会互相覆盖——每个 channel 独立播放,但共享音量控制。


四、混音器

Sisyphus 在 settings-sound.ts 里加了一个混音器界面。

四个滑块:主音量、音效、环境、音乐。每个滑块对应一个 getGroupVolume / setGroupVolume 调用。

typescript
slideRow({
    label: t('menu.sound.sfx'),
    icon: 'mdi:ear-hearing',
    get: () => audioBus.getGroupVolume('sfx'),
    set: (v) => audioBus.setGroupVolume('sfx', v),
});

用户在设置页调整脚步声的音量,不需要重启应用,不需要改代码。滑块一拖,即时生效。

这不是什么复杂的功能——四个滑块,四行代码。但它让"调音量"从一种开发者的特权变成了用户的操作。


尾声

Sisyphus 提交了代码。

脚步声归 sfx,风声归 ambient,音乐归 music。三个组,一个 master。

声音终于有了各自的去处。