12 KiB
音频系统
2.B 有了与 2.A 完全不同的音频系统,新的音频系统更加自由,功能更加丰富,可以创建多种自定义效果器。本文将讲解如何使用音频系统。
:::tip 多数情况下,你应该不需要使用本文所介绍的内容,因为样板已经将音效、背景音乐等处理完善。如果你想实现高级效果,例如混响效果等,才需要阅读本文。 :::
获取音频播放器
音频播放器在 @user/client-modules
模块中,直接引入即可:
// 在其他模块中使用模块化语法引入
import { audioPlayer } from '@user/client-modules';
// 在 client-modules 模块中使用模块化语法引入
import { audioPlayer } from '../audio'; // 改为你自己的相对路径
// 使用 Mota 全局变量引入
const { audioPlayer } = Mota.require('@user/client-modules');
音频系统工作流程
音频播放流程如下:
graph LR;
A(音频源) --> B(效果器) --> C(目的地(扬声器、耳机))
创建音频源
:::tip 本小节的内容极大概率用不到,如果不是需要非常底层的音频接口,可以不看本小节。 :::
样板内置了几种音频源,它们包括:
类型 | 适用场景 | 创建方法 |
---|---|---|
BufferSource |
预加载的完整音频文件 | createBufferSource() |
ElementSource |
通过 <audio> 标签播放 |
createElementSource() |
StreamSource |
流式音频/长音频 | createStreamSource() |
StreamSource
音频源
一般情况下,我们推荐使用 opus
格式的音频,这时候需要使用 StreamSource
音频源来播放。这个音频源包含了对 IOS 的适配,可以正确播放 opus
格式的音频。在使用它之前,我们需要先创建一个 StreamLoader
类,来对音频流式加载。假如你在 project/mybgm/
文件夹下有一个 xxx.opus
音频,你可以这么创建:
import { StreamLoader } from '@user/client-modules';
const stream = new StreamLoader('project/mybgm/xxx.opus');
然后,创建音频源,并将流加载对象泵入音频源:
const source = audioPlayer.createStreamSource();
stream.pipe(source);
stream.start(); // 开始流式加载,如果不需要实时性,也可以不调用,音频播放时会自动开始加载
ElementSource
音频源
从一个 audio
元素创建音频源,假设你想要播放 project/mybgm/xxx.mp3
,那么你可以这么创建:
const source = audioPlayer.createElementSource();
source.setSource('project/mybgm/xxx.mp3');
BufferSource
音频源
从音频缓冲创建音频源。音频缓冲是直接存储在内存中的一段原始音频波形数据,不经过任何压缩。假如你想播放 project/mysound/xxx.wav
,可以这么写:
async function loadWav(url: string) {
// 使用浏览器接口 fetch 来请求文件
const response = await fetch(url);
// 将文件接收为 ArrayBuffer 形式
const buffer = await response.arraybuffer();
// 创建音频源
const source = audioPlayer.createBufferSource();
// 直接传入 ArrayBuffer,内部会自动解析,当然也可以自己解析,传入 AudioBuffer
await source.setBuffer(source);
// 将音频源返回,供后续使用
return source;
}
创建音频路由
音频路由包含了音频播放的所有流程,想要播放一段音频,必须首先创建音频路由,然后使用 audioPlayer
播放这个音频路由。如下例所示:
import { AudioRoute, audioPlayer } from '@user/client-modules';
const route = audioPlayer.createRoute(source);
下面,我们需要将音频路由添加至音频播放器:
audioPlayer.addRoute('my-route', route);
之后,我们就可以使用 audioPlayer
播放这个音频了:
audioPlayer.play('my-route');
音频效果器
新的音频系统中最有用的功能就是音频效果器了。音频效果器允许你对音频进行处理,可以实现调节声道音量、回声效果、延迟效果,以及各种自定义效果等。
内置效果器包含这些:
效果器类型 | 功能说明 | 创建方法 |
---|---|---|
VolumeEffect |
音量控制 | createVolumeEffect() |
StereoEffect |
立体声场调节 | createStereoEffect() |
EchoEffect |
回声效果 | createEchoEffect() |
DelayEffect |
延迟效果 | createDelay() |
ChannelVolumeEffect |
调节某个声道的音量 | createChannelVolumeEffect |
每个效果器都有自己可以调节的属性,具体可以查看对应效果器的 API 文档,比较简单,这里不在讲解。下面主要讲解一下如何使用效果器,我们直接通过例子来看(代码由 DeepSeek R1
模型生成并微调):
// 创建效果链
const volume = audioPlayer.createVolumeEffect();
const echo = audioPlayer.createEchoEffect();
// 配置效果参数
volume.setGain(0.7); // 振幅变为 0.7 倍
echo.setEchoDelay(0.3); // 回声延迟 0.3 秒
echo.setFeedbackGain(0.5); // 回声增益为 0.5
// 应用效果到音频路由
const route = audioPlayer.getRoute('my-route')!;
route.addEffect([volume, echo]);
// 之后播放就有效果了
route.play();
空间音效
本音频系统还支持空间音效,可以设置听者位置和音频位置,示例如下(代码由 DeepSeek R1
模型生成并微调):
// 设置听者位置
audioPlayer.setListenerPosition(0, 1.7, 0); // 1.7米高度
audioPlayer.setListenerOrientation(0, 0, -1); // 面朝屏幕内
// 设置声源位置(使用 StereoEffect 效果器)
const stereo = audioPlayer.createStereoEffect();
stereo.setPosition(5, 0, -2); // 右方5米,地面下方2米
淡入淡出效果
音频系统提供了淡入淡出接口,可以搭配 mutate-animate
库实现淡入淡出效果:
import { Transition, linear } from 'mutate-animate';
// 创建音量效果器
const volume = audioPlayer.createVolumeEffect();
// 创建渐变类,使用渐变是因为可以避免来回播放暂停时的音量突变
const trans = new Transition();
trans.value.volume = 0;
// 每帧设置音量
trans.ticker.add(() => {
volume.setVolume(trans.value.volume);
});
// 当音频播放时执行淡入
route.onStart(() => {
// 两秒钟时间线性淡入
trans.time(2000).mode(linear()).transition('volume', 1);
});
route.onEnd(() => {
// 三秒钟时间线性淡出
trans.time(3000).mode(linear()).transition('volume', 0);
});
// 添加音量效果器
route.addEffect(volume);
音效系统
为了方便播放音效,音频系统内置提供了音效的播放器,允许你播放空间音效。
播放音效
样板已经自动将所有注册的音效加入到音效系统中,你只需要直接播放即可,不需要手动加载。播放时,可以指定音频的播放位置,听者(玩家)位置可以通过 audioPlayer.setPosition
及 audioPlayer.setOrientation
设置。示例如下:
import { soundPlayer } from '@user/client-modules';
// 播放已加载的音效
const soundId = soundPlayer.play(
'mysound.opus',
[1, 0, 0], // 音源位置,在听者前方 1m 处
[0, 1, 0] // 音源朝向,朝向天花板
);
// 停止指定音效
soundPlayer.stop(soundId);
// 停止所有音效
soundPlayer.stopAllSounds();
设置是否启用音效
你可以自行设置是否启用音效系统:
soundPlayer.setEnabled(false); // 关闭音效系统
soundPlayer.setEnabled(true); // 启用音效系统
音乐系统
音乐系统的使用与音频系统类似,包含播放、暂停、继续等功能。示例如下:
import { bgmController } from '@user/client-modules';
bgmController.play('bgm1.opus'); // 切换到目标音乐
bgmController.pause(); // 暂停当前音乐,会有渐变效果
bgmController.resume(); // 继续当前音乐,会有渐变效果
bgmController.blockChange(); // 禁用音乐切换,之后调用 play, pause, resume 将没有效果
bgmController.unblockChange(); // 启用音乐切换
自定义效果器
本小节内容由 DeepSeek R1
模型生成并微调。
效果器是新的音频系统最强大的功能,而且此系统也允许你自定义一些效果器,实现自定义效果。效果器的工作流程如下:
graph LR
Input[输入源] --> EffectInput[效果器输入]
EffectInput --> Processing[处理节点]
Processing --> EffectOutput[效果器输出]
EffectOutput --> NextEffect[下一效果器]
:::info 这一节难度较大,如果你不需要复杂的音效效果,不需要看这一节。 :::
创建效果器类
所有效果器都需要继承 AudioEffect
抽象类,需要实现这些内容:
abstract class AudioEffect implements IAudioInput, IAudioOutput {
abstract output: AudioNode; // 输出节点
abstract input: AudioNode; // 输入节点
abstract start(): void; // 效果激活时调用
abstract end(): void; // 效果结束时调用
}
实现效果器
下面以一个双线性低通滤波器为例,展示如何创建一个自定义滤波器。首先,我们需要继承 AudioEffect
抽象类:
class CustomEffect extends AudioEffect {
// 实现抽象成员
output: AudioNode;
input: AudioNode;
}
接下来,我们需要构建音频节点,创建一个 BiquadFilter
:
class CustomEffect extends AudioEffect {
constructor(ac: AudioContext) {
super(ac);
// 创建处理节点链
const filter = ac.createBiquadFilter(); // 滤波器节点
filter.type = 'lowpass'; // 低通滤波器
// 输入节点和输出节点都是滤波器节点
this.input = filter;
this.output = filter;
}
}
然后,我们可以提供接口来让外部能够调整这个效果器的参数:
class CustomEffect extends AudioEffect {
private Q: number = 1;
private frequency: number = 1000;
/** 设置截止频率 */
setCutoff(freq: number) {
this.frequency = Math.min(20000, Math.max(20, freq));
this.output.frequency.value = this.frequency;
}
/** 设置共振系数 */
setResonance(q: number) {
this.Q = Math.min(10, Math.max(0.1, q));
this.output.Q.value = this.Q;
}
}
最后,别忘了实现 start
方法和 end
方法,虽然不需要有任何内容:
class CustomEffect extends AudioEffect {
start() {}
end() {}
}
使用效果器
就如内置的效果器一样,创建效果器实例并添加入路由图即可:
const myEffect = new CustomEffect(audioPlayer.ac);
myRoute.addEffect(myEffect);
高级技巧
动画修改属性:
// 创建参数渐变
rampFrequency(target: number, duration: number) {
const current = this.output.frequency.value;
this.output.frequency.setValueAtTime(current, this.ac.currentTime);
this.output.frequency.linearRampToValueAtTime(
target,
this.ac.currentTime + duration
);
}
在一个效果器内添加多个音频节点:
class ReverbEffect extends AudioEffect {
private convolver: ConvolverNode;
private wetGain: GainNode;
constructor(ac: AudioContext) {
super(ac);
this.input = ac.createGain(); // 输入增益节点
this.wetGain = ac.createGain(); // 卷积增益节点
this.convolver = ac.createConvolver(); // 卷积节点
// 构建混合电路
const dryGain = ac.createGain(); // 原始音频增益节点
this.input.connect(dryGain);
this.input.connect(this.convolver);
this.convolver.connect(this.wetGain);
// 合并输出
const merger = ac.createChannelMerger();
dryGain.connect(merger, 0, 0);
this.wetGain.connect(merger, 0, 1);
this.output = merger;
}
}
以上效果器的流程图如下:
graph LR;
A(input 增益节点) --> B(dryGain 增益节点);
A --> C(convolver 卷积节点) --> D(wetGain 增益节点)
B & D --> E(output 声道合并节点)