|
@@ -38,41 +38,88 @@ export class StreamingAudioPlayer {
|
|
|
* 在用户交互时调用此方法,激活AudioContext
|
|
* 在用户交互时调用此方法,激活AudioContext
|
|
|
*/
|
|
*/
|
|
|
prepareForPlayback() {
|
|
prepareForPlayback() {
|
|
|
- if (this.audioReady) {
|
|
|
|
|
- console.log('🎯 AudioContext已激活,跳过重复激活')
|
|
|
|
|
|
|
+ if (this.audioReady && this.audioContext?.state === 'running') {
|
|
|
|
|
+ console.log('🎯 AudioContext已激活且运行中,跳过重复激活')
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
console.log('🎯 在用户交互中激活AudioContext(解决Safari自动播放限制)')
|
|
console.log('🎯 在用户交互中激活AudioContext(解决Safari自动播放限制)')
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
- // 创建AudioContext
|
|
|
|
|
- const AudioContextClass = window.AudioContext || (window as any).webkitAudioContext
|
|
|
|
|
- this.audioContext = new AudioContextClass()
|
|
|
|
|
-
|
|
|
|
|
- // 创建增益节点(音量放大器)
|
|
|
|
|
- this.gainNode = this.audioContext.createGain()
|
|
|
|
|
- this.gainNode.gain.value = 4.5 // 4.5倍音量增益
|
|
|
|
|
- console.log('🔊 音频增益设置为:', this.gainNode.gain.value, '倍')
|
|
|
|
|
-
|
|
|
|
|
- // 连接到输出
|
|
|
|
|
- this.gainNode.connect(this.audioContext.destination)
|
|
|
|
|
|
|
+ // 如果AudioContext不存在,创建新的
|
|
|
|
|
+ if (!this.audioContext) {
|
|
|
|
|
+ const AudioContextClass = window.AudioContext || (window as any).webkitAudioContext
|
|
|
|
|
+ this.audioContext = new AudioContextClass()
|
|
|
|
|
+
|
|
|
|
|
+ // 创建增益节点(音量放大器)
|
|
|
|
|
+ this.gainNode = this.audioContext.createGain()
|
|
|
|
|
+ this.gainNode.gain.value = 4.5 // 4.5倍音量增益
|
|
|
|
|
+ console.log('🔊 音频增益设置为:', this.gainNode.gain.value, '倍')
|
|
|
|
|
+
|
|
|
|
|
+ // 连接到输出
|
|
|
|
|
+ this.gainNode.connect(this.audioContext.destination)
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
// 激活AudioContext(Safari需要用户交互)
|
|
// 激活AudioContext(Safari需要用户交互)
|
|
|
if (this.audioContext.state === 'suspended') {
|
|
if (this.audioContext.state === 'suspended') {
|
|
|
this.audioContext.resume().then(() => {
|
|
this.audioContext.resume().then(() => {
|
|
|
- console.log('✅ AudioContext已激活,状态:', this.audioContext?.state)
|
|
|
|
|
|
|
+ console.log('✅ AudioContext已恢复,状态:', this.audioContext?.state)
|
|
|
this.audioReady = true
|
|
this.audioReady = true
|
|
|
})
|
|
})
|
|
|
- } else {
|
|
|
|
|
- console.log('✅ AudioContext已激活,状态:', this.audioContext.state)
|
|
|
|
|
|
|
+ } else if (this.audioContext.state === 'running') {
|
|
|
|
|
+ console.log('✅ AudioContext运行中,状态:', this.audioContext.state)
|
|
|
this.audioReady = true
|
|
this.audioReady = true
|
|
|
|
|
+ } else {
|
|
|
|
|
+ console.warn('⚠️ AudioContext状态异常:', this.audioContext.state)
|
|
|
}
|
|
}
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
console.error('❌ AudioContext初始化失败:', error)
|
|
console.error('❌ AudioContext初始化失败:', error)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 确保AudioContext处于运行状态并验证音量增益
|
|
|
|
|
+ * 🔑 关键:在每次播放前调用,防止移动端AudioContext被挂起导致无声或音量小
|
|
|
|
|
+ */
|
|
|
|
|
+ private async ensureAudioContextReady(): Promise<boolean> {
|
|
|
|
|
+ if (!this.audioContext || !this.gainNode) {
|
|
|
|
|
+ console.error('❌ AudioContext或GainNode未初始化')
|
|
|
|
|
+ return false
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 检查AudioContext状态
|
|
|
|
|
+ const currentState = this.audioContext.state
|
|
|
|
|
+ console.log(`🔍 AudioContext状态检查: ${currentState}`)
|
|
|
|
|
+
|
|
|
|
|
+ if (currentState === 'suspended') {
|
|
|
|
|
+ console.warn('⚠️ AudioContext被挂起,尝试恢复...')
|
|
|
|
|
+ try {
|
|
|
|
|
+ await this.audioContext.resume()
|
|
|
|
|
+ console.log('✅ AudioContext已恢复:', this.audioContext.state)
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('❌ AudioContext恢复失败:', error)
|
|
|
|
|
+ return false
|
|
|
|
|
+ }
|
|
|
|
|
+ } else if (currentState === 'closed') {
|
|
|
|
|
+ console.error('❌ AudioContext已关闭,需要重新初始化')
|
|
|
|
|
+ return false
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 验证并恢复音量增益
|
|
|
|
|
+ const currentGain = this.gainNode.gain.value
|
|
|
|
|
+ const targetGain = 4.5
|
|
|
|
|
+
|
|
|
|
|
+ if (Math.abs(currentGain - targetGain) > 0.1) {
|
|
|
|
|
+ console.warn(`⚠️ 音量增益异常 (当前: ${currentGain}, 目标: ${targetGain}),正在恢复...`)
|
|
|
|
|
+ this.gainNode.gain.value = targetGain
|
|
|
|
|
+ console.log(`✅ 音量增益已恢复为: ${targetGain}倍`)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ console.log(`✅ 音量增益正常: ${currentGain}倍`)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return true
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
appendAudioChunk(chunk: ArrayBuffer) {
|
|
appendAudioChunk(chunk: ArrayBuffer) {
|
|
|
const totalChunks = this.audioChunks.length + 1
|
|
const totalChunks = this.audioChunks.length + 1
|
|
|
console.log(`📦 收集音频块: ${chunk.byteLength} bytes (总计: ${totalChunks} 块)`)
|
|
console.log(`📦 收集音频块: ${chunk.byteLength} bytes (总计: ${totalChunks} 块)`)
|
|
@@ -101,6 +148,14 @@ export class StreamingAudioPlayer {
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // 🔑 关键:每次播放前都检查并恢复AudioContext状态
|
|
|
|
|
+ const isReady = await this.ensureAudioContextReady()
|
|
|
|
|
+ if (!isReady) {
|
|
|
|
|
+ console.error('❌ AudioContext未就绪,无法开始播放')
|
|
|
|
|
+ this.config.onError?.(new Error('音频环境未就绪'))
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
this.playing = true
|
|
this.playing = true
|
|
|
this.startTime = this.audioContext.currentTime
|
|
this.startTime = this.audioContext.currentTime
|
|
|
this.lastPlayTime = this.startTime
|
|
this.lastPlayTime = this.startTime
|
|
@@ -195,6 +250,23 @@ export class StreamingAudioPlayer {
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // 🔑 关键:确保AudioContext处于运行状态(同步检查)
|
|
|
|
|
+ if (this.audioContext.state === 'suspended') {
|
|
|
|
|
+ console.warn('⚠️ AudioContext被挂起,尝试快速恢复...')
|
|
|
|
|
+ this.audioContext.resume().then(() => {
|
|
|
|
|
+ console.log('✅ AudioContext已快速恢复')
|
|
|
|
|
+ }).catch((error) => {
|
|
|
|
|
+ console.error('❌ AudioContext恢复失败:', error)
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 验证音量增益(同步)
|
|
|
|
|
+ const currentGain = this.gainNode.gain.value
|
|
|
|
|
+ if (currentGain < 4.0) {
|
|
|
|
|
+ console.warn(`⚠️ 播放前检测到音量过低 (${currentGain}),恢复至4.5`)
|
|
|
|
|
+ this.gainNode.gain.value = 4.5
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// 创建音频源节点
|
|
// 创建音频源节点
|
|
|
const source = this.audioContext.createBufferSource()
|
|
const source = this.audioContext.createBufferSource()
|
|
|
source.buffer = fullBuffer
|
|
source.buffer = fullBuffer
|
|
@@ -248,6 +320,14 @@ export class StreamingAudioPlayer {
|
|
|
async play() {
|
|
async play() {
|
|
|
console.log(`🎵 play()被调用 (ai_response_end) - 音频块数: ${this.audioChunks.length}, 播放状态: ${this.playing ? '播放中' : '空闲'}`)
|
|
console.log(`🎵 play()被调用 (ai_response_end) - 音频块数: ${this.audioChunks.length}, 播放状态: ${this.playing ? '播放中' : '空闲'}`)
|
|
|
|
|
|
|
|
|
|
+ // 🔑 关键:每次播放前都检查并恢复AudioContext状态
|
|
|
|
|
+ if (this.audioContext && this.gainNode) {
|
|
|
|
|
+ const isReady = await this.ensureAudioContextReady()
|
|
|
|
|
+ if (!isReady) {
|
|
|
|
|
+ console.error('❌ AudioContext未就绪,播放可能失败')
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// 标记所有音频块已接收完毕
|
|
// 标记所有音频块已接收完毕
|
|
|
this.allChunksReceived = true
|
|
this.allChunksReceived = true
|
|
|
|
|
|