使用百度智能云AI语音合成API和pcm-player组件,开发移动端H5文章AI语音实时朗读功能

发布时间:2026/6/10 15:31:50
使用百度智能云AI语音合成API和pcm-player组件,开发移动端H5文章AI语音实时朗读功能 项目需求开发手机H5前端页面有一篇文章点击朗读按钮开始播放AI语音朗读文章内容。客户已选择使用百度智能云产品。关键技术栈Vue3 websocket pcm-player在百度智能云文档中心-语音合成栏目中百度智能云API支持短文本在线合成、长文本在线合成与流式文本在线合成。实现方案因为文章字数较多首先pass短文本在线合成API。从文档描述来看需求更符合长文本在线合成API但实际开发后发现长文本在线合成的返回速度实在是太慢创建一次任务要等1分钟左右才能返回音频结果不符合即时播放的需求该方案也pass。使用流式文本在线合成API因为要求点下按钮即时播放所以不能等音频流全部传完合成后再播放这样也会有等待时间。采用将返回的音频流存储在播放队列中按次序循环播放的方法。问题第三种方案是靠谱的但这种方法有一个坑开发到最后PC端测试没有问题但是在移动端访问会出现播放报错输出内容为Uncaught (in promise) DOMException: play() can only be initiated by a user gesture.测试手机为安卓机。因为手机的浏览器有一种安全措施禁止自动播放音视频。只有用户手动触发点击才能够播放触发执行的代码结束后后续代码调用的播放全部无效。因此函数循环递归触发连续序列播放的方法在手机上行不通。那么只好修改一下方案3了既然不能循环调用那么有没有方法不用递归自动就能按顺序播放呢经过搜索发现了pcm-player这个组件可以满足需求。它不像原生的audio每个实例只能播放一个文件生成的url而是使用feed方法直接将音频流一个一个放进去自动按顺序播放。代码1.创建pcm-player实例varplayernewpcmPlayer({inputCodec:Int16,// 采样位数sampleRate:24000,channels:1,// 通道flushTime:0,onstatechange:(node,event,type){},// 播放状态变化事件onended:(node,event){currVoiceIndex;// 播完一段1// console.log(currVoiceIndex, currVoiceIndex);if(currVoiceIndexvoiceLength){// 已播放完毕重置音频播放状态player.pause();voiceLength0;currVoiceIndex0;voicePlayedfalse;playStatusfalse;}},// 播放结束事件});player.volume(1);注意sampleRate和channels这两个参数乱改会出现音频音调改变的问题 要跟语音合成API配置的audio_ctrl保持一致。比如audio_ctrl设置为*{“sampling_rate”:16000}*则sampleRate为16000、channels为1或者sampleRate为8000、channels为2时是正常的。volume设置音量虽然文档写的 0 to ∞ 但可设置范围实际为0到1超过会出现噪音而且默认值也很小明显不是1。这里把volume设置成1最为合适。onended方法每个音频流播完都会调用并不是所有音频流播完时才调用。2.websokect请求exportconstbaiduStreamSpeechSynthesisasync(text:Arraystring){// text为分段传输文本数组// 切换播放状态因为这里需求只有一个按钮控制播放暂停此处逻辑根据具体需求做调整playStatus!playStatus;if(!playStatus){// 暂停播放// stopAudio();player.pause();return;}elseif(playStatusvoicePlayed){player.continue();return;}// 正式代码try{// 1. 获取 Access TokenconstaccessTokenawaitgetAccessToken();// 2. 建立 WebSocket 连接wsnewWebSocket(BAIDU_SPEECH_WS_URL?access_token${accessToken}per${0});ws.binaryTypearraybuffer;// 3. websocket 连接成功ws.onopen(){console.log(百度语音流式合成连接成功开始发送鉴权和文本数据);// 初始化constinitData{type:system.start,payload:{spd:5,pit:5,vol:5,audio_ctrl:{sampling_rate:24000},aue:4,},};ws.send(JSON.stringify(initData));};// 4. 接收 websocket 消息ws.onmessage(event:MessageEvent){if(typeofevent.datastring){// 接收状态消息try{constresJSON.parse(event.data);if(res.messagesuccessres.typesystem.started){// 初始化成功开始传输文本text.forEach((text:string){wsSendText(text);});// 传输完毕constfinishData{type:system.finish,};ws.send(JSON.stringify(finishData));}elseif(res.messagesuccessres.typesystem.finished){// 文本传输完毕时关闭ws.close();wsnull;}}catch(error){consterrerrorasError;console.error(解析新版接口消息异常,err);}}else{// 接收二进制音频流console.log(接收音频流);voiceLength;player.feed(event.data);if(!voicePlayed){player.continue();}voicePlayedtrue;}};// 5. websocket 错误回调ws.onerror(error:Event){consterrorMsg语音流式合成连接/传输异常;consterrnewError(${errorMsg}${(errorasErrorEvent).message||未知错误});console.error(err,error);};// 返回 websocket 实例便于外部手动关闭连接returnws;}catch(error){consterrerrorasError;throwerr;}};/** * websocket传输文本 * param {number} text 发送需要生成的文本 */exportconstwsSendText(text:string){constsynthesizeData{type:text,payload:{text:text,},};ws.send(JSON.stringify(synthesizeData));};重点getAccessToken()通过key获取用户token参考百度智能云文档鉴权认证栏编写代码。由于pcm-player.feed()只接收ArrayBuffer类型或者TypedArray 类型创建websocket连接时用ws.binaryType arraybuffer’将接收数据流设置为ArrayBuffer类型更方便。发送system.start时记得将传输参数payload.aue音频格式设置为4否则合成的语音只有杂音3mp3-16k/24k4pcm-16k/24k5pcm-8k6wav-16k/24k默认为3。payload.audio_ctrl可以将采样率设置为16k或者24k。发送合成文本可以循环调用发送所有文本发送完毕后可以立即发送system.finish接收的数据流是按顺序返回的不会干扰合成进程。pcm-player属性audioCtx.state可以判断播放器状态running为正在播放suspended为暂停。全部数据流播放完毕后并不会将状态切为suspended依旧为running只有调用player.pause()才会将状态切换为suspended。补充流式数据接收为异步onended方法又是多次触发如何判断数据接收完毕首先在创建pcmPlayer实例的时候在设置中将flushTime设置为0保证接收几条数据触发几次onended。然后设两个状态变量voiceLength记录音频段个数currVoiceIndex记录播放完的序列。每次feed()时voiceLength1onended()时currVoiceIndex1如果播放结束后触发的onended判断voiceLength等于currVoiceIndex说明所有音频播放完毕。代码仅供参考具体逻辑还需根据实际业务需求做调整。二编有的时候接收的音频段数与播放结束后触发的onended对应不上还有一种绿皮解决办法在接收结束时使用player.feed(new ArrayBuffer(200));硬塞进去一段数据流然后在onended方法中判断node!.buffer!.length 100传入数字200的数据流长度为100如果符合该条件说明播放完毕虽然这个解决方法有点暴力但确实能用。