import { LAppDelegate } from '@/lib/live2d/src/lappdelegate'; import { ResourceModel } from '@/lib/protocol'; export class Live2dManager { // 单例 public static getInstance(): Live2dManager { if (! this._instance) { this._instance = new Live2dManager(); } return this._instance; } public setReady(ready: boolean) { this._ready = ready; } public isReady(): boolean { return this._ready; } public changeCharacter(character: ResourceModel | null) { // _subdelegates中只有一个画布, 所以设置第一个即可 this._ready = false; LAppDelegate.getInstance().changeCharacter(character) } public setLipFactor(weight: number): void { this._lipFactor = weight; } public getLipFactor(): number { return this._lipFactor; } public pushAudioQueue(audioData: ArrayBuffer): void { this._ttsQueue.push(audioData); } public popAudioQueue(): ArrayBuffer | null { if (this._ttsQueue.length > 0) { const audioData = this._ttsQueue.shift(); return audioData; } else { return null; } } public clearAudioQueue(): void { this._ttsQueue = []; } public playAudio(): ArrayBuffer | null { // 如果已停止,不再播放任何音频 if (this._isStopped) return null; if (this._audioIsPlaying) return null; // 如果正在播放则返回 const audioData = this.popAudioQueue(); if (audioData == null) return null; // 没有音频数据则返回 this._audioIsPlaying = true; // 播放音频 const playAudioBuffer = (buffer: AudioBuffer) => { // 再次检查是否已停止 if (this._isStopped) { this._audioIsPlaying = false; return; } var source = this._audioContext.createBufferSource(); source.buffer = buffer; source.connect(this._audioContext.destination); // 监听音频播放完毕事件 source.onended = () => { this._audioIsPlaying = false; // 检查是否所有音频都已播放完成 this.notifyAudioComplete(); }; source.start(); this._audioSource = source; } // 创建一个新的 ArrayBuffer 并复制数据, 防止原始数据被decodeAudioData释放 const newAudioData = audioData.slice(0); this._audioContext.decodeAudioData(newAudioData).then( buffer => { // 在 decodeAudioData 完成后,再次检查是否已停止 if (!this._isStopped) { playAudioBuffer(buffer); } else { this._audioIsPlaying = false; } } ).catch((error) => { // decodeAudioData 失败时,重置播放状态 this._audioIsPlaying = false; }); return audioData; } public stopAudio(): void { // 设置停止标志,防止新的音频开始播放 this._isStopped = true; // 清空队列 this.clearAudioQueue(); // 停止当前播放的音频 if (this._audioSource) { try { this._audioSource.stop(); } catch (e) { // 如果音频已经停止,忽略错误 console.log('[Live2dManager] Audio source already stopped'); } this._audioSource = null; } this._audioIsPlaying = false; } // 重置停止标志,允许新的音频播放 public resetStopFlag(): void { this._isStopped = false; } public isAudioPlaying(): boolean { return this._audioIsPlaying; } public isAudioQueueEmpty(): boolean { return this._ttsQueue.length === 0; } public isChatComplete(): boolean { return !this._audioIsPlaying && this._ttsQueue.length === 0; } public setOnAudioCompleteCallback(callback: (() => void) | null): void { this._onAudioCompleteCallback = callback; } private notifyAudioComplete(): void { // 延迟检查,避免在音频转换间隙错误触发回调 // 当一段音频播放完成时,可能还有音频正在转换中,需要等待一小段时间 // 使用 requestAnimationFrame 确保在下一帧检查,给音频转换留出时间 // 注意:不需要在这里调用 playAudio(),因为 playAudio() 已经在动画循环中每帧被调用了 requestAnimationFrame(() => { // 延迟一小段时间后再检查,确保音频转换有时间完成 // 这样可以避免在音频转换间隙错误触发回调 // 使用较长的延迟(100ms),确保音频转换有足够时间完成 setTimeout(() => { // 再次检查队列是否为空,如果队列中有新音频,说明还有音频在转换中,不应该触发回调 if (this._onAudioCompleteCallback && this.isChatComplete()) { this._onAudioCompleteCallback(); } }, 100); // 延迟100ms,给音频转换留出足够时间 }); } constructor() { this._audioContext = new (window.AudioContext || (window as any).webkitAudioContext)(); this._audioIsPlaying = false; this._audioSource = null; this._lipFactor = 1.0; this._ready = false; this._onAudioCompleteCallback = null; } private static _instance: Live2dManager; private _ttsQueue: ArrayBuffer[] = []; private _audioContext: AudioContext; private _audioIsPlaying: boolean; private _audioSource: AudioBufferSourceNode | null; private _lipFactor: number; private _ready: boolean; private _onAudioCompleteCallback: (() => void) | null; private _isStopped: boolean = false; // 标记是否已停止 }