import { IStreamController, IStreamReader } from '@motajs/loader'; import { logger } from '@motajs/common'; import CodecParser, { CodecFrame, MimeType, OggPage } from 'codec-parser'; import { isNil } from 'lodash-es'; import { AudioType, EAudioSourceEvent, IAudioBufferSource, IAudioDecodeData, IAudioDecoder, IAudioElementSource, IAudioInput, IAudioStreamSource, IMotaAudioContext } from './types'; import EventEmitter from 'eventemitter3'; const mimeTypeMap: Record = { [AudioType.Unknown]: 'unknown', [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 EventEmitter implements IAudioStreamSource, IStreamReader { readonly ac: AudioContext; /** 音频源节点 */ output: AudioBufferSourceNode; /** 音频数据 */ buffer: AudioBuffer | null = null; /** 是否已经完全加载完毕 */ loaded: boolean = false; /** 已经缓冲了多长时间,如果缓冲完那么跟歌曲时长一致 */ buffered: number = 0; /** 已经缓冲的采样点数量 */ bufferedSamples: number = 0; /** 当前是否正在播放 */ playing: boolean = false; /** 歌曲时长,加载完毕之前保持为 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 | null = null; 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: IAudioDecoder | null = null; /** 音频解析器 */ private parser: CodecParser | null = null; /** 每多长时间组成一个缓存 Float32Array */ private bufferChunkSize: number = 10; /** 缓存音频数据,每 bufferChunkSize 秒钟组成一个 Float32Array,用于流式解码 */ private audioData: Float32Array[][] = []; private errored: boolean = false; constructor(readonly motaAC: IMotaAudioContext) { super(); this.ac = motaAC.ac; this.output = motaAC.ac.createBufferSource(); } /** * 设置每个缓存数据的大小,默认为10秒钟一个缓存数据 * @param size 每个缓存数据的时长,单位秒 */ setChunkSize(size: number) { if (this.controller?.loading || this.loaded) return; this.bufferChunkSize = size; } free(): void { this.stop(); this.audioData = []; this.decoder?.destroy(); this.decoder = null; this.parser = null; this.audioType = ''; this.headerRecieved = false; this.errored = false; this.duration = 0; this.buffered = 0; this.bufferedSamples = 0; this.loaded = false; this.sampleRate = 0; this.buffer = null; this.output.buffer = null; } piped(controller: IStreamController): void { this.controller = controller; } unpiped(controller: IStreamController): void { if (this.controller === controller) { this.controller = null; } } async pump(data: Uint8Array | undefined, done: boolean): Promise { if (!data || this.errored) return; if (!this.headerRecieved) { // 检查头文件获取音频类型,仅检查前256个字节 const toCheck = data.slice(0, 256); const type = this.motaAC.getAudioTypeFromData(data); this.audioType = type; if (type === AudioType.Unknown) { logger.error( 25, [...toCheck] .map(v => v.toString(16).padStart(2, '0')) .join(' ') .toUpperCase() ); return; } const decoder = this.motaAC.createDecoder(type); 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 = decoder; // 创建数据解析器 const mime = mimeTypeMap[this.audioType]; if (mime !== 'unknown') { const parser = new CodecParser(mime); this.parser = parser; await 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: IAudioDecoder, 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: IAudioDecoder, 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() { this.buffer = null; 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; this.controller = null; this.mergeBuffers(); this.duration = this.buffered; this.audioData = []; this.decoder?.destroy(); this.decoder = null; this.parser = null; this.emit('load'); } 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.createSourceNode(this.buffer); this.output.start(0, when); this.playing = true; this.emit('play'); this.output.addEventListener('ended', () => { this.playing = false; if (this.loop && !this.output.loop) this.play(0); this.emit('end'); }); } 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 EventEmitter implements IAudioElementSource { readonly ac: AudioContext; output: MediaElementAudioSourceNode; /** 当前是否正在播放 */ playing: boolean = false; /** audio 元素 */ readonly audio: HTMLAudioElement; get duration(): number { return this.audio.duration; } get currentTime(): number { return this.audio.currentTime; } constructor(readonly motaAC: IMotaAudioContext) { super(); this.ac = motaAC.ac; const audio = new Audio(); audio.preload = 'none'; this.output = motaAC.ac.createMediaElementSource(audio); this.audio = audio; audio.addEventListener('play', () => { this.playing = true; this.emit('play'); }); audio.addEventListener('ended', () => { this.playing = false; this.emit('end'); }); audio.addEventListener('load', () => { if (audio.src.length > 0) { this.emit('load'); } }); } /** * 设置音频源的路径 * @param url 音频路径 */ setSource(url: string) { this.audio.src = url; } free(): void { this.stop(); this.audio.src = ''; this.audio.load(); } play(when: number = 0): void { if (this.playing) return; this.audio.currentTime = when; this.audio.play(); } stop(): number { this.audio.pause(); this.playing = false; 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 EventEmitter implements IAudioBufferSource { readonly ac: AudioContext; output: AudioBufferSourceNode; /** 音频数据 */ buffer: AudioBuffer | null = null; /** 是否循环 */ private loop: boolean = false; /** 当前是否正在播放 */ playing: 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(readonly motaAC: IMotaAudioContext) { super(); this.ac = motaAC.ac; this.output = motaAC.ac.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; this.emit('load'); } free(): void { this.stop(); this.output.buffer = null; this.buffer = null; } play(when?: number): void { if (this.playing || !this.buffer) return; this.playing = true; this.lastStartTime = this.ac.currentTime; this.createSourceNode(this.buffer); this.output.start(0, when); this.emit('play'); this.output.addEventListener('ended', () => { this.playing = false; if (this.loop && !this.output.loop) this.play(0); this.emit('end'); }); } 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; } }