微信小程序 SpeechSynthesizer 实战指南

微信小程序 SpeechSynthesizer 实战指南

一、引言

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

二、SpeechSynthesizer 简介

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

主要特点

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

三、基本使用方法

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

八、参考资料

  1. 微信小程序
  2. 腾讯云语音合成文档 接口说明

希望本文对你有所帮助,如果你有任何问题或建议,欢迎留言交流。

相关推荐
你的眼睛會笑2 小时前
微信小程序定位权限获取最佳实践
微信小程序·小程序·notepad++
liu_bees2 小时前
微信小程序Canvas生成图片失败:canvas is empty问题解析
微信小程序·小程序·uni-app·vue
码农客栈2 小时前
小程序学习(十七)之获取热门推荐数据类型并渲染
小程序
一点程序3 小时前
基于微信小程序的英语词汇学习小程序
学习·微信小程序·小程序
星尘库3 小时前
[开发者服务器响应] 发货请求调用失败. 【ret:172935489】
微信小程序·小程序·小游戏
开利网络3 小时前
第2天:构建多维标签体系——立体化勾勒客户轮廓
大数据·微信小程序
2501_915921431 天前
傻瓜式 HTTPS 抓包,简单抓取iOS设备数据
android·网络协议·ios·小程序·https·uni-app·iphone
2501_915918411 天前
把 iOS 性能监控融入日常开发与测试流程的做法
android·ios·小程序·https·uni-app·iphone·webview
2601_949804921 天前
开源多商户商城源码最新版_适配微信小程序+H5+APP+PC多端
微信小程序·小程序