| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433 |
- // 定义PCMAudioPlayer配置类型
- type PCMAudioPlayerConfig = {
- sampleRate?: number;
- channels?: number;
- bitDepth?: number;
- littleEndian?: boolean;
- loop?: boolean;
- bufferThreshold?: number; // 新增:缓冲阈值(秒)
- };
- // 定义播放完成回调类型
- type PlaybackCompleteCallback = () => void;
- // PCMAudioPlayer类用于播放PCM格式的音频数据(微信小程序版)
- export default class PCMAudioPlayer {
- // WebAudio上下文(优先使用)
- private audioContext: any = null;
- // 内部音频上下文(兼容低版本)
- private innerAudioContext: UniApp.InnerAudioContext | null = null;
- // 音频队列,存储待播放的音频数据
- private audioQueue: ArrayBuffer[] = [];
- // 原始音频队列,用于重新播放
- private originalQueue: ArrayBuffer[] = [];
- // 标记当前是否正在播放音频
- private isPlaying = false;
- // 配置信息
- private config: Required<PCMAudioPlayerConfig>;
- // 表示音频是否处于暂停状态
- private isPaused: boolean = false;
- // 播放完成回调函数
- private onPlaybackComplete: PlaybackCompleteCallback | null = null;
- // WebAudio节点队列
- private sourceNodes: any[] = [];
- // 下一个播放时间点
- private nextPlayTime: number = 0;
- // 当前播放位置
- private currentPosition: number = 0;
- // 总播放时长
- private totalDuration: number = 0;
- // 是否使用WebAudio API
- private useWebAudio: boolean = false;
- // 缓冲阈值
- private bufferThreshold: number = 0.3; // 300ms
- constructor(options: PCMAudioPlayerConfig = {}) {
- this.config = {
- sampleRate: options.sampleRate || 16000,
- channels: options.channels || 1,
- bitDepth: options.bitDepth || 16,
- littleEndian: options.littleEndian !== false,
- loop: options.loop || false,
- bufferThreshold: options.bufferThreshold || 0.3
- };
- // 初始化音频上下文
- this.initAudioContext();
- }
- /**
- * 初始化音频上下文
- */
- private initAudioContext(): void {
- try {
- // 尝试使用WebAudio API(基础库>=2.14.0)
- if (typeof wx.createWebAudioContext === 'function') {
- this.audioContext = wx.createWebAudioContext();
- this.useWebAudio = true;
- console.log('使用WebAudio API');
- } else {
- // 回退到InnerAudioContext
- this.innerAudioContext = uni.createInnerAudioContext();
- this.innerAudioContext.onEnded(() => this.playNextChunk());
- this.innerAudioContext.onError((res) => {
- console.error('音频播放错误:', res.errMsg);
- this.playNextChunk();
- });
- this.useWebAudio = false;
- console.log('使用InnerAudioContext');
- }
- } catch (error) {
- console.error('初始化音频上下文失败:', error);
- throw error;
- }
- }
- /**
- * 设置播放完成回调
- */
- public setOnPlaybackComplete(callback: PlaybackCompleteCallback): void {
- this.onPlaybackComplete = callback;
- }
- /**
- * 输入Base64编码的音频数据
- */
- public async feedBase64(base64Data: string, autoPlay: boolean = false): Promise<void> {
- try {
- const arrayBuffer = uni.base64ToArrayBuffer(base64Data);
- await this.feed(arrayBuffer, autoPlay);
- } catch (error) {
- console.error('处理Base64数据失败:', error);
- throw error;
- }
- }
- /**
- * 将PCM数据输入到音频队列中
- */
- public async feed(pcmData: ArrayBuffer, autoPlay: boolean = false): Promise<void> {
- // 计算音频时长并更新总时长
- const duration = pcmData.byteLength / (this.config.sampleRate * this.config.channels * (this.config.bitDepth / 8));
- this.totalDuration += duration;
- this.audioQueue.push(pcmData);
- this.originalQueue.push(pcmData);
- if (autoPlay && !this.isPlaying && !this.isPaused) {
- await this.playNextChunk();
- }
- }
- /**
- * 播放下一个音频数据块(WebAudio实现)
- */
- private async playWebAudioChunk(): Promise<void> {
- if (this.isPaused || this.audioQueue.length === 0) return;
- this.isPlaying = true;
- this.isPaused = false;
- const pcmData = this.audioQueue.shift()!;
- const audioBuffer = await this.decodePCMToAudioBuffer(pcmData);
- // 创建音频源节点
- const source = this.audioContext.createBufferSource();
- source.buffer = audioBuffer;
- source.connect(this.audioContext.destination);
- // 设置播放时间
- if (this.nextPlayTime < this.audioContext.currentTime) {
- this.nextPlayTime = this.audioContext.currentTime;
- }
- source.start(this.nextPlayTime);
- this.nextPlayTime += audioBuffer.duration;
- // 更新播放位置
- this.currentPosition += audioBuffer.duration;
- // 播放下一个片段
- source.onended = () => {
- if (this.audioQueue.length > 0) {
- this.playWebAudioChunk();
- } else {
- this.handlePlaybackComplete();
- }
- };
- this.sourceNodes.push(source);
- }
- /**
- * 解码PCM数据为AudioBuffer
- */
- private async decodePCMToAudioBuffer(pcmData: ArrayBuffer): Promise<any> {
- return new Promise((resolve, reject) => {
- // 将PCM转换为WAV格式
- const wavData = this.pcmToWav(pcmData);
- this.audioContext.decodeAudioData(
- wavData,
- (buffer) => resolve(buffer),
- (err) => {
- console.error('解码音频失败:', err);
- reject(err);
- }
- );
- });
- }
- /**
- * 播放下一个音频数据块(InnerAudioContext实现)
- */
- private async playInnerAudioChunk(): Promise<void> {
- if (this.isPaused || this.audioQueue.length === 0) return;
- try {
- this.isPlaying = true;
- this.isPaused = false;
- const pcmData = this.audioQueue[0];
- const wavData = this.pcmToWav(pcmData);
- // 创建临时文件播放
- const tempFilePath = await this.arrayBufferToTempFilePath(wavData);
- if (!this.innerAudioContext) {
- throw new Error('音频上下文未初始化');
- }
- this.innerAudioContext.src = tempFilePath;
- this.innerAudioContext.play();
- } catch (error) {
- console.error('播放失败:', error);
- this.audioQueue.shift();
- this.playNextChunk();
- }
- }
- /**
- * 通用播放下一个音频块
- */
- private async playNextChunk(): Promise<void> {
- if (this.useWebAudio) {
- return this.playWebAudioChunk();
- } else {
- return this.playInnerAudioChunk();
- }
- }
- /**
- * 将PCM数据转换为WAV格式
- */
- private pcmToWav(pcmData: ArrayBuffer): ArrayBuffer {
- const bytesPerSample = this.config.bitDepth / 8;
- const byteRate = this.config.sampleRate * this.config.channels * bytesPerSample;
- const blockAlign = this.config.channels * bytesPerSample;
- const dataSize = pcmData.byteLength;
- const buffer = new ArrayBuffer(44 + dataSize);
- const view = new DataView(buffer);
- // 写入WAV头
- this.writeString(view, 0, 'RIFF');
- view.setUint32(4, 36 + dataSize, true);
- this.writeString(view, 8, 'WAVE');
- this.writeString(view, 12, 'fmt ');
- view.setUint32(16, 16, true); // 子块大小
- view.setUint16(20, 1, true); // PCM格式
- view.setUint16(22, this.config.channels, true);
- view.setUint32(24, this.config.sampleRate, true);
- view.setUint32(28, byteRate, true);
- view.setUint16(32, blockAlign, true);
- view.setUint16(34, this.config.bitDepth, true);
- this.writeString(view, 36, 'data');
- view.setUint32(40, dataSize, true);
- // 写入PCM数据
- const pcmView = new Uint8Array(pcmData);
- const wavView = new Uint8Array(buffer, 44);
- wavView.set(pcmView);
- return buffer;
- }
- /**
- * 将ArrayBuffer转换为临时文件路径
- */
- private arrayBufferToTempFilePath(arrayBuffer: ArrayBuffer): Promise<string> {
- return new Promise((resolve, reject) => {
- const fileManager = uni.getFileSystemManager();
- const tempFilePath = `${wx.env.USER_DATA_PATH}/tts_${Date.now()}.wav`;
- fileManager.writeFile({
- filePath: tempFilePath,
- data: arrayBuffer,
- encoding: 'binary',
- success: () => resolve(tempFilePath),
- fail: (err) => reject(err)
- });
- });
- }
- /**
- * 暂停当前播放
- */
- public pause(): void {
- if (!this.isPlaying || this.isPaused) return;
- if (this.useWebAudio) {
- // WebAudio暂停需要特殊处理
- this.audioContext.suspend().then(() => {
- this.isPaused = true;
- this.isPlaying = false;
- });
- } else if (this.innerAudioContext) {
- this.innerAudioContext.pause();
- this.isPaused = true;
- this.isPlaying = false;
- }
- }
- /**
- * 从暂停处继续播放
- */
- public async resume(): Promise<void> {
- if (!this.isPaused) return;
- if (this.useWebAudio) {
- await this.audioContext.resume();
- this.isPaused = false;
- this.isPlaying = true;
- // WebAudio需要重新调度
- if (this.audioQueue.length > 0) {
- this.nextPlayTime = this.audioContext.currentTime;
- this.playWebAudioChunk();
- }
- } else if (this.innerAudioContext) {
- this.innerAudioContext.play();
- this.isPaused = false;
- this.isPlaying = true;
- }
- }
- /**
- * 停止播放并清空队列
- */
- public stop(): void {
- if (this.useWebAudio) {
- // 停止所有源节点
- this.sourceNodes.forEach(source => {
- try {
- source.stop();
- source.disconnect();
- } catch (e) {
- console.warn('停止WebAudio节点失败', e);
- }
- });
- this.sourceNodes = [];
- this.audioContext.close().catch(console.error);
- this.initAudioContext(); // 重新初始化
- } else if (this.innerAudioContext) {
- this.innerAudioContext.stop();
- }
- this.audioQueue = [];
- this.isPlaying = false;
- this.isPaused = false;
- this.nextPlayTime = 0;
- this.currentPosition = 0;
- }
- /**
- * 重新播放
- */
- public async replay(): Promise<void> {
- this.stop();
- this.audioQueue = [...this.originalQueue];
- if (this.audioQueue.length > 0) {
- await this.playNextChunk();
- }
- }
- /**
- * 设置循环播放
- */
- public setLoop(loop: boolean): void {
- this.config.loop = loop;
- if (this.innerAudioContext) {
- this.innerAudioContext.loop = loop;
- }
- // WebAudio在播放完成回调中处理循环
- }
- /**
- * 处理播放完成
- */
- private handlePlaybackComplete(): void {
- this.isPlaying = false;
- this.isPaused = false;
- // 处理循环播放
- if (this.config.loop && this.originalQueue.length > 0) {
- this.audioQueue = [...this.originalQueue];
- this.playNextChunk();
- return;
- }
- if (this.onPlaybackComplete) {
- try {
- this.onPlaybackComplete();
- } catch (error) {
- console.error('播放完成回调执行失败:', error);
- }
- }
- }
- /**
- * 写入字符串到DataView
- */
- private writeString(view: DataView, offset: number, str: string): void {
- for (let i = 0; i < str.length; i++) {
- view.setUint8(offset + i, str.charCodeAt(i));
- }
- }
- /**
- * 获取当前状态
- */
- public getStatus() {
- return {
- isPlaying: this.isPlaying,
- isPaused: this.isPaused,
- queueLength: this.audioQueue.length,
- isLooping: this.config.loop,
- currentPosition: this.currentPosition,
- totalDuration: this.totalDuration,
- useWebAudio: this.useWebAudio
- };
- }
- /**
- * 清空音频队列
- */
- public clearQueue(): void {
- this.audioQueue = [];
- }
- /**
- * 销毁播放器
- */
- public destroy(): void {
- this.stop();
- if (this.audioContext) {
- this.audioContext.close().catch(console.error);
- }
- if (this.innerAudioContext) {
- this.innerAudioContext.destroy();
- }
- }
- }
|