欢迎光临
我们一直在努力

微信小程序 SpeechSynthesizer 实战指南

微信小程序 SpeechSynthesizer 实战指南

一、引言

在移动应用开发中,文本转语音(TTS)功能可以极大地提升用户体验,尤其是在需要解放双手的场景下。微信小程序提供了强大的 SpeechSynthesizer API,让开发者可以轻松实现高质量的语音合成功能。本文将结合实际项目,详细介绍微信小程序 SpeechSynthesizer 的使用方法和最佳实践。

二、SpeechSynthesizer 简介

SpeechSynthesizer 是微信小程序提供的文本转语音 API,它基于腾讯云的语音合成技术,可以将文本转换为自然流畅的语音。该 API 支持多种语言、音色和语速调节,满足不同场景的需求。

主要特点

  • 高质量语音合成 :基于腾讯云的先进语音合成技术,提供自然流畅的语音输出。
  • 多语言支持 :支持中文、英文等多种语言。
  • 丰富的音色选择 :提供多种音色供选择,包括男声、女声等。
  • 灵活的参数调节 :可以调节语速、音量、音调等参数。
  • 实时合成 :支持实时将文本转换为语音,无需等待。
  • 三、基本使用方法

    1. 创建 SpeechSynthesizer 实例

    // 初始化语音合成实例
    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. 合成并播放语音

    // 合成并播放语音
    this.ttsInstance.speak({
    text: '欢迎使用微信小程序 SpeechSynthesizer',
    success: () => {
    console.log('语音播放成功');
    },
    fail: (err) => {
    console.error('语音播放失败', err);
    }
    });

    3. 暂停和继续播放

    // 暂停播放
    this.ttsInstance.pause();

    // 继续播放
    this.ttsInstance.resume();

    4. 停止播放

    // 停止播放
    this.ttsInstance.stop();

    四、高级应用

    1. 实时语音合成

    在聊天应用中,我们可以实时将用户输入的文本转换为语音:

    // 监听用户输入
    onInputChange(e) {
    const text = e.detail.value;
    // 实时合成语音
    this.ttsInstance.speak({
    text: text,
    success: () => {
    console.log('语音合成成功');
    }
    });
    }

    2. 多段文本合成

    在需要合成多段文本时,可以使用 queue 方法:

    // 合成多段文本
    this.ttsInstance.queue([
    { text: '第一段文本' },
    { text: '第二段文本' },
    { text: '第三段文本' }
    ]);

    3. 自定义音色和语速

    根据不同的场景,我们可以自定义音色和语速:

    // 设置音色为男声
    this.ttsInstance.setVoiceName('xiaoyu');

    // 设置语速为慢速
    this.ttsInstance.setRate(0.7);

    // 设置音量为最大
    this.ttsInstance.setVolume(1.0);

    五、常见问题及解决方案

    1. 语音合成失败

    问题描述 :调用 speak 方法时,返回失败。

    解决方案 :

    • 检查网络连接是否正常。
    • 检查文本内容是否过长(建议不超过 500 字)。
    • 检查参数设置是否正确。

    2. 语音播放不流畅

    问题描述 :语音播放时出现卡顿或断句不自然。

    解决方案 :

    • 检查网络连接是否稳定。
    • 调整语速参数,适当降低语速。
    • 分割长文本,分段合成。

    3. 音量调节无效

    问题描述 :设置音量后,语音播放音量没有变化。

    解决方案 :

    • 检查音量参数是否在 0-1 范围内。
    • 检查设备音量是否设置正确。

    六、实战案例:智能语音助手

    下面我们将结合实际项目,实现一个智能语音助手功能:

    1. 初始化语音合成实例 (复制可用)

    // 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. 在页面中使用

    // 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 的基本使用方法和高级应用技巧。在实际项目中,你可以根据需求灵活运用这些技巧,为用户提供更好的体验。

    八、参考资料

  • 微信小程序
  • 腾讯云语音合成文档 接口说明
  • 希望本文对你有所帮助,如果你有任何问题或建议,欢迎留言交流。

    赞(0)
    未经允许不得转载:171主机测评 » 微信小程序 SpeechSynthesizer 实战指南
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址