refactor: 音频系统

This commit is contained in:
AncTe committed 2026-03-17 19:34:30 +08:00
1 parent 191ba8d1db
commit ee54148558
29 files changed
+2170 -1273

No files matched your search

@@ -1,268 +0,0 @@
import EventEmitter from 'eventemitter3';
import { audioPlayer, AudioPlayer, AudioRoute, AudioStatus } from './player';
import { guessTypeByExt, isAudioSupport } from './support';
import { logger } from '@motajs/common';
import { StreamLoader } from '../loader';
import { linear, sleep, Transition } from 'mutate-animate';
import { VolumeEffect } from './effect';
interface BgmVolume {
effect: VolumeEffect;
transition: Transition;
}
interface BgmControllerEvent {
play: [];
pause: [];
resume: [];
stop: [];
}
export class BgmController<
T extends string = BgmIds
> extends EventEmitter<BgmControllerEvent> {
/** bgm音频名称的前缀 */
prefix: string = 'bgms.';
/** 每个 bgm 的音量控制器 */
readonly gain: Map<T, BgmVolume> = new Map();
/** 正在播放的 bgm */
playingBgm?: T;
/** 是否正在播放 */
playing: boolean = false;
/** 是否已经启用 */
enabled: boolean = true;
/** 主音量控制器 */
private readonly mainGain: VolumeEffect;
/** 是否屏蔽所有的音乐切换 */
private blocking: boolean = false;
/** 渐变时长 */
private transitionTime: number = 2000;
constructor(public readonly player: AudioPlayer) {
super();
this.mainGain = player.createVolumeEffect();
}
/**
* 设置音频渐变时长
* @param time 渐变时长
*/
setTransitionTime(time: number) {
this.transitionTime = time;
for (const [, value] of this.gain) {
value.transition.time(time);
}
}
/**
* 屏蔽音乐切换
*/
blockChange() {
this.blocking = true;
}
/**
* 取消屏蔽音乐切换
*/
unblockChange() {
this.blocking = false;
}
/**
* 设置总音量大小
* @param volume 音量大小
*/
setVolume(volume: number) {
this.mainGain.setVolume(volume);
}
/**
* 获取总音量大小
*/
getVolume() {
return this.mainGain.getVolume();
}
/**
* 设置是否启用
* @param enabled 是否启用
*/
setEnabled(enabled: boolean) {
if (enabled) this.resume();
else this.stop();
this.enabled = enabled;
}
/**
* 设置 bgm 音频名称的前缀
*/
setPrefix(prefix: string) {
this.prefix = prefix;
}
private getId(name: T) {
return `${this.prefix}${name}`;
}
/**
* 根据 bgm 名称获取其 AudioRoute 实例
* @param id 音频名称
*/
get(id: T) {
return this.player.getRoute(this.getId(id));
}
/**
* 添加一个 bgm
* @param id 要添加的 bgm 的名称
* @param url 指定 bgm 的加载地址
*/
addBgm(id: T, url: string = `project/bgms/${id}`) {
const type = guessTypeByExt(id);
if (!type) {
logger.warn(50, id.split('.').slice(0, -1).join('.'));
return;
}
const gain = this.player.createVolumeEffect();
if (isAudioSupport(type)) {
const source = audioPlayer.createElementSource();
source.setSource(url);
source.setLoop(true);
const route = new AudioRoute(source, audioPlayer);
route.addEffect([gain, this.mainGain]);
audioPlayer.addRoute(this.getId(id), route);
this.setTransition(id, route, gain);
} else {
const source = audioPlayer.createStreamSource();
const stream = new StreamLoader(url);
stream.pipe(source);
source.setLoop(true);
const route = new AudioRoute(source, audioPlayer);
route.addEffect([gain, this.mainGain]);
audioPlayer.addRoute(this.getId(id), route);
this.setTransition(id, route, gain);
}
}
/**
* 移除一个 bgm
* @param id 要移除的 bgm 的名称
*/
removeBgm(id: T) {
this.player.removeRoute(this.getId(id));
const gain = this.gain.get(id);
gain?.transition.ticker.destroy();
this.gain.delete(id);
}
private setTransition(id: T, route: AudioRoute, gain: VolumeEffect) {
const transition = new Transition();
transition
.time(this.transitionTime)
.mode(linear())
.transition('volume', 0);
const tick = () => {
gain.setVolume(transition.value.volume);
};
/**
* @param expect 在结束时应该是正在播放还是停止
*/
const setTick = async (expect: AudioStatus) => {
transition.ticker.remove(tick);
transition.ticker.add(tick);
const identifier = route.stopIdentifier;
await sleep(this.transitionTime + 500);
if (
route.status === expect &&
identifier === route.stopIdentifier
) {
transition.ticker.remove(tick);
if (route.status === AudioStatus.Playing) {
gain.setVolume(1);
} else {
gain.setVolume(0);
}
}
};
route.onStart(async () => {
transition.transition('volume', 1);
setTick(AudioStatus.Playing);
});
route.onEnd(() => {
transition.transition('volume', 0);
setTick(AudioStatus.Paused);
});
route.setEndTime(this.transitionTime);
this.gain.set(id, { effect: gain, transition });
}
/**
* 播放一个 bgm
* @param id 要播放的 bgm 名称
*/
play(id: T, when?: number) {
if (this.blocking) return;
if (id !== this.playingBgm && this.playingBgm) {
this.player.pause(this.getId(this.playingBgm));
}
this.playingBgm = id;
if (!this.enabled) return;
this.player.play(this.getId(id), when);
this.playing = true;
this.emit('play');
}
/**
* 继续当前的 bgm
*/
resume() {
if (this.blocking || !this.enabled || this.playing) return;
if (this.playingBgm) {
this.player.resume(this.getId(this.playingBgm));
}
this.playing = true;
this.emit('resume');
}
/**
* 暂停当前的 bgm
*/
pause() {
if (this.blocking || !this.enabled) return;
if (this.playingBgm) {
this.player.pause(this.getId(this.playingBgm));
}
this.playing = false;
this.emit('pause');
}
/**
* 停止当前的 bgm
*/
stop() {
if (this.blocking || !this.enabled) return;
if (this.playingBgm) {
this.player.stop(this.getId(this.playingBgm));
}
this.playing = false;
this.emit('stop');
}
}
export const bgmController = new BgmController<BgmIds>(audioPlayer);
export function loadAllBgm() {
const { loading } = Mota.require('@user/data-base');
loading.once('coreInit', () => {
const data = data_a1e2fb4a_e986_4524_b0da_9b7ba7c0874d;
for (const bgm of data.main.bgms) {
bgmController.addBgm(bgm);
}
});
}
@@ -1,203 +0,0 @@
import { logger } from '@motajs/common';
import { OggVorbisDecoderWebWorker } from '@wasm-audio-decoders/ogg-vorbis';
import { OggOpusDecoderWebWorker } from 'ogg-opus-decoder';
import { AudioType, isAudioSupport } from './support';
import type { AudioPlayer } from './player';
const fileSignatures: [AudioType, number[]][] = [
[AudioType.Mp3, [0x49, 0x44, 0x33]],
[AudioType.Ogg, [0x4f, 0x67, 0x67, 0x53]],
[AudioType.Wav, [0x52, 0x49, 0x46, 0x46]],
[AudioType.Flac, [0x66, 0x4c, 0x61, 0x43]],
[AudioType.Aac, [0xff, 0xf1]],
[AudioType.Aac, [0xff, 0xf9]]
];
const oggHeaders: [AudioType, number[]][] = [
[AudioType.Opus, [0x4f, 0x70, 0x75, 0x73, 0x48, 0x65, 0x61, 0x64]]
];
export function checkAudioType(data: Uint8Array) {
let audioType: AudioType | '' = '';
// 检查头文件获取音频类型,仅检查前256个字节
const toCheck = data.slice(0, 256);
for (const [type, value] of fileSignatures) {
if (value.every((v, i) => toCheck[i] === v)) {
audioType = type;
break;
}
}
if (audioType === AudioType.Ogg) {
// 如果是ogg的话,进一步判断是不是opus
for (const [key, value] of oggHeaders) {
const has = toCheck.some((_, i) => {
return value.every((v, ii) => toCheck[i + ii] === v);
});
if (has) {
audioType = key;
break;
}
}
}
return audioType;
}
export interface IAudioDecodeError {
/** 错误信息 */
message: string;
}
export interface IAudioDecodeData {
/** 每个声道的音频信息 */
channelData: Float32Array<ArrayBuffer>[];
/** 已经被解码的 PCM 采样数 */
samplesDecoded: number;
/** 音频采样率 */
sampleRate: number;
/** 解码错误信息 */
errors: IAudioDecodeError[];
}
export abstract class AudioDecoder {
static readonly decoderMap: Map<AudioType, new () => AudioDecoder> =
new Map();
/**
* 注册一个解码器
* @param type 要注册的解码器允许解码的类型
* @param decoder 解码器对象
*/
static registerDecoder(type: AudioType, decoder: new () => AudioDecoder) {
if (this.decoderMap.has(type)) {
logger.warn(47, type);
return;
}
this.decoderMap.set(type, decoder);
}
/**
* 解码音频数据
* @param data 音频文件数据
* @param player AudioPlayer实例
*/
static async decodeAudioData(data: Uint8Array, player: AudioPlayer) {
// 检查头文件获取音频类型,仅检查前256个字节
const toCheck = data.slice(0, 256);
const type = checkAudioType(data);
if (type === '') {
logger.error(
25,
[...toCheck]
.map(v => v.toString(16).padStart(2, '0'))
.join(' ')
.toUpperCase()
);
return null;
}
if (isAudioSupport(type)) {
if (data.buffer instanceof ArrayBuffer) {
return player.ac.decodeAudioData(data.buffer);
} else {
return null;
}
} else {
const Decoder = this.decoderMap.get(type);
if (!Decoder) {
return null;
} else {
const decoder = new Decoder();
await decoder.create();
const decodedData = await decoder.decodeAll(data);
if (!decodedData) return null;
const buffer = player.ac.createBuffer(
decodedData.channelData.length,
decodedData.channelData[0].length,
decodedData.sampleRate
);
decodedData.channelData.forEach((v, i) => {
buffer.copyToChannel(v, i);
});
decoder.destroy();
return buffer;
}
}
}
/**
* 创建音频解码器
*/
abstract create(): Promise<void>;
/**
* 摧毁这个解码器
*/
abstract destroy(): void;
/**
* 解码流数据
* @param data 流数据
*/
abstract decode(data: Uint8Array): Promise<IAudioDecodeData | undefined>;
/**
* 解码整个文件
* @param data 文件数据
*/
abstract decodeAll(data: Uint8Array): Promise<IAudioDecodeData | undefined>;
/**
* 当音频解码完成后,会调用此函数,需要返回之前还未解析或未返回的音频数据。调用后,该解码器将不会被再次使用
*/
abstract flush(): Promise<IAudioDecodeData | undefined>;
}
export class VorbisDecoder extends AudioDecoder {
decoder?: OggVorbisDecoderWebWorker;
async create(): Promise<void> {
this.decoder = new OggVorbisDecoderWebWorker();
await this.decoder.ready;
}
destroy(): void {
this.decoder?.free();
}
async decode(data: Uint8Array): Promise<IAudioDecodeData | undefined> {
return this.decoder?.decode(data) as Promise<IAudioDecodeData>;
}
async decodeAll(data: Uint8Array): Promise<IAudioDecodeData | undefined> {
return this.decoder?.decodeFile(data) as Promise<IAudioDecodeData>;
}
async flush(): Promise<IAudioDecodeData | undefined> {
return this.decoder?.flush() as Promise<IAudioDecodeData>;
}
}
export class OpusDecoder extends AudioDecoder {
decoder?: OggOpusDecoderWebWorker;
async create(): Promise<void> {
this.decoder = new OggOpusDecoderWebWorker({
speechQualityEnhancement: 'none'
});
await this.decoder.ready;
}
destroy(): void {
this.decoder?.free();
}
async decode(data: Uint8Array): Promise<IAudioDecodeData | undefined> {
return this.decoder?.decode(data) as Promise<IAudioDecodeData>;
}
async decodeAll(data: Uint8Array): Promise<IAudioDecodeData | undefined> {
return this.decoder?.decodeFile(data) as Promise<IAudioDecodeData>;
}
async flush(): Promise<IAudioDecodeData | undefined> {
return this.decoder?.flush() as Promise<IAudioDecodeData>;
}
}
@@ -1,288 +0,0 @@
import { isNil } from 'lodash-es';
import { sleep } from 'mutate-animate';
export interface IAudioInput {
/** 输入节点 */
input: AudioNode;
}
export interface IAudioOutput {
/** 输出节点 */
output: AudioNode;
}
export abstract class AudioEffect implements IAudioInput, IAudioOutput {
/** 输出节点 */
abstract output: AudioNode;
/** 输入节点 */
abstract input: AudioNode;
constructor(public readonly ac: AudioContext) {}
/**
* 当音频播放结束时触发,可以用于节点结束后处理
*/
abstract end(): void;
/**
* 当音频开始播放时触发,可以用于节点初始化
*/
abstract start(): void;
/**
* 连接至其他效果器
* @param target 目标输入
* @param output 当前效果器输出通道
* @param input 目标效果器的输入通道
*/
connect(target: IAudioInput, output?: number, input?: number) {
this.output.connect(target.input, output, input);
}
/**
* 与其他效果器取消连接
* @param target 目标输入
* @param output 当前效果器输出通道
* @param input 目标效果器的输入通道
*/
disconnect(target?: IAudioInput, output?: number, input?: number) {
if (!target) {
if (!isNil(output)) {
this.output.disconnect(output);
} else {
this.output.disconnect();
}
} else {
if (!isNil(output)) {
if (!isNil(input)) {
this.output.disconnect(target.input, output, input);
} else {
this.output.disconnect(target.input, output);
}
} else {
this.output.disconnect(target.input);
}
}
}
}
export class StereoEffect extends AudioEffect {
output: PannerNode;
input: PannerNode;
constructor(ac: AudioContext) {
super(ac);
const panner = ac.createPanner();
this.input = panner;
this.output = panner;
}
/**
* 设置音频朝向,x正方形水平向右,y正方形垂直于地面向上,z正方向垂直屏幕远离用户
* @param x 朝向x坐标
* @param y 朝向y坐标
* @param z 朝向z坐标
*/
setOrientation(x: number, y: number, z: number) {
this.output.orientationX.value = x;
this.output.orientationY.value = y;
this.output.orientationZ.value = z;
}
/**
* 设置音频位置,x正方形水平向右,y正方形垂直于地面向上,z正方向垂直屏幕远离用户
* @param x 位置x坐标
* @param y 位置y坐标
* @param z 位置z坐标
*/
setPosition(x: number, y: number, z: number) {
this.output.positionX.value = x;
this.output.positionY.value = y;
this.output.positionZ.value = z;
}
end(): void {}
start(): void {}
}
export class VolumeEffect extends AudioEffect {
output: GainNode;
input: GainNode;
constructor(ac: AudioContext) {
super(ac);
const gain = ac.createGain();
this.input = gain;
this.output = gain;
}
/**
* 设置音量大小
* @param volume 音量大小
*/
setVolume(volume: number) {
this.output.gain.value = volume;
}
/**
* 获取音量大小
*/
getVolume(): number {
return this.output.gain.value;
}
end(): void {}
start(): void {}
}
export class ChannelVolumeEffect extends AudioEffect {
output: ChannelMergerNode;
input: ChannelSplitterNode;
/** 所有的音量控制节点 */
private readonly gain: GainNode[] = [];
constructor(ac: AudioContext) {
super(ac);
const splitter = ac.createChannelSplitter();
const merger = ac.createChannelMerger();
this.output = merger;
this.input = splitter;
for (let i = 0; i < 6; i++) {
const gain = ac.createGain();
splitter.connect(gain, i);
gain.connect(merger, 0, i);
this.gain.push(gain);
}
}
/**
* 设置某个声道的音量大小
* @param channel 要设置的声道,可填0-5
* @param volume 这个声道的音量大小
*/
setVolume(channel: number, volume: number) {
if (!this.gain[channel]) return;
this.gain[channel].gain.value = volume;
}
/**
* 获取某个声道的音量大小,可填0-5
* @param channel 要获取的声道
*/
getVolume(channel: number): number {
if (!this.gain[channel]) return 0;
return this.gain[channel].gain.value;
}
end(): void {}
start(): void {}
}
export class DelayEffect extends AudioEffect {
output: DelayNode;
input: DelayNode;
constructor(ac: AudioContext) {
super(ac);
const delay = ac.createDelay();
this.input = delay;
this.output = delay;
}
/**
* 设置延迟时长
* @param delay 延迟时长,单位秒
*/
setDelay(delay: number) {
this.output.delayTime.value = delay;
}
/**
* 获取延迟时长
*/
getDelay() {
return this.output.delayTime.value;
}
end(): void {}
start(): void {}
}
export class EchoEffect extends AudioEffect {
output: GainNode;
input: GainNode;
/** 延迟节点 */
private readonly delay: DelayNode;
/** 反馈增益节点 */
private readonly gainNode: GainNode;
/** 当前增益 */
private gain: number = 0.5;
/** 是否正在播放 */
private playing: boolean = false;
constructor(ac: AudioContext) {
super(ac);
const delay = ac.createDelay();
const gain = ac.createGain();
gain.gain.value = 0.5;
delay.delayTime.value = 0.05;
delay.connect(gain);
gain.connect(delay);
this.delay = delay;
this.gainNode = gain;
this.input = gain;
this.output = gain;
}
/**
* 设置回声反馈增益大小
* @param gain 增益大小,范围 0-1,大于等于1的视为0.5,小于0的视为0
*/
setFeedbackGain(gain: number) {
const resolved = gain >= 1 ? 0.5 : gain < 0 ? 0 : gain;
this.gain = resolved;
if (this.playing) this.gainNode.gain.value = resolved;
}
/**
* 设置回声间隔时长
* @param delay 回声时长,范围 0.01-Infinity,小于0.01的视为0.01
*/
setEchoDelay(delay: number) {
const resolved = delay < 0.01 ? 0.01 : delay;
this.delay.delayTime.value = resolved;
}
/**
* 获取反馈节点增益
*/
getFeedbackGain() {
return this.gain;
}
/**
* 获取回声间隔时长
*/
getEchoDelay() {
return this.delay.delayTime.value;
}
end(): void {
this.playing = false;
const echoTime = Math.ceil(Math.log(0.001) / Math.log(this.gain)) + 10;
sleep(this.delay.delayTime.value * echoTime).then(() => {
if (!this.playing) this.gainNode.gain.value = 0;
});
}
start(): void {
this.playing = true;
this.gainNode.gain.value = this.gain;
}
}
@@ -1,18 +0,0 @@
import { loadAllBgm } from './bgm';
import { OpusDecoder, VorbisDecoder } from './decoder';
import { AudioType } from './support';
import { AudioDecoder } from './decoder';
export function createAudio() {
loadAllBgm();
AudioDecoder.registerDecoder(AudioType.Ogg, VorbisDecoder);
AudioDecoder.registerDecoder(AudioType.Opus, OpusDecoder);
}
export * from './support';
export * from './effect';
export * from './player';
export * from './source';
export * from './bgm';
export * from './decoder';
export * from './sound';
@@ -1,605 +0,0 @@
import EventEmitter from 'eventemitter3';
import {
AudioBufferSource,
AudioElementSource,
AudioSource,
AudioStreamSource
} from './source';
import {
AudioEffect,
ChannelVolumeEffect,
DelayEffect,
EchoEffect,
IAudioOutput,
StereoEffect,
VolumeEffect
} from './effect';
import { isNil } from 'lodash-es';
import { logger } from '@motajs/common';
import { sleep } from 'mutate-animate';
import { AudioDecoder } from './decoder';
interface AudioPlayerEvent {}
export class AudioPlayer extends EventEmitter<AudioPlayerEvent> {
/** 音频播放上下文 */
readonly ac: AudioContext;
/** 所有的音频播放路由 */
readonly audioRoutes: Map<string, AudioRoute> = new Map();
/** 音量节点 */
readonly gain: GainNode;
constructor() {
super();
this.ac = new AudioContext();
this.gain = this.ac.createGain();
this.gain.connect(this.ac.destination);
}
/**
* 解码音频数据
* @param data 音频数据
*/
decodeAudioData(data: Uint8Array) {
return AudioDecoder.decodeAudioData(data, this);
}
/**
* 设置音量
* @param volume 音量
*/
setVolume(volume: number) {
this.gain.gain.value = volume;
}
/**
* 获取音量
*/
getVolume() {
return this.gain.gain.value;
}
/**
* 创建一个音频源
* @param Source 音频源类
*/
createSource<T extends AudioSource>(
Source: new (ac: AudioContext) => T
): T {
return new Source(this.ac);
}
/**
* 创建一个兼容流式音频源,可以与流式加载相结合,主要用于处理 opus ogg 不兼容的情况
*/
createStreamSource() {
return new AudioStreamSource(this.ac);
}
/**
* 创建一个通过 audio 元素播放的音频源
*/
createElementSource() {
return new AudioElementSource(this.ac);
}
/**
* 创建一个通过 AudioBuffer 播放的音频源
*/
createBufferSource() {
return new AudioBufferSource(this.ac);
}
/**
* 获取音频目的地
*/
getDestination() {
return this.gain;
}
/**
* 创建一个音频效果器
* @param Effect 效果器类
*/
createEffect<T extends AudioEffect>(
Effect: new (ac: AudioContext) => T
): T {
return new Effect(this.ac);
}
/**
* 创建一个修改音量的效果器
* ```txt
* |----------|
* Input ----> | GainNode | ----> Output
* |----------|
* ```
*/
createVolumeEffect() {
return new VolumeEffect(this.ac);
}
/**
* 创建一个立体声效果器
* ```txt
* |------------|
* Input ----> | PannerNode | ----> Output
* |------------|
* ```
*/
createStereoEffect() {
return new StereoEffect(this.ac);
}
/**
* 创建一个修改单个声道音量的效果器
* ```txt
* |----------|
* -> | GainNode | \
* |--------------| / |----------| -> |------------|
* Input ----> | SplitterNode | ...... | MergerNode | ----> Output
* |--------------| \ |----------| -> |------------|
* -> | GainNode | /
* |----------|
* ```
*/
createChannelVolumeEffect() {
return new ChannelVolumeEffect(this.ac);
}
/**
* 创建一个延迟效果器
* ```txt
* |-----------|
* Input ----> | DelayNode | ----> Output
* |-----------|
* ```
*/
createDelayEffect() {
return new DelayEffect(this.ac);
}
/**
* 创建一个回声效果器
* ```txt
* |----------|
* Input ----> | GainNode | ----> Output
* ^ |----------| |
* | |
* | |------------| ↓
* |-- | Delay Node | <--
* |------------|
* ```
*/
createEchoEffect() {
return new EchoEffect(this.ac);
}
/**
* 创建一个音频播放路由
* @param source 音频源
*/
createRoute(source: AudioSource) {
return new AudioRoute(source, this);
}
/**
* 添加一个音频播放路由,可以直接被播放
* @param id 这个音频播放路由的名称
* @param route 音频播放路由对象
*/
addRoute(id: string, route: AudioRoute) {
if (this.audioRoutes.has(id)) {
logger.warn(45, id);
}
this.audioRoutes.set(id, route);
}
/**
* 根据名称获取音频播放路由对象
* @param id 音频播放路由的名称
*/
getRoute(id: string) {
return this.audioRoutes.get(id);
}
/**
* 移除一个音频播放路由
* @param id 要移除的播放路由的名称
*/
removeRoute(id: string) {
const route = this.audioRoutes.get(id);
if (route) {
route.destroy();
}
this.audioRoutes.delete(id);
}
/**
* 播放音频
* @param id 音频名称
* @param when 从音频的哪个位置开始播放,单位秒
*/
play(id: string, when: number = 0) {
const route = this.getRoute(id);
if (!route) {
logger.warn(53, 'play', id);
return;
}
route.play(when);
}
/**
* 暂停音频播放
* @param id 音频名称
* @returns 当音乐真正停止时兑现
*/
pause(id: string) {
const route = this.getRoute(id);
if (!route) {
logger.warn(53, 'pause', id);
return;
}
return route.pause();
}
/**
* 停止音频播放
* @param id 音频名称
* @returns 当音乐真正停止时兑现
*/
stop(id: string) {
const route = this.getRoute(id);
if (!route) {
logger.warn(53, 'stop', id);
return;
}
return route.stop();
}
/**
* 继续音频播放
* @param id 音频名称
*/
resume(id: string) {
const route = this.getRoute(id);
if (!route) {
logger.warn(53, 'resume', id);
return;
}
route.resume();
}
/**
* 设置听者位置,x正方向水平向右,y正方向垂直于地面向上,z正方向垂直屏幕远离用户
* @param x 位置x坐标
* @param y 位置y坐标
* @param z 位置z坐标
*/
setListenerPosition(x: number, y: number, z: number) {
const listener = this.ac.listener;
listener.positionX.value = x;
listener.positionY.value = y;
listener.positionZ.value = z;
}
/**
* 设置听者朝向,x正方向水平向右,y正方向垂直于地面向上,z正方向垂直屏幕远离用户
* @param x 朝向x坐标
* @param y 朝向y坐标
* @param z 朝向z坐标
*/
setListenerOrientation(x: number, y: number, z: number) {
const listener = this.ac.listener;
listener.forwardX.value = x;
listener.forwardY.value = y;
listener.forwardZ.value = z;
}
/**
* 设置听者头顶朝向,x正方向水平向右,y正方向垂直于地面向上,z正方向垂直屏幕远离用户
* @param x 头顶朝向x坐标
* @param y 头顶朝向y坐标
* @param z 头顶朝向z坐标
*/
setListenerUp(x: number, y: number, z: number) {
const listener = this.ac.listener;
listener.upX.value = x;
listener.upY.value = y;
listener.upZ.value = z;
}
}
export const enum AudioStatus {
Playing,
Pausing,
Paused,
Stoping,
Stoped
}
type AudioStartHook = (route: AudioRoute) => void;
type AudioEndHook = (time: number, route: AudioRoute) => void;
interface AudioRouteEvent {
updateEffect: [];
play: [];
stop: [];
pause: [];
resume: [];
}
export class AudioRoute
extends EventEmitter<AudioRouteEvent>
implements IAudioOutput
{
output: AudioNode;
/** 效果器路由图 */
readonly effectRoute: AudioEffect[] = [];
/** 结束时长,当音频暂停或停止时,会经过这么长时间之后才真正终止播放,期间可以做音频淡入淡出等效果 */
endTime: number = 0;
/** 当前播放状态 */
status: AudioStatus = AudioStatus.Stoped;
/** 暂停时刻 */
private pauseTime: number = 0;
/** 暂停时播放了多长时间 */
private pauseCurrentTime: number = 0;
/** 音频时长,单位秒 */
get duration() {
return this.source.duration;
}
/** 当前播放了多长时间,单位秒 */
get currentTime() {
if (this.status === AudioStatus.Paused) {
return this.pauseCurrentTime;
} else {
return this.source.currentTime;
}
}
set currentTime(time: number) {
this.source.stop();
this.source.play(time);
}
private shouldStop: boolean = false;
/**
* 每次暂停或停止时自增,用于判断当前正在处理的情况。
* 假如暂停后很快播放,然后很快暂停,那么需要根据这个来判断实际是否应该执行暂停后操作
*/
stopIdentifier: number = 0;
private audioStartHook?: AudioStartHook;
private audioEndHook?: AudioEndHook;
constructor(
public readonly source: AudioSource,
public readonly player: AudioPlayer
) {
super();
this.output = source.output;
source.on('end', () => {
if (this.status === AudioStatus.Playing) {
this.status = AudioStatus.Stoped;
}
});
source.on('play', () => {
if (this.status !== AudioStatus.Playing) {
this.status = AudioStatus.Playing;
}
});
}
/**
* 设置结束时间,暂停或停止时,会经过这么长时间才终止音频的播放,这期间可以做一下音频淡出的效果。
* @param time 暂停或停止时,经过多长时间之后才会结束音频的播放
*/
setEndTime(time: number) {
this.endTime = time;
}
/**
* 当音频播放时执行的函数,可以用于音频淡入效果
* @param fn 音频开始播放时执行的函数
*/
onStart(fn?: AudioStartHook) {
this.audioStartHook = fn;
}
/**
* 当音频暂停或停止时执行的函数,可以用于音频淡出效果
* @param fn 音频在暂停或停止时执行的函数,不填时表示取消这个钩子。
* 包含两个参数,第一个参数是结束时长,第二个参数是当前音频播放路由对象
*/
onEnd(fn?: AudioEndHook) {
this.audioEndHook = fn;
}
/**
* 开始播放这个音频
* @param when 从音频的什么时候开始播放,单位秒
*/
async play(when: number = 0) {
if (this.status === AudioStatus.Playing) return;
this.link();
await this.player.ac.resume();
if (this.effectRoute.length > 0) {
const first = this.effectRoute[0];
this.source.connect(first);
const last = this.effectRoute.at(-1)!;
last.connect({ input: this.player.getDestination() });
} else {
this.source.connect({ input: this.player.getDestination() });
}
this.source.play(when);
this.status = AudioStatus.Playing;
this.pauseTime = 0;
this.audioStartHook?.(this);
this.startAllEffect();
this.emit('play');
}
/**
* 暂停音频播放
*/
async pause() {
if (this.status !== AudioStatus.Playing) return;
this.status = AudioStatus.Pausing;
this.stopIdentifier++;
const identifier = this.stopIdentifier;
if (this.audioEndHook) {
this.audioEndHook(this.endTime, this);
await sleep(this.endTime);
}
if (
this.status !== AudioStatus.Pausing ||
this.stopIdentifier !== identifier
) {
return;
}
this.pauseCurrentTime = this.source.currentTime;
const time = this.source.stop();
this.pauseTime = time;
if (this.shouldStop) {
this.status = AudioStatus.Stoped;
this.endAllEffect();
this.emit('stop');
this.shouldStop = false;
} else {
this.status = AudioStatus.Paused;
this.endAllEffect();
this.emit('pause');
}
}
/**
* 继续音频播放
*/
resume() {
if (this.status === AudioStatus.Playing) return;
if (
this.status === AudioStatus.Pausing ||
this.status === AudioStatus.Stoping
) {
this.audioStartHook?.(this);
this.emit('resume');
return;
}
if (this.status === AudioStatus.Paused) {
this.play(this.pauseTime);
} else {
this.play(0);
}
this.status = AudioStatus.Playing;
this.pauseTime = 0;
this.audioStartHook?.(this);
this.startAllEffect();
this.emit('resume');
}
/**
* 停止音频播放
*/
async stop() {
if (this.status !== AudioStatus.Playing) {
if (this.status === AudioStatus.Pausing) {
this.shouldStop = true;
}
return;
}
this.status = AudioStatus.Stoping;
this.stopIdentifier++;
const identifier = this.stopIdentifier;
if (this.audioEndHook) {
this.audioEndHook(this.endTime, this);
await sleep(this.endTime);
}
if (
this.status !== AudioStatus.Stoping ||
this.stopIdentifier !== identifier
) {
return;
}
this.source.stop();
this.status = AudioStatus.Stoped;
this.pauseTime = 0;
this.endAllEffect();
this.emit('stop');
}
/**
* 添加效果器
* @param effect 要添加的效果,可以是数组,表示一次添加多个
* @param index 从哪个位置开始添加,如果大于数组长度,那么加到末尾,如果小于0,那么将会从后面往前数。默认添加到末尾
*/
addEffect(effect: AudioEffect | AudioEffect[], index?: number) {
if (isNil(index)) {
if (effect instanceof Array) {
this.effectRoute.push(...effect);
} else {
this.effectRoute.push(effect);
}
} else {
if (effect instanceof Array) {
this.effectRoute.splice(index, 0, ...effect);
} else {
this.effectRoute.splice(index, 0, effect);
}
}
this.setOutput();
if (this.source.playing) this.link();
this.emit('updateEffect');
}
/**
* 移除一个效果器
* @param effect 要移除的效果
*/
removeEffect(effect: AudioEffect) {
const index = this.effectRoute.indexOf(effect);
if (index === -1) return;
this.effectRoute.splice(index, 1);
effect.disconnect();
this.setOutput();
if (this.source.playing) this.link();
this.emit('updateEffect');
}
destroy() {
this.effectRoute.forEach(v => v.disconnect());
}
private setOutput() {
const effect = this.effectRoute.at(-1);
if (!effect) this.output = this.source.output;
else this.output = effect.output;
}
/**
* 连接音频路由图
*/
private link() {
this.effectRoute.forEach(v => v.disconnect());
this.effectRoute.forEach((v, i) => {
const next = this.effectRoute[i + 1];
if (next) {
v.connect(next);
}
});
}
private startAllEffect() {
this.effectRoute.forEach(v => v.start());
}
private endAllEffect() {
this.effectRoute.forEach(v => v.end());
}
}
export const audioPlayer = new AudioPlayer();
// window.audioPlayer = audioPlayer;
@@ -1,135 +0,0 @@
import EventEmitter from 'eventemitter3';
import { audioPlayer, AudioPlayer } from './player';
import { logger } from '@motajs/common';
import { VolumeEffect } from './effect';
type LocationArray = [number, number, number];
interface SoundPlayerEvent {}
export class SoundPlayer<
T extends string = SoundIds
> extends EventEmitter<SoundPlayerEvent> {
/** 每个音效的唯一标识符 */
private num: number = 0;
/** 每个音效的数据 */
readonly buffer: Map<T, AudioBuffer> = new Map();
/** 所有正在播放的音乐 */
readonly playing: Set<number> = new Set();
/** 音量节点 */
readonly gain: VolumeEffect;
/** 是否已经启用 */
enabled: boolean = true;
constructor(public readonly player: AudioPlayer) {
super();
this.gain = player.createVolumeEffect();
}
/**
* 设置是否启用音效
* @param enabled 是否启用音效
*/
setEnabled(enabled: boolean) {
if (!enabled) this.stopAllSounds();
this.enabled = enabled;
}
/**
* 设置音量大小
* @param volume 音量大小
*/
setVolume(volume: number) {
this.gain.setVolume(volume);
}
/**
* 获取音量大小
*/
getVolume() {
return this.gain.getVolume();
}
/**
* 添加一个音效
* @param id 音效名称
* @param data 音效的Uint8Array数据
*/
async add(id: T, data: Uint8Array) {
const buffer = await this.player.decodeAudioData(data);
if (!buffer) {
logger.warn(51, id);
return;
}
this.buffer.set(id, buffer);
}
/**
* 播放一个音效
* @param id 音效名称
* @param position 音频位置,[0, 0, 0]表示正中心,x轴指向水平向右,y轴指向水平向上,z轴指向竖直向上
* @param orientation 音频朝向,[0, 1, 0]表示朝向前方
*/
play(
id: T,
position: LocationArray = [0, 0, 0],
orientation: LocationArray = [1, 0, 0]
) {
if (!this.enabled) return -1;
const buffer = this.buffer.get(id);
if (!buffer) {
logger.warn(52, id);
return -1;
}
const soundNum = this.num++;
const source = this.player.createBufferSource();
source.setBuffer(buffer);
const route = this.player.createRoute(source);
const stereo = this.player.createStereoEffect();
stereo.setPosition(position[0], position[1], position[2]);
stereo.setOrientation(orientation[0], orientation[1], orientation[2]);
route.addEffect([stereo, this.gain]);
this.player.addRoute(`sounds.${soundNum}`, route);
route.play();
// 清理垃圾
source.output.addEventListener('ended', () => {
this.playing.delete(soundNum);
this.player.removeRoute(`sounds.${soundNum}`);
});
this.playing.add(soundNum);
return soundNum;
}
/**
* 停止一个音效
* @param num 音效的唯一 id
*/
stop(num: number) {
const id = `sounds.${num}`;
const route = this.player.getRoute(id);
if (route) {
route.stop();
this.player.removeRoute(id);
this.playing.delete(num);
}
}
/**
* 停止播放所有音效
*/
stopAllSounds() {
this.playing.forEach(v => {
const id = `sounds.${v}`;
const route = this.player.getRoute(id);
if (route) {
route.stop();
this.player.removeRoute(id);
}
});
this.playing.clear();
}
}
export const soundPlayer = new SoundPlayer<SoundIds>(audioPlayer);
@@ -1,562 +0,0 @@
import EventEmitter from 'eventemitter3';
import { IStreamController, IStreamReader } from '../loader';
import { IAudioInput, IAudioOutput } from './effect';
import { logger } from '@motajs/common';
import { AudioType } from './support';
import CodecParser, { CodecFrame, MimeType, OggPage } from 'codec-parser';
import { isNil } from 'lodash-es';
import { IAudioDecodeData, AudioDecoder, checkAudioType } from './decoder';
interface AudioSourceEvent {
play: [];
end: [];
}
export abstract class AudioSource
extends EventEmitter<AudioSourceEvent>
implements IAudioOutput
{
/** 音频源的输出节点 */
abstract readonly output: AudioNode;
/** 是否正在播放 */
playing: boolean = false;
/** 获取音频时长 */
abstract get duration(): number;
/** 获取当前音频播放了多长时间 */
abstract get currentTime(): number;
constructor(public readonly ac: AudioContext) {
super();
}
/**
* 开始播放这个音频源
*/
abstract play(when?: number): void;
/**
* 停止播放这个音频源
* @returns 音频暂停的时刻
*/
abstract stop(): number;
/**
* 连接到音频路由图上,每次调用播放的时候都会执行一次
* @param target 连接至的目标
*/
abstract connect(target: IAudioInput): void;
/**
* 设置是否循环播放
* @param loop 是否循环
*/
abstract setLoop(loop: boolean): void;
}
const mimeTypeMap: Record<AudioType, MimeType> = {
[AudioType.Aac]: 'audio/aac',
[AudioType.Flac]: 'audio/flac',
[AudioType.Mp3]: 'audio/mpeg',
[AudioType.Ogg]: 'application/ogg',
[AudioType.Opus]: 'application/ogg',
[AudioType.Wav]: 'application/ogg'
};
function isOggPage(data: any): data is OggPage {
return !isNil(data.isFirstPage);
}
export class AudioStreamSource extends AudioSource implements IStreamReader {
output: AudioBufferSourceNode;
/** 音频数据 */
buffer?: AudioBuffer;
/** 是否已经完全加载完毕 */
loaded: boolean = false;
/** 已经缓冲了多长时间,如果缓冲完那么跟歌曲时长一致 */
buffered: number = 0;
/** 已经缓冲的采样点数量 */
bufferedSamples: number = 0;
/** 歌曲时长,加载完毕之前保持为 0 */
duration: number = 0;
/** 当前已经播放了多长时间 */
get currentTime(): number {
return this.ac.currentTime - this.lastStartTime + this.lastStartWhen;
}
/** 在流传输阶段,至少缓冲多长时间的音频之后才开始播放,单位秒 */
bufferPlayDuration: number = 1;
/** 音频的采样率,未成功解析出之前保持为 0 */
sampleRate: number = 0;
private controller?: IStreamController;
private loop: boolean = false;
private target?: IAudioInput;
/** 上一次播放是从何时开始的 */
private lastStartWhen: number = 0;
/** 开始播放时刻 */
private lastStartTime: number = 0;
/** 上一次播放的缓存长度 */
private lastBufferSamples: number = 0;
/** 是否已经获取到头文件 */
private headerRecieved: boolean = false;
/** 音频类型 */
private audioType: AudioType | '' = '';
/** 音频解码器 */
private decoder?: AudioDecoder;
/** 音频解析器 */
private parser?: CodecParser;
/** 每多长时间组成一个缓存 Float32Array */
private bufferChunkSize: number = 10;
/** 缓存音频数据,每 bufferChunkSize 秒钟组成一个 Float32Array,用于流式解码 */
private audioData: Float32Array[][] = [];
private errored: boolean = false;
constructor(context: AudioContext) {
super(context);
this.output = context.createBufferSource();
}
/**
* 设置每个缓存数据的大小,默认为10秒钟一个缓存数据
* @param size 每个缓存数据的时长,单位秒
*/
setChunkSize(size: number) {
if (this.controller?.loading || this.loaded) return;
this.bufferChunkSize = size;
}
piped(controller: IStreamController): void {
this.controller = controller;
}
async pump(data: Uint8Array | undefined, done: boolean): Promise<void> {
if (!data || this.errored) return;
if (!this.headerRecieved) {
// 检查头文件获取音频类型,仅检查前256个字节
const toCheck = data.slice(0, 256);
this.audioType = checkAudioType(data);
if (!this.audioType) {
logger.error(
25,
[...toCheck]
.map(v => v.toString(16).padStart(2, '0'))
.join(' ')
.toUpperCase()
);
return;
}
// 创建解码器
const Decoder = AudioDecoder.decoderMap.get(this.audioType);
if (!Decoder) {
this.errored = true;
logger.error(24, this.audioType);
return Promise.reject(
`Cannot decode stream source type of '${this.audioType}', since there is no registered decoder for that type.`
);
}
this.decoder = new Decoder();
// 创建数据解析器
const mime = mimeTypeMap[this.audioType];
const parser = new CodecParser(mime);
this.parser = parser;
await this.decoder.create();
this.headerRecieved = true;
}
const decoder = this.decoder;
const parser = this.parser;
if (!decoder || !parser) {
this.errored = true;
return Promise.reject(
'No parser or decoder attached in this AudioStreamSource'
);
}
await this.decodeData(data, decoder, parser);
if (done) await this.decodeFlushData(decoder, parser);
this.checkBufferedPlay();
}
/**
* 检查采样率,如果还未解析出采样率,那么将设置采样率,如果当前采样率与之前不同,那么发出警告
*/
private checkSampleRate(info: (OggPage | CodecFrame)[]) {
for (const one of info) {
const frame = isOggPage(one) ? one.codecFrames[0] : one;
if (frame) {
const rate = frame.header.sampleRate;
if (this.sampleRate === 0) {
this.sampleRate = rate;
break;
} else {
if (rate !== this.sampleRate) {
logger.warn(48);
}
}
}
}
}
/**
* 解析音频数据
*/
private async decodeData(
data: Uint8Array,
decoder: AudioDecoder,
parser: CodecParser
) {
// 解析音频数据
const audioData = await decoder.decode(data);
if (!audioData) return;
// @ts-expect-error 库类型声明错误
const audioInfo = [...parser.parseChunk(data)] as (
| OggPage
| CodecFrame
)[];
// 检查采样率
this.checkSampleRate(audioInfo);
// 追加音频数据
this.appendDecodedData(audioData, audioInfo);
}
/**
* 解码剩余数据
*/
private async decodeFlushData(decoder: AudioDecoder, parser: CodecParser) {
const audioData = await decoder.flush();
if (!audioData) return;
// @ts-expect-error 库类型声明错误
const audioInfo = [...parser.flush()] as (OggPage | CodecFrame)[];
this.checkSampleRate(audioInfo);
this.appendDecodedData(audioData, audioInfo);
}
/**
* 追加音频数据
*/
private appendDecodedData(
data: IAudioDecodeData,
info: (CodecFrame | OggPage)[]
) {
const channels = data.channelData.length;
if (channels === 0) return;
if (this.audioData.length !== channels) {
this.audioData = [];
for (let i = 0; i < channels; i++) {
this.audioData.push([]);
}
}
// 计算出应该放在哪
const chunk = this.sampleRate * this.bufferChunkSize;
const sampled = this.bufferedSamples;
const pushIndex = Math.floor(sampled / chunk);
const bufferIndex = sampled % chunk;
const dataLength = data.channelData[0].length;
let buffered = 0;
let nowIndex = pushIndex;
let toBuffer = bufferIndex;
while (buffered < dataLength) {
const rest = toBuffer !== 0 ? chunk - bufferIndex : chunk;
for (let i = 0; i < channels; i++) {
const audioData = this.audioData[i];
if (!audioData[nowIndex]) {
audioData.push(new Float32Array(chunk));
}
const toPush = data.channelData[i].slice(
buffered,
buffered + rest
);
audioData[nowIndex].set(toPush, toBuffer);
}
buffered += rest;
nowIndex++;
toBuffer = 0;
}
this.buffered +=
info.reduce((prev, curr) => prev + curr.duration, 0) / 1000;
this.bufferedSamples += info.reduce(
(prev, curr) => prev + curr.samples,
0
);
}
/**
* 检查已缓冲内容,并在未开始播放时播放
*/
private checkBufferedPlay() {
if (this.playing || this.sampleRate === 0) return;
const played = this.lastBufferSamples / this.sampleRate;
const dt = this.buffered - played;
if (this.loaded) {
this.playAudio(played);
return;
}
if (dt < this.bufferPlayDuration) return;
this.lastBufferSamples = this.bufferedSamples;
// 需要播放
this.mergeBuffers();
if (!this.buffer) return;
if (this.playing) this.output.stop();
this.createSourceNode(this.buffer);
this.output.loop = false;
this.output.start(0, played);
this.lastStartTime = this.ac.currentTime;
this.playing = true;
this.output.addEventListener('ended', () => {
this.playing = false;
this.checkBufferedPlay();
});
}
private mergeBuffers() {
const buffer = this.ac.createBuffer(
this.audioData.length,
this.bufferedSamples,
this.sampleRate
);
const chunk = this.sampleRate * this.bufferChunkSize;
const bufferedChunks = Math.floor(this.bufferedSamples / chunk);
const restLength = this.bufferedSamples % chunk;
for (let i = 0; i < this.audioData.length; i++) {
const audio = this.audioData[i];
const data = new Float32Array(this.bufferedSamples);
for (let j = 0; j < bufferedChunks; j++) {
data.set(audio[j], chunk * j);
}
if (restLength !== 0) {
data.set(
audio[bufferedChunks].slice(0, restLength),
chunk * bufferedChunks
);
}
buffer.copyToChannel(data, i, 0);
}
this.buffer = buffer;
}
async start() {
delete this.buffer;
this.headerRecieved = false;
this.audioType = '';
this.errored = false;
this.buffered = 0;
this.sampleRate = 0;
this.bufferedSamples = 0;
this.duration = 0;
this.loaded = false;
if (this.playing) this.output.stop();
this.playing = false;
this.lastStartTime = this.ac.currentTime;
}
end(done: boolean, reason?: string): void {
if (done && this.buffer) {
this.loaded = true;
delete this.controller;
this.mergeBuffers();
this.duration = this.buffered;
this.audioData = [];
this.decoder?.destroy();
delete this.decoder;
delete this.parser;
} else {
logger.warn(44, reason ?? '');
}
}
private playAudio(when?: number) {
if (!this.buffer) return;
this.lastStartTime = this.ac.currentTime;
if (this.playing) this.output.stop();
this.emit('play');
this.createSourceNode(this.buffer);
this.output.start(0, when);
this.playing = true;
this.output.addEventListener('ended', () => {
this.playing = false;
this.emit('end');
if (this.loop && !this.output.loop) this.play(0);
});
}
play(when?: number): void {
if (this.playing || this.errored) return;
if (this.loaded && this.buffer) {
this.playing = true;
this.playAudio(when);
} else {
this.controller?.start();
}
}
private createSourceNode(buffer: AudioBuffer) {
if (!this.target) return;
const node = this.ac.createBufferSource();
node.buffer = buffer;
if (this.playing) this.output.stop();
this.playing = false;
this.output = node;
node.connect(this.target.input);
node.loop = this.loop;
}
stop(): number {
if (this.playing) this.output.stop();
this.playing = false;
return this.ac.currentTime - this.lastStartTime;
}
connect(target: IAudioInput): void {
this.target = target;
}
setLoop(loop: boolean): void {
this.loop = loop;
}
}
export class AudioElementSource extends AudioSource {
output: MediaElementAudioSourceNode;
/** audio 元素 */
readonly audio: HTMLAudioElement;
get duration(): number {
return this.audio.duration;
}
get currentTime(): number {
return this.audio.currentTime;
}
constructor(context: AudioContext) {
super(context);
const audio = new Audio();
audio.preload = 'none';
this.output = context.createMediaElementSource(audio);
this.audio = audio;
audio.addEventListener('play', () => {
this.playing = true;
this.emit('play');
});
audio.addEventListener('ended', () => {
this.playing = false;
this.emit('end');
});
}
/**
* 设置音频源的路径
* @param url 音频路径
*/
setSource(url: string) {
this.audio.src = url;
}
play(when: number = 0): void {
if (this.playing) return;
this.audio.currentTime = when;
this.audio.play();
}
stop(): number {
this.audio.pause();
this.playing = false;
this.emit('end');
return this.audio.currentTime;
}
connect(target: IAudioInput): void {
this.output.connect(target.input);
}
setLoop(loop: boolean): void {
this.audio.loop = loop;
}
}
export class AudioBufferSource extends AudioSource {
output: AudioBufferSourceNode;
/** 音频数据 */
buffer?: AudioBuffer;
/** 是否循环 */
private loop: boolean = false;
duration: number = 0;
get currentTime(): number {
return this.ac.currentTime - this.lastStartTime + this.lastStartWhen;
}
/** 上一次播放是从何时开始的 */
private lastStartWhen: number = 0;
/** 播放开始时刻 */
private lastStartTime: number = 0;
private target?: IAudioInput;
constructor(context: AudioContext) {
super(context);
this.output = context.createBufferSource();
}
/**
* 设置音频源数据
* @param buffer 音频源,可以是未解析的 ArrayBuffer,也可以是已解析的 AudioBuffer
*/
async setBuffer(buffer: ArrayBuffer | AudioBuffer) {
if (buffer instanceof ArrayBuffer) {
this.buffer = await this.ac.decodeAudioData(buffer);
} else {
this.buffer = buffer;
}
this.duration = this.buffer.duration;
}
play(when?: number): void {
if (this.playing || !this.buffer) return;
this.playing = true;
this.lastStartTime = this.ac.currentTime;
this.emit('play');
this.createSourceNode(this.buffer);
this.output.start(0, when);
this.output.addEventListener('ended', () => {
this.playing = false;
this.emit('end');
if (this.loop && !this.output.loop) this.play(0);
});
}
private createSourceNode(buffer: AudioBuffer) {
if (!this.target) return;
const node = this.ac.createBufferSource();
node.buffer = buffer;
this.output = node;
node.connect(this.target.input);
node.loop = this.loop;
}
stop(): number {
this.output.stop();
return this.ac.currentTime - this.lastStartTime;
}
connect(target: IAudioInput): void {
this.target = target;
}
setLoop(loop: boolean): void {
this.loop = loop;
}
}
@@ -1,53 +0,0 @@
const audio = new Audio();
const supportMap = new Map<string, boolean>();
export const enum AudioType {
Mp3 = 'audio/mpeg',
Wav = 'audio/wav; codecs="1"',
Flac = 'audio/flac',
Opus = 'audio/ogg; codecs="opus"',
Ogg = 'audio/ogg; codecs="vorbis"',
Aac = 'audio/aac'
}
/**
* 检查一种音频类型是否能被播放
* @param type 音频类型
*/
export function isAudioSupport(type: AudioType): boolean {
if (supportMap.has(type)) return supportMap.get(type)!;
else {
const support = audio.canPlayType(type);
const canPlay = support === 'maybe' || support === 'probably';
supportMap.set(type, canPlay);
return canPlay;
}
}
const typeMap = new Map<string, AudioType>([
['ogg', AudioType.Ogg],
['mp3', AudioType.Mp3],
['wav', AudioType.Wav],
['flac', AudioType.Flac],
['opus', AudioType.Opus],
['aac', AudioType.Aac]
]);
/**
* 根据文件名拓展猜测其类型
* @param file 文件名
*/
export function guessTypeByExt(file: string): AudioType | '' {
const ext = /\.[a-zA-Z\d]+$/.exec(file);
if (!ext?.[0]) return '';
const type = ext[0].slice(1);
return typeMap.get(type.toLocaleLowerCase()) ?? '';
}
isAudioSupport(AudioType.Ogg);
isAudioSupport(AudioType.Mp3);
isAudioSupport(AudioType.Wav);
isAudioSupport(AudioType.Flac);
isAudioSupport(AudioType.Opus);
isAudioSupport(AudioType.Aac);
@@ -1,7 +1,7 @@
import { Patch, PatchClass } from '@motajs/legacy-common';
import { audioPlayer, bgmController, soundPlayer } from '../audio';
import { audioContext, bgmPlayer, soundPlayer } from '@user/client-base';
import { mainSetting } from '@motajs/legacy-ui';
import { sleep } from 'mutate-animate';
import { sleep } from '@motajs/common';
import { isNil } from 'lodash-es';
// todo: 添加弃用警告 logger.warn(56)
@@ -10,10 +10,10 @@ export function patchAudio() {
const patch = new Patch(PatchClass.Control);
const play = (bgm: BgmIds, when?: number) => {
bgmController.play(bgm, when);
bgmPlayer.play(bgm, when);
};
const pause = () => {
bgmController.pause();
bgmPlayer.pause();
};
patch.add('playBgm', function (bgm, startTime) {
@@ -23,13 +23,13 @@ export function patchAudio() {
pause();
});
patch.add('resumeBgm', function () {
bgmController.resume();
bgmPlayer.resume();
});
patch.add('checkBgm', function () {
if (bgmController.playing) return;
if (bgmPlayer.playing) return;
if (mainSetting.getValue('audio.bgmEnabled')) {
if (bgmController.playingBgm) {
bgmController.play(bgmController.playingBgm);
if (bgmPlayer.playingBgm) {
bgmPlayer.play(bgmPlayer.playingBgm);
} else {
play(main.startBgm, 0);
}
@@ -38,8 +38,8 @@ export function patchAudio() {
}
});
patch.add('triggerBgm', function () {
if (bgmController.playing) bgmController.pause();
else bgmController.resume();
if (bgmPlayer.playing) bgmPlayer.pause();
else bgmPlayer.resume();
});
patch.add(
@@ -47,7 +47,7 @@ export function patchAudio() {
function (sound, _pitch, callback, position, orientation) {
const name = core.getMappedName(sound) as SoundIds;
const num = soundPlayer.play(name, position, orientation);
const route = audioPlayer.getRoute(`sounds.${num}`);
const route = audioContext.getRoute(`sounds.${num}`);
if (!route) {
callback?.();
return -1;
@@ -1,11 +1,9 @@
import { loading } from '@user/data-base';
import { createAudio } from './audio';
import { patchAll } from './fallback';
import { createGameRenderer, createRender } from './render';
export function create() {
patchAll();
createAudio();
createRender();
loading.once('coreInit', () => {
createGameRenderer();
@@ -13,7 +11,5 @@ export function create() {
}
export * from './action';
export * from './audio';
export * from './fallback';
export * from './loader';
export * from './render';
@@ -1 +0,0 @@
export * from './stream';
@@ -1,128 +0,0 @@
import { logger } from '@motajs/common';
import EventEmitter from 'eventemitter3';
export interface IStreamController<T = void> {
readonly loading: boolean;
/**
* 开始流传输
*/
start(): Promise<T>;
/**
* 主动终止流传输
* @param reason 终止原因
*/
cancel(reason?: string): void;
}
export interface IStreamReader<T = any> {
/**
* 接受字节流流传输的数据
* @param data 传入的字节流数据,只包含本分块的内容
* @param done 是否传输完成
*/
pump(
data: Uint8Array | undefined,
done: boolean,
response: Response
): Promise<void>;
/**
* 当前对象被传递给加载流时执行的函数
* @param controller 传输流控制对象
*/
piped(controller: IStreamController<T>): void;
/**
* 开始流传输
* @param stream 传输流对象
* @param controller 传输流控制对象
*/
start(
stream: ReadableStream,
controller: IStreamController<T>,
response: Response
): Promise<void>;
/**
* 结束流传输
* @param done 是否传输完成,如果为 false 的话,说明可能是由于出现错误导致的终止
* @param reason 如果没有传输完成,那么表示失败的原因
*/
end(done: boolean, reason?: string): void;
}
interface StreamLoaderEvent {
data: [data: Uint8Array | undefined, done: boolean];
}
export class StreamLoader
extends EventEmitter<StreamLoaderEvent>
implements IStreamController<void>
{
/** 传输目标 */
private target: Set<IStreamReader> = new Set();
/** 读取流对象 */
private stream?: ReadableStream;
loading: boolean = false;
constructor(public readonly url: string) {
super();
}
/**
* 将加载流传递给字节流读取对象
* @param reader 字节流读取对象
*/
pipe(reader: IStreamReader) {
if (this.loading) {
logger.warn(46);
return;
}
this.target.add(reader);
reader.piped(this);
return this;
}
async start() {
if (this.loading) return;
this.loading = true;
const response = await window.fetch(this.url);
const stream = response.body;
if (!stream) {
logger.error(23);
return;
}
// 获取读取器
this.stream = stream;
const reader = response.body?.getReader();
const targets = [...this.target];
await Promise.all(targets.map(v => v.start(stream, this, response)));
if (reader && reader.read) {
// 开始流传输
while (true) {
const { value, done } = await reader.read();
await Promise.all(
targets.map(v => v.pump(value, done, response))
);
if (done) break;
}
} else {
// 如果不支持流传输
const buffer = await response.arrayBuffer();
const data = new Uint8Array(buffer);
await Promise.all(targets.map(v => v.pump(data, true, response)));
}
this.loading = false;
targets.forEach(v => v.end(true));
}
cancel(reason?: string) {
if (!this.stream) return;
this.stream.cancel(reason);
this.loading = false;
this.target.forEach(v => v.end(false, reason));
}
}