Compare commits

...

3 Commits

11 changed files with 346 additions and 118 deletions

View File

@ -38,7 +38,7 @@ var data_comment_c456ea59_6018_45ef_8bcc_211a24c627dc = {
"_range": "editor.mode.checkImages(thiseval, './project/images/')",
"_directory": "./project/images/",
"_transform": (function (one) {
if (one.endsWith('.png') || one.endsWith('.jpg') || one.endsWith('.jpeg') || one.endsWith('.gif'))
if (one.endsWith('.png') || one.endsWith('.jpg') || one.endsWith('.jpeg') || one.endsWith('.gif') || one.endsWith('.webp'))
return one;
return null;
}).toString(),
@ -96,7 +96,7 @@ var data_comment_c456ea59_6018_45ef_8bcc_211a24c627dc = {
"_range": "editor.mode.checkUnique(thiseval)",
"_directory": "./project/bgms/",
"_transform": (function (one) {
if (one.endsWith('.mp3') || one.endsWith('.ogg') || one.endsWith('.wav') || one.endsWith('.m4a') || one.endsWith('.flac'))
if (one.endsWith('.mp3') || one.endsWith('.ogg') || one.endsWith('.wav') || one.endsWith('.m4a') || one.endsWith('.flac') || one.endsWith('.opus'))
return one;
return null;
}).toString(),

View File

@ -193,13 +193,14 @@ var data_a1e2fb4a_e986_4524_b0da_9b7ba7c0874d =
"zone"
],
"bgms": [
"beforeBoss.mp3",
"beforeBoss.opus",
"cave.mp3",
"escape.mp3",
"escape2.mp3",
"grass.mp3",
"mount.mp3",
"mount.opus",
"night.mp3",
"output6.ogg",
"palaceCenter.mp3",
"palaceNorth.mp3",
"palaceSouth.mp3",

View File

@ -0,0 +1,28 @@
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);
}
}
});
}

View File

@ -0,0 +1,45 @@
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();
}
}

View File

@ -83,7 +83,11 @@ export class StereoEffect extends AudioEffect {
* @param y y坐标
* @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正方向垂直屏幕远离用户
@ -91,7 +95,11 @@ export class StereoEffect extends AudioEffect {
* @param y y坐标
* @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 {}
@ -113,12 +121,16 @@ export class VolumeEffect extends AudioEffect {
*
* @param volume
*/
setVolume(volume: number) {}
setVolume(volume: number) {
this.output.gain.value = volume;
}
/**
*
*/
getVolume(): number {}
getVolume(): number {
return this.output.gain.value;
}
end(): void {}
@ -148,16 +160,22 @@ export class ChannelVolumeEffect extends AudioEffect {
/**
*
* @param channel
* @param channel 0-5
* @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
*/
getVolume(channel: number): number {}
getVolume(channel: number): number {
if (!this.gain[channel]) return 0;
return this.gain[channel].gain.value;
}
end(): void {}
@ -179,12 +197,16 @@ export class DelayEffect extends AudioEffect {
*
* @param delay
*/
setDelay(delay: number) {}
setDelay(delay: number) {
this.output.delayTime.value = delay;
}
/**
*
*/
getDelay() {}
getDelay() {
return this.output.delayTime.value;
}
end(): void {}

View File

@ -1,4 +1,14 @@
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 './effect';
export * from './player';
export * from './source';
export * from './bgmLoader';

View File

@ -8,6 +8,7 @@ import {
import {
AudioEffect,
ChannelVolumeEffect,
DelayEffect,
EchoEffect,
IAudioOutput,
StereoEffect,
@ -138,6 +139,16 @@ export class AudioPlayer extends EventEmitter<AudioPlayerEvent> {
return new ChannelVolumeEffect(this.ac);
}
/**
*
* |-----------|
* Input ----> | DelayNode | ----> Output
* |-----------|
*/
createDelay() {
return new DelayEffect(this.ac);
}
/**
*
* ```txt
@ -187,10 +198,40 @@ export class AudioPlayer extends EventEmitter<AudioPlayerEvent> {
* @param id
* @param when
*/
play(id: string, when?: number) {
play(id: string, when: number = 0) {
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正方向垂直屏幕远离用户
* @param x x坐标
@ -299,7 +340,7 @@ export class AudioRoute
*
* @param when
*/
play(when?: number) {
play(when: number = 0) {
if (this.source.playing) return;
this.link();
if (this.effectRoute.length > 0) {
@ -430,3 +471,5 @@ export class AudioRoute
this.effectRoute.forEach(v => v.end());
}
}
export const audioPlayer = new AudioPlayer();

View File

@ -80,23 +80,25 @@ export interface IAudioDecoder {
*
* @param data
*/
decode(data: Uint8Array): Promise<IAudioDecodeData>;
decode(data: Uint8Array): Promise<IAudioDecodeData | undefined>;
/**
* 使
*/
flush(): Promise<IAudioDecodeData>;
flush(): Promise<IAudioDecodeData | undefined>;
}
const fileSignatures: Map<string, AudioType> = new Map([
['49 44 33', AudioType.Mp3],
['4F 67 67 53', AudioType.Ogg],
['52 49 46 46', AudioType.Wav],
['66 4C 61 43', AudioType.Flac],
['4F 70 75 73', AudioType.Opus],
['FF F1', AudioType.Aac],
['FF F9', AudioType.Aac]
]);
const fileSignatures: [AudioType, number[]][] = [
[AudioType.Mp3, [0x49, 0x44, 0x33]],
[AudioType.Ogg, [0x4f, 0x67, 0x67, 0x53]],
[AudioType.Wav, [52, 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]]
];
const mimeTypeMap: Record<AudioType, MimeType> = {
[AudioType.Aac]: 'audio/aac',
@ -112,7 +114,8 @@ function isOggPage(data: any): data is OggPage {
}
export class AudioStreamSource extends AudioSource implements IStreamReader {
static readonly decoderMap: Map<AudioType, IAudioDecoder> = new Map();
static readonly decoderMap: Map<AudioType, new () => IAudioDecoder> =
new Map();
output: AudioBufferSourceNode;
/** 音频数据 */
@ -138,6 +141,8 @@ export class AudioStreamSource extends AudioSource implements IStreamReader {
/** 开始播放时刻 */
private lastStartTime: number = 0;
/** 上一次播放的缓存长度 */
private lastBufferSamples: number = 0;
/** 是否已经获取到头文件 */
private headerRecieved: boolean = false;
@ -152,12 +157,14 @@ export class AudioStreamSource extends AudioSource implements IStreamReader {
/** 缓存音频数据,每 bufferChunkSize 秒钟组成一个 Float32Array用于流式解码 */
private audioData: Float32Array[][] = [];
private errored: boolean = false;
/**
*
* @param type
* @param decoder
*/
static registerDecoder(type: AudioType, decoder: IAudioDecoder) {
static registerDecoder(type: AudioType, decoder: new () => IAudioDecoder) {
if (this.decoderMap.has(type)) {
logger.warn(47, type);
return;
@ -184,42 +191,60 @@ export class AudioStreamSource extends AudioSource implements IStreamReader {
}
async pump(data: Uint8Array | undefined, done: boolean): Promise<void> {
if (!data) return;
if (!data || this.errored) return;
if (!this.headerRecieved) {
// 检查头文件获取音频类型
const toCheck = [...data.slice(0, 16)];
const hexArray = toCheck.map(v => v.toString(16).padStart(2, '0'));
const hex = hexArray.join(' ');
for (const [key, value] of fileSignatures) {
if (hex.startsWith(key)) {
this.audioType = value;
// 检查头文件获取音频类型仅检查前256个字节
const toCheck = data.slice(0, 256);
for (const [type, value] of fileSignatures) {
if (value.every((v, i) => toCheck[i] === v)) {
this.audioType = type;
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) {
logger.error(25, hex);
logger.error(
25,
[...toCheck]
.map(v => v.toString().padStart(2, '0'))
.join(' ')
.toUpperCase()
);
return;
}
// 创建解码器
const decoder = AudioStreamSource.decoderMap.get(this.audioType);
this.decoder = decoder;
if (!decoder) {
const Decoder = AudioStreamSource.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 decoder.create();
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'
);
@ -234,13 +259,13 @@ export class AudioStreamSource extends AudioSource implements IStreamReader {
*
*/
private checkSampleRate(info: (OggPage | CodecFrame)[]) {
const first = info[0];
if (first) {
const frame = isOggPage(first) ? first.codecFrames[0] : first;
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);
@ -260,6 +285,7 @@ export class AudioStreamSource extends AudioSource implements IStreamReader {
) {
// 解析音频数据
const audioData = await decoder.decode(data);
if (!audioData) return;
// @ts-expect-error 库类型声明错误
const audioInfo = [...parser.parseChunk(data)] as (
| OggPage
@ -277,6 +303,7 @@ export class AudioStreamSource extends AudioSource implements IStreamReader {
*/
private async decodeFlushData(decoder: IAudioDecoder, parser: CodecParser) {
const audioData = await decoder.flush();
if (!audioData) return;
// @ts-expect-error 库类型声明错误
const audioInfo = [...parser.flush()] as (OggPage | CodecFrame)[];
@ -303,23 +330,33 @@ export class AudioStreamSource extends AudioSource implements IStreamReader {
const chunk = this.sampleRate * this.bufferChunkSize;
const sampled = this.bufferedSamples;
const pushIndex = Math.floor(sampled / chunk);
const bufferIndex = sampled % (this.sampleRate * chunk);
const bufferIndex = sampled % chunk;
const dataLength = data.channelData[0].length;
const restLength = chunk - bufferIndex;
// 把数据放入缓存
for (let i = 0; i < channels; i++) {
const audioData = this.audioData[i];
if (!audioData[pushIndex]) {
audioData.push(new Float32Array(chunk * this.sampleRate));
}
audioData[pushIndex].set(data.channelData[i], bufferIndex);
if (restLength < dataLength) {
const nextData = new Float32Array(chunk * this.sampleRate);
nextData.set(data.channelData[i].slice(restLength), 0);
audioData.push(nextData);
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);
this.buffered +=
info.reduce((prev, curr) => prev + curr.duration, 0) / 1000;
this.bufferedSamples += info.reduce(
(prev, curr) => prev + curr.samples,
0
@ -330,71 +367,112 @@ export class AudioStreamSource extends AudioSource implements IStreamReader {
*
*/
private checkBufferedPlay() {
if (this.playing || this.loaded) return;
const played = this.ac.currentTime - this.lastStartTime;
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;
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(
this.audioData.length,
this.bufferedSamples,
this.sampleRate
);
this.buffer = buffer;
const chunk = this.sampleRate * this.bufferChunkSize;
const bufferedChunks = Math.floor(this.buffered / chunk);
const restLength = this.buffered % chunk;
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], 0);
if (restLength !== 0) {
data.set(
audio[bufferedChunks].slice(0, restLength),
chunk * bufferedChunks
);
}
buffer.copyToChannel(data, i, 0);
}
this.createSourceNode(buffer);
this.output.start(played);
this.lastStartTime = this.ac.currentTime;
this.output.addEventListener('ended', () => {
this.checkBufferedPlay();
});
this.buffer = buffer;
}
private mergeBuffers() {}
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) {
if (done && this.buffer) {
this.loaded = true;
delete this.controller;
this.mergeBuffers();
const played = this.ac.currentTime - this.lastStartTime;
this.output.stop();
this.play(played);
// const played = this.lastBufferSamples / this.sampleRate;
// this.playAudio(played);
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;
console.log(when);
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) return;
if (this.playing || this.errored) return;
if (this.loaded && this.buffer) {
this.playing = true;
this.lastStartTime = this.ac.currentTime;
this.emit('play');
this.createSourceNode(this.buffer);
this.output.start(when);
this.output.addEventListener('ended', () => {
this.playing = false;
this.emit('end');
if (this.loop && !this.output.loop) this.play(0);
});
this.playAudio(when);
} else {
this.controller?.start();
}
@ -404,13 +482,16 @@ export class AudioStreamSource extends AudioSource implements IStreamReader {
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 {
this.output.stop();
if (this.playing) this.output.stop();
this.playing = false;
return this.ac.currentTime - this.lastStartTime;
}
@ -453,7 +534,7 @@ export class AudioElementSource extends AudioSource {
this.audio.src = url;
}
play(when: number): void {
play(when: number = 0): void {
if (this.playing) return;
this.audio.currentTime = when;
this.audio.play();
@ -510,7 +591,7 @@ export class AudioBufferSource extends AudioSource {
this.lastStartTime = this.ac.currentTime;
this.emit('play');
this.createSourceNode(this.buffer);
this.output.start(when);
this.output.start(0, when);
this.output.addEventListener('ended', () => {
this.playing = false;
this.emit('end');

View File

@ -25,21 +25,21 @@ export function isAudioSupport(type: AudioType): boolean {
}
}
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"'],
['aac', 'audio/aac']
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) {
const ext = /\.[a-zA-Z]$/.exec(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()) ?? '';

View File

@ -82,6 +82,7 @@ export class StreamLoader
return;
}
this.target.add(reader);
reader.piped(this);
return this;
}
@ -98,25 +99,21 @@ export class StreamLoader
this.stream = stream;
const reader = response.body?.getReader();
const targets = [...this.target];
try {
await Promise.all(
targets.map(v => v.start(stream, this, response))
);
// try {
await Promise.all(targets.map(v => v.start(stream, this, response)));
// 开始流传输
while (true) {
const { value, done } = await reader.read();
await Promise.all(
targets.map(v => v.pump(value, done, response))
);
if (done) break;
}
this.loading = false;
targets.forEach(v => v.end(true));
} catch (e) {
logger.error(26, this.url, String(e));
// 开始流传输
while (true) {
const { value, done } = await reader.read();
await Promise.all(targets.map(v => v.pump(value, done, response)));
if (done) break;
}
this.loading = false;
targets.forEach(v => v.end(true));
// } catch (e) {
// logger.error(26, this.url, String(e));
// }
}
cancel(reason?: string) {

View File

@ -210,13 +210,14 @@ type SoundIds =
| 'zone.mp3'
type BgmIds =
| 'beforeBoss.mp3'
| 'beforeBoss.opus'
| 'cave.mp3'
| 'escape.mp3'
| 'escape2.mp3'
| 'grass.mp3'
| 'mount.mp3'
| 'mount.opus'
| 'night.mp3'
| 'output6.ogg'
| 'palaceCenter.mp3'
| 'palaceNorth.mp3'
| 'palaceSouth.mp3'