微信小程序 SpeechSynthesizer 实战指南
一、引言
在移动应用开发中,文本转语音(TTS)功能可以极大地提升用户体验,尤其是在需要解放双手的场景下。微信小程序提供了强大的 SpeechSynthesizer API,让开发者可以轻松实现高质量的语音合成功能。本文将结合实际项目,详细介绍微信小程序 SpeechSynthesizer 的使用方法和最佳实践。
二、SpeechSynthesizer 简介
SpeechSynthesizer 是微信小程序提供的文本转语音 API,它基于腾讯云的语音合成技术,可以将文本转换为自然流畅的语音。该 API 支持多种语言、音色和语速调节,满足不同场景的需求。
主要特点
- 高质量语音合成 :基于腾讯云的先进语音合成技术,提供自然流畅的语音输出。
- 多语言支持 :支持中文、英文等多种语言。
- 丰富的音色选择 :提供多种音色供选择,包括男声、女声等。
- 灵活的参数调节 :可以调节语速、音量、音调等参数。
- 实时合成 :支持实时将文本转换为语音,无需等待。
三、基本使用方法
1. 创建 SpeechSynthesizer 实例
javascript
// 初始化语音合成实例
this.ttsInstance = new SpeechSynthesizer({
volume: 1.0, // 音量,范围 0-1
rate: 1.0, // 语速,范围 0.5-2.0
pitch: 1.0, // 音调,范围 0.5-2.0
language: 'zh-CN', // 语言,支持 'zh-CN'、'en-US' 等
voiceName: 'xiaoyan' // 音色,支持 'xiaoyan'、'xiaoyu' 等
});
2. 合成并播放语音
javascript
// 合成并播放语音
this.ttsInstance.speak({
text: '欢迎使用微信小程序 SpeechSynthesizer',
success: () => {
console.log('语音播放成功');
},
fail: (err) => {
console.error('语音播放失败', err);
}
});
3. 暂停和继续播放
javascript
// 暂停播放
this.ttsInstance.pause();
// 继续播放
this.ttsInstance.resume();
4. 停止播放
javascript
// 停止播放
this.ttsInstance.stop();
四、高级应用
1. 实时语音合成
在聊天应用中,我们可以实时将用户输入的文本转换为语音:
javascript
// 监听用户输入
onInputChange(e) {
const text = e.detail.value;
// 实时合成语音
this.ttsInstance.speak({
text: text,
success: () => {
console.log('语音合成成功');
}
});
}
2. 多段文本合成
在需要合成多段文本时,可以使用 queue 方法:
javascript
// 合成多段文本
this.ttsInstance.queue([
{ text: '第一段文本' },
{ text: '第二段文本' },
{ text: '第三段文本' }
]);
3. 自定义音色和语速
根据不同的场景,我们可以自定义音色和语速:
javascript
// 设置音色为男声
this.ttsInstance.setVoiceName('xiaoyu');
// 设置语速为慢速
this.ttsInstance.setRate(0.7);
// 设置音量为最大
this.ttsInstance.setVolume(1.0);
五、常见问题及解决方案
1. 语音合成失败
问题描述 :调用 speak 方法时,返回失败。
解决方案 :
- 检查网络连接是否正常。
- 检查文本内容是否过长(建议不超过 500 字)。
- 检查参数设置是否正确。
2. 语音播放不流畅
问题描述 :语音播放时出现卡顿或断句不自然。
解决方案 :
- 检查网络连接是否稳定。
- 调整语速参数,适当降低语速。
- 分割长文本,分段合成。
3. 音量调节无效
问题描述 :设置音量后,语音播放音量没有变化。
解决方案 :
- 检查音量参数是否在 0-1 范围内。
- 检查设备音量是否设置正确。
六、实战案例:智能语音助手
下面我们将结合实际项目,实现一个智能语音助手功能:
1. 初始化语音合成实例 (复制可用)
javascript
// utils/ttsUtil.js
const SpeechSynthesizer = require("../../components/alibabacloud-nls-wx-sdk-master/utils/tts")
const fs = wx.getFileSystemManager();
// 格式化时间(工具函数)
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}`;
}
// 休眠(工具函数)
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 = []; // 待合成的文本队列
this.isProcessingQueue = false; // 是否正在处理队列
}
// 初始化TTS(修复:增加错误捕获,确保事件监听生效)
initTTS() {
return new Promise((resolve, reject) => {
try {
if (this.ttsInstance) {
resolve(true);
return;
}
this.ttsInstance = new SpeechSynthesizer(this.config);
console.log("[TTSUtil] 实例初始化成功");
// 监听音频数据(确保二进制数据写入文件)
this.ttsInstance.on("data", (binaryData) => {
if (this.dataInfos.saveFile && this.dataInfos.saveFd) {
try {
// 小程序中position传-1等价于SEEK_END
fs.write({
fd: this.dataInfos.saveFd,
data: binaryData,
position: -1,
encoding: "binary",
success: () => {
console.log(`[TTSUtil] 写入音频数据:${binaryData.byteLength}字节`);
},
fail: (e) => {
console.error("[TTSUtil] 写入音频数据失败:", e);
}
});
} catch (e) {
console.error("[TTSUtil] 写入音频数据异常:", e);
}
}
});
this.ttsInstance.on("completed", async (res) => {
console.log("[TTSUtil] 合成完成回调触发", res);
await sleep(800); // 延长等待,确保数据完全写入文件
if (this.dataInfos.saveFd) {
// 改用异步close
fs.close({
fd: this.dataInfos.saveFd,
success: () => {
console.log("[TTSUtil] 文件已异步关闭");
},
fail: (err) => {
console.error("[TTSUtil] 关闭文件失败:", err);
}
});
this.dataInfos.saveFd = null;
}
const taskItem = this.audioTaskList.find(item => item.filePath === this.dataInfos.saveFile);
if (taskItem) taskItem.status = "completed";
this.clearCurrentFileResource();
// 修复:确保任务项存在才播放
if (taskItem) {
this.playCurrentTaskAndNext(taskItem);
} else {
this.processNextQueueItem(); // 无任务项则直接处理下一个
}
});
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(); // 失败后继续处理下一个
});
resolve(true);
} catch (err) {
console.error("[TTSUtil] 初始化失败:", err);
reject(false);
}
});
}
// 向队列中添加文本(外部调用)
addToQueue(text, customParams = {}) {
if (!this.ttsInstance) {
uni.showToast({ title: "请先初始化TTS", icon: "none" });
return;
}
const texts = Array.isArray(text) ? text : [text];
texts.forEach(t => {
this.textQueue.push({ text: t, params: customParams });
console.log(`[TTSUtil] 文本加入队列:${t}`);
});
// 修复:队列未处理时,立即启动(增加防抖,避免重复触发)
if (!this.isProcessingQueue && !this.isPlaying) {
this.processNextQueueItem();
}
}
// 处理队列的下一个文本(修复:确保异步执行顺序)
async processNextQueueItem() {
if (this.textQueue.length === 0) {
this.isProcessingQueue = false;
console.log("[TTSUtil] 队列已空,停止处理");
return;
}
this.isProcessingQueue = true;
const queueItem = this.textQueue.shift();
console.log(`[TTSUtil] 开始处理队列文本:${queueItem.text}`);
// 修复:统一用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);
// 等待合成完成(修复:await确保合成完成后再执行后续)
await this.synthesizeSingleAudio(task, queueItem.params);
}
// 播放当前任务 + 自动处理下一个(核心修复:确保音频播放触发)
async playCurrentTaskAndNext(taskItem) {
if (!taskItem || !taskItem.filePath) {
this.processNextQueueItem();
return;
}
console.log(`[TTSUtil] 准备播放:${taskItem.filePath}`);
// 修复:先检查文件是否存在
try {
fs.accessSync(taskItem.filePath);
} catch (e) {
console.error("[TTSUtil] 音频文件不存在:", e);
this.processNextQueueItem();
return;
}
// 播放当前音频,等待播放完成后处理下一个
const playSuccess = await this.playAudioFile(taskItem.filePath);
console.log(`[TTSUtil] 当前音频播放${playSuccess ? "完成" : "失败"}`);
// 无论播放成功/失败,都处理下一个队列项
this.processNextQueueItem();
}
// 合成单个音频(修复:等待tts.start真正完成,确保合成启动)
synthesizeSingleAudio(task, customParams = {}) {
return new Promise((resolve) => {
// 修复:先清空当前文件资源,避免残留
this.clearCurrentFileResource();
// 统一用wav格式,提升兼容性
const format = customParams.format || "wav";
fs.open({
filePath: task.filePath,
flag: "w+", // 修复:用w+替代a+,确保文件重新创建
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, // 强制用wav
sample_rate: customParams.sampleRate || 16000,
volume: customParams.volume || 100,
speech_rate: customParams.speechRate || 0,
pitch_rate: customParams.pitchRate || 0,
enable_subtitle: false
};
try {
// 修复:await确保start执行完成
await this.ttsInstance.start(playParams);
task.status = "synthesizing";
console.log(`[TTSUtil] 开始合成文本:${task.text}`);
// 不立即resolve,等待合成完成(由completed/failed回调处理)
// 这里resolve仅标记合成启动,不影响后续流程
resolve(true);
} catch (e) {
console.error("[TTSUtil] 合成启动失败:", 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);
}
});
});
}
// 播放单个音频(核心修复:确保自动播放生效)
playAudioFile(filePath) {
return new Promise((resolve) => {
// 停止当前播放的音频
this.stopCurrentAudio();
// 增加文件路径空值校验
if (!filePath) {
console.error("[TTSUtil] 音频路径为空");
resolve(false);
return;
}
// 创建音频上下文
this.currentAudioCtx = wx.createInnerAudioContext();
if (!this.currentAudioCtx) {
console.error("[TTSUtil] 音频上下文创建失败");
resolve(false);
return;
}
this.currentAudioCtx.src = filePath;
this.isPlaying = true;
// 监听音频加载完成
this.currentAudioCtx.onCanplay(() => {
console.log(`[TTSUtil] 音频加载完成,开始播放:${filePath}`);
// 直接调用play(同步方法,无返回值),通过onError捕获错误
this.currentAudioCtx.play();
});
this.currentAudioCtx.onPlay(() => {
console.log(`[TTSUtil] 音频开始播放:${filePath}`);
});
// 用onError替代catch捕获播放错误
this.currentAudioCtx.onError((err) => {
console.error("[TTSUtil] 播放错误:", 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);
});
// 超时兜底:5秒未播放则判定失败
setTimeout(() => {
if (this.isPlaying && !this.currentAudioCtx?.paused) return;
console.error(`[TTSUtil] 音频播放超时:${filePath}`);
this.isPlaying = false;
this.deleteFile(filePath);
resolve(false);
}, 5000);
});
}
// 停止当前音频(保持不变)
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] 已停止所有合成/播放,清空队列");
}
// 清理文件资源(保持不变)
clearCurrentFileResource() {
if (this.dataInfos.saveFd) {
// 改用异步close
fs.close({
fd: this.dataInfos.saveFd,
success: () => {
console.log("[TTSUtil] 文件句柄已关闭");
},
fail: (e) => {
console.error("[TTSUtil] 关闭文件句柄失败:", 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;
}
// 新增:获取队列长度(方便调试)
getQueueLength() {
return this.textQueue.length;
}
}
// 单例导出
let ttsInstance = null;
export function getTTSUtil(config) {
if (!ttsInstance) {
ttsInstance = new TTSUtil(config);
}
return ttsInstance;
}
export default TTSUtil;
2. 在页面中使用
javascript
// index.vue
import ttsAudio from './ttsAudio.js';
export default {
data() {
return {
inputText: '',
ttsConfig: {
appkey: "xxxxxxxxxx", // 你的appkey
token: "你的阿里云语音合成token", // 你的token(需通过appkey+secret换取)直接让后台写接口返回
url: "wss://nls-gateway.cn-shanghai.aliyuncs.com/ws/v1" // 阿里云语音合成服务地址
},
};
},
methods: {
// 播放语音
// 初始化TTS实例
async initTTS() {
this.ttsUtil = getTTSUtil(this.ttsConfig);
await this.ttsUtil.initTTS();
// 调用
let text = '一只穿着京剧戏服的可爱的灰白临清狮猫,拟人化,现实风格,疯狂的细节,毛茸茸的,超清晰的毛发,大眼睛,全身照,在戏台上耍着戏曲里的花枪,摆着动作表演,对着镜头,Q版超萌,超高清,杰作'
this.ttsUtil.addToQueue(text, {
voice: "zhistella",
speechRate: 0
});
// res ? uni.showToast({ title: "TTS初始化成功", icon: "success" }) : uni.showToast({ title: "初始化失败", icon: "none" });
},
// 暂停播放
pauseVoice() {
ttsAudio.pause();
},
// 继续播放
resumeVoice() {
ttsAudio.resume();
},
// 停止播放
stopVoice() {
ttsAudio.stop();
}
}
};
七、总结
微信小程序 SpeechSynthesizer 是一个功能强大的文本转语音 API,它可以帮助开发者轻松实现高质量的语音合成功能。通过本文的介绍,相信你已经掌握了 SpeechSynthesizer 的基本使用方法和高级应用技巧。在实际项目中,你可以根据需求灵活运用这些技巧,为用户提供更好的体验。
八、参考资料
希望本文对你有所帮助,如果你有任何问题或建议,欢迎留言交流。