mirror of
				https://github.com/unanmed/HumanBreak.git
				synced 2025-10-31 12:12:58 +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 声道合并节点)
 | ||
| ```
 |