|
|
|
@ -110,6 +110,10 @@ export default { |
|
|
|
pagePay: { |
|
|
|
type: Array, |
|
|
|
default: () => [] |
|
|
|
}, |
|
|
|
isWordAudioPlaying: { |
|
|
|
type: Boolean, |
|
|
|
default: false |
|
|
|
} |
|
|
|
}, |
|
|
|
data() { |
|
|
|
@ -152,6 +156,8 @@ export default { |
|
|
|
shouldCancelRequest: false, // 是否应该取消当前请求 |
|
|
|
// 本地音色ID(避免直接修改prop) |
|
|
|
localVoiceId: '', // 本地音色ID,从prop初始化 |
|
|
|
// 倍速检查相关 |
|
|
|
lastSpeedCheckTime: -1, // 上次检查倍速的时间点 |
|
|
|
} |
|
|
|
}, |
|
|
|
computed: { |
|
|
|
@ -433,9 +439,9 @@ export default { |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
// 每个请求之间间隔400ms,避免请求过于频繁 |
|
|
|
// 每个请求之间间隔200ms,避免请求过于频繁 |
|
|
|
if (i < segments.length - 1) { |
|
|
|
await new Promise(resolve => setTimeout(resolve, 400)); |
|
|
|
await new Promise(resolve => setTimeout(resolve, 200)); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
@ -450,6 +456,34 @@ export default { |
|
|
|
|
|
|
|
// 获取当前页面的音频内容 |
|
|
|
async getCurrentPageAudio(autoPlay = false) { |
|
|
|
// 🎯 确保音色ID已加载完成后再获取音频 |
|
|
|
if (!this.localVoiceId || this.localVoiceId === '' || this.localVoiceId === null || this.localVoiceId === undefined) { |
|
|
|
console.log('⚠️ 音色ID未加载,无法获取音频'); |
|
|
|
console.log(' localVoiceId:', this.localVoiceId); |
|
|
|
console.log(' 类型:', typeof this.localVoiceId); |
|
|
|
|
|
|
|
// 设置加载失败状态 |
|
|
|
this.isAudioLoading = false; |
|
|
|
this.audioLoadFailed = true; |
|
|
|
this.hasAudioData = false; |
|
|
|
|
|
|
|
// 通知父组件音频状态变化 |
|
|
|
this.$emit('audio-state-change', { |
|
|
|
hasAudioData: false, |
|
|
|
isLoading: false, |
|
|
|
currentHighlightIndex: -1 |
|
|
|
}); |
|
|
|
|
|
|
|
uni.showToast({ |
|
|
|
title: '音色未加载,请稍后重试', |
|
|
|
icon: 'none', |
|
|
|
duration: 2000 |
|
|
|
}); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
console.log('✅ 音色ID验证通过:', this.localVoiceId); |
|
|
|
|
|
|
|
// 检查是否正在页面切换中,如果是则不加载音频 |
|
|
|
if (this.isPageChanging) { |
|
|
|
console.log('页面切换中,跳过音频加载'); |
|
|
|
@ -1005,6 +1039,9 @@ export default { |
|
|
|
|
|
|
|
if (this.currentPageAudios.length === 0) return; |
|
|
|
|
|
|
|
// 🎯 全局音频管理:停止单词音频播放,确保只有一个音频播放 |
|
|
|
this.stopWordAudio(); |
|
|
|
|
|
|
|
// 如果没有当前音频实例或者需要切换音频 |
|
|
|
if (!this.currentAudio || this.currentAudio.src !== this.currentPageAudios[this.currentAudioIndex].url) { |
|
|
|
this.createAudioInstance(); |
|
|
|
@ -1021,6 +1058,10 @@ export default { |
|
|
|
this.currentAudio.play(); |
|
|
|
// isPlaying状态会在onPlay事件中自动设置 |
|
|
|
this.updateHighlightIndex(); |
|
|
|
// 确保倍速设置正确 |
|
|
|
setTimeout(() => { |
|
|
|
this.applyPlaybackRate(this.currentAudio); |
|
|
|
}, 100); |
|
|
|
} |
|
|
|
}, |
|
|
|
|
|
|
|
@ -1036,13 +1077,124 @@ export default { |
|
|
|
this.emitHighlightChange(-1); |
|
|
|
}, |
|
|
|
|
|
|
|
// 文本标准化函数 - 移除多余空格、标点符号等 |
|
|
|
normalizeText(text) { |
|
|
|
if (!text || typeof text !== 'string') return ''; |
|
|
|
|
|
|
|
return text |
|
|
|
.trim() |
|
|
|
.replace(/\s+/g, ' ') // 将多个空格替换为单个空格 |
|
|
|
.replace(/[,。!?;:""''()【】《》]/g, '') // 移除中文标点 |
|
|
|
.replace(/[,.!?;:"'()\[\]<>]/g, '') // 移除英文标点 |
|
|
|
.toLowerCase(); // 转为小写(对英文有效) |
|
|
|
}, |
|
|
|
|
|
|
|
// 备用方案:使用 TTS API 实时生成并播放音频 |
|
|
|
async playTextWithTTS(textContent) { |
|
|
|
try { |
|
|
|
console.log('🔊 使用 TTS 实时生成音频:', textContent); |
|
|
|
|
|
|
|
// 停止当前播放的音频 |
|
|
|
if (this.currentAudio) { |
|
|
|
this.currentAudio.pause(); |
|
|
|
this.currentAudio.destroy(); |
|
|
|
this.currentAudio = null; |
|
|
|
} |
|
|
|
|
|
|
|
// 显示加载提示 |
|
|
|
uni.showLoading({ |
|
|
|
title: '正在生成音频...' |
|
|
|
}); |
|
|
|
|
|
|
|
// 调用 TTS API |
|
|
|
const audioRes = await this.$api.music.textToVoice({ |
|
|
|
text: textContent, |
|
|
|
voiceType: this.voiceId || 1 // 使用当前语音类型,默认为1 |
|
|
|
}); |
|
|
|
|
|
|
|
uni.hideLoading(); |
|
|
|
|
|
|
|
console.log('🔊 TTS API 响应:', audioRes); |
|
|
|
|
|
|
|
if (audioRes && audioRes.result && audioRes.result.url) { |
|
|
|
const audioUrl = audioRes.result.url; |
|
|
|
|
|
|
|
// 创建并播放音频 |
|
|
|
const audio = uni.createInnerAudioContext(); |
|
|
|
audio.src = audioUrl; |
|
|
|
|
|
|
|
audio.onPlay(() => { |
|
|
|
console.log('🔊 TTS 音频开始播放'); |
|
|
|
this.isPlaying = true; |
|
|
|
}); |
|
|
|
|
|
|
|
audio.onEnded(() => { |
|
|
|
console.log('🔊 TTS 音频播放完成'); |
|
|
|
this.isPlaying = false; |
|
|
|
audio.destroy(); |
|
|
|
if (this.currentAudio === audio) { |
|
|
|
this.currentAudio = null; |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
audio.onError((error) => { |
|
|
|
console.error('🔊 TTS 音频播放失败:', error); |
|
|
|
this.isPlaying = false; |
|
|
|
audio.destroy(); |
|
|
|
if (this.currentAudio === audio) { |
|
|
|
this.currentAudio = null; |
|
|
|
} |
|
|
|
uni.showToast({ |
|
|
|
title: '音频播放失败', |
|
|
|
icon: 'none' |
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
// 保存当前音频实例并播放 |
|
|
|
this.currentAudio = audio; |
|
|
|
audio.play(); |
|
|
|
|
|
|
|
console.log('✅ TTS 音频播放成功'); |
|
|
|
return true; |
|
|
|
} else { |
|
|
|
console.error('❌ TTS API 请求失败:', audioRes); |
|
|
|
uni.showToast({ |
|
|
|
title: '音频生成失败', |
|
|
|
icon: 'none' |
|
|
|
}); |
|
|
|
return false; |
|
|
|
} |
|
|
|
} catch (error) { |
|
|
|
uni.hideLoading(); |
|
|
|
console.error('❌ TTS 音频生成异常:', error); |
|
|
|
uni.showToast({ |
|
|
|
title: '音频生成失败', |
|
|
|
icon: 'none' |
|
|
|
}); |
|
|
|
return false; |
|
|
|
} |
|
|
|
}, |
|
|
|
|
|
|
|
// 播放指定的音频段落(通过文本内容匹配) |
|
|
|
playSpecificAudio(textContent) { |
|
|
|
console.log('尝试播放指定音频段落:', textContent); |
|
|
|
console.log('🎯 尝试播放指定音频段落:', textContent); |
|
|
|
console.log('📊 当前页面音频数组长度:', this.currentPageAudios.length); |
|
|
|
console.log('📋 音频加载状态:', this.isAudioLoading); |
|
|
|
|
|
|
|
// 检查单词音频播放状态 |
|
|
|
console.log('🎵 检查单词音频播放状态:', this.isWordAudioPlaying); |
|
|
|
if (this.isWordAudioPlaying) { |
|
|
|
console.log('⚠️ 单词音频正在播放,阻止句子音频播放'); |
|
|
|
uni.showToast({ |
|
|
|
title: '请等待单词音频播放完成', |
|
|
|
icon: 'none' |
|
|
|
}); |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
// 检查textContent是否有效 |
|
|
|
if (!textContent || typeof textContent !== 'string') { |
|
|
|
console.log('无效的文本内容:', textContent); |
|
|
|
console.error('❌ 无效的文本内容:', textContent); |
|
|
|
uni.showToast({ |
|
|
|
title: '无效的文本内容', |
|
|
|
icon: 'none' |
|
|
|
@ -1050,37 +1202,170 @@ export default { |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
// 在当前页面音频数组中查找匹配的音频 |
|
|
|
let audioIndex = this.currentPageAudios.findIndex(audio => |
|
|
|
audio.text && audio.text.trim() === textContent.trim() |
|
|
|
); |
|
|
|
// 检查音频数据是否已加载 |
|
|
|
if (this.currentPageAudios.length === 0) { |
|
|
|
console.warn('⚠️ 当前页面音频数据为空,可能还在加载中'); |
|
|
|
uni.showToast({ |
|
|
|
title: '音频正在加载中,请稍后再试', |
|
|
|
icon: 'none' |
|
|
|
}); |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
// 标准化目标文本 |
|
|
|
const normalizedTarget = this.normalizeText(textContent); |
|
|
|
console.log('🔍 标准化后的目标文本:', normalizedTarget); |
|
|
|
|
|
|
|
// 如果直接匹配失败,尝试匹配分段音频 |
|
|
|
if (audioIndex === -1) { |
|
|
|
console.log('直接匹配失败,尝试匹配分段音频'); |
|
|
|
// 打印所有音频文本用于调试 |
|
|
|
console.log('📝 当前页面所有音频文本:'); |
|
|
|
this.currentPageAudios.forEach((audio, index) => { |
|
|
|
console.log(` [${index}] 原始文本: "${audio.text}"`); |
|
|
|
console.log(` [${index}] 标准化文本: "${this.normalizeText(audio.text)}"`); |
|
|
|
if (audio.originalText) { |
|
|
|
console.log(` [${index}] 原始完整文本: "${audio.originalText}"`); |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
let audioIndex = -1; |
|
|
|
|
|
|
|
// 第一步:精确匹配(标准化后) |
|
|
|
audioIndex = this.currentPageAudios.findIndex(audio => { |
|
|
|
if (!audio.text) return false; |
|
|
|
const normalizedAudio = this.normalizeText(audio.text); |
|
|
|
return normalizedAudio === normalizedTarget; |
|
|
|
}); |
|
|
|
|
|
|
|
if (audioIndex !== -1) { |
|
|
|
console.log('✅ 精确匹配成功,索引:', audioIndex); |
|
|
|
} else { |
|
|
|
console.log('❌ 精确匹配失败,尝试模糊匹配'); |
|
|
|
|
|
|
|
// 查找包含该文本的分段音频 |
|
|
|
// 第二步:包含匹配 |
|
|
|
audioIndex = this.currentPageAudios.findIndex(audio => { |
|
|
|
if (!audio.text) return false; |
|
|
|
const normalizedAudio = this.normalizeText(audio.text); |
|
|
|
|
|
|
|
// 检查是否为分段音频,且原始文本包含目标文本 |
|
|
|
if (audio.isSegmented && audio.originalText) { |
|
|
|
return audio.originalText.trim() === textContent.trim(); |
|
|
|
} |
|
|
|
|
|
|
|
// 检查目标文本是否包含在当前音频文本中,或者当前音频文本包含在目标文本中 |
|
|
|
const audioText = audio.text.trim(); |
|
|
|
const targetText = textContent.trim(); |
|
|
|
return audioText.includes(targetText) || targetText.includes(audioText); |
|
|
|
// 双向包含检查 |
|
|
|
return normalizedAudio.includes(normalizedTarget) || normalizedTarget.includes(normalizedAudio); |
|
|
|
}); |
|
|
|
|
|
|
|
if (audioIndex !== -1) { |
|
|
|
console.log('找到分段音频匹配,索引:', audioIndex); |
|
|
|
console.log('✅ 包含匹配成功,索引:', audioIndex); |
|
|
|
} else { |
|
|
|
console.log('❌ 包含匹配失败,尝试分段音频匹配'); |
|
|
|
|
|
|
|
// 第三步:分段音频匹配 |
|
|
|
audioIndex = this.currentPageAudios.findIndex(audio => { |
|
|
|
if (!audio.text) return false; |
|
|
|
|
|
|
|
// 检查是否为分段音频,且原始文本匹配 |
|
|
|
if (audio.isSegmented && audio.originalText) { |
|
|
|
const normalizedOriginal = this.normalizeText(audio.originalText); |
|
|
|
return normalizedOriginal === normalizedTarget || |
|
|
|
normalizedOriginal.includes(normalizedTarget) || |
|
|
|
normalizedTarget.includes(normalizedOriginal); |
|
|
|
} |
|
|
|
|
|
|
|
return false; |
|
|
|
}); |
|
|
|
|
|
|
|
if (audioIndex !== -1) { |
|
|
|
console.log('✅ 分段音频匹配成功,索引:', audioIndex); |
|
|
|
} else { |
|
|
|
console.log('❌ 所有匹配方式都失败'); |
|
|
|
|
|
|
|
// 第四步:句子分割匹配(针对长句子) |
|
|
|
console.log('❌ 尝试句子分割匹配...'); |
|
|
|
|
|
|
|
// 将目标句子按标点符号分割 |
|
|
|
const targetSentences = normalizedTarget.split(/[,。!?;:,!?;:]/).filter(s => s.trim().length > 0); |
|
|
|
console.log('🔪 目标句子分割结果:', targetSentences); |
|
|
|
|
|
|
|
if (targetSentences.length > 1) { |
|
|
|
// 尝试匹配分割后的句子片段 |
|
|
|
for (let i = 0; i < targetSentences.length; i++) { |
|
|
|
const sentence = targetSentences[i].trim(); |
|
|
|
if (sentence.length < 3) continue; // 跳过太短的片段 |
|
|
|
|
|
|
|
audioIndex = this.currentPageAudios.findIndex(audio => { |
|
|
|
if (!audio.text) return false; |
|
|
|
const normalizedAudio = this.normalizeText(audio.text); |
|
|
|
return normalizedAudio.includes(sentence) || sentence.includes(normalizedAudio); |
|
|
|
}); |
|
|
|
|
|
|
|
if (audioIndex !== -1) { |
|
|
|
console.log(`✅ 句子片段匹配成功,片段: "${sentence}", 索引: ${audioIndex}`); |
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if (audioIndex === -1) { |
|
|
|
console.log('❌ 句子分割匹配失败,尝试关键词匹配...'); |
|
|
|
|
|
|
|
// 第五步:关键词匹配(提取关键词进行匹配) |
|
|
|
const keywords = normalizedTarget.split(/\s+/).filter(word => word.length > 2); |
|
|
|
console.log('🔑 提取的关键词:', keywords); |
|
|
|
|
|
|
|
let bestKeywordMatch = -1; |
|
|
|
let bestKeywordCount = 0; |
|
|
|
|
|
|
|
this.currentPageAudios.forEach((audio, index) => { |
|
|
|
if (!audio.text) return; |
|
|
|
const normalizedAudio = this.normalizeText(audio.text); |
|
|
|
|
|
|
|
// 计算匹配的关键词数量 |
|
|
|
const matchedKeywords = keywords.filter(keyword => normalizedAudio.includes(keyword)); |
|
|
|
const matchCount = matchedKeywords.length; |
|
|
|
|
|
|
|
if (matchCount > bestKeywordCount && matchCount >= Math.min(2, keywords.length)) { |
|
|
|
bestKeywordCount = matchCount; |
|
|
|
bestKeywordMatch = index; |
|
|
|
console.log(` [${index}] 关键词匹配: ${matchCount}/${keywords.length}, 匹配词: [${matchedKeywords.join(', ')}]`); |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
if (bestKeywordMatch !== -1) { |
|
|
|
audioIndex = bestKeywordMatch; |
|
|
|
console.log(`✅ 关键词匹配成功,索引: ${audioIndex}, 匹配数: ${bestKeywordCount}/${keywords.length}`); |
|
|
|
} else { |
|
|
|
console.log('❌ 关键词匹配失败,尝试相似度匹配...'); |
|
|
|
|
|
|
|
// 第六步:相似度匹配(最后的尝试) |
|
|
|
let bestMatch = -1; |
|
|
|
let bestSimilarity = 0; |
|
|
|
|
|
|
|
this.currentPageAudios.forEach((audio, index) => { |
|
|
|
if (!audio.text) return; |
|
|
|
|
|
|
|
const normalizedAudio = this.normalizeText(audio.text); |
|
|
|
|
|
|
|
// 计算简单的相似度(共同字符数 / 较长文本长度) |
|
|
|
const commonChars = [...normalizedTarget].filter(char => normalizedAudio.includes(char)).length; |
|
|
|
const maxLength = Math.max(normalizedTarget.length, normalizedAudio.length); |
|
|
|
const similarity = maxLength > 0 ? commonChars / maxLength : 0; |
|
|
|
|
|
|
|
console.log(` [${index}] 相似度: ${similarity.toFixed(2)}, 文本: "${audio.text}"`); |
|
|
|
|
|
|
|
if (similarity > bestSimilarity && similarity > 0.5) { // 降低相似度阈值到50% |
|
|
|
bestSimilarity = similarity; |
|
|
|
bestMatch = index; |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
if (bestMatch !== -1) { |
|
|
|
audioIndex = bestMatch; |
|
|
|
console.log(`✅ 相似度匹配成功,索引: ${audioIndex}, 相似度: ${bestSimilarity.toFixed(2)}`); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if (audioIndex !== -1) { |
|
|
|
console.log('找到匹配的音频,索引:', audioIndex); |
|
|
|
console.log('🎵 找到匹配的音频,开始播放,索引:', audioIndex); |
|
|
|
console.log('🎵 匹配的音频文本:', this.currentPageAudios[audioIndex].text); |
|
|
|
|
|
|
|
// 停止当前播放的音频 |
|
|
|
if (this.currentAudio) { |
|
|
|
@ -1108,18 +1393,19 @@ export default { |
|
|
|
setTimeout(() => { |
|
|
|
if (this.currentAudio) { |
|
|
|
this.currentAudio.play(); |
|
|
|
console.log('开始播放指定音频段落'); |
|
|
|
console.log('🎵 开始播放指定音频段落'); |
|
|
|
} else { |
|
|
|
console.error('❌ 音频实例创建失败'); |
|
|
|
} |
|
|
|
}, 100); |
|
|
|
|
|
|
|
return true; // 成功找到并播放 |
|
|
|
} else { |
|
|
|
console.log('未找到匹配的音频段落:', textContent); |
|
|
|
uni.showToast({ |
|
|
|
title: '未找到对应的音频', |
|
|
|
icon: 'none' |
|
|
|
}); |
|
|
|
return false; // 未找到匹配的音频 |
|
|
|
console.error('❌ 未找到匹配的音频段落:', textContent); |
|
|
|
console.log('💡 尝试使用备用方案:实时生成音频'); |
|
|
|
|
|
|
|
// 备用方案:使用 textToVoice API 实时生成音频 |
|
|
|
return this.playTextWithTTS(textContent); |
|
|
|
} |
|
|
|
}, |
|
|
|
|
|
|
|
@ -1141,8 +1427,14 @@ export default { |
|
|
|
console.log('使用微信原生音頻API'); |
|
|
|
audio = wx.createInnerAudioContext(); |
|
|
|
} else { |
|
|
|
console.log('使用uni-app音頻API'); |
|
|
|
audio = uni.createInnerAudioContext(); |
|
|
|
// 在H5环境下,尝试使用原生HTML5 Audio API来支持倍速 |
|
|
|
if (typeof window !== 'undefined' && window.Audio) { |
|
|
|
console.log('使用原生HTML5 Audio API以支持倍速'); |
|
|
|
audio = this.createHTML5Audio(); |
|
|
|
} else { |
|
|
|
console.log('使用uni-app音頻API'); |
|
|
|
audio = uni.createInnerAudioContext(); |
|
|
|
} |
|
|
|
} |
|
|
|
const audioUrl = this.currentPageAudios[this.currentAudioIndex].url; |
|
|
|
audio.src = audioUrl; |
|
|
|
@ -1154,43 +1446,47 @@ export default { |
|
|
|
|
|
|
|
// 在音頻可以播放時檢測playbackRate支持 |
|
|
|
audio.onCanplay(() => { |
|
|
|
console.log('🎵 音頻可以播放,開始檢測playbackRate支持'); |
|
|
|
this.checkPlaybackRateSupport(audio); |
|
|
|
console.log('🎵 音頻可以播放事件觸發,開始檢測playbackRate支持'); |
|
|
|
console.log('🔍 音頻實例基本信息:', { |
|
|
|
src: audio.src, |
|
|
|
duration: audio.duration, |
|
|
|
readyState: audio.readyState || '未知', |
|
|
|
constructor: audio.constructor.name |
|
|
|
}); |
|
|
|
|
|
|
|
// 檢測完成後,設置用戶期望的播放速度 |
|
|
|
setTimeout(() => { |
|
|
|
if (this.playbackRateSupported) { |
|
|
|
console.log('設置播放速度:', this.playSpeed); |
|
|
|
audio.playbackRate = this.playSpeed; |
|
|
|
|
|
|
|
// 驗證設置結果 |
|
|
|
setTimeout(() => { |
|
|
|
console.log('最終播放速度:', audio.playbackRate); |
|
|
|
if (Math.abs(audio.playbackRate - this.playSpeed) > 0.01) { |
|
|
|
console.log('⚠️ 播放速度設置可能未生效'); |
|
|
|
} else { |
|
|
|
console.log('✅ 播放速度設置成功'); |
|
|
|
} |
|
|
|
}, 50); |
|
|
|
} else { |
|
|
|
console.log('❌ 當前環境不支持播放速度控制'); |
|
|
|
} |
|
|
|
}, 50); |
|
|
|
this.checkPlaybackRateSupport(audio); |
|
|
|
}); |
|
|
|
|
|
|
|
// 音频事件监听 |
|
|
|
audio.onPlay(() => { |
|
|
|
console.log('音频开始播放'); |
|
|
|
console.log('🎵 音频开始播放'); |
|
|
|
this.isPlaying = true; |
|
|
|
|
|
|
|
// 在播放开始时立即设置倍速 |
|
|
|
this.applyPlaybackRate(audio); |
|
|
|
}); |
|
|
|
|
|
|
|
audio.onPause(() => { |
|
|
|
console.log('音频暂停'); |
|
|
|
console.log('⏸️ 音频暂停'); |
|
|
|
console.log('📊 暫停時倍速狀態:', { |
|
|
|
當前播放速度: audio.playbackRate + 'x', |
|
|
|
期望播放速度: this.playSpeed + 'x' |
|
|
|
}); |
|
|
|
this.isPlaying = false; |
|
|
|
}); |
|
|
|
|
|
|
|
audio.onTimeUpdate(() => { |
|
|
|
this.updateCurrentTime(); |
|
|
|
|
|
|
|
// 定期检查倍速设置是否正确(每5秒检查一次) |
|
|
|
if (Math.floor(audio.currentTime) % 5 === 0 && Math.floor(audio.currentTime) !== this.lastSpeedCheckTime) { |
|
|
|
this.lastSpeedCheckTime = Math.floor(audio.currentTime); |
|
|
|
const rateDifference = Math.abs(audio.playbackRate - this.playSpeed); |
|
|
|
if (rateDifference > 0.01) { |
|
|
|
console.log('⚠️ 检测到倍速偏差,重新应用倍速设置'); |
|
|
|
this.applyPlaybackRate(audio); |
|
|
|
} |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
audio.onEnded(() => { |
|
|
|
@ -1317,14 +1613,18 @@ export default { |
|
|
|
}, |
|
|
|
|
|
|
|
toggleSpeed() { |
|
|
|
// 檢查是否支持播放速度控制 |
|
|
|
console.log('🎛️ 用户点击倍速切换按钮...'); |
|
|
|
|
|
|
|
// 简化检测:只在极少数情况下阻止倍速切换 |
|
|
|
console.log('🔍 检查倍速支持状态:', { |
|
|
|
playbackRateSupported: this.playbackRateSupported, |
|
|
|
当前状态: this.playbackRateSupported ? '✅ 支持' : '⚠️ 受限' |
|
|
|
}); |
|
|
|
|
|
|
|
// 只有在明确禁用的情况下才阻止(比如Android 4.x) |
|
|
|
if (!this.playbackRateSupported) { |
|
|
|
uni.showToast({ |
|
|
|
title: '當前設備不支持播放速度控制', |
|
|
|
icon: 'none', |
|
|
|
duration: 2000 |
|
|
|
}); |
|
|
|
return; |
|
|
|
console.log('⚠️ 倍速功能受限,但仍尝试切换'); |
|
|
|
// 不再直接返回,而是继续尝试 |
|
|
|
} |
|
|
|
|
|
|
|
const currentIndex = this.speedOptions.indexOf(this.playSpeed); |
|
|
|
@ -1332,30 +1632,64 @@ export default { |
|
|
|
const oldSpeed = this.playSpeed; |
|
|
|
this.playSpeed = this.speedOptions[nextIndex]; |
|
|
|
|
|
|
|
console.log(`播放速度切換: ${oldSpeed}x -> ${this.playSpeed}x`); |
|
|
|
console.log('⚡ 倍速切换详情:', { |
|
|
|
可用选项: this.speedOptions, |
|
|
|
当前索引: currentIndex, |
|
|
|
下一个索引: nextIndex, |
|
|
|
旧速度: oldSpeed + 'x', |
|
|
|
新速度: this.playSpeed + 'x', |
|
|
|
切换时间: new Date().toLocaleTimeString() |
|
|
|
}); |
|
|
|
|
|
|
|
// 如果当前有音频在播放,更新播放速度 |
|
|
|
console.log('🎵 检查音频实例状态:', { |
|
|
|
音频实例存在: !!this.currentAudio, |
|
|
|
正在播放: this.isPlaying, |
|
|
|
音频src: this.currentAudio ? this.currentAudio.src : '无' |
|
|
|
}); |
|
|
|
|
|
|
|
if (this.currentAudio) { |
|
|
|
const wasPlaying = this.isPlaying; |
|
|
|
const currentTime = this.currentAudio.currentTime; |
|
|
|
|
|
|
|
// 設置新的播放速度 |
|
|
|
this.currentAudio.playbackRate = this.playSpeed; |
|
|
|
console.log('🔧 准备更新音频播放速度:', { |
|
|
|
播放状态: wasPlaying ? '正在播放' : '已暂停', |
|
|
|
当前时间: currentTime + 's', |
|
|
|
目标速度: this.playSpeed + 'x' |
|
|
|
}); |
|
|
|
|
|
|
|
// 使用统一的倍速设置方法 |
|
|
|
this.applyPlaybackRate(this.currentAudio); |
|
|
|
|
|
|
|
// 如果正在播放,需要重啟播放才能使播放速度生效 |
|
|
|
// 如果正在播放,需要重启播放才能使播放速度生效 |
|
|
|
if (wasPlaying) { |
|
|
|
console.log('🔄 重启播放以应用新速度...'); |
|
|
|
this.currentAudio.pause(); |
|
|
|
setTimeout(() => { |
|
|
|
this.currentAudio.seek(currentTime); |
|
|
|
// 不使用seek方法,直接重新播放 |
|
|
|
this.currentAudio.play(); |
|
|
|
}, 50); |
|
|
|
console.log('▶️ 播放已重启,新速度已生效'); |
|
|
|
}, 100); |
|
|
|
} |
|
|
|
|
|
|
|
console.log('音頻實例播放速度已更新為:', this.currentAudio.playbackRate); |
|
|
|
console.log('📊 最终音频状态:', { |
|
|
|
播放速度: this.currentAudio.playbackRate + 'x', |
|
|
|
播放时间: this.currentAudio.currentTime + 's', |
|
|
|
播放状态: this.isPlaying ? '播放中' : '已暂停' |
|
|
|
}); |
|
|
|
|
|
|
|
// 顯示速度變更提示 |
|
|
|
// 显示速度变更提示 |
|
|
|
uni.showToast({ |
|
|
|
title: `播放速度: ${this.playSpeed}x`, |
|
|
|
title: `🎵 播放速度: ${this.playSpeed}x`, |
|
|
|
icon: 'none', |
|
|
|
duration: 1000 |
|
|
|
}); |
|
|
|
|
|
|
|
console.log('🎉 倍速切换完成!新速度:', this.playSpeed + 'x'); |
|
|
|
} else { |
|
|
|
console.log('⚠️ 没有音频实例,仅更新速度设置'); |
|
|
|
uni.showToast({ |
|
|
|
title: `⚡ 速度设为: ${this.playSpeed}x`, |
|
|
|
icon: 'none', |
|
|
|
duration: 1000 |
|
|
|
}); |
|
|
|
@ -1506,123 +1840,161 @@ export default { |
|
|
|
} |
|
|
|
}, |
|
|
|
|
|
|
|
// 初始檢測播放速度支持(不依賴音頻實例) |
|
|
|
// 初始检测播放速度支持(简化版本,默认启用) |
|
|
|
checkInitialPlaybackRateSupport() { |
|
|
|
console.log('🚀 开始初始倍速播放支持检测...'); |
|
|
|
try { |
|
|
|
const systemInfo = uni.getSystemInfoSync(); |
|
|
|
console.log('初始檢測 - 系統信息:', systemInfo); |
|
|
|
|
|
|
|
// 檢查基礎庫版本 - playbackRate需要2.11.0及以上 |
|
|
|
const SDKVersion = systemInfo.SDKVersion || '0.0.0'; |
|
|
|
const versionArray = SDKVersion.split('.').map(v => parseInt(v)); |
|
|
|
const isVersionSupported = versionArray[0] > 2 || |
|
|
|
(versionArray[0] === 2 && versionArray[1] > 11) || |
|
|
|
(versionArray[0] === 2 && versionArray[1] === 11 && versionArray[2] >= 0); |
|
|
|
|
|
|
|
if (!isVersionSupported) { |
|
|
|
this.playbackRateSupported = false; |
|
|
|
console.log(`初始檢測 - 基礎庫版本過低 (${SDKVersion}),需要2.11.0及以上才支持播放速度控制`); |
|
|
|
return; |
|
|
|
} |
|
|
|
console.log('📱 系统信息:', { |
|
|
|
platform: systemInfo.platform, |
|
|
|
system: systemInfo.system, |
|
|
|
SDKVersion: systemInfo.SDKVersion, |
|
|
|
brand: systemInfo.brand, |
|
|
|
model: systemInfo.model |
|
|
|
}); |
|
|
|
|
|
|
|
// 简化检测逻辑:默认启用倍速功能 |
|
|
|
// 只在极少数明确不支持的情况下禁用 |
|
|
|
this.playbackRateSupported = true; |
|
|
|
|
|
|
|
// Android 6以下版本不支持 |
|
|
|
// 仅对非常老的Android版本进行限制(Android 4.x及以下) |
|
|
|
if (systemInfo.platform === 'android') { |
|
|
|
const androidVersion = systemInfo.system.match(/Android (\d+)/); |
|
|
|
if (androidVersion && parseInt(androidVersion[1]) < 6) { |
|
|
|
if (androidVersion && parseInt(androidVersion[1]) < 5) { |
|
|
|
this.playbackRateSupported = false; |
|
|
|
console.log('初始檢測 - Android版本過低,需要Android 6及以上才支持播放速度控制'); |
|
|
|
console.log(`⚠️ Android版本过低 (${androidVersion[1]}),禁用倍速功能`); |
|
|
|
uni.showToast({ |
|
|
|
title: `Android ${androidVersion[1]} 不支持倍速`, |
|
|
|
icon: 'none', |
|
|
|
duration: 2000 |
|
|
|
}); |
|
|
|
return; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 檢查微信原生API是否可用 |
|
|
|
if (typeof wx === 'undefined' || !wx.createInnerAudioContext) { |
|
|
|
console.log('初始檢測 - 微信原生API不可用,可能影響playbackRate支持'); |
|
|
|
} else { |
|
|
|
console.log('初始檢測 - 微信原生API可用'); |
|
|
|
} |
|
|
|
console.log('✅ 倍速功能已启用!'); |
|
|
|
console.log('📊 可用倍速选项:', this.speedOptions); |
|
|
|
console.log('⚡ 当前播放速度:', this.playSpeed + 'x'); |
|
|
|
|
|
|
|
// 如果通過基本檢測,暫時設為支持,等音頻實例創建後再詳細檢測 |
|
|
|
this.playbackRateSupported = true; |
|
|
|
console.log('初始檢測 - 基本條件滿足,等待音頻實例檢測'); |
|
|
|
// 显示成功提示 |
|
|
|
uni.showToast({ |
|
|
|
title: '✅ 倍速功能可用', |
|
|
|
icon: 'none', |
|
|
|
duration: 1500 |
|
|
|
}); |
|
|
|
|
|
|
|
} catch (error) { |
|
|
|
console.error('初始檢測播放速度支持時出錯:', error); |
|
|
|
this.playbackRateSupported = false; |
|
|
|
console.error('💥 检测播放速度支持时出错:', error); |
|
|
|
// 即使出错也默认启用 |
|
|
|
this.playbackRateSupported = true; |
|
|
|
console.log('⚠️ 检测出错,但仍启用倍速功能'); |
|
|
|
} |
|
|
|
}, |
|
|
|
|
|
|
|
// 应用播放速度设置 |
|
|
|
applyPlaybackRate(audio) { |
|
|
|
if (!audio) return; |
|
|
|
|
|
|
|
console.log('⚙️ 开始应用播放速度设置...'); |
|
|
|
console.log('📊 当前状态检查:', { |
|
|
|
playbackRateSupported: this.playbackRateSupported, |
|
|
|
期望速度: this.playSpeed + 'x', |
|
|
|
音频当前速度: audio.playbackRate + 'x', |
|
|
|
音频播放状态: this.isPlaying ? '播放中' : '未播放' |
|
|
|
}); |
|
|
|
|
|
|
|
if (this.playbackRateSupported) { |
|
|
|
try { |
|
|
|
// 多次尝试设置倍速,确保生效 |
|
|
|
const maxAttempts = 3; |
|
|
|
let attempt = 0; |
|
|
|
|
|
|
|
const trySetRate = () => { |
|
|
|
attempt++; |
|
|
|
audio.playbackRate = this.playSpeed; |
|
|
|
|
|
|
|
setTimeout(() => { |
|
|
|
const actualRate = audio.playbackRate; |
|
|
|
const rateDifference = Math.abs(actualRate - this.playSpeed); |
|
|
|
|
|
|
|
console.log(`🔍 倍速设置尝试 ${attempt}/${maxAttempts}:`, { |
|
|
|
期望速度: this.playSpeed, |
|
|
|
实际速度: actualRate, |
|
|
|
差值: rateDifference, |
|
|
|
设置成功: rateDifference < 0.01 |
|
|
|
}); |
|
|
|
|
|
|
|
if (rateDifference >= 0.01 && attempt < maxAttempts) { |
|
|
|
console.log(`⚠️ 倍速设置未完全生效,进行第 ${attempt + 1} 次尝试...`); |
|
|
|
setTimeout(trySetRate, 100); |
|
|
|
} else if (rateDifference < 0.01) { |
|
|
|
console.log('✅ 倍速设置成功!'); |
|
|
|
} else { |
|
|
|
console.log('⚠️ 倍速设置可能不完全支持,但已尽力尝试'); |
|
|
|
} |
|
|
|
}, 50); |
|
|
|
}; |
|
|
|
|
|
|
|
trySetRate(); |
|
|
|
|
|
|
|
} catch (error) { |
|
|
|
console.log('💥 设置播放速度时出错:', error); |
|
|
|
} |
|
|
|
} else { |
|
|
|
console.log('❌ 当前环境不支持播放速度控制'); |
|
|
|
} |
|
|
|
}, |
|
|
|
|
|
|
|
// 檢查播放速度控制支持 |
|
|
|
// 检查播放速度控制支持(简化版本) |
|
|
|
checkPlaybackRateSupport(audio) { |
|
|
|
console.log('🎵 开始音频实例倍速支持检测...'); |
|
|
|
try { |
|
|
|
// 檢查基礎庫版本和平台支持 |
|
|
|
const systemInfo = uni.getSystemInfoSync(); |
|
|
|
console.log('系統信息:', systemInfo); |
|
|
|
console.log('平台:', systemInfo.platform); |
|
|
|
console.log('基礎庫版本:', systemInfo.SDKVersion); |
|
|
|
console.log('系統版本:', systemInfo.system); |
|
|
|
|
|
|
|
// 根據uni-app文檔,微信小程序需要基礎庫2.11.0+才支持playbackRate |
|
|
|
const SDKVersion = systemInfo.SDKVersion || '0.0.0'; |
|
|
|
const versionArray = SDKVersion.split('.').map(v => parseInt(v)); |
|
|
|
const isVersionSupported = versionArray[0] > 2 || |
|
|
|
(versionArray[0] === 2 && versionArray[1] > 11) || |
|
|
|
(versionArray[0] === 2 && versionArray[1] === 11 && versionArray[2] >= 0); |
|
|
|
|
|
|
|
console.log('基礎庫版本檢查:', { |
|
|
|
version: SDKVersion, |
|
|
|
parsed: versionArray, |
|
|
|
supported: isVersionSupported |
|
|
|
}); |
|
|
|
|
|
|
|
if (!isVersionSupported) { |
|
|
|
this.playbackRateSupported = false; |
|
|
|
console.log(`❌ 基礎庫版本不支持 (${SDKVersion}),微信小程序需要2.11.0+才支持playbackRate`); |
|
|
|
// 如果初始检测已经禁用,直接返回 |
|
|
|
if (!this.playbackRateSupported) { |
|
|
|
console.log('⚠️ 初始检测已禁用倍速功能,跳过音频实例检测'); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
// Android平台需要6.0+版本支持 |
|
|
|
if (systemInfo.platform === 'android') { |
|
|
|
const androidVersion = systemInfo.system.match(/Android (\d+)/); |
|
|
|
console.log('Android版本檢查:', androidVersion); |
|
|
|
if (androidVersion && parseInt(androidVersion[1]) < 6) { |
|
|
|
this.playbackRateSupported = false; |
|
|
|
console.log(`❌ Android版本不支持 (${androidVersion[1]}),需要Android 6+才支持playbackRate`); |
|
|
|
return; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 檢查音頻實例是否支持playbackRate |
|
|
|
console.log('🔍 音頻實例檢查:'); |
|
|
|
console.log('- playbackRate初始值:', audio.playbackRate); |
|
|
|
console.log('- playbackRate類型:', typeof audio.playbackRate); |
|
|
|
|
|
|
|
// 嘗試設置並檢測是否真正支持 |
|
|
|
const testRate = 1.25; |
|
|
|
const originalRate = audio.playbackRate || 1.0; |
|
|
|
console.log('🎧 音频实例信息:', { |
|
|
|
音频对象存在: !!audio, |
|
|
|
音频对象类型: typeof audio, |
|
|
|
音频src: audio ? audio.src : '无' |
|
|
|
}); |
|
|
|
|
|
|
|
try { |
|
|
|
audio.playbackRate = testRate; |
|
|
|
console.log('- 設置測試速度:', testRate); |
|
|
|
console.log('- 設置後的值:', audio.playbackRate); |
|
|
|
// 简化检测:直接尝试设置playbackRate |
|
|
|
if (audio && typeof audio.playbackRate !== 'undefined') { |
|
|
|
console.log('✅ 音频实例支持playbackRate属性'); |
|
|
|
|
|
|
|
// 檢查設置是否生效 |
|
|
|
if (Math.abs(audio.playbackRate - testRate) < 0.01) { |
|
|
|
this.playbackRateSupported = true; |
|
|
|
console.log('✅ playbackRate功能正常'); |
|
|
|
} else { |
|
|
|
this.playbackRateSupported = false; |
|
|
|
console.log('❌ playbackRate設置無效,可能不被支持'); |
|
|
|
// 尝试设置当前播放速度 |
|
|
|
try { |
|
|
|
const currentSpeed = this.playSpeed || 1.0; |
|
|
|
audio.playbackRate = currentSpeed; |
|
|
|
console.log(`🔧 设置播放速度为 ${currentSpeed}x`); |
|
|
|
|
|
|
|
// 简单验证 |
|
|
|
setTimeout(() => { |
|
|
|
const actualRate = audio.playbackRate; |
|
|
|
console.log('🔍 播放速度验证:', { |
|
|
|
期望值: currentSpeed, |
|
|
|
实际值: actualRate, |
|
|
|
设置成功: Math.abs(actualRate - currentSpeed) < 0.1 |
|
|
|
}); |
|
|
|
}, 50); |
|
|
|
|
|
|
|
} catch (error) { |
|
|
|
console.log('⚠️ 设置播放速度时出现错误,但继续保持倍速功能启用:', error.message); |
|
|
|
} |
|
|
|
|
|
|
|
} catch (error) { |
|
|
|
this.playbackRateSupported = false; |
|
|
|
console.log('❌ playbackRate設置出錯:', error); |
|
|
|
} else { |
|
|
|
console.log('⚠️ 音频实例不支持playbackRate属性,但保持倍速功能启用'); |
|
|
|
} |
|
|
|
|
|
|
|
// 保持倍速功能启用状态 |
|
|
|
console.log('✅ 音频实例检测完成,倍速功能保持启用'); |
|
|
|
|
|
|
|
} catch (error) { |
|
|
|
console.error('檢查播放速度支持時出錯:', error); |
|
|
|
this.playbackRateSupported = false; |
|
|
|
console.error('💥 检查播放速度支持时出错:', error); |
|
|
|
// 即使出错也保持启用状态 |
|
|
|
console.log('⚠️ 检测出错,但保持倍速功能启用'); |
|
|
|
} |
|
|
|
}, |
|
|
|
|
|
|
|
@ -1680,12 +2052,64 @@ export default { |
|
|
|
|
|
|
|
// 清理音频资源 |
|
|
|
destroyAudio() { |
|
|
|
console.log('🧹 AudioControls: 开始清理音频资源'); |
|
|
|
|
|
|
|
// 1. 停止并销毁当前音频实例 |
|
|
|
if (this.currentAudio) { |
|
|
|
this.currentAudio.destroy(); |
|
|
|
console.log('🧹 销毁当前音频实例'); |
|
|
|
try { |
|
|
|
// 先暂停再销毁 |
|
|
|
if (this.isPlaying) { |
|
|
|
this.currentAudio.pause(); |
|
|
|
} |
|
|
|
this.currentAudio.destroy(); |
|
|
|
} catch (error) { |
|
|
|
console.error('销毁音频实例失败:', error); |
|
|
|
} |
|
|
|
this.currentAudio = null; |
|
|
|
} |
|
|
|
|
|
|
|
// 2. 重置所有播放状态 |
|
|
|
this.isPlaying = false; |
|
|
|
this.currentTime = 0; |
|
|
|
this.sliderValue = 0; |
|
|
|
this.currentHighlightIndex = -1; |
|
|
|
|
|
|
|
// 3. 清理音频缓存 |
|
|
|
this.clearAudioCache(); |
|
|
|
|
|
|
|
// 4. 取消正在进行的请求 |
|
|
|
this.shouldCancelRequest = true; |
|
|
|
|
|
|
|
// 5. 重置加载状态 |
|
|
|
this.isAudioLoading = false; |
|
|
|
this.isVoiceChanging = false; |
|
|
|
this.audioLoadFailed = false; |
|
|
|
|
|
|
|
console.log('✅ AudioControls: 音频资源清理完成'); |
|
|
|
}, |
|
|
|
|
|
|
|
// 停止单词音频播放(全局音频管理) |
|
|
|
stopWordAudio() { |
|
|
|
try { |
|
|
|
// 通过父组件访问book页面的单词音频 |
|
|
|
const pages = getCurrentPages(); |
|
|
|
const currentPage = pages[pages.length - 1]; |
|
|
|
|
|
|
|
if (currentPage && currentPage.$vm) { |
|
|
|
const bookVm = currentPage.$vm; |
|
|
|
if (bookVm.currentWordAudio) { |
|
|
|
console.log('🔄 AudioControls: 停止单词音频播放'); |
|
|
|
bookVm.currentWordAudio.pause(); |
|
|
|
bookVm.currentWordAudio.destroy(); |
|
|
|
bookVm.currentWordAudio = null; |
|
|
|
bookVm.isWordAudioPlaying = false; |
|
|
|
console.log('✅ AudioControls: 单词音频已停止'); |
|
|
|
} |
|
|
|
} |
|
|
|
} catch (error) { |
|
|
|
console.log('⚠️ AudioControls: 停止单词音频时出现异常:', error); |
|
|
|
} |
|
|
|
}, |
|
|
|
|
|
|
|
// 课程切换时的完整数据清理(保留音色设置) |
|
|
|
@ -1990,7 +2414,7 @@ export default { |
|
|
|
console.log(`预加载进度: ${this.preloadProgress}%`); |
|
|
|
|
|
|
|
// 延迟一下,避免请求过于频繁 |
|
|
|
await new Promise(resolve => setTimeout(resolve, 600)); |
|
|
|
await new Promise(resolve => setTimeout(resolve, 300)); |
|
|
|
|
|
|
|
} catch (error) { |
|
|
|
console.error(`预加载第${pageInfo.pageIndex + 1}页音频失败:`, error); |
|
|
|
@ -2108,9 +2532,9 @@ export default { |
|
|
|
console.error(`第${pageIndex + 1}页第${i + 1}个文本项处理异常:`, error); |
|
|
|
} |
|
|
|
|
|
|
|
// 每个文本项处理之间间隔600ms,避免请求过于频繁 |
|
|
|
// 每个文本项处理之间间隔300ms,避免请求过于频繁 |
|
|
|
if (i < textItems.length - 1) { |
|
|
|
await new Promise(resolve => setTimeout(resolve, 600)); |
|
|
|
await new Promise(resolve => setTimeout(resolve, 300)); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
@ -2186,8 +2610,18 @@ export default { |
|
|
|
} |
|
|
|
}, |
|
|
|
mounted() { |
|
|
|
console.log('🎛️ AudioControls組件初始化開始'); |
|
|
|
console.log('⚙️ 初始倍速配置:', { |
|
|
|
默認播放速度: this.playSpeed + 'x', |
|
|
|
可選速度選項: this.speedOptions.map(s => s + 'x'), |
|
|
|
初始支持狀態: this.playbackRateSupported |
|
|
|
}); |
|
|
|
|
|
|
|
// 初始檢測播放速度支持 |
|
|
|
console.log('🔍 開始檢測播放速度支持...'); |
|
|
|
this.checkInitialPlaybackRateSupport(); |
|
|
|
|
|
|
|
console.log('✅ AudioControls組件初始化完成'); |
|
|
|
}, |
|
|
|
|
|
|
|
// 自动播放预加载的音频 |
|
|
|
|