tts音频播放(uniapp-h5-vue2)
ps:第二次播放不用请求接口版本,拿取缓存数据
页面:
<audioPlay ref="audioPlay" @listenOver="listenOver"></audioPlay>
listen(item) {
if(this.isListen) return;
this.isListen = true;
this.$refs.audioPlay.listen(item)
}
listenOver(){
this.isListen = false;
},
封装的组件:
<template>
<view>
<!-- 无UI元素 -->
</view>
</template>
<script>
export default {
data() {
return {
innerAudioContext: null,
audioList: [],
currentIndex: 0,
TTS_API: '/otherApi/api/tts',
MALE_VOICE_ID: "Andrew Chipper",
FEMALE_VOICE_ID: "Claribel Dervla",
isPlaying: false,
dingAudio: null,
currentItem: null,
audioCache: {} // 音频缓存
}
},
created() {
this.initAudio();
this.dingAudio = uni.createInnerAudioContext();
this.dingAudio.src = '/static/ding.mp3';
},
methods: {
initAudio() {
if (this.innerAudioContext) {
this.innerAudioContext.destroy();
}
this.innerAudioContext = uni.createInnerAudioContext();
this.innerAudioContext.onEnded(() => {
this.playNext();
});
this.innerAudioContext.onError((res) => {
console.error('音频播放错误:', res);
this.handlePlayError();
});
},
async listen(item) {
console.log(item.topicContentJson.listenContent)
try {
if (this.isPlaying) {
this.stopPlayback();
await this.delay(200);
}
this.currentItem = item;
uni.showLoading({
title: "音频加载中"
});
const speechParts = this.processTextToSpeech(item.topicContentJson.listenContent);
this.audioList = await this.prepareAudioList(speechParts);
this.currentIndex = 0;
this.isPlaying = true;
await this.startPlayback();
} catch (error) {
console.error('播放初始化失败:', error);
this.handlePlayError();
uni.hideLoading();
} finally {
uni.hideLoading();
}
},
async prepareAudioList(speechParts) {
const result = [];
for (const part of speechParts) {
const cacheKey = `${part.text}-${part.isMale}`;
if (!this.audioCache[cacheKey]) {
// 使用uni.request获取音频
const audioUrl = await this.getTTSAudio(part.text, part.isMale);
this.audioCache[cacheKey] = audioUrl;
}
result.push({
url: this.audioCache[cacheKey],
text: part.text
});
}
return result;
},
// 使用uni.request获取TTS音频
getTTSAudio(text, isMale) {
return new Promise((resolve, reject) => {
uni.request({
url: this.TTS_API,
method: 'GET',
data: {
text: text,
speaker_id: isMale ? this.MALE_VOICE_ID : this.FEMALE_VOICE_ID,
language_id: "en",
style_wav: ""
},
responseType: 'arraybuffer', // 重要:获取二进制数据
success: (res) => {
if (res.statusCode === 200) {
// H5环境处理
const blob = new Blob([res.data], { type: 'audio/mp3' });
const audioUrl = URL.createObjectURL(blob);
resolve(audioUrl);
} else {
reject(new Error(`TTS请求失败,状态码: ${res.statusCode}`));
}
},
fail: (err) => {
reject(err);
}
});
});
},
async startPlayback() {
try {
await this.playDingSound();
await this.delay(100);
await this.playCurrentAudio();
} catch (error) {
console.error('播放流程错误:', error);
throw error;
}
},
playCurrentAudio() {
return new Promise((resolve, reject) => {
if (this.currentIndex >= this.audioList.length || !this.isPlaying) {
this.isPlaying = false;
resolve();
return;
}
const currentAudio = this.audioList[this.currentIndex];
// console.log('播放音频:', currentAudio.text);
this.innerAudioContext.src = currentAudio.url;
this.innerAudioContext.play();
this.innerAudioContext.onEnded(() => {
if (this.currentIndex >= this.audioList.length || !this.isPlaying){
console.log('音频自然结束');
this.$emit("listenOver");
}
});
const playTimer = setTimeout(() => {
reject(new Error('音频播放超时'));
}, 10000);
const playSuccess = () => {
clearTimeout(playTimer);
resolve();
};
this.innerAudioContext.onPlay(playSuccess);
this.innerAudioContext.onError((err) => {
clearTimeout(playTimer);
reject(err);
});
});
},
async playNext() {
this.currentIndex++;
if (this.currentIndex < this.audioList.length) {
await this.delay(400);
await this.playCurrentAudio();
} else {
this.isPlaying = false;
}
},
stopPlayback() {
this.isPlaying = false;
if (this.innerAudioContext) {
this.innerAudioContext.stop();
}
this.audioList = [];
this.currentIndex = 0;
},
playDingSound() {
return new Promise((resolve) => {
this.dingAudio.stop();
this.dingAudio.play();
this.dingAudio.onEnded(resolve);
this.dingAudio.onError(resolve);
});
},
handlePlayError() {
this.isPlaying = false;
uni.showToast({
title: '音频播放失败,请稍后再试',
icon: 'none'
});
},
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
},
// 文本处理函数保持不变
processTextToSpeech(text) {
const lines = text.split(/\n|<br>|\/br>/)
.map(line => line.trim())
.filter(line => line.length > 0);
// 判断是否为对话模式(检测是否有角色标记)
const isDialogue = lines.some(line =>
/^(M|m|W|w|男|女|Male|Female)[::]/.test(line)
);
if (!isDialogue) {
return [{ text: lines.join(' '), isMale: false }];
}
const normalizedText = text.replace(/<br>|\/br>/g, '\n');
// 2. 终极正则表达式:精确匹配角色和完整内容
const dialogueRegex = /([MW男女])\s*[::]\s*([\s\S]*?)(?=\n?[MW男女]\s*[::]|$)/gi;
const parts = [];
let lastIndex = 0;
let currentSpeaker = null;
// 3. 遍历所有对话段落
while (true) {
const match = dialogueRegex.exec(normalizedText);
if (!match) break;
const [fullMatch, role, content] = match;
const isMale = /^[M男]/i.test(role);
currentSpeaker = isMale ? 'M' : 'W';
// 4. 处理匹配前的文本(如果有)
if (match.index > lastIndex) {
const betweenText = normalizedText.slice(lastIndex, match.index).trim();
if (betweenText && currentSpeaker) {
parts.push(...this.splitCompleteSentences(betweenText, isMale));
}
}
// 5. 处理当前内容(确保完整保留)
if (content.trim()) {
parts.push(...this.splitCompleteSentences(content.trim(), isMale));
}
lastIndex = dialogueRegex.lastIndex;
}
// 6. 处理最后一段文本
if (lastIndex < normalizedText.length) {
const remainingText = normalizedText.slice(lastIndex).trim();
if (remainingText && currentSpeaker) {
parts.push(...this.splitCompleteSentences(remainingText, currentSpeaker === 'M'));
}
}
return parts;
},
splitCompleteSentences(text, isMale) {
const sentences = text.match(/[^.!?]+[.!?]*/g) || [text];
return sentences
.map(s => s.trim())
.filter(s => s.length > 0)
.map(sentence => ({
text: sentence,
isMale
}));
}
},
beforeDestroy() {
// 释放音频资源
if (this.innerAudioContext) {
this.innerAudioContext.destroy();
}
if (this.dingAudio) {
this.dingAudio.destroy();
}
// 释放Blob URL
Object.values(this.audioCache).forEach(url => {
URL.revokeObjectURL(url);
});
}
}
</script>
更多推荐


所有评论(0)