PCMAudioPlayer.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433
  1. // 定义PCMAudioPlayer配置类型
  2. type PCMAudioPlayerConfig = {
  3. sampleRate?: number;
  4. channels?: number;
  5. bitDepth?: number;
  6. littleEndian?: boolean;
  7. loop?: boolean;
  8. bufferThreshold?: number; // 新增:缓冲阈值(秒)
  9. };
  10. // 定义播放完成回调类型
  11. type PlaybackCompleteCallback = () => void;
  12. // PCMAudioPlayer类用于播放PCM格式的音频数据(微信小程序版)
  13. export default class PCMAudioPlayer {
  14. // WebAudio上下文(优先使用)
  15. private audioContext: any = null;
  16. // 内部音频上下文(兼容低版本)
  17. private innerAudioContext: UniApp.InnerAudioContext | null = null;
  18. // 音频队列,存储待播放的音频数据
  19. private audioQueue: ArrayBuffer[] = [];
  20. // 原始音频队列,用于重新播放
  21. private originalQueue: ArrayBuffer[] = [];
  22. // 标记当前是否正在播放音频
  23. private isPlaying = false;
  24. // 配置信息
  25. private config: Required<PCMAudioPlayerConfig>;
  26. // 表示音频是否处于暂停状态
  27. private isPaused: boolean = false;
  28. // 播放完成回调函数
  29. private onPlaybackComplete: PlaybackCompleteCallback | null = null;
  30. // WebAudio节点队列
  31. private sourceNodes: any[] = [];
  32. // 下一个播放时间点
  33. private nextPlayTime: number = 0;
  34. // 当前播放位置
  35. private currentPosition: number = 0;
  36. // 总播放时长
  37. private totalDuration: number = 0;
  38. // 是否使用WebAudio API
  39. private useWebAudio: boolean = false;
  40. // 缓冲阈值
  41. private bufferThreshold: number = 0.3; // 300ms
  42. constructor(options: PCMAudioPlayerConfig = {}) {
  43. this.config = {
  44. sampleRate: options.sampleRate || 16000,
  45. channels: options.channels || 1,
  46. bitDepth: options.bitDepth || 16,
  47. littleEndian: options.littleEndian !== false,
  48. loop: options.loop || false,
  49. bufferThreshold: options.bufferThreshold || 0.3
  50. };
  51. // 初始化音频上下文
  52. this.initAudioContext();
  53. }
  54. /**
  55. * 初始化音频上下文
  56. */
  57. private initAudioContext(): void {
  58. try {
  59. // 尝试使用WebAudio API(基础库>=2.14.0)
  60. if (typeof wx.createWebAudioContext === 'function') {
  61. this.audioContext = wx.createWebAudioContext();
  62. this.useWebAudio = true;
  63. console.log('使用WebAudio API');
  64. } else {
  65. // 回退到InnerAudioContext
  66. this.innerAudioContext = uni.createInnerAudioContext();
  67. this.innerAudioContext.onEnded(() => this.playNextChunk());
  68. this.innerAudioContext.onError((res) => {
  69. console.error('音频播放错误:', res.errMsg);
  70. this.playNextChunk();
  71. });
  72. this.useWebAudio = false;
  73. console.log('使用InnerAudioContext');
  74. }
  75. } catch (error) {
  76. console.error('初始化音频上下文失败:', error);
  77. throw error;
  78. }
  79. }
  80. /**
  81. * 设置播放完成回调
  82. */
  83. public setOnPlaybackComplete(callback: PlaybackCompleteCallback): void {
  84. this.onPlaybackComplete = callback;
  85. }
  86. /**
  87. * 输入Base64编码的音频数据
  88. */
  89. public async feedBase64(base64Data: string, autoPlay: boolean = false): Promise<void> {
  90. try {
  91. const arrayBuffer = uni.base64ToArrayBuffer(base64Data);
  92. await this.feed(arrayBuffer, autoPlay);
  93. } catch (error) {
  94. console.error('处理Base64数据失败:', error);
  95. throw error;
  96. }
  97. }
  98. /**
  99. * 将PCM数据输入到音频队列中
  100. */
  101. public async feed(pcmData: ArrayBuffer, autoPlay: boolean = false): Promise<void> {
  102. // 计算音频时长并更新总时长
  103. const duration = pcmData.byteLength / (this.config.sampleRate * this.config.channels * (this.config.bitDepth / 8));
  104. this.totalDuration += duration;
  105. this.audioQueue.push(pcmData);
  106. this.originalQueue.push(pcmData);
  107. if (autoPlay && !this.isPlaying && !this.isPaused) {
  108. await this.playNextChunk();
  109. }
  110. }
  111. /**
  112. * 播放下一个音频数据块(WebAudio实现)
  113. */
  114. private async playWebAudioChunk(): Promise<void> {
  115. if (this.isPaused || this.audioQueue.length === 0) return;
  116. this.isPlaying = true;
  117. this.isPaused = false;
  118. const pcmData = this.audioQueue.shift()!;
  119. const audioBuffer = await this.decodePCMToAudioBuffer(pcmData);
  120. // 创建音频源节点
  121. const source = this.audioContext.createBufferSource();
  122. source.buffer = audioBuffer;
  123. source.connect(this.audioContext.destination);
  124. // 设置播放时间
  125. if (this.nextPlayTime < this.audioContext.currentTime) {
  126. this.nextPlayTime = this.audioContext.currentTime;
  127. }
  128. source.start(this.nextPlayTime);
  129. this.nextPlayTime += audioBuffer.duration;
  130. // 更新播放位置
  131. this.currentPosition += audioBuffer.duration;
  132. // 播放下一个片段
  133. source.onended = () => {
  134. if (this.audioQueue.length > 0) {
  135. this.playWebAudioChunk();
  136. } else {
  137. this.handlePlaybackComplete();
  138. }
  139. };
  140. this.sourceNodes.push(source);
  141. }
  142. /**
  143. * 解码PCM数据为AudioBuffer
  144. */
  145. private async decodePCMToAudioBuffer(pcmData: ArrayBuffer): Promise<any> {
  146. return new Promise((resolve, reject) => {
  147. // 将PCM转换为WAV格式
  148. const wavData = this.pcmToWav(pcmData);
  149. this.audioContext.decodeAudioData(
  150. wavData,
  151. (buffer) => resolve(buffer),
  152. (err) => {
  153. console.error('解码音频失败:', err);
  154. reject(err);
  155. }
  156. );
  157. });
  158. }
  159. /**
  160. * 播放下一个音频数据块(InnerAudioContext实现)
  161. */
  162. private async playInnerAudioChunk(): Promise<void> {
  163. if (this.isPaused || this.audioQueue.length === 0) return;
  164. try {
  165. this.isPlaying = true;
  166. this.isPaused = false;
  167. const pcmData = this.audioQueue[0];
  168. const wavData = this.pcmToWav(pcmData);
  169. // 创建临时文件播放
  170. const tempFilePath = await this.arrayBufferToTempFilePath(wavData);
  171. if (!this.innerAudioContext) {
  172. throw new Error('音频上下文未初始化');
  173. }
  174. this.innerAudioContext.src = tempFilePath;
  175. this.innerAudioContext.play();
  176. } catch (error) {
  177. console.error('播放失败:', error);
  178. this.audioQueue.shift();
  179. this.playNextChunk();
  180. }
  181. }
  182. /**
  183. * 通用播放下一个音频块
  184. */
  185. private async playNextChunk(): Promise<void> {
  186. if (this.useWebAudio) {
  187. return this.playWebAudioChunk();
  188. } else {
  189. return this.playInnerAudioChunk();
  190. }
  191. }
  192. /**
  193. * 将PCM数据转换为WAV格式
  194. */
  195. private pcmToWav(pcmData: ArrayBuffer): ArrayBuffer {
  196. const bytesPerSample = this.config.bitDepth / 8;
  197. const byteRate = this.config.sampleRate * this.config.channels * bytesPerSample;
  198. const blockAlign = this.config.channels * bytesPerSample;
  199. const dataSize = pcmData.byteLength;
  200. const buffer = new ArrayBuffer(44 + dataSize);
  201. const view = new DataView(buffer);
  202. // 写入WAV头
  203. this.writeString(view, 0, 'RIFF');
  204. view.setUint32(4, 36 + dataSize, true);
  205. this.writeString(view, 8, 'WAVE');
  206. this.writeString(view, 12, 'fmt ');
  207. view.setUint32(16, 16, true); // 子块大小
  208. view.setUint16(20, 1, true); // PCM格式
  209. view.setUint16(22, this.config.channels, true);
  210. view.setUint32(24, this.config.sampleRate, true);
  211. view.setUint32(28, byteRate, true);
  212. view.setUint16(32, blockAlign, true);
  213. view.setUint16(34, this.config.bitDepth, true);
  214. this.writeString(view, 36, 'data');
  215. view.setUint32(40, dataSize, true);
  216. // 写入PCM数据
  217. const pcmView = new Uint8Array(pcmData);
  218. const wavView = new Uint8Array(buffer, 44);
  219. wavView.set(pcmView);
  220. return buffer;
  221. }
  222. /**
  223. * 将ArrayBuffer转换为临时文件路径
  224. */
  225. private arrayBufferToTempFilePath(arrayBuffer: ArrayBuffer): Promise<string> {
  226. return new Promise((resolve, reject) => {
  227. const fileManager = uni.getFileSystemManager();
  228. const tempFilePath = `${wx.env.USER_DATA_PATH}/tts_${Date.now()}.wav`;
  229. fileManager.writeFile({
  230. filePath: tempFilePath,
  231. data: arrayBuffer,
  232. encoding: 'binary',
  233. success: () => resolve(tempFilePath),
  234. fail: (err) => reject(err)
  235. });
  236. });
  237. }
  238. /**
  239. * 暂停当前播放
  240. */
  241. public pause(): void {
  242. if (!this.isPlaying || this.isPaused) return;
  243. if (this.useWebAudio) {
  244. // WebAudio暂停需要特殊处理
  245. this.audioContext.suspend().then(() => {
  246. this.isPaused = true;
  247. this.isPlaying = false;
  248. });
  249. } else if (this.innerAudioContext) {
  250. this.innerAudioContext.pause();
  251. this.isPaused = true;
  252. this.isPlaying = false;
  253. }
  254. }
  255. /**
  256. * 从暂停处继续播放
  257. */
  258. public async resume(): Promise<void> {
  259. if (!this.isPaused) return;
  260. if (this.useWebAudio) {
  261. await this.audioContext.resume();
  262. this.isPaused = false;
  263. this.isPlaying = true;
  264. // WebAudio需要重新调度
  265. if (this.audioQueue.length > 0) {
  266. this.nextPlayTime = this.audioContext.currentTime;
  267. this.playWebAudioChunk();
  268. }
  269. } else if (this.innerAudioContext) {
  270. this.innerAudioContext.play();
  271. this.isPaused = false;
  272. this.isPlaying = true;
  273. }
  274. }
  275. /**
  276. * 停止播放并清空队列
  277. */
  278. public stop(): void {
  279. if (this.useWebAudio) {
  280. // 停止所有源节点
  281. this.sourceNodes.forEach(source => {
  282. try {
  283. source.stop();
  284. source.disconnect();
  285. } catch (e) {
  286. console.warn('停止WebAudio节点失败', e);
  287. }
  288. });
  289. this.sourceNodes = [];
  290. this.audioContext.close().catch(console.error);
  291. this.initAudioContext(); // 重新初始化
  292. } else if (this.innerAudioContext) {
  293. this.innerAudioContext.stop();
  294. }
  295. this.audioQueue = [];
  296. this.isPlaying = false;
  297. this.isPaused = false;
  298. this.nextPlayTime = 0;
  299. this.currentPosition = 0;
  300. }
  301. /**
  302. * 重新播放
  303. */
  304. public async replay(): Promise<void> {
  305. this.stop();
  306. this.audioQueue = [...this.originalQueue];
  307. if (this.audioQueue.length > 0) {
  308. await this.playNextChunk();
  309. }
  310. }
  311. /**
  312. * 设置循环播放
  313. */
  314. public setLoop(loop: boolean): void {
  315. this.config.loop = loop;
  316. if (this.innerAudioContext) {
  317. this.innerAudioContext.loop = loop;
  318. }
  319. // WebAudio在播放完成回调中处理循环
  320. }
  321. /**
  322. * 处理播放完成
  323. */
  324. private handlePlaybackComplete(): void {
  325. this.isPlaying = false;
  326. this.isPaused = false;
  327. // 处理循环播放
  328. if (this.config.loop && this.originalQueue.length > 0) {
  329. this.audioQueue = [...this.originalQueue];
  330. this.playNextChunk();
  331. return;
  332. }
  333. if (this.onPlaybackComplete) {
  334. try {
  335. this.onPlaybackComplete();
  336. } catch (error) {
  337. console.error('播放完成回调执行失败:', error);
  338. }
  339. }
  340. }
  341. /**
  342. * 写入字符串到DataView
  343. */
  344. private writeString(view: DataView, offset: number, str: string): void {
  345. for (let i = 0; i < str.length; i++) {
  346. view.setUint8(offset + i, str.charCodeAt(i));
  347. }
  348. }
  349. /**
  350. * 获取当前状态
  351. */
  352. public getStatus() {
  353. return {
  354. isPlaying: this.isPlaying,
  355. isPaused: this.isPaused,
  356. queueLength: this.audioQueue.length,
  357. isLooping: this.config.loop,
  358. currentPosition: this.currentPosition,
  359. totalDuration: this.totalDuration,
  360. useWebAudio: this.useWebAudio
  361. };
  362. }
  363. /**
  364. * 清空音频队列
  365. */
  366. public clearQueue(): void {
  367. this.audioQueue = [];
  368. }
  369. /**
  370. * 销毁播放器
  371. */
  372. public destroy(): void {
  373. this.stop();
  374. if (this.audioContext) {
  375. this.audioContext.close().catch(console.error);
  376. }
  377. if (this.innerAudioContext) {
  378. this.innerAudioContext.destroy();
  379. }
  380. }
  381. }