微信小程序 SpeechSynthesizer基本使用方法
微信小程序 SpeechSynthesizer 實(shí)戰(zhàn)指南
一、引言
在移動(dòng)應(yīng)用開發(fā)中,文本轉(zhuǎn)語(yǔ)音(TTS)功能可以極大地提升用戶體驗(yàn),尤其是在需要解放雙手的場(chǎng)景下。微信小程序提供了強(qiáng)大的 SpeechSynthesizer API,讓開發(fā)者可以輕松實(shí)現(xiàn)高質(zhì)量的語(yǔ)音合成功能。本文將結(jié)合實(shí)際項(xiàng)目,詳細(xì)介紹微信小程序 SpeechSynthesizer 的使用方法和最佳實(shí)踐。
二、SpeechSynthesizer 簡(jiǎn)介
SpeechSynthesizer 是微信小程序提供的文本轉(zhuǎn)語(yǔ)音 API,它基于騰訊云的語(yǔ)音合成技術(shù),可以將文本轉(zhuǎn)換為自然流暢的語(yǔ)音。該 API 支持多種語(yǔ)言、音色和語(yǔ)速調(diào)節(jié),滿足不同場(chǎng)景的需求。
主要特點(diǎn)
- 高質(zhì)量語(yǔ)音合成 :基于騰訊云的先進(jìn)語(yǔ)音合成技術(shù),提供自然流暢的語(yǔ)音輸出。
- 多語(yǔ)言支持 :支持中文、英文等多種語(yǔ)言。
- 豐富的音色選擇 :提供多種音色供選擇,包括男聲、女聲等。
- 靈活的參數(shù)調(diào)節(jié) :可以調(diào)節(jié)語(yǔ)速、音量、音調(diào)等參數(shù)。
- 實(shí)時(shí)合成 :支持實(shí)時(shí)將文本轉(zhuǎn)換為語(yǔ)音,無(wú)需等待。
三、基本使用方法
1. 創(chuàng)建 SpeechSynthesizer 實(shí)例
// 初始化語(yǔ)音合成實(shí)例
this.ttsInstance = new SpeechSynthesizer({
volume: 1.0, // 音量,范圍 0-1
rate: 1.0, // 語(yǔ)速,范圍 0.5-2.0
pitch: 1.0, // 音調(diào),范圍 0.5-2.0
language: 'zh-CN', // 語(yǔ)言,支持 'zh-CN'、'en-US' 等
voiceName: 'xiaoyan' // 音色,支持 'xiaoyan'、'xiaoyu' 等
});
2. 合成并播放語(yǔ)音
// 合成并播放語(yǔ)音
this.ttsInstance.speak({
text: '歡迎使用微信小程序 SpeechSynthesizer',
success: () => {
console.log('語(yǔ)音播放成功');
},
fail: (err) => {
console.error('語(yǔ)音播放失敗', err);
}
});
3. 暫停和繼續(xù)播放
// 暫停播放 this.ttsInstance.pause(); // 繼續(xù)播放 this.ttsInstance.resume();
4. 停止播放
// 停止播放 this.ttsInstance.stop();
四、高級(jí)應(yīng)用
1. 實(shí)時(shí)語(yǔ)音合成
在聊天應(yīng)用中,我們可以實(shí)時(shí)將用戶輸入的文本轉(zhuǎn)換為語(yǔ)音:
// 監(jiān)聽用戶輸入
onInputChange(e) {
const text = e.detail.value;
// 實(shí)時(shí)合成語(yǔ)音
this.ttsInstance.speak({
text: text,
success: () => {
console.log('語(yǔ)音合成成功');
}
});
}
2. 多段文本合成
在需要合成多段文本時(shí),可以使用 queue 方法:
// 合成多段文本
this.ttsInstance.queue([
{ text: '第一段文本' },
{ text: '第二段文本' },
{ text: '第三段文本' }
]);
3. 自定義音色和語(yǔ)速
根據(jù)不同的場(chǎng)景,我們可以自定義音色和語(yǔ)速:
// 設(shè)置音色為男聲
this.ttsInstance.setVoiceName('xiaoyu');
// 設(shè)置語(yǔ)速為慢速
this.ttsInstance.setRate(0.7);
// 設(shè)置音量為最大
this.ttsInstance.setVolume(1.0);五、常見問(wèn)題及解決方案
1. 語(yǔ)音合成失敗
問(wèn)題描述 :調(diào)用 speak 方法時(shí),返回失敗。
解決方案 :
- 檢查網(wǎng)絡(luò)連接是否正常。
- 檢查文本內(nèi)容是否過(guò)長(zhǎng)(建議不超過(guò) 500 字)。
- 檢查參數(shù)設(shè)置是否正確。
2. 語(yǔ)音播放不流暢
問(wèn)題描述 :語(yǔ)音播放時(shí)出現(xiàn)卡頓或斷句不自然。
解決方案 :
- 檢查網(wǎng)絡(luò)連接是否穩(wěn)定。
- 調(diào)整語(yǔ)速參數(shù),適當(dāng)降低語(yǔ)速。
- 分割長(zhǎng)文本,分段合成。
3. 音量調(diào)節(jié)無(wú)效
問(wèn)題描述 :設(shè)置音量后,語(yǔ)音播放音量沒(méi)有變化。
解決方案 :
- 檢查音量參數(shù)是否在 0-1 范圍內(nèi)。
- 檢查設(shè)備音量是否設(shè)置正確。
六、實(shí)戰(zhàn)案例:智能語(yǔ)音助手
下面我們將結(jié)合實(shí)際項(xiàng)目,實(shí)現(xiàn)一個(gè)智能語(yǔ)音助手功能:
1. 初始化語(yǔ)音合成實(shí)例(復(fù)制可用)
// utils/ttsUtil.js
const SpeechSynthesizer = require("../../components/alibabacloud-nls-wx-sdk-master/utils/tts")
const fs = wx.getFileSystemManager();
// 格式化時(shí)間(工具函數(shù))
function formatTime(date) {
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const day = date.getDate().toString().padStart(2, '0');
const hour = date.getHours().toString().padStart(2, '0');
const minute = date.getMinutes().toString().padStart(2, '0');
const second = date.getSeconds().toString().padStart(2, '0');
return `${year}${month}${day}${hour}${minute}${second}`;
}
// 休眠(工具函數(shù))
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
class TTSUtil {
constructor(config) {
this.config = config;
this.ttsInstance = null;
this.isPlaying = false;
this.currentAudioCtx = null;
this.dataInfos = { saveFile: null, saveFd: null, ttsStart: false };
this.audioTaskList = [];
this.textQueue = []; // 待合成的文本隊(duì)列
this.isProcessingQueue = false; // 是否正在處理隊(duì)列
}
// 初始化TTS(修復(fù):增加錯(cuò)誤捕獲,確保事件監(jiān)聽生效)
initTTS() {
return new Promise((resolve, reject) => {
try {
if (this.ttsInstance) {
resolve(true);
return;
}
this.ttsInstance = new SpeechSynthesizer(this.config);
console.log("[TTSUtil] 實(shí)例初始化成功");
// 監(jiān)聽音頻數(shù)據(jù)(確保二進(jìn)制數(shù)據(jù)寫入文件)
this.ttsInstance.on("data", (binaryData) => {
if (this.dataInfos.saveFile && this.dataInfos.saveFd) {
try {
// 小程序中position傳-1等價(jià)于SEEK_END
fs.write({
fd: this.dataInfos.saveFd,
data: binaryData,
position: -1,
encoding: "binary",
success: () => {
console.log(`[TTSUtil] 寫入音頻數(shù)據(jù):${binaryData.byteLength}字節(jié)`);
},
fail: (e) => {
console.error("[TTSUtil] 寫入音頻數(shù)據(jù)失?。?, e);
}
});
} catch (e) {
console.error("[TTSUtil] 寫入音頻數(shù)據(jù)異常:", e);
}
}
});
this.ttsInstance.on("completed", async (res) => {
console.log("[TTSUtil] 合成完成回調(diào)觸發(fā)", res);
await sleep(800); // 延長(zhǎng)等待,確保數(shù)據(jù)完全寫入文件
if (this.dataInfos.saveFd) {
// 改用異步close
fs.close({
fd: this.dataInfos.saveFd,
success: () => {
console.log("[TTSUtil] 文件已異步關(guān)閉");
},
fail: (err) => {
console.error("[TTSUtil] 關(guān)閉文件失?。?, err);
}
});
this.dataInfos.saveFd = null;
}
const taskItem = this.audioTaskList.find(item => item.filePath === this.dataInfos.saveFile);
if (taskItem) taskItem.status = "completed";
this.clearCurrentFileResource();
// 修復(fù):確保任務(wù)項(xiàng)存在才播放
if (taskItem) {
this.playCurrentTaskAndNext(taskItem);
} else {
this.processNextQueueItem(); // 無(wú)任務(wù)項(xiàng)則直接處理下一個(gè)
}
});
this.ttsInstance.on("failed", (err) => {
console.error("[TTSUtil] 合成失?。?, err);
const taskItem = this.audioTaskList.find(item => item.filePath === this.dataInfos.saveFile);
if (taskItem) {
taskItem.status = "failed";
taskItem.error = err;
}
this.clearCurrentFileResource();
this.processNextQueueItem(); // 失敗后繼續(xù)處理下一個(gè)
});
resolve(true);
} catch (err) {
console.error("[TTSUtil] 初始化失敗:", err);
reject(false);
}
});
}
// 向隊(duì)列中添加文本(外部調(diào)用)
addToQueue(text, customParams = {}) {
if (!this.ttsInstance) {
uni.showToast({ title: "請(qǐng)先初始化TTS", icon: "none" });
return;
}
const texts = Array.isArray(text) ? text : [text];
texts.forEach(t => {
this.textQueue.push({ text: t, params: customParams });
console.log(`[TTSUtil] 文本加入隊(duì)列:${t}`);
});
// 修復(fù):隊(duì)列未處理時(shí),立即啟動(dòng)(增加防抖,避免重復(fù)觸發(fā))
if (!this.isProcessingQueue && !this.isPlaying) {
this.processNextQueueItem();
}
}
// 處理隊(duì)列的下一個(gè)文本(修復(fù):確保異步執(zhí)行順序)
async processNextQueueItem() {
if (this.textQueue.length === 0) {
this.isProcessingQueue = false;
console.log("[TTSUtil] 隊(duì)列已空,停止處理");
return;
}
this.isProcessingQueue = true;
const queueItem = this.textQueue.shift();
console.log(`[TTSUtil] 開始處理隊(duì)列文本:${queueItem.text}`);
// 修復(fù):統(tǒng)一用wav格式,提升兼容性
const format = queueItem.params.format || "wav";
const task = {
id: this.audioTaskList.length + 1,
text: queueItem.text,
filePath: `${wx.env.USER_DATA_PATH}/${formatTime(new Date())}_${this.audioTaskList.length + 1}.${format}`,
status: "pending",
params: queueItem.params,
error: null
};
this.audioTaskList.push(task);
// 等待合成完成(修復(fù):await確保合成完成后再執(zhí)行后續(xù))
await this.synthesizeSingleAudio(task, queueItem.params);
}
// 播放當(dāng)前任務(wù) + 自動(dòng)處理下一個(gè)(核心修復(fù):確保音頻播放觸發(fā))
async playCurrentTaskAndNext(taskItem) {
if (!taskItem || !taskItem.filePath) {
this.processNextQueueItem();
return;
}
console.log(`[TTSUtil] 準(zhǔn)備播放:${taskItem.filePath}`);
// 修復(fù):先檢查文件是否存在
try {
fs.accessSync(taskItem.filePath);
} catch (e) {
console.error("[TTSUtil] 音頻文件不存在:", e);
this.processNextQueueItem();
return;
}
// 播放當(dāng)前音頻,等待播放完成后處理下一個(gè)
const playSuccess = await this.playAudioFile(taskItem.filePath);
console.log(`[TTSUtil] 當(dāng)前音頻播放${playSuccess ? "完成" : "失敗"}`);
// 無(wú)論播放成功/失敗,都處理下一個(gè)隊(duì)列項(xiàng)
this.processNextQueueItem();
}
// 合成單個(gè)音頻(修復(fù):等待tts.start真正完成,確保合成啟動(dòng))
synthesizeSingleAudio(task, customParams = {}) {
return new Promise((resolve) => {
// 修復(fù):先清空當(dāng)前文件資源,避免殘留
this.clearCurrentFileResource();
// 統(tǒng)一用wav格式,提升兼容性
const format = customParams.format || "wav";
fs.open({
filePath: task.filePath,
flag: "w+", // 修復(fù):用w+替代a+,確保文件重新創(chuàng)建
success: async (res) => {
this.dataInfos.saveFd = res.fd;
this.dataInfos.saveFile = task.filePath;
console.log(`[TTSUtil] 打開文件成功:${task.filePath}`);
const playParams = {
text: task.text,
voice: customParams.voice || "zhistella",
format: format, // 強(qiáng)制用wav
sample_rate: customParams.sampleRate || 16000,
volume: customParams.volume || 100,
speech_rate: customParams.speechRate || 0,
pitch_rate: customParams.pitchRate || 0,
enable_subtitle: false
};
try {
// 修復(fù):await確保start執(zhí)行完成
await this.ttsInstance.start(playParams);
task.status = "synthesizing";
console.log(`[TTSUtil] 開始合成文本:${task.text}`);
// 不立即resolve,等待合成完成(由completed/failed回調(diào)處理)
// 這里resolve僅標(biāo)記合成啟動(dòng),不影響后續(xù)流程
resolve(true);
} catch (e) {
console.error("[TTSUtil] 合成啟動(dòng)失敗:", e);
task.status = "failed";
task.error = e;
this.clearCurrentFileResource();
resolve(false);
}
},
fail: (err) => {
console.error(`[TTSUtil] 打開文件失?。?{err.errMsg}`);
task.status = "failed";
task.error = err;
resolve(false);
}
});
});
}
// 播放單個(gè)音頻(核心修復(fù):確保自動(dòng)播放生效)
playAudioFile(filePath) {
return new Promise((resolve) => {
// 停止當(dāng)前播放的音頻
this.stopCurrentAudio();
// 增加文件路徑空值校驗(yàn)
if (!filePath) {
console.error("[TTSUtil] 音頻路徑為空");
resolve(false);
return;
}
// 創(chuàng)建音頻上下文
this.currentAudioCtx = wx.createInnerAudioContext();
if (!this.currentAudioCtx) {
console.error("[TTSUtil] 音頻上下文創(chuàng)建失敗");
resolve(false);
return;
}
this.currentAudioCtx.src = filePath;
this.isPlaying = true;
// 監(jiān)聽音頻加載完成
this.currentAudioCtx.onCanplay(() => {
console.log(`[TTSUtil] 音頻加載完成,開始播放:${filePath}`);
// 直接調(diào)用play(同步方法,無(wú)返回值),通過(guò)onError捕獲錯(cuò)誤
this.currentAudioCtx.play();
});
this.currentAudioCtx.onPlay(() => {
console.log(`[TTSUtil] 音頻開始播放:${filePath}`);
});
// 用onError替代catch捕獲播放錯(cuò)誤
this.currentAudioCtx.onError((err) => {
console.error("[TTSUtil] 播放錯(cuò)誤:", err);
this.isPlaying = false;
this.deleteFile(filePath);
resolve(false);
});
this.currentAudioCtx.onEnded(() => {
console.log(`[TTSUtil] 音頻播放完成:${filePath}`);
this.isPlaying = false;
this.deleteFile(filePath);
this.audioTaskList = this.audioTaskList.filter(item => item.filePath !== filePath);
resolve(true);
});
// 超時(shí)兜底:5秒未播放則判定失敗
setTimeout(() => {
if (this.isPlaying && !this.currentAudioCtx?.paused) return;
console.error(`[TTSUtil] 音頻播放超時(shí):${filePath}`);
this.isPlaying = false;
this.deleteFile(filePath);
resolve(false);
}, 5000);
});
}
// 停止當(dāng)前音頻(保持不變)
stopCurrentAudio() {
if (this.currentAudioCtx) {
try {
this.currentAudioCtx.stop();
this.currentAudioCtx.destroy();
} catch (e) { }
this.currentAudioCtx = null;
}
this.isPlaying = false;
}
// 停止所有(保持不變)
stopAll() {
this.stopCurrentAudio();
if (this.ttsInstance) {
this.ttsInstance.shutdown();
}
this.clearCurrentFileResource();
this.textQueue = [];
this.isProcessingQueue = false;
console.log("[TTSUtil] 已停止所有合成/播放,清空隊(duì)列");
}
// 清理文件資源(保持不變)
clearCurrentFileResource() {
if (this.dataInfos.saveFd) {
// 改用異步close
fs.close({
fd: this.dataInfos.saveFd,
success: () => {
console.log("[TTSUtil] 文件句柄已關(guān)閉");
},
fail: (e) => {
console.error("[TTSUtil] 關(guān)閉文件句柄失?。?, e);
}
});
this.dataInfos.saveFd = null;
}
this.dataInfos.saveFile = null;
this.dataInfos.ttsStart = false;
}
// 刪除文件(保持不變)
deleteFile(filePath) {
try {
fs.unlinkSync(filePath);
console.log(`[TTSUtil] 刪除文件:${filePath}`);
} catch (e) {
console.error(`[TTSUtil] 刪除文件失敗:${e}`);
}
}
// 銷毀資源(保持不變)
destroy() {
this.stopAll();
this.audioTaskList.forEach(task => {
if (task.filePath) this.deleteFile(task.filePath);
});
this.audioTaskList = [];
this.ttsInstance = null;
}
// 新增:獲取隊(duì)列長(zhǎng)度(方便調(diào)試)
getQueueLength() {
return this.textQueue.length;
}
}
// 單例導(dǎo)出
let ttsInstance = null;
export function getTTSUtil(config) {
if (!ttsInstance) {
ttsInstance = new TTSUtil(config);
}
return ttsInstance;
}
export default TTSUtil;2. 在頁(yè)面中使用
// index.vue
import ttsAudio from './ttsAudio.js';
export default {
data() {
return {
inputText: '',
ttsConfig: {
appkey: "xxxxxxxxxx", // 你的appkey
token: "你的阿里云語(yǔ)音合成token", // 你的token(需通過(guò)appkey+secret換?。┲苯幼尯笈_(tái)寫接口返回
url: "wss://nls-gateway.cn-shanghai.aliyuncs.com/ws/v1" // 阿里云語(yǔ)音合成服務(wù)地址
},
};
},
methods: {
// 播放語(yǔ)音
// 初始化TTS實(shí)例
async initTTS() {
this.ttsUtil = getTTSUtil(this.ttsConfig);
await this.ttsUtil.initTTS();
// 調(diào)用
let text = '一只穿著京劇戲服的可愛的灰白臨清獅貓,擬人化,現(xiàn)實(shí)風(fēng)格,瘋狂的細(xì)節(jié),毛茸茸的,超清晰的毛發(fā),大眼睛,全身照,在戲臺(tái)上耍著戲曲里的花槍,擺著動(dòng)作表演,對(duì)著鏡頭,Q版超萌,超高清,杰作'
this.ttsUtil.addToQueue(text, {
voice: "zhistella",
speechRate: 0
});
// res ? uni.showToast({ title: "TTS初始化成功", icon: "success" }) : uni.showToast({ title: "初始化失敗", icon: "none" });
},
// 暫停播放
pauseVoice() {
ttsAudio.pause();
},
// 繼續(xù)播放
resumeVoice() {
ttsAudio.resume();
},
// 停止播放
stopVoice() {
ttsAudio.stop();
}
}
};七、總結(jié)
微信小程序 SpeechSynthesizer 是一個(gè)功能強(qiáng)大的文本轉(zhuǎn)語(yǔ)音 API,它可以幫助開發(fā)者輕松實(shí)現(xiàn)高質(zhì)量的語(yǔ)音合成功能。通過(guò)本文的介紹,相信你已經(jīng)掌握了 SpeechSynthesizer 的基本使用方法和高級(jí)應(yīng)用技巧。在實(shí)際項(xiàng)目中,你可以根據(jù)需求靈活運(yùn)用這些技巧,為用戶提供更好的體驗(yàn)。
八、參考資料
- 微信小程序
- 騰訊云語(yǔ)音合成文檔 接口說(shuō)明
到此這篇關(guān)于微信小程序 SpeechSynthesizer基本使用方法的文章就介紹到這了,更多相關(guān)微信小程序 SpeechSynthesizer使用內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
ES6?Promise.all的使用方法以及其細(xì)節(jié)詳解
Promise對(duì)象用于表示一個(gè)異步操作的最終完成(或失敗)及其結(jié)果值,下面這篇文章主要給大家介紹了關(guān)于ES6?Promise.all的使用方法以及其細(xì)節(jié)的相關(guān)資料,需要的朋友可以參考下2022-07-07
完美實(shí)現(xiàn)八種js焦點(diǎn)輪播圖(下篇)
這篇文章主要介紹了完美實(shí)現(xiàn)八種js焦點(diǎn)輪播圖的具體代碼,基于完美運(yùn)動(dòng)框架move2.js實(shí)現(xiàn)的焦點(diǎn)錄播圖,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-07-07
Javascript前端事件循環(huán)機(jī)制詳細(xì)講解
單線程的同步等待極大影響效率,任務(wù)不得不一個(gè)一個(gè)等待執(zhí)行,對(duì)于網(wǎng)頁(yè)應(yīng)用是無(wú)法接受的。所以Javascript使用事件循環(huán)機(jī)制來(lái)解決異步任務(wù)的問(wèn)題。本文就來(lái)講講Javascript的事件循環(huán)機(jī)制,希望對(duì)你有所幫助2022-12-12
JS實(shí)現(xiàn)pasteHTML兼容ie,firefox,chrome的方法
這篇文章主要介紹了JS實(shí)現(xiàn)pasteHTML兼容ie,firefox,chrome的方法,涉及javascript針對(duì)頁(yè)面元素的動(dòng)態(tài)操作技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2016-06-06
JavaScript實(shí)現(xiàn)表格點(diǎn)擊排序的方法
這篇文章主要介紹了JavaScript實(shí)現(xiàn)表格點(diǎn)擊排序的方法,可實(shí)現(xiàn)點(diǎn)擊頂部數(shù)據(jù)項(xiàng)標(biāo)題實(shí)現(xiàn)對(duì)應(yīng)數(shù)據(jù)列的排序效果,涉及javascript鼠標(biāo)事件及數(shù)據(jù)排序的技巧,需要的朋友可以參考下2015-05-05
Handtrack.js庫(kù)實(shí)現(xiàn)實(shí)時(shí)監(jiān)測(cè)手部運(yùn)動(dòng)(推薦)
這篇文章主要介紹了實(shí)時(shí)監(jiān)測(cè)手部運(yùn)動(dòng)的 JS 庫(kù),可以實(shí)現(xiàn)很多有趣功能,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-02-02
javascript高級(jí)編程之函數(shù)表達(dá)式 遞歸和閉包函數(shù)
這篇文章主要介紹了javascript高級(jí)編程之函數(shù)表達(dá)式 遞歸和閉包函數(shù)的相關(guān)資料,需要的朋友可以參考下2015-11-11

