mirror of
https://github.com/unanmed/HumanBreak.git
synced 2025-04-19 17:16:08 +08:00
394 lines
12 KiB
Markdown
394 lines
12 KiB
Markdown
# 音频系统
|
||
|
||
2.B 有了与 2.A 完全不同的音频系统,新的音频系统更加自由,功能更加丰富,可以创建多种自定义效果器。本文将讲解如何使用音频系统。
|
||
|
||
:::tip
|
||
多数情况下,你应该不需要使用本文所介绍的内容,因为样板已经将音效、背景音乐等处理完善。如果你想实现高级效果,例如混响效果等,才需要阅读本文。
|
||
:::
|
||
|
||
## 获取音频播放器
|
||
|
||
音频播放器在 `@user/client-modules` 模块中,直接引入即可:
|
||
|
||
```ts
|
||
// 在其他模块中使用模块化语法引入
|
||
import { audioPlayer } from '@user/client-modules';
|
||
// 在 client-modules 模块中使用模块化语法引入
|
||
import { audioPlayer } from '../audio'; // 改为你自己的相对路径
|
||
|
||
// 使用 Mota 全局变量引入
|
||
const { audioPlayer } = Mota.require('@user/client-modules');
|
||
```
|
||
|
||
## 音频系统工作流程
|
||
|
||
音频播放流程如下:
|
||
|
||
```mermaid
|
||
graph LR;
|
||
A(音频源) --> B(效果器) --> C(目的地(扬声器、耳机))
|
||
```
|
||
|
||
## 创建音频源
|
||
|
||
:::tip
|
||
本小节的内容极大概率用不到,如果不是需要非常底层的音频接口,可以不看本小节。
|
||
:::
|
||
|
||
样板内置了几种音频源,它们包括:
|
||
|
||
| 类型 | 适用场景 | 创建方法 |
|
||
| --------------- | ----------------------- | ----------------------- |
|
||
| `BufferSource` | 预加载的完整音频文件 | `createBufferSource()` |
|
||
| `ElementSource` | 通过 `<audio>` 标签播放 | `createElementSource()` |
|
||
| `StreamSource` | 流式音频/长音频 | `createStreamSource()` |
|
||
|
||
### `StreamSource` 音频源
|
||
|
||
一般情况下,我们推荐使用 `opus` 格式的音频,这时候需要使用 `StreamSource` 音频源来播放。这个音频源包含了对 IOS 的适配,可以正确播放 `opus` 格式的音频。在使用它之前,我们需要先创建一个 `StreamLoader` 类,来对音频流式加载。假如你在 `project/mybgm/` 文件夹下有一个 `xxx.opus` 音频,你可以这么创建:
|
||
|
||
```ts
|
||
import { StreamLoader } from '@user/client-modules';
|
||
|
||
const stream = new StreamLoader('project/mybgm/xxx.opus');
|
||
```
|
||
|
||
然后,创建音频源,并将流加载对象泵入音频源:
|
||
|
||
```ts
|
||
const source = audioPlayer.createStreamSource();
|
||
stream.pipe(source);
|
||
stream.start(); // 开始流式加载,如果不需要实时性,也可以不调用,音频播放时会自动开始加载
|
||
```
|
||
|
||
### `ElementSource` 音频源
|
||
|
||
从一个 `audio` 元素创建音频源,假设你想要播放 `project/mybgm/xxx.mp3`,那么你可以这么创建:
|
||
|
||
```ts
|
||
const source = audioPlayer.createElementSource();
|
||
source.setSource('project/mybgm/xxx.mp3');
|
||
```
|
||
|
||
### `BufferSource` 音频源
|
||
|
||
从音频缓冲创建音频源。音频缓冲是直接存储在内存中的一段原始音频波形数据,不经过任何压缩。假如你想播放 `project/mysound/xxx.wav`,可以这么写:
|
||
|
||
```ts
|
||
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` 播放这个音频路由。如下例所示:
|
||
|
||
```ts
|
||
import { AudioRoute, audioPlayer } from '@user/client-modules';
|
||
|
||
const route = audioPlayer.createRoute(source);
|
||
```
|
||
|
||
下面,我们需要将音频路由添加至音频播放器:
|
||
|
||
```ts
|
||
audioPlayer.addRoute('my-route', route);
|
||
```
|
||
|
||
之后,我们就可以使用 `audioPlayer` 播放这个音频了:
|
||
|
||
```ts
|
||
audioPlayer.play('my-route');
|
||
```
|
||
|
||
## 音频效果器
|
||
|
||
新的音频系统中最有用的功能就是音频效果器了。音频效果器允许你对音频进行处理,可以实现调节声道音量、回声效果、延迟效果,以及各种自定义效果等。
|
||
|
||
内置效果器包含这些:
|
||
|
||
| 效果器类型 | 功能说明 | 创建方法 |
|
||
| --------------------- | ------------------ | --------------------------- |
|
||
| `VolumeEffect` | 音量控制 | `createVolumeEffect()` |
|
||
| `StereoEffect` | 立体声场调节 | `createStereoEffect()` |
|
||
| `EchoEffect` | 回声效果 | `createEchoEffect()` |
|
||
| `DelayEffect` | 延迟效果 | `createDelay()` |
|
||
| `ChannelVolumeEffect` | 调节某个声道的音量 | `createChannelVolumeEffect` |
|
||
|
||
每个效果器都有自己可以调节的属性,具体可以查看对应效果器的 API 文档,比较简单,这里不在讲解。下面主要讲解一下如何使用效果器,我们直接通过例子来看(代码由 `DeepSeek R1` 模型生成并微调):
|
||
|
||
```ts
|
||
// 创建效果链
|
||
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` 模型生成并微调):
|
||
|
||
```ts
|
||
// 设置听者位置
|
||
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` 库实现淡入淡出效果:
|
||
|
||
```ts
|
||
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` 设置。示例如下:
|
||
|
||
```ts
|
||
import { soundPlayer } from '@user/client-modules';
|
||
|
||
// 播放已加载的音效
|
||
const soundId = soundPlayer.play(
|
||
'mysound.opus',
|
||
[1, 0, 0], // 音源位置,在听者前方 1m 处
|
||
[0, 1, 0] // 音源朝向,朝向天花板
|
||
);
|
||
|
||
// 停止指定音效
|
||
soundPlayer.stop(soundId);
|
||
// 停止所有音效
|
||
soundPlayer.stopAllSounds();
|
||
```
|
||
|
||
### 设置是否启用音效
|
||
|
||
你可以自行设置是否启用音效系统:
|
||
|
||
```ts
|
||
soundPlayer.setEnabled(false); // 关闭音效系统
|
||
soundPlayer.setEnabled(true); // 启用音效系统
|
||
```
|
||
|
||
## 音乐系统
|
||
|
||
音乐系统的使用与音频系统类似,包含播放、暂停、继续等功能。示例如下:
|
||
|
||
```ts
|
||
import { bgmController } from '@user/client-modules';
|
||
|
||
bgmController.play('bgm1.opus'); // 切换到目标音乐
|
||
bgmController.pause(); // 暂停当前音乐,会有渐变效果
|
||
bgmController.resume(); // 继续当前音乐,会有渐变效果
|
||
|
||
bgmController.blockChange(); // 禁用音乐切换,之后调用 play, pause, resume 将没有效果
|
||
bgmController.unblockChange(); // 启用音乐切换
|
||
```
|
||
|
||
## 自定义效果器
|
||
|
||
本小节内容由 `DeepSeek R1` 模型生成并微调。
|
||
|
||
效果器是新的音频系统最强大的功能,而且此系统也允许你自定义一些效果器,实现自定义效果。效果器的工作流程如下:
|
||
|
||
```mermaid
|
||
graph LR
|
||
Input[输入源] --> EffectInput[效果器输入]
|
||
EffectInput --> Processing[处理节点]
|
||
Processing --> EffectOutput[效果器输出]
|
||
EffectOutput --> NextEffect[下一效果器]
|
||
```
|
||
|
||
:::info
|
||
这一节难度较大,如果你不需要复杂的音效效果,不需要看这一节。
|
||
:::
|
||
|
||
### 创建效果器类
|
||
|
||
所有效果器都需要继承 `AudioEffect` 抽象类,需要实现这些内容:
|
||
|
||
```ts
|
||
abstract class AudioEffect implements IAudioInput, IAudioOutput {
|
||
abstract output: AudioNode; // 输出节点
|
||
abstract input: AudioNode; // 输入节点
|
||
abstract start(): void; // 效果激活时调用
|
||
abstract end(): void; // 效果结束时调用
|
||
}
|
||
```
|
||
|
||
### 实现效果器
|
||
|
||
下面以一个双线性低通滤波器为例,展示如何创建一个自定义滤波器。首先,我们需要继承 `AudioEffect` 抽象类:
|
||
|
||
```ts
|
||
class CustomEffect extends AudioEffect {
|
||
// 实现抽象成员
|
||
output: AudioNode;
|
||
input: AudioNode;
|
||
}
|
||
```
|
||
|
||
接下来,我们需要构建音频节点,创建一个 `BiquadFilter`:
|
||
|
||
```ts
|
||
class CustomEffect extends AudioEffect {
|
||
constructor(ac: AudioContext) {
|
||
super(ac);
|
||
|
||
// 创建处理节点链
|
||
const filter = ac.createBiquadFilter(); // 滤波器节点
|
||
filter.type = 'lowpass'; // 低通滤波器
|
||
// 输入节点和输出节点都是滤波器节点
|
||
this.input = filter;
|
||
this.output = filter;
|
||
}
|
||
}
|
||
```
|
||
|
||
然后,我们可以提供接口来让外部能够调整这个效果器的参数:
|
||
|
||
```ts
|
||
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` 方法,虽然不需要有任何内容:
|
||
|
||
```ts
|
||
class CustomEffect extends AudioEffect {
|
||
start() {}
|
||
end() {}
|
||
}
|
||
```
|
||
|
||
### 使用效果器
|
||
|
||
就如内置的效果器一样,创建效果器实例并添加入路由图即可:
|
||
|
||
```ts
|
||
const myEffect = new CustomEffect(audioPlayer.ac);
|
||
myRoute.addEffect(myEffect);
|
||
```
|
||
|
||
### 高级技巧
|
||
|
||
动画修改属性:
|
||
|
||
```ts
|
||
// 创建参数渐变
|
||
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
|
||
);
|
||
}
|
||
```
|
||
|
||
在一个效果器内添加多个音频节点:
|
||
|
||
```ts
|
||
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;
|
||
}
|
||
}
|
||
```
|
||
|
||
以上效果器的流程图如下:
|
||
|
||
```mermaid
|
||
graph LR;
|
||
A(input 增益节点) --> B(dryGain 增益节点);
|
||
A --> C(convolver 卷积节点) --> D(wetGain 增益节点)
|
||
B & D --> E(output 声道合并节点)
|
||
```
|