live2dManager.ts 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. import { LAppDelegate } from '@/lib/live2d/src/lappdelegate';
  2. import { ResourceModel } from '@/lib/protocol';
  3. export class Live2dManager {
  4. // 单例
  5. public static getInstance(): Live2dManager {
  6. if (! this._instance) {
  7. this._instance = new Live2dManager();
  8. }
  9. return this._instance;
  10. }
  11. public setReady(ready: boolean) {
  12. this._ready = ready;
  13. }
  14. public isReady(): boolean {
  15. return this._ready;
  16. }
  17. public changeCharacter(character: ResourceModel | null) {
  18. // _subdelegates中只有一个画布, 所以设置第一个即可
  19. this._ready = false;
  20. LAppDelegate.getInstance().changeCharacter(character)
  21. }
  22. public setLipFactor(weight: number): void {
  23. this._lipFactor = weight;
  24. }
  25. public getLipFactor(): number {
  26. return this._lipFactor;
  27. }
  28. public pushAudioQueue(audioData: ArrayBuffer): void {
  29. this._ttsQueue.push(audioData);
  30. }
  31. public popAudioQueue(): ArrayBuffer | null {
  32. if (this._ttsQueue.length > 0) {
  33. const audioData = this._ttsQueue.shift();
  34. return audioData;
  35. } else {
  36. return null;
  37. }
  38. }
  39. public clearAudioQueue(): void {
  40. this._ttsQueue = [];
  41. }
  42. public playAudio(): ArrayBuffer | null {
  43. // 如果已停止,不再播放任何音频
  44. if (this._isStopped) return null;
  45. if (this._audioIsPlaying) return null; // 如果正在播放则返回
  46. const audioData = this.popAudioQueue();
  47. if (audioData == null) return null; // 没有音频数据则返回
  48. this._audioIsPlaying = true;
  49. // 播放音频
  50. const playAudioBuffer = (buffer: AudioBuffer) => {
  51. // 再次检查是否已停止
  52. if (this._isStopped) {
  53. this._audioIsPlaying = false;
  54. return;
  55. }
  56. var source = this._audioContext.createBufferSource();
  57. source.buffer = buffer;
  58. source.connect(this._audioContext.destination);
  59. // 监听音频播放完毕事件
  60. source.onended = () => {
  61. this._audioIsPlaying = false;
  62. // 检查是否所有音频都已播放完成
  63. this.notifyAudioComplete();
  64. };
  65. source.start();
  66. this._audioSource = source;
  67. }
  68. // 创建一个新的 ArrayBuffer 并复制数据, 防止原始数据被decodeAudioData释放
  69. const newAudioData = audioData.slice(0);
  70. this._audioContext.decodeAudioData(newAudioData).then(
  71. buffer => {
  72. // 在 decodeAudioData 完成后,再次检查是否已停止
  73. if (!this._isStopped) {
  74. playAudioBuffer(buffer);
  75. } else {
  76. this._audioIsPlaying = false;
  77. }
  78. }
  79. ).catch((error) => {
  80. // decodeAudioData 失败时,重置播放状态
  81. this._audioIsPlaying = false;
  82. });
  83. return audioData;
  84. }
  85. public stopAudio(): void {
  86. // 设置停止标志,防止新的音频开始播放
  87. this._isStopped = true;
  88. // 清空队列
  89. this.clearAudioQueue();
  90. // 停止当前播放的音频
  91. if (this._audioSource) {
  92. try {
  93. this._audioSource.stop();
  94. } catch (e) {
  95. // 如果音频已经停止,忽略错误
  96. console.log('[Live2dManager] Audio source already stopped');
  97. }
  98. this._audioSource = null;
  99. }
  100. this._audioIsPlaying = false;
  101. }
  102. // 重置停止标志,允许新的音频播放
  103. public resetStopFlag(): void {
  104. this._isStopped = false;
  105. }
  106. public isAudioPlaying(): boolean {
  107. return this._audioIsPlaying;
  108. }
  109. public isAudioQueueEmpty(): boolean {
  110. return this._ttsQueue.length === 0;
  111. }
  112. public isChatComplete(): boolean {
  113. return !this._audioIsPlaying && this._ttsQueue.length === 0;
  114. }
  115. public setOnAudioCompleteCallback(callback: (() => void) | null): void {
  116. this._onAudioCompleteCallback = callback;
  117. }
  118. private notifyAudioComplete(): void {
  119. // 延迟检查,避免在音频转换间隙错误触发回调
  120. // 当一段音频播放完成时,可能还有音频正在转换中,需要等待一小段时间
  121. // 使用 requestAnimationFrame 确保在下一帧检查,给音频转换留出时间
  122. // 注意:不需要在这里调用 playAudio(),因为 playAudio() 已经在动画循环中每帧被调用了
  123. requestAnimationFrame(() => {
  124. // 延迟一小段时间后再检查,确保音频转换有时间完成
  125. // 这样可以避免在音频转换间隙错误触发回调
  126. // 使用较长的延迟(100ms),确保音频转换有足够时间完成
  127. setTimeout(() => {
  128. // 再次检查队列是否为空,如果队列中有新音频,说明还有音频在转换中,不应该触发回调
  129. if (this._onAudioCompleteCallback && this.isChatComplete()) {
  130. this._onAudioCompleteCallback();
  131. }
  132. }, 100); // 延迟100ms,给音频转换留出足够时间
  133. });
  134. }
  135. constructor() {
  136. this._audioContext = new (window.AudioContext || (window as any).webkitAudioContext)();
  137. this._audioIsPlaying = false;
  138. this._audioSource = null;
  139. this._lipFactor = 1.0;
  140. this._ready = false;
  141. this._onAudioCompleteCallback = null;
  142. }
  143. private static _instance: Live2dManager;
  144. private _ttsQueue: ArrayBuffer[] = [];
  145. private _audioContext: AudioContext;
  146. private _audioIsPlaying: boolean;
  147. private _audioSource: AudioBufferSourceNode | null;
  148. private _lipFactor: number;
  149. private _ready: boolean;
  150. private _onAudioCompleteCallback: (() => void) | null;
  151. private _isStopped: boolean = false; // 标记是否已停止
  152. }