mirror of
https://github.com/unanmed/HumanBreak.git
synced 2025-01-19 04:19:30 +08:00
Compare commits
2 Commits
6dde0334e1
...
5265b0a90e
Author | SHA1 | Date | |
---|---|---|---|
5265b0a90e | |||
231a72e78c |
@ -1531,7 +1531,7 @@ export class GL2Program extends EventEmitter<ShaderProgramEvent> {
|
||||
/**
|
||||
* 定义一个 attribute 常量,并存入本着色器程序的 attribute 常量映射,在 es 300 版本中叫做 in
|
||||
* @param attrib attribute 常量名
|
||||
* @param type attribute 类型,可选 {@link Shader.Attrib1f} 至 {@link Shader.AttribI4uiv}
|
||||
* @param type attribute 类型,可选 {@link GL2.ATTRIB_1f} 至 {@link GL2.ATTRIB_I4uiv}
|
||||
* @returns attribute 常量的操作对象,可用于设置其值
|
||||
*/
|
||||
defineAttribute<T extends AttribType>(
|
||||
|
@ -69,7 +69,6 @@ Mota.require('var', 'loading').once('coreInit', () => {
|
||||
</layer-group>
|
||||
<Textbox id="main-textbox" {...mainTextboxProps}></Textbox>
|
||||
<FloorChange id="floor-change" zIndex={50}></FloorChange>
|
||||
<icon icon={13} animate></icon>
|
||||
</container>
|
||||
);
|
||||
});
|
||||
|
@ -22,6 +22,7 @@
|
||||
"20": "Cannot create render element for tag '$1', since there's no registration for it.",
|
||||
"21": "Incorrect render prop type is delivered. key: '$1', expected type: '$2', delivered type: '$3'",
|
||||
"22": "Incorrect props for custom tag. Please ensure you have delivered 'item' prop and other required props.",
|
||||
"23": "Cannot get reader when fetching '$1'.",
|
||||
"1101": "Shadow extension needs 'floor-hero' extension as dependency.",
|
||||
"1201": "Floor-damage extension needs 'floor-binder' extension as dependency.",
|
||||
"1301": "Portal extension need 'floor-binder' extension as dependency.",
|
||||
@ -71,6 +72,8 @@
|
||||
"41": "Width of text content components must be positive. receive: $1",
|
||||
"42": "Repeated Textbox id: '$1'.",
|
||||
"43": "Cannot set icon of '$1', since it does not exists. Please ensure you have delivered correct icon id or number.",
|
||||
"44": "Unexpected end when loading stream audio, reason: '$1'",
|
||||
"45": "Audio route with id of '$1' has already existed. New route will override old route.",
|
||||
"1001": "Item-detail extension needs 'floor-binder' and 'floor-damage' extension as dependency.",
|
||||
"1101": "Cannot add new effect to point effect instance, for there's no more reserve space for it. Please increase the max count of the instance."
|
||||
}
|
||||
|
266
src/module/audio/effect.ts
Normal file
266
src/module/audio/effect.ts
Normal file
@ -0,0 +1,266 @@
|
||||
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) {}
|
||||
|
||||
/**
|
||||
* 设置音频位置,x正方形水平向右,y正方形垂直于地面向上,z正方向垂直屏幕远离用户
|
||||
* @param x 位置x坐标
|
||||
* @param y 位置y坐标
|
||||
* @param z 位置z坐标
|
||||
*/
|
||||
setPosition(x: number, y: number, z: number) {}
|
||||
|
||||
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) {}
|
||||
|
||||
/**
|
||||
* 获取音量大小
|
||||
*/
|
||||
getVolume(): number {}
|
||||
|
||||
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 要设置的声道
|
||||
* @param volume 这个声道的音量大小
|
||||
*/
|
||||
setVolume(channel: number, volume: number) {}
|
||||
|
||||
/**
|
||||
* 获取某个声道的音量大小
|
||||
* @param channel 要获取的声道
|
||||
*/
|
||||
getVolume(channel: number): number {}
|
||||
|
||||
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) {}
|
||||
|
||||
/**
|
||||
* 获取延迟时长
|
||||
*/
|
||||
getDelay() {}
|
||||
|
||||
end(): void {}
|
||||
|
||||
start(): void {}
|
||||
}
|
||||
|
||||
export class EchoEffect extends AudioEffect {
|
||||
output: DelayNode;
|
||||
input: DelayNode;
|
||||
|
||||
/** 延迟节点 */
|
||||
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();
|
||||
this.input = delay;
|
||||
this.output = delay;
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置回声反馈增益大小
|
||||
* @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;
|
||||
}
|
||||
}
|
4
src/module/audio/index.ts
Normal file
4
src/module/audio/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from './support';
|
||||
export * from './effect';
|
||||
export * from './player';
|
||||
export * from './source';
|
404
src/module/audio/player.ts
Normal file
404
src/module/audio/player.ts
Normal file
@ -0,0 +1,404 @@
|
||||
import EventEmitter from 'eventemitter3';
|
||||
import {
|
||||
AudioBufferSource,
|
||||
AudioElementSource,
|
||||
AudioSource,
|
||||
AudioStreamSource
|
||||
} from './source';
|
||||
import {
|
||||
AudioEffect,
|
||||
ChannelVolumeEffect,
|
||||
EchoEffect,
|
||||
IAudioOutput,
|
||||
StereoEffect,
|
||||
VolumeEffect
|
||||
} from './effect';
|
||||
import { isNil } from 'lodash-es';
|
||||
import { logger } from '@/core/common/logger';
|
||||
import { sleep } from 'mutate-animate';
|
||||
|
||||
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 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个修改音量的效果器
|
||||
*/
|
||||
createVolumeEffect() {
|
||||
return new VolumeEffect(this.ac);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个立体声效果器
|
||||
*/
|
||||
createStereoEffect() {
|
||||
return new StereoEffect(this.ac);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个修改单个声道音量的效果器
|
||||
*/
|
||||
createChannelVolumeEffect() {
|
||||
return new ChannelVolumeEffect(this.ac);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个回声效果器
|
||||
*/
|
||||
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 音频名称
|
||||
* @param when 从音频的哪个位置开始播放,单位秒
|
||||
*/
|
||||
play(id: string, when?: number) {
|
||||
this.getRoute(id)?.play(when);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置听者位置,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;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
/** 是否已暂停,注意停止播放是不算暂停的 */
|
||||
paused: boolean = false;
|
||||
/** 暂停时刻 */
|
||||
private pauseTime: number = 0;
|
||||
|
||||
private audioStartHook?: AudioStartHook;
|
||||
private audioEndHook?: AudioEndHook;
|
||||
|
||||
constructor(
|
||||
public readonly source: AudioSource,
|
||||
public readonly player: AudioPlayer
|
||||
) {
|
||||
super();
|
||||
this.output = source.output;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置结束时间,暂停或停止时,会经过这么长时间才终止音频的播放,这期间可以做一下音频淡出的效果。
|
||||
* @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 从音频的什么时候开始播放,单位秒
|
||||
*/
|
||||
play(when?: number) {
|
||||
if (this.source.playing) return;
|
||||
this.link();
|
||||
if (this.effectRoute.length > 0) {
|
||||
const first = this.effectRoute[0];
|
||||
this.source.connect(first);
|
||||
} else {
|
||||
this.source.connect({ input: this.player.getDestination() });
|
||||
}
|
||||
this.source.play(when);
|
||||
this.paused = false;
|
||||
this.pauseTime = 0;
|
||||
this.audioStartHook?.(this);
|
||||
this.startAllEffect();
|
||||
this.emit('play');
|
||||
}
|
||||
|
||||
/**
|
||||
* 暂停音频播放
|
||||
*/
|
||||
async pause() {
|
||||
if (this.paused || !this.source.playing) return;
|
||||
if (this.audioEndHook) {
|
||||
this.audioEndHook(this.endTime, this);
|
||||
await sleep(this.endTime);
|
||||
}
|
||||
const time = this.source.stop();
|
||||
this.pauseTime = time;
|
||||
this.paused = true;
|
||||
this.endAllEffect();
|
||||
this.emit('pause');
|
||||
}
|
||||
|
||||
/**
|
||||
* 继续音频播放
|
||||
*/
|
||||
resume() {
|
||||
if (this.source.playing) return;
|
||||
if (this.paused) {
|
||||
this.play(this.pauseTime);
|
||||
} else {
|
||||
this.play(0);
|
||||
}
|
||||
this.paused = false;
|
||||
this.pauseTime = 0;
|
||||
this.audioStartHook?.(this);
|
||||
this.startAllEffect();
|
||||
this.emit('resume');
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止音频播放
|
||||
*/
|
||||
async stop() {
|
||||
if (!this.source.playing) return;
|
||||
if (this.audioEndHook) {
|
||||
this.audioEndHook(this.endTime, this);
|
||||
await sleep(this.endTime);
|
||||
}
|
||||
this.source.stop();
|
||||
this.paused = false;
|
||||
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');
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
234
src/module/audio/source.ts
Normal file
234
src/module/audio/source.ts
Normal file
@ -0,0 +1,234 @@
|
||||
import EventEmitter from 'eventemitter3';
|
||||
import { IStreamController, IStreamReader } from '../loader';
|
||||
import { IAudioInput, IAudioOutput } from './effect';
|
||||
import { logger } from '@/core/common/logger';
|
||||
|
||||
interface AudioSourceEvent {
|
||||
play: [];
|
||||
end: [];
|
||||
}
|
||||
|
||||
export abstract class AudioSource
|
||||
extends EventEmitter<AudioSourceEvent>
|
||||
implements IAudioOutput
|
||||
{
|
||||
/** 音频源的输出节点 */
|
||||
abstract readonly output: AudioNode;
|
||||
|
||||
/** 是否正在播放 */
|
||||
playing: boolean = false;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export class AudioStreamSource extends AudioSource implements IStreamReader {
|
||||
output: AudioBufferSourceNode;
|
||||
|
||||
/** 音频数据 */
|
||||
buffer?: AudioBuffer;
|
||||
|
||||
/** 是否已经完全加载完毕 */
|
||||
loaded: boolean = false;
|
||||
|
||||
private controller?: IStreamController;
|
||||
private loop: boolean = false;
|
||||
|
||||
/** 开始播放时刻 */
|
||||
private lastStartTime: number = 0;
|
||||
|
||||
constructor(context: AudioContext) {
|
||||
super(context);
|
||||
this.output = context.createBufferSource();
|
||||
}
|
||||
|
||||
piped(controller: IStreamController): void {
|
||||
this.controller = controller;
|
||||
}
|
||||
|
||||
pump(data: Uint8Array | undefined): void {
|
||||
if (!data) return;
|
||||
}
|
||||
|
||||
start(): void {
|
||||
delete this.buffer;
|
||||
}
|
||||
|
||||
end(done: boolean, reason?: string): void {
|
||||
if (done) {
|
||||
this.loaded = true;
|
||||
delete this.controller;
|
||||
} else {
|
||||
logger.warn(44, reason ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
play(when?: number): void {
|
||||
if (this.playing) return;
|
||||
if (this.loaded && this.buffer) {
|
||||
this.playing = true;
|
||||
this.lastStartTime = this.ac.currentTime;
|
||||
this.emit('play');
|
||||
this.output.start(when);
|
||||
this.output.addEventListener('ended', () => {
|
||||
this.playing = false;
|
||||
this.emit('end');
|
||||
if (this.loop) this.play(0);
|
||||
});
|
||||
} else {
|
||||
this.controller?.start();
|
||||
}
|
||||
}
|
||||
|
||||
stop(): number {
|
||||
this.output.stop();
|
||||
return this.ac.currentTime - this.lastStartTime;
|
||||
}
|
||||
|
||||
connect(target: IAudioInput): void {
|
||||
if (!this.buffer) return;
|
||||
const node = this.ac.createBufferSource();
|
||||
node.buffer = this.buffer;
|
||||
this.output = node;
|
||||
node.connect(target.input);
|
||||
node.loop = this.loop;
|
||||
}
|
||||
|
||||
setLoop(loop: boolean): void {
|
||||
this.loop = loop;
|
||||
}
|
||||
}
|
||||
|
||||
export class AudioElementSource extends AudioSource {
|
||||
output: MediaElementAudioSourceNode;
|
||||
|
||||
/** audio 元素 */
|
||||
readonly audio: HTMLAudioElement;
|
||||
|
||||
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): 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;
|
||||
|
||||
/** 播放开始时刻 */
|
||||
private lastStartTime: number = 0;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
play(when?: number): void {
|
||||
if (this.playing) return;
|
||||
this.playing = true;
|
||||
this.lastStartTime = this.ac.currentTime;
|
||||
this.emit('play');
|
||||
this.output.start(when);
|
||||
this.output.addEventListener('ended', () => {
|
||||
this.playing = false;
|
||||
this.emit('end');
|
||||
if (this.loop) this.play(0);
|
||||
});
|
||||
}
|
||||
|
||||
stop(): number {
|
||||
this.output.stop();
|
||||
return this.ac.currentTime - this.lastStartTime;
|
||||
}
|
||||
|
||||
connect(target: IAudioInput): void {
|
||||
if (!this.buffer) return;
|
||||
const node = this.ac.createBufferSource();
|
||||
node.buffer = this.buffer;
|
||||
node.connect(target.input);
|
||||
}
|
||||
|
||||
setLoop(loop: boolean): void {
|
||||
this.loop = loop;
|
||||
}
|
||||
}
|
46
src/module/audio/support.ts
Normal file
46
src/module/audio/support.ts
Normal file
@ -0,0 +1,46 @@
|
||||
const audio = new Audio();
|
||||
|
||||
const supportMap = new Map<string, boolean>();
|
||||
|
||||
/**
|
||||
* 检查一种音频类型是否能被播放
|
||||
* @param type 音频类型
|
||||
*/
|
||||
export function isAudioSupport(type: string): 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, string>([
|
||||
['ogg', 'audio/ogg; codecs="vorbis"'],
|
||||
['mp3', 'audio/mpeg'],
|
||||
['wav', 'audio/wav; codecs="1"'],
|
||||
['flac', 'audio/flac'],
|
||||
['opus', 'audio/ogg; codecs="opus"'],
|
||||
['acc', 'audio/acc']
|
||||
]);
|
||||
|
||||
/**
|
||||
* 根据文件名拓展猜测其类型
|
||||
* @param file 文件名
|
||||
*/
|
||||
export function guessTypeByExt(file: string) {
|
||||
const ext = /\.[a-zA-Z]$/.exec(file);
|
||||
if (!ext?.[0]) return '';
|
||||
const type = ext[0].slice(1);
|
||||
return typeMap.get(type.toLocaleLowerCase()) ?? '';
|
||||
}
|
||||
|
||||
isAudioSupport('audio/ogg; codecs="vorbis"');
|
||||
isAudioSupport('audio/mpeg');
|
||||
isAudioSupport('audio/wav; codecs="1"');
|
||||
isAudioSupport('audio/flac');
|
||||
isAudioSupport('audio/ogg; codecs="opus"');
|
||||
isAudioSupport('audio/acc');
|
||||
|
||||
console.log(supportMap);
|
@ -7,3 +7,7 @@ Mota.register('module', 'Weather', {
|
||||
WeatherController,
|
||||
RainWeather
|
||||
});
|
||||
|
||||
export * from './weather';
|
||||
export * from './audio';
|
||||
export * from './loader';
|
||||
|
1
src/module/loader/index.ts
Normal file
1
src/module/loader/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './stream';
|
105
src/module/loader/stream.ts
Normal file
105
src/module/loader/stream.ts
Normal file
@ -0,0 +1,105 @@
|
||||
import { logger } from '@/core/common/logger';
|
||||
import EventEmitter from 'eventemitter3';
|
||||
|
||||
export interface IStreamController<T = void> {
|
||||
/**
|
||||
* 开始流传输
|
||||
*/
|
||||
start(): Promise<T>;
|
||||
|
||||
/**
|
||||
* 主动终止流传输
|
||||
* @param reason 终止原因
|
||||
*/
|
||||
cancel(reason?: string): void;
|
||||
}
|
||||
|
||||
export interface IStreamReader<T = any> {
|
||||
/**
|
||||
* 接受字节流流传输的数据
|
||||
* @param data 传入的字节流数据,只包含本分块的内容
|
||||
* @param done 是否传输完成
|
||||
*/
|
||||
pump(data: Uint8Array | undefined, done: boolean): void;
|
||||
|
||||
/**
|
||||
* 当前对象被传递给加载流时执行的函数
|
||||
* @param controller 传输流控制对象
|
||||
*/
|
||||
piped(controller: IStreamController<T>): void;
|
||||
|
||||
/**
|
||||
* 开始流传输
|
||||
* @param stream 传输流对象
|
||||
* @param controller 传输流控制对象
|
||||
*/
|
||||
start(stream: ReadableStream, controller: IStreamController<T>): 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;
|
||||
|
||||
private loading: boolean = false;
|
||||
|
||||
constructor(public readonly url: string) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将加载流传递给字节流读取对象
|
||||
* @param reader 字节流读取对象
|
||||
*/
|
||||
pipe(reader: IStreamReader) {
|
||||
this.target.add(reader);
|
||||
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();
|
||||
this.target.forEach(v => v.start(stream, this));
|
||||
|
||||
// 开始流传输
|
||||
while (true) {
|
||||
const { value, done } = await reader.read();
|
||||
this.target.forEach(v => v.pump(value, done));
|
||||
if (done) break;
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
this.target.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));
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user