Appearance
音效总线
背景:脚步声、环境音效等分散在各模块中,没有统一的音量控制和混音管理。ADR-088 决策引入 audio-bus 音效总线。 过程:定义 AudioBus 类 → 音量分组(SFX/Ambient/Music)→ 全局音量控制 → 混音器 UI 接入。
序
七月十日,下午。
Jieling 站在 Riku 身后,看着屏幕上的初音未来在跳舞。
"脚步声," 她说,"太大了。"
Riku 调低了脚步声的音量。但环境音效(风声、树叶声)也跟着变小了。
"我想只调脚步声," Jieling 说,"不想影响风声。"
"现在音量是一个全局变量," Riku 说,"改一个,全改。"
"那不行。"
总线
Riku 打开 ADR-088 的文档,标题是「Audio Bus 音效总线」。
核心思路:把音效按类型分组,每组一个"总线",每个总线有独立的音量控制。
typescript
class AudioBus {
private channels: Map<string, AudioChannel>;
constructor() {
this.channels = new Map();
}
createChannel(name: string, group: AudioGroup): AudioChannel {
const channel = new AudioChannel(group);
this.channels.set(name, channel);
return channel;
}
setGroupVolume(group: AudioGroup, volume: number): void {
for (const channel of this.channels.values()) {
if (channel.group === group) {
channel.setVolume(volume);
}
}
}
}"三个总线," Riku 说,"SFX(音效)、Ambient(环境音)、Music(音乐)。每个总线独立控制。"
"脚步声归 SFX?" Jieling 问。
"对," Riku 说,"风声归 Ambient。背景音乐归 Music。"
音量
Riku 在 src/core/audio-bus.ts 里实现了 AudioBus 类。
每个 AudioChannel 维护一个 GainNode(Web Audio API 的音量节点):
typescript
class AudioChannel {
private gainNode: GainNode;
private group: AudioGroup;
constructor(group: AudioGroup) {
this.group = group;
this.gainNode = audioContext.createGain();
this.gainNode.connect(audioContext.destination);
}
setVolume(volume: number): void {
this.gainNode.gain.setValueAtTime(volume, audioContext.currentTime);
}
getSource(): GainNode {
return this.gainNode;
}
}"GainNode 是 Web Audio API 的标准音量控制节点," Riku 解释,"所有音效都通过它输出,修改它的 gain 值就能控制音量。"
"全局音量呢?" Jieling 问。
"全局音量是 master gain," Riku 说,"所有 channel 的输出都先经过 master gain,再输出到扬声器。"
typescript
class AudioBus {
private masterGain: GainNode;
constructor() {
this.masterGain = audioContext.createGain();
this.masterGain.connect(audioContext.destination);
}
setMasterVolume(volume: number): void {
this.masterGain.gain.setValueAtTime(volume, audioContext.currentTime);
}
}UI
Riku 在 src/menus/settings-sound.ts 里加了混音器 UI:
- Master 音量滑块(0-100%)
- SFX 音量滑块(0-100%)
- Ambient 音量滑块(0-100%)
- Music 音量滑块(0-100%)
typescript
function buildSoundMixerLevel(): PopupLevel {
return {
title: '混音器',
items: [
slideRow({
label: '主音量',
icon: 'mdi:volume-high',
get: () => audioBus.getMasterVolume(),
set: (v) => audioBus.setMasterVolume(v),
}),
slideRow({
label: '音效',
icon: 'mdi:ear-hearing',
get: () => audioBus.getGroupVolume('sfx'),
set: (v) => audioBus.setGroupVolume('sfx', v),
}),
// ... Ambient, Music
],
};
}"现在用户可以独立控制每个总线的音量," Riku 说,"调脚步声不影响风声。"
"不错," Jieling 说。
脚步声
脚步声是 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);
}"脚步声通过 SFX 总线播放," Riku 说,"用户调 SFX 音量,脚步声跟着变。"
"多个角色同时走路呢?" Jieling 问。
"每个角色的脚步声是独立的 channel," Riku 说,"但它们都属于 SFX 总线。调总线音量,所有角色的脚步声一起变。"
尾声
Riku 提交了代码。
feat: AudioBus 音效总线(ADR-088)— SFX/Ambient/Music 分组 + 全局音量控制
feat: 混音器 UI 接入(settings-sound.ts)他站在窗边,看着远处的天际线。七月的阳光已经落下去了,路灯一盏一盏亮起来。
音效总线。不是复杂的混音台,只是三个滑块。但它让用户能精确控制声音——脚步声大了调小点,风声小了调大点,音乐关掉也行。
"声音是城市的背景音乐," Riku 对自己说,"总线是调音台。"
教训:音效管理不是"播放声音"那么简单。分组、混音、全局控制——这些是让用户"听到"想要的声音的基础。AudioBus 不是银弹,但它把"所有声音一个音量"的问题解决了。