mirror of
https://github.com/unanmed/HumanBreak.git
synced 2025-02-07 20:09:27 +08:00
Compare commits
No commits in common. "a058dfda4a8ddf2e8bb5d454a9548ce6102c19c0" and "88c5e39f5cdc7e9fb5744bb8be5e1ce7e724bf5e" have entirely different histories.
a058dfda4a
...
88c5e39f5c
@ -38,7 +38,7 @@ var data_comment_c456ea59_6018_45ef_8bcc_211a24c627dc = {
|
|||||||
"_range": "editor.mode.checkImages(thiseval, './project/images/')",
|
"_range": "editor.mode.checkImages(thiseval, './project/images/')",
|
||||||
"_directory": "./project/images/",
|
"_directory": "./project/images/",
|
||||||
"_transform": (function (one) {
|
"_transform": (function (one) {
|
||||||
if (one.endsWith('.png') || one.endsWith('.jpg') || one.endsWith('.jpeg') || one.endsWith('.gif') || one.endsWith('.webp'))
|
if (one.endsWith('.png') || one.endsWith('.jpg') || one.endsWith('.jpeg') || one.endsWith('.gif'))
|
||||||
return one;
|
return one;
|
||||||
return null;
|
return null;
|
||||||
}).toString(),
|
}).toString(),
|
||||||
@ -96,7 +96,7 @@ var data_comment_c456ea59_6018_45ef_8bcc_211a24c627dc = {
|
|||||||
"_range": "editor.mode.checkUnique(thiseval)",
|
"_range": "editor.mode.checkUnique(thiseval)",
|
||||||
"_directory": "./project/bgms/",
|
"_directory": "./project/bgms/",
|
||||||
"_transform": (function (one) {
|
"_transform": (function (one) {
|
||||||
if (one.endsWith('.mp3') || one.endsWith('.ogg') || one.endsWith('.wav') || one.endsWith('.m4a') || one.endsWith('.flac') || one.endsWith('.opus'))
|
if (one.endsWith('.mp3') || one.endsWith('.ogg') || one.endsWith('.wav') || one.endsWith('.m4a') || one.endsWith('.flac'))
|
||||||
return one;
|
return one;
|
||||||
return null;
|
return null;
|
||||||
}).toString(),
|
}).toString(),
|
||||||
|
@ -193,14 +193,13 @@ var data_a1e2fb4a_e986_4524_b0da_9b7ba7c0874d =
|
|||||||
"zone"
|
"zone"
|
||||||
],
|
],
|
||||||
"bgms": [
|
"bgms": [
|
||||||
"beforeBoss.opus",
|
"beforeBoss.mp3",
|
||||||
"cave.mp3",
|
"cave.mp3",
|
||||||
"escape.mp3",
|
"escape.mp3",
|
||||||
"escape2.mp3",
|
"escape2.mp3",
|
||||||
"grass.mp3",
|
"grass.mp3",
|
||||||
"mount.opus",
|
"mount.mp3",
|
||||||
"night.mp3",
|
"night.mp3",
|
||||||
"output6.ogg",
|
|
||||||
"palaceCenter.mp3",
|
"palaceCenter.mp3",
|
||||||
"palaceNorth.mp3",
|
"palaceNorth.mp3",
|
||||||
"palaceSouth.mp3",
|
"palaceSouth.mp3",
|
||||||
|
@ -1,28 +0,0 @@
|
|||||||
import { StreamLoader } from '../loader';
|
|
||||||
import { audioPlayer, AudioRoute } from './player';
|
|
||||||
import { guessTypeByExt, isAudioSupport } from './support';
|
|
||||||
|
|
||||||
export function loadAllBgm() {
|
|
||||||
const loading = Mota.require('var', 'loading');
|
|
||||||
loading.once('coreInit', () => {
|
|
||||||
const data = data_a1e2fb4a_e986_4524_b0da_9b7ba7c0874d;
|
|
||||||
for (const bgm of data.main.bgms) {
|
|
||||||
const type = guessTypeByExt(bgm);
|
|
||||||
if (!type) continue;
|
|
||||||
if (isAudioSupport(type)) {
|
|
||||||
const source = audioPlayer.createElementSource();
|
|
||||||
source.setSource(`project/bgms/${bgm}`);
|
|
||||||
source.setLoop(true);
|
|
||||||
const route = new AudioRoute(source, audioPlayer);
|
|
||||||
audioPlayer.addRoute(`bgms.${bgm}`, route);
|
|
||||||
} else {
|
|
||||||
const source = audioPlayer.createStreamSource();
|
|
||||||
const stream = new StreamLoader(`project/bgms/${bgm}`);
|
|
||||||
stream.pipe(source);
|
|
||||||
source.setLoop(true);
|
|
||||||
const route = new AudioRoute(source, audioPlayer);
|
|
||||||
audioPlayer.addRoute(`bgms.${bgm}`, route);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
@ -1,45 +0,0 @@
|
|||||||
import { OggVorbisDecoder } from '@wasm-audio-decoders/ogg-vorbis';
|
|
||||||
import { IAudioDecodeData, IAudioDecoder } from './source';
|
|
||||||
import { OggOpusDecoder } from 'ogg-opus-decoder';
|
|
||||||
|
|
||||||
export class VorbisDecoder implements IAudioDecoder {
|
|
||||||
decoder?: OggVorbisDecoder;
|
|
||||||
|
|
||||||
async create(): Promise<void> {
|
|
||||||
this.decoder = new OggVorbisDecoder();
|
|
||||||
await this.decoder.ready;
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy(): void {
|
|
||||||
this.decoder?.free();
|
|
||||||
}
|
|
||||||
|
|
||||||
async decode(data: Uint8Array): Promise<IAudioDecodeData | undefined> {
|
|
||||||
return this.decoder?.decode(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
async flush(): Promise<IAudioDecodeData | undefined> {
|
|
||||||
return await this.decoder?.flush();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class OpusDecoder implements IAudioDecoder {
|
|
||||||
decoder?: OggOpusDecoder;
|
|
||||||
|
|
||||||
async create(): Promise<void> {
|
|
||||||
this.decoder = new OggOpusDecoder();
|
|
||||||
await this.decoder.ready;
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy(): void {
|
|
||||||
this.decoder?.free();
|
|
||||||
}
|
|
||||||
|
|
||||||
async decode(data: Uint8Array): Promise<IAudioDecodeData | undefined> {
|
|
||||||
return this.decoder?.decode(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
async flush(): Promise<IAudioDecodeData | undefined> {
|
|
||||||
return await this.decoder?.flush();
|
|
||||||
}
|
|
||||||
}
|
|
@ -83,11 +83,7 @@ export class StereoEffect extends AudioEffect {
|
|||||||
* @param y 朝向y坐标
|
* @param y 朝向y坐标
|
||||||
* @param z 朝向z坐标
|
* @param z 朝向z坐标
|
||||||
*/
|
*/
|
||||||
setOrientation(x: number, y: number, z: number) {
|
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正方向垂直屏幕远离用户
|
* 设置音频位置,x正方形水平向右,y正方形垂直于地面向上,z正方向垂直屏幕远离用户
|
||||||
@ -95,11 +91,7 @@ export class StereoEffect extends AudioEffect {
|
|||||||
* @param y 位置y坐标
|
* @param y 位置y坐标
|
||||||
* @param z 位置z坐标
|
* @param z 位置z坐标
|
||||||
*/
|
*/
|
||||||
setPosition(x: number, y: number, z: number) {
|
setPosition(x: number, y: number, z: number) {}
|
||||||
this.output.positionX.value = x;
|
|
||||||
this.output.positionY.value = y;
|
|
||||||
this.output.positionZ.value = z;
|
|
||||||
}
|
|
||||||
|
|
||||||
end(): void {}
|
end(): void {}
|
||||||
|
|
||||||
@ -121,16 +113,12 @@ export class VolumeEffect extends AudioEffect {
|
|||||||
* 设置音量大小
|
* 设置音量大小
|
||||||
* @param volume 音量大小
|
* @param volume 音量大小
|
||||||
*/
|
*/
|
||||||
setVolume(volume: number) {
|
setVolume(volume: number) {}
|
||||||
this.output.gain.value = volume;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取音量大小
|
* 获取音量大小
|
||||||
*/
|
*/
|
||||||
getVolume(): number {
|
getVolume(): number {}
|
||||||
return this.output.gain.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
end(): void {}
|
end(): void {}
|
||||||
|
|
||||||
@ -160,22 +148,16 @@ export class ChannelVolumeEffect extends AudioEffect {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置某个声道的音量大小
|
* 设置某个声道的音量大小
|
||||||
* @param channel 要设置的声道,可填0-5
|
* @param channel 要设置的声道
|
||||||
* @param volume 这个声道的音量大小
|
* @param volume 这个声道的音量大小
|
||||||
*/
|
*/
|
||||||
setVolume(channel: number, volume: number) {
|
setVolume(channel: number, volume: number) {}
|
||||||
if (!this.gain[channel]) return;
|
|
||||||
this.gain[channel].gain.value = volume;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取某个声道的音量大小,可填0-5
|
* 获取某个声道的音量大小
|
||||||
* @param channel 要获取的声道
|
* @param channel 要获取的声道
|
||||||
*/
|
*/
|
||||||
getVolume(channel: number): number {
|
getVolume(channel: number): number {}
|
||||||
if (!this.gain[channel]) return 0;
|
|
||||||
return this.gain[channel].gain.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
end(): void {}
|
end(): void {}
|
||||||
|
|
||||||
@ -197,16 +179,12 @@ export class DelayEffect extends AudioEffect {
|
|||||||
* 设置延迟时长
|
* 设置延迟时长
|
||||||
* @param delay 延迟时长,单位秒
|
* @param delay 延迟时长,单位秒
|
||||||
*/
|
*/
|
||||||
setDelay(delay: number) {
|
setDelay(delay: number) {}
|
||||||
this.output.delayTime.value = delay;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取延迟时长
|
* 获取延迟时长
|
||||||
*/
|
*/
|
||||||
getDelay() {
|
getDelay() {}
|
||||||
return this.output.delayTime.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
end(): void {}
|
end(): void {}
|
||||||
|
|
||||||
|
@ -1,14 +1,4 @@
|
|||||||
import { loadAllBgm } from './bgmLoader';
|
|
||||||
import { OpusDecoder, VorbisDecoder } from './decoder';
|
|
||||||
import { AudioStreamSource } from './source';
|
|
||||||
import { AudioType } from './support';
|
|
||||||
|
|
||||||
loadAllBgm();
|
|
||||||
AudioStreamSource.registerDecoder(AudioType.Ogg, VorbisDecoder);
|
|
||||||
AudioStreamSource.registerDecoder(AudioType.Opus, OpusDecoder);
|
|
||||||
|
|
||||||
export * from './support';
|
export * from './support';
|
||||||
export * from './effect';
|
export * from './effect';
|
||||||
export * from './player';
|
export * from './player';
|
||||||
export * from './source';
|
export * from './source';
|
||||||
export * from './bgmLoader';
|
|
||||||
|
@ -8,7 +8,6 @@ import {
|
|||||||
import {
|
import {
|
||||||
AudioEffect,
|
AudioEffect,
|
||||||
ChannelVolumeEffect,
|
ChannelVolumeEffect,
|
||||||
DelayEffect,
|
|
||||||
EchoEffect,
|
EchoEffect,
|
||||||
IAudioOutput,
|
IAudioOutput,
|
||||||
StereoEffect,
|
StereoEffect,
|
||||||
@ -139,16 +138,6 @@ export class AudioPlayer extends EventEmitter<AudioPlayerEvent> {
|
|||||||
return new ChannelVolumeEffect(this.ac);
|
return new ChannelVolumeEffect(this.ac);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建一个延迟效果器
|
|
||||||
* |-----------|
|
|
||||||
* Input ----> | DelayNode | ----> Output
|
|
||||||
* |-----------|
|
|
||||||
*/
|
|
||||||
createDelay() {
|
|
||||||
return new DelayEffect(this.ac);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建一个回声效果器
|
* 创建一个回声效果器
|
||||||
* ```txt
|
* ```txt
|
||||||
@ -198,40 +187,10 @@ export class AudioPlayer extends EventEmitter<AudioPlayerEvent> {
|
|||||||
* @param id 音频名称
|
* @param id 音频名称
|
||||||
* @param when 从音频的哪个位置开始播放,单位秒
|
* @param when 从音频的哪个位置开始播放,单位秒
|
||||||
*/
|
*/
|
||||||
play(id: string, when: number = 0) {
|
play(id: string, when?: number) {
|
||||||
this.getRoute(id)?.play(when);
|
this.getRoute(id)?.play(when);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 暂停音频播放
|
|
||||||
* @param id 音频名称
|
|
||||||
* @returns 当音乐真正停止时兑现
|
|
||||||
*/
|
|
||||||
pause(id: string) {
|
|
||||||
const route = this.getRoute(id);
|
|
||||||
if (!route) return Promise.resolve();
|
|
||||||
else return route.pause();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 停止音频播放
|
|
||||||
* @param id 音频名称
|
|
||||||
* @returns 当音乐真正停止时兑现
|
|
||||||
*/
|
|
||||||
stop(id: string) {
|
|
||||||
const route = this.getRoute(id);
|
|
||||||
if (!route) return Promise.resolve();
|
|
||||||
else return route.stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 继续音频播放
|
|
||||||
* @param id 音频名称
|
|
||||||
*/
|
|
||||||
resume(id: string) {
|
|
||||||
this.getRoute(id)?.resume();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置听者位置,x正方形水平向右,y正方形垂直于地面向上,z正方向垂直屏幕远离用户
|
* 设置听者位置,x正方形水平向右,y正方形垂直于地面向上,z正方向垂直屏幕远离用户
|
||||||
* @param x 位置x坐标
|
* @param x 位置x坐标
|
||||||
@ -340,7 +299,7 @@ export class AudioRoute
|
|||||||
* 开始播放这个音频
|
* 开始播放这个音频
|
||||||
* @param when 从音频的什么时候开始播放,单位秒
|
* @param when 从音频的什么时候开始播放,单位秒
|
||||||
*/
|
*/
|
||||||
play(when: number = 0) {
|
play(when?: number) {
|
||||||
if (this.source.playing) return;
|
if (this.source.playing) return;
|
||||||
this.link();
|
this.link();
|
||||||
if (this.effectRoute.length > 0) {
|
if (this.effectRoute.length > 0) {
|
||||||
@ -471,5 +430,3 @@ export class AudioRoute
|
|||||||
this.effectRoute.forEach(v => v.end());
|
this.effectRoute.forEach(v => v.end());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const audioPlayer = new AudioPlayer();
|
|
||||||
|
@ -80,25 +80,23 @@ export interface IAudioDecoder {
|
|||||||
* 解码流数据
|
* 解码流数据
|
||||||
* @param data 流数据
|
* @param data 流数据
|
||||||
*/
|
*/
|
||||||
decode(data: Uint8Array): Promise<IAudioDecodeData | undefined>;
|
decode(data: Uint8Array): Promise<IAudioDecodeData>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 当音频解码完成后,会调用此函数,需要返回之前还未解析或未返回的音频数据。调用后,该解码器将不会被再次使用
|
* 当音频解码完成后,会调用此函数,需要返回之前还未解析或未返回的音频数据。调用后,该解码器将不会被再次使用
|
||||||
*/
|
*/
|
||||||
flush(): Promise<IAudioDecodeData | undefined>;
|
flush(): Promise<IAudioDecodeData>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileSignatures: [AudioType, number[]][] = [
|
const fileSignatures: Map<string, AudioType> = new Map([
|
||||||
[AudioType.Mp3, [0x49, 0x44, 0x33]],
|
['49 44 33', AudioType.Mp3],
|
||||||
[AudioType.Ogg, [0x4f, 0x67, 0x67, 0x53]],
|
['4F 67 67 53', AudioType.Ogg],
|
||||||
[AudioType.Wav, [52, 0x49, 0x46, 0x46]],
|
['52 49 46 46', AudioType.Wav],
|
||||||
[AudioType.Flac, [0x66, 0x4c, 0x61, 0x43]],
|
['66 4C 61 43', AudioType.Flac],
|
||||||
[AudioType.Aac, [0xff, 0xf1]],
|
['4F 70 75 73', AudioType.Opus],
|
||||||
[AudioType.Aac, [0xff, 0xf9]]
|
['FF F1', AudioType.Aac],
|
||||||
];
|
['FF F9', AudioType.Aac]
|
||||||
const oggHeaders: [AudioType, number[]][] = [
|
]);
|
||||||
[AudioType.Opus, [0x4f, 0x70, 0x75, 0x73, 0x48, 0x65, 0x61, 0x64]]
|
|
||||||
];
|
|
||||||
|
|
||||||
const mimeTypeMap: Record<AudioType, MimeType> = {
|
const mimeTypeMap: Record<AudioType, MimeType> = {
|
||||||
[AudioType.Aac]: 'audio/aac',
|
[AudioType.Aac]: 'audio/aac',
|
||||||
@ -114,8 +112,7 @@ function isOggPage(data: any): data is OggPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class AudioStreamSource extends AudioSource implements IStreamReader {
|
export class AudioStreamSource extends AudioSource implements IStreamReader {
|
||||||
static readonly decoderMap: Map<AudioType, new () => IAudioDecoder> =
|
static readonly decoderMap: Map<AudioType, IAudioDecoder> = new Map();
|
||||||
new Map();
|
|
||||||
output: AudioBufferSourceNode;
|
output: AudioBufferSourceNode;
|
||||||
|
|
||||||
/** 音频数据 */
|
/** 音频数据 */
|
||||||
@ -141,8 +138,6 @@ export class AudioStreamSource extends AudioSource implements IStreamReader {
|
|||||||
|
|
||||||
/** 开始播放时刻 */
|
/** 开始播放时刻 */
|
||||||
private lastStartTime: number = 0;
|
private lastStartTime: number = 0;
|
||||||
/** 上一次播放的缓存长度 */
|
|
||||||
private lastBufferSamples: number = 0;
|
|
||||||
|
|
||||||
/** 是否已经获取到头文件 */
|
/** 是否已经获取到头文件 */
|
||||||
private headerRecieved: boolean = false;
|
private headerRecieved: boolean = false;
|
||||||
@ -157,14 +152,12 @@ export class AudioStreamSource extends AudioSource implements IStreamReader {
|
|||||||
/** 缓存音频数据,每 bufferChunkSize 秒钟组成一个 Float32Array,用于流式解码 */
|
/** 缓存音频数据,每 bufferChunkSize 秒钟组成一个 Float32Array,用于流式解码 */
|
||||||
private audioData: Float32Array[][] = [];
|
private audioData: Float32Array[][] = [];
|
||||||
|
|
||||||
private errored: boolean = false;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 注册一个解码器
|
* 注册一个解码器
|
||||||
* @param type 要注册的解码器允许解码的类型
|
* @param type 要注册的解码器允许解码的类型
|
||||||
* @param decoder 解码器对象
|
* @param decoder 解码器对象
|
||||||
*/
|
*/
|
||||||
static registerDecoder(type: AudioType, decoder: new () => IAudioDecoder) {
|
static registerDecoder(type: AudioType, decoder: IAudioDecoder) {
|
||||||
if (this.decoderMap.has(type)) {
|
if (this.decoderMap.has(type)) {
|
||||||
logger.warn(47, type);
|
logger.warn(47, type);
|
||||||
return;
|
return;
|
||||||
@ -191,60 +184,42 @@ export class AudioStreamSource extends AudioSource implements IStreamReader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async pump(data: Uint8Array | undefined, done: boolean): Promise<void> {
|
async pump(data: Uint8Array | undefined, done: boolean): Promise<void> {
|
||||||
if (!data || this.errored) return;
|
if (!data) return;
|
||||||
if (!this.headerRecieved) {
|
if (!this.headerRecieved) {
|
||||||
// 检查头文件获取音频类型,仅检查前256个字节
|
// 检查头文件获取音频类型
|
||||||
const toCheck = data.slice(0, 256);
|
const toCheck = [...data.slice(0, 16)];
|
||||||
for (const [type, value] of fileSignatures) {
|
const hexArray = toCheck.map(v => v.toString(16).padStart(2, '0'));
|
||||||
if (value.every((v, i) => toCheck[i] === v)) {
|
const hex = hexArray.join(' ');
|
||||||
this.audioType = type;
|
for (const [key, value] of fileSignatures) {
|
||||||
|
if (hex.startsWith(key)) {
|
||||||
|
this.audioType = value;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this.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) {
|
|
||||||
this.audioType = key;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!this.audioType) {
|
if (!this.audioType) {
|
||||||
logger.error(
|
logger.error(25, hex);
|
||||||
25,
|
|
||||||
[...toCheck]
|
|
||||||
.map(v => v.toString().padStart(2, '0'))
|
|
||||||
.join(' ')
|
|
||||||
.toUpperCase()
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 创建解码器
|
// 创建解码器
|
||||||
const Decoder = AudioStreamSource.decoderMap.get(this.audioType);
|
const decoder = AudioStreamSource.decoderMap.get(this.audioType);
|
||||||
if (!Decoder) {
|
this.decoder = decoder;
|
||||||
this.errored = true;
|
if (!decoder) {
|
||||||
logger.error(24, this.audioType);
|
logger.error(24, this.audioType);
|
||||||
return Promise.reject(
|
return Promise.reject(
|
||||||
`Cannot decode stream source type of '${this.audioType}', since there is no registered decoder for that type.`
|
`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 mime = mimeTypeMap[this.audioType];
|
||||||
const parser = new CodecParser(mime);
|
const parser = new CodecParser(mime);
|
||||||
this.parser = parser;
|
this.parser = parser;
|
||||||
await this.decoder.create();
|
await decoder.create();
|
||||||
this.headerRecieved = true;
|
this.headerRecieved = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const decoder = this.decoder;
|
const decoder = this.decoder;
|
||||||
const parser = this.parser;
|
const parser = this.parser;
|
||||||
if (!decoder || !parser) {
|
if (!decoder || !parser) {
|
||||||
this.errored = true;
|
|
||||||
return Promise.reject(
|
return Promise.reject(
|
||||||
'No parser or decoder attached in this AudioStreamSource'
|
'No parser or decoder attached in this AudioStreamSource'
|
||||||
);
|
);
|
||||||
@ -259,13 +234,13 @@ export class AudioStreamSource extends AudioSource implements IStreamReader {
|
|||||||
* 检查采样率,如果还未解析出采样率,那么将设置采样率,如果当前采样率与之前不同,那么发出警告
|
* 检查采样率,如果还未解析出采样率,那么将设置采样率,如果当前采样率与之前不同,那么发出警告
|
||||||
*/
|
*/
|
||||||
private checkSampleRate(info: (OggPage | CodecFrame)[]) {
|
private checkSampleRate(info: (OggPage | CodecFrame)[]) {
|
||||||
for (const one of info) {
|
const first = info[0];
|
||||||
const frame = isOggPage(one) ? one.codecFrames[0] : one;
|
if (first) {
|
||||||
|
const frame = isOggPage(first) ? first.codecFrames[0] : first;
|
||||||
if (frame) {
|
if (frame) {
|
||||||
const rate = frame.header.sampleRate;
|
const rate = frame.header.sampleRate;
|
||||||
if (this.sampleRate === 0) {
|
if (this.sampleRate === 0) {
|
||||||
this.sampleRate = rate;
|
this.sampleRate = rate;
|
||||||
break;
|
|
||||||
} else {
|
} else {
|
||||||
if (rate !== this.sampleRate) {
|
if (rate !== this.sampleRate) {
|
||||||
logger.warn(48);
|
logger.warn(48);
|
||||||
@ -285,7 +260,6 @@ export class AudioStreamSource extends AudioSource implements IStreamReader {
|
|||||||
) {
|
) {
|
||||||
// 解析音频数据
|
// 解析音频数据
|
||||||
const audioData = await decoder.decode(data);
|
const audioData = await decoder.decode(data);
|
||||||
if (!audioData) return;
|
|
||||||
// @ts-expect-error 库类型声明错误
|
// @ts-expect-error 库类型声明错误
|
||||||
const audioInfo = [...parser.parseChunk(data)] as (
|
const audioInfo = [...parser.parseChunk(data)] as (
|
||||||
| OggPage
|
| OggPage
|
||||||
@ -303,7 +277,6 @@ export class AudioStreamSource extends AudioSource implements IStreamReader {
|
|||||||
*/
|
*/
|
||||||
private async decodeFlushData(decoder: IAudioDecoder, parser: CodecParser) {
|
private async decodeFlushData(decoder: IAudioDecoder, parser: CodecParser) {
|
||||||
const audioData = await decoder.flush();
|
const audioData = await decoder.flush();
|
||||||
if (!audioData) return;
|
|
||||||
// @ts-expect-error 库类型声明错误
|
// @ts-expect-error 库类型声明错误
|
||||||
const audioInfo = [...parser.flush()] as (OggPage | CodecFrame)[];
|
const audioInfo = [...parser.flush()] as (OggPage | CodecFrame)[];
|
||||||
|
|
||||||
@ -330,33 +303,23 @@ export class AudioStreamSource extends AudioSource implements IStreamReader {
|
|||||||
const chunk = this.sampleRate * this.bufferChunkSize;
|
const chunk = this.sampleRate * this.bufferChunkSize;
|
||||||
const sampled = this.bufferedSamples;
|
const sampled = this.bufferedSamples;
|
||||||
const pushIndex = Math.floor(sampled / chunk);
|
const pushIndex = Math.floor(sampled / chunk);
|
||||||
const bufferIndex = sampled % chunk;
|
const bufferIndex = sampled % (this.sampleRate * chunk);
|
||||||
const dataLength = data.channelData[0].length;
|
const dataLength = data.channelData[0].length;
|
||||||
let buffered = 0;
|
const restLength = chunk - bufferIndex;
|
||||||
let nowIndex = pushIndex;
|
// 把数据放入缓存
|
||||||
let toBuffer = bufferIndex;
|
|
||||||
while (buffered < dataLength) {
|
|
||||||
const rest = toBuffer !== 0 ? chunk - bufferIndex : chunk;
|
|
||||||
|
|
||||||
for (let i = 0; i < channels; i++) {
|
for (let i = 0; i < channels; i++) {
|
||||||
const audioData = this.audioData[i];
|
const audioData = this.audioData[i];
|
||||||
if (!audioData[nowIndex]) {
|
if (!audioData[pushIndex]) {
|
||||||
audioData.push(new Float32Array(chunk));
|
audioData.push(new Float32Array(chunk * this.sampleRate));
|
||||||
}
|
}
|
||||||
const toPush = data.channelData[i].slice(
|
audioData[pushIndex].set(data.channelData[i], bufferIndex);
|
||||||
buffered,
|
if (restLength < dataLength) {
|
||||||
buffered + rest
|
const nextData = new Float32Array(chunk * this.sampleRate);
|
||||||
);
|
nextData.set(data.channelData[i].slice(restLength), 0);
|
||||||
|
audioData.push(nextData);
|
||||||
audioData[nowIndex].set(toPush, toBuffer);
|
|
||||||
}
|
}
|
||||||
buffered += rest;
|
|
||||||
nowIndex++;
|
|
||||||
toBuffer = 0;
|
|
||||||
}
|
}
|
||||||
|
this.buffered += info.reduce((prev, curr) => prev + curr.duration, 0);
|
||||||
this.buffered +=
|
|
||||||
info.reduce((prev, curr) => prev + curr.duration, 0) / 1000;
|
|
||||||
this.bufferedSamples += info.reduce(
|
this.bufferedSamples += info.reduce(
|
||||||
(prev, curr) => prev + curr.samples,
|
(prev, curr) => prev + curr.samples,
|
||||||
0
|
0
|
||||||
@ -367,112 +330,71 @@ export class AudioStreamSource extends AudioSource implements IStreamReader {
|
|||||||
* 检查已缓冲内容,并在未开始播放时播放
|
* 检查已缓冲内容,并在未开始播放时播放
|
||||||
*/
|
*/
|
||||||
private checkBufferedPlay() {
|
private checkBufferedPlay() {
|
||||||
if (this.playing || this.sampleRate === 0) return;
|
if (this.playing || this.loaded) return;
|
||||||
const played = this.lastBufferSamples / this.sampleRate;
|
const played = this.ac.currentTime - this.lastStartTime;
|
||||||
const dt = this.buffered - played;
|
const dt = this.buffered - played;
|
||||||
if (this.loaded) {
|
|
||||||
this.playAudio(played);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (dt < this.bufferPlayDuration) return;
|
if (dt < this.bufferPlayDuration) return;
|
||||||
console.log(played, this.lastBufferSamples, this.sampleRate);
|
|
||||||
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(
|
const buffer = this.ac.createBuffer(
|
||||||
this.audioData.length,
|
this.audioData.length,
|
||||||
this.bufferedSamples,
|
this.bufferedSamples,
|
||||||
this.sampleRate
|
this.sampleRate
|
||||||
);
|
);
|
||||||
|
this.buffer = buffer;
|
||||||
const chunk = this.sampleRate * this.bufferChunkSize;
|
const chunk = this.sampleRate * this.bufferChunkSize;
|
||||||
const bufferedChunks = Math.floor(this.bufferedSamples / chunk);
|
const bufferedChunks = Math.floor(this.buffered / chunk);
|
||||||
const restLength = this.bufferedSamples % chunk;
|
const restLength = this.buffered % chunk;
|
||||||
for (let i = 0; i < this.audioData.length; i++) {
|
for (let i = 0; i < this.audioData.length; i++) {
|
||||||
const audio = this.audioData[i];
|
const audio = this.audioData[i];
|
||||||
const data = new Float32Array(this.bufferedSamples);
|
const data = new Float32Array(this.bufferedSamples);
|
||||||
for (let j = 0; j < bufferedChunks; j++) {
|
for (let j = 0; j < bufferedChunks; j++) {
|
||||||
data.set(audio[j], chunk * j);
|
data.set(audio[j], chunk * j);
|
||||||
}
|
}
|
||||||
if (restLength !== 0) {
|
if (restLength !== 0) data.set(audio[bufferedChunks], 0);
|
||||||
data.set(
|
|
||||||
audio[bufferedChunks].slice(0, restLength),
|
|
||||||
chunk * bufferedChunks
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer.copyToChannel(data, i, 0);
|
buffer.copyToChannel(data, i, 0);
|
||||||
}
|
}
|
||||||
this.buffer = buffer;
|
this.createSourceNode(buffer);
|
||||||
|
this.output.start(played);
|
||||||
|
this.lastStartTime = this.ac.currentTime;
|
||||||
|
this.output.addEventListener('ended', () => {
|
||||||
|
this.checkBufferedPlay();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private mergeBuffers() {}
|
||||||
|
|
||||||
async start() {
|
async start() {
|
||||||
delete this.buffer;
|
delete this.buffer;
|
||||||
this.headerRecieved = false;
|
this.headerRecieved = false;
|
||||||
this.audioType = '';
|
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 {
|
end(done: boolean, reason?: string): void {
|
||||||
if (done && this.buffer) {
|
if (done) {
|
||||||
this.loaded = true;
|
this.loaded = true;
|
||||||
delete this.controller;
|
delete this.controller;
|
||||||
this.mergeBuffers();
|
this.mergeBuffers();
|
||||||
// const played = this.lastBufferSamples / this.sampleRate;
|
const played = this.ac.currentTime - this.lastStartTime;
|
||||||
// this.playAudio(played);
|
this.output.stop();
|
||||||
this.duration = this.buffered;
|
this.play(played);
|
||||||
this.audioData = [];
|
|
||||||
this.decoder?.destroy();
|
|
||||||
delete this.decoder;
|
|
||||||
delete this.parser;
|
|
||||||
} else {
|
} else {
|
||||||
logger.warn(44, reason ?? '');
|
logger.warn(44, reason ?? '');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private playAudio(when?: number) {
|
play(when?: number): void {
|
||||||
if (!this.buffer) return;
|
if (this.playing) return;
|
||||||
|
if (this.loaded && this.buffer) {
|
||||||
|
this.playing = true;
|
||||||
this.lastStartTime = this.ac.currentTime;
|
this.lastStartTime = this.ac.currentTime;
|
||||||
if (this.playing) this.output.stop();
|
|
||||||
this.emit('play');
|
this.emit('play');
|
||||||
this.createSourceNode(this.buffer);
|
this.createSourceNode(this.buffer);
|
||||||
this.output.start(0, when);
|
this.output.start(when);
|
||||||
this.playing = true;
|
|
||||||
console.log(when);
|
|
||||||
|
|
||||||
this.output.addEventListener('ended', () => {
|
this.output.addEventListener('ended', () => {
|
||||||
this.playing = false;
|
this.playing = false;
|
||||||
this.emit('end');
|
this.emit('end');
|
||||||
if (this.loop && !this.output.loop) this.play(0);
|
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 {
|
} else {
|
||||||
this.controller?.start();
|
this.controller?.start();
|
||||||
}
|
}
|
||||||
@ -482,16 +404,13 @@ export class AudioStreamSource extends AudioSource implements IStreamReader {
|
|||||||
if (!this.target) return;
|
if (!this.target) return;
|
||||||
const node = this.ac.createBufferSource();
|
const node = this.ac.createBufferSource();
|
||||||
node.buffer = buffer;
|
node.buffer = buffer;
|
||||||
if (this.playing) this.output.stop();
|
|
||||||
this.playing = false;
|
|
||||||
this.output = node;
|
this.output = node;
|
||||||
node.connect(this.target.input);
|
node.connect(this.target.input);
|
||||||
node.loop = this.loop;
|
node.loop = this.loop;
|
||||||
}
|
}
|
||||||
|
|
||||||
stop(): number {
|
stop(): number {
|
||||||
if (this.playing) this.output.stop();
|
this.output.stop();
|
||||||
this.playing = false;
|
|
||||||
return this.ac.currentTime - this.lastStartTime;
|
return this.ac.currentTime - this.lastStartTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -534,7 +453,7 @@ export class AudioElementSource extends AudioSource {
|
|||||||
this.audio.src = url;
|
this.audio.src = url;
|
||||||
}
|
}
|
||||||
|
|
||||||
play(when: number = 0): void {
|
play(when: number): void {
|
||||||
if (this.playing) return;
|
if (this.playing) return;
|
||||||
this.audio.currentTime = when;
|
this.audio.currentTime = when;
|
||||||
this.audio.play();
|
this.audio.play();
|
||||||
@ -591,7 +510,7 @@ export class AudioBufferSource extends AudioSource {
|
|||||||
this.lastStartTime = this.ac.currentTime;
|
this.lastStartTime = this.ac.currentTime;
|
||||||
this.emit('play');
|
this.emit('play');
|
||||||
this.createSourceNode(this.buffer);
|
this.createSourceNode(this.buffer);
|
||||||
this.output.start(0, when);
|
this.output.start(when);
|
||||||
this.output.addEventListener('ended', () => {
|
this.output.addEventListener('ended', () => {
|
||||||
this.playing = false;
|
this.playing = false;
|
||||||
this.emit('end');
|
this.emit('end');
|
||||||
|
@ -25,21 +25,21 @@ export function isAudioSupport(type: AudioType): boolean {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const typeMap = new Map<string, AudioType>([
|
const typeMap = new Map<string, string>([
|
||||||
['ogg', AudioType.Ogg],
|
['ogg', 'audio/ogg; codecs="vorbis"'],
|
||||||
['mp3', AudioType.Mp3],
|
['mp3', 'audio/mpeg'],
|
||||||
['wav', AudioType.Wav],
|
['wav', 'audio/wav; codecs="1"'],
|
||||||
['flac', AudioType.Flac],
|
['flac', 'audio/flac'],
|
||||||
['opus', AudioType.Opus],
|
['opus', 'audio/ogg; codecs="opus"'],
|
||||||
['aac', AudioType.Aac]
|
['aac', 'audio/aac']
|
||||||
]);
|
]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据文件名拓展猜测其类型
|
* 根据文件名拓展猜测其类型
|
||||||
* @param file 文件名
|
* @param file 文件名
|
||||||
*/
|
*/
|
||||||
export function guessTypeByExt(file: string): AudioType | '' {
|
export function guessTypeByExt(file: string) {
|
||||||
const ext = /\.[a-zA-Z\d]+$/.exec(file);
|
const ext = /\.[a-zA-Z]$/.exec(file);
|
||||||
if (!ext?.[0]) return '';
|
if (!ext?.[0]) return '';
|
||||||
const type = ext[0].slice(1);
|
const type = ext[0].slice(1);
|
||||||
return typeMap.get(type.toLocaleLowerCase()) ?? '';
|
return typeMap.get(type.toLocaleLowerCase()) ?? '';
|
||||||
|
@ -82,7 +82,6 @@ export class StreamLoader
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.target.add(reader);
|
this.target.add(reader);
|
||||||
reader.piped(this);
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,21 +98,25 @@ export class StreamLoader
|
|||||||
this.stream = stream;
|
this.stream = stream;
|
||||||
const reader = response.body?.getReader();
|
const reader = response.body?.getReader();
|
||||||
const targets = [...this.target];
|
const targets = [...this.target];
|
||||||
// try {
|
try {
|
||||||
await Promise.all(targets.map(v => v.start(stream, this, response)));
|
await Promise.all(
|
||||||
|
targets.map(v => v.start(stream, this, response))
|
||||||
|
);
|
||||||
|
|
||||||
// 开始流传输
|
// 开始流传输
|
||||||
while (true) {
|
while (true) {
|
||||||
const { value, done } = await reader.read();
|
const { value, done } = await reader.read();
|
||||||
await Promise.all(targets.map(v => v.pump(value, done, response)));
|
await Promise.all(
|
||||||
|
targets.map(v => v.pump(value, done, response))
|
||||||
|
);
|
||||||
if (done) break;
|
if (done) break;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
targets.forEach(v => v.end(true));
|
targets.forEach(v => v.end(true));
|
||||||
// } catch (e) {
|
} catch (e) {
|
||||||
// logger.error(26, this.url, String(e));
|
logger.error(26, this.url, String(e));
|
||||||
// }
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cancel(reason?: string) {
|
cancel(reason?: string) {
|
||||||
|
5
src/source/data.d.ts
vendored
5
src/source/data.d.ts
vendored
@ -210,14 +210,13 @@ type SoundIds =
|
|||||||
| 'zone.mp3'
|
| 'zone.mp3'
|
||||||
|
|
||||||
type BgmIds =
|
type BgmIds =
|
||||||
| 'beforeBoss.opus'
|
| 'beforeBoss.mp3'
|
||||||
| 'cave.mp3'
|
| 'cave.mp3'
|
||||||
| 'escape.mp3'
|
| 'escape.mp3'
|
||||||
| 'escape2.mp3'
|
| 'escape2.mp3'
|
||||||
| 'grass.mp3'
|
| 'grass.mp3'
|
||||||
| 'mount.opus'
|
| 'mount.mp3'
|
||||||
| 'night.mp3'
|
| 'night.mp3'
|
||||||
| 'output6.ogg'
|
|
||||||
| 'palaceCenter.mp3'
|
| 'palaceCenter.mp3'
|
||||||
| 'palaceNorth.mp3'
|
| 'palaceNorth.mp3'
|
||||||
| 'palaceSouth.mp3'
|
| 'palaceSouth.mp3'
|
||||||
|
Loading…
Reference in New Issue
Block a user