背景:为了挽留一个潜在客户
上个周末(周日),有个用户突然在 Github 上开了一个 Issue(uniApp的版本是否支持音频模式的回复?谢谢),之前已经完成基于 Web 的实时语音对话 SDK(基于 Coze Websocket Open API)。
说实话,一直很想在微信小程序上实现实时语音对话,有不少潜在的用户,之前基于火山 RTC 封装的 Web SDK,由于微信的限制,需要开通两个权限(live-player 和 live-pusher),这两个权限需要一定的资质才能申请,我们没法搞定这个,所以一直没法封装,导致用户只能通过 WebView 嵌 H5的方式使用,使用体验比较差。
火山的 RTC 是基于 WebRTC 的,它必须依赖微信提供的 live-player 和 live-pusher 这两个组件,现在扣子提供了 Websocket 版本的 API了,微信对 Websocket 的支持度还挺好的,用来实现实时语音对话也许可行,于是开始行动...
还有一个事项:如果让产品提需求做这个SDK,估计 ROI 太低提不上日程,最近有个需求前端UI已经开发差不多了,等后端联调,有几天的buffer,恰好可以来做这事,但时间也不会太多,于是周日下午就开始编码了,也希望能尽快能给用户提供一个 SDK
可行性分析:基本可行
用户是使用 UniApp SDK,所以先在这个 SDK 上实现支持,以下都是基于这个 SDK 开发的
目前要在小程序上实时语音对话,需要实现以下功能:
-
支持 Websocket API
- 这个之前用 axios 适配 HTTP 请求已经有了一些经验,可以采用 mixin 的方式来实现
- 另外微信小程序对 websocket 的支持度也挺高的,基本 API 能对齐
-
支持实时语音播放
- 难点主要在流式,也即音频是分 chunk 通过 WS 返回的,微信小程序提供了几种播放音频的方式,需要做下调研,看哪种方案可行
-
支持实时语音录制
- 微信小程序提供了 RecorderManager,它支持通过配置的方式实现实时回调事件,将音频片段发送给后端
以上这三个功能,两两组合,还能实现语音合成、语音识别这两个 SDK,于是在这次开发过程中,还希望实现这两个:
-
Websocket + 实时流式语音播放 = 实时语音合成 SDK
-
Websocket + 实时语音录制 = 实时语音识别(语音转写) SDK
-
Websocket + 实时流式语音播放 + 实时语音录制 = 实时语音对话 SDK
说明一下:本次使用 Windsurf 来开发,之前没怎么开发过微信小程序应用,对各种 API 都不是很熟悉,如果没有 AI 编程辅助工具,也不可能敢接下这活,毕竟这代码量太大了,换做以前,这样的功能,没有十天半个月是不可能完成的
开发实战:一步填一坑
语音合成
实现源码位置:ws-tool/speech
先从最简单的开发,语音合成不涉及录音功能,可以在微信开发者工具跑起来,不用通过真机测试,比较方便,实现包括两部分,先对 Websocket 进行封装,然后再实现语音合成的功能
支持 Websocket API:顺利
实现源码位置:ws-tool/websocket
有了之前 Http 接口的适配,之前 Http 接口适配是采用混入的方式实现的,对外使用方式是一样的,只是改了一个包的路径(从 import { CozeAPI } from '@coze/api'
改为 import { CozeAPI } from '@coze/uniapp-api'
),也希望 Websocket 也是一样的,我先让 Windsurf 梳理一下现在基于 Http 的适配方案,然后让它参考现在的方式实现 Websocket 的适配,把相关上线文也带上。
总结:这一步还算顺利,生成的代码基本没啥问题,有些小问题手动改改就通了
支持流式语音播放
实现源码位置:ws-tool/pcm-stream-player
Websocket 返回的是 PCM 格式的音频数据,先去问下 DeepSeek,DeepSeek 给了几种实现方案,比较适合的只有先将 PCM 格式转成 wav 格式的,然后使用 uni.createInnerAudioContext()
来播放,它只能先将 wav 格式写入本地文件,然后再通过本地路径进行播放,这可能存在一个问题,就是在上一个音频和下一个音频之间可能存在极小的延迟,导致音频播放不连贯,刚开始有这个疑惑,不过也先试试看,最后跑起来了,在本地 IDE 上看起来还行,但是用手机测试的时候,延时就非常明显,期间做了不少优化,比如提前缓存音频资源,使用缓存对了,效果有提升但没法根本解决,只能放弃这个方案。
后来在与 DeepSeek 对话中,还发现有 uni.createWebAudioContext()
这个 API,这个 API 就跟原生的 WebAudioContext 很像,但不支持新版的 AudioWorkletNode 这种 worker 方式,只能使用最原始的方式了,果然,用这个就顺畅多了,效果也好很多。
另外还遇到微信小程序在iPhone静音模式下播放无声音的问题,这个 DeepSeek 也给了解决方案:播放一个静音包,大概示例如下代码:
javascript
// 在播放前调用
wx.setInnerAudioOption({
obeyMuteSwitch: false, // 设置为false以忽略静音开关
success: () => {
console.log('配置成功,将忽略静音开关')
},
fail: (err) => {
console.error('配置失败:', err)
}
})
// 然后开始你的音频播放
this.startPlayback()
// 创建innerAudioContext实例
const innerAudioContext = wx.createInnerAudioContext()
// 配置忽略静音模式
innerAudioContext.obeyMuteSwitch = false
// 设置音频源(一个静音包,可以是一个 base64编码格式的)
innerAudioContext.src = 'xx'
// 播放音频
innerAudioContext.play()
不得不说,微信小程序相比原生浏览器,几乎所有 API 都重写了,这导致很多特性跟不上,或者只能用老的API,时间久了也不维护升级了,这某种程度上就很痛苦,导致一些功能要移植到小程序上就会有诸多限制与兼容,包括后面将要提到的回音无法消除,也是如此!
真机测试
最后一步就上真机测试,没想到这里却遇到一个坑,控制台出现 Connected Refused 问题,大概率猜到是域名没有配置白名单的问题,于是就去配置了白名单,没想到,配置了白名单后还是无法生效,各种地方找答案,大家的解决方案都是配置白名单,并没有其它思路,搞了很久,心灰意冷就去吃饭了,吃完饭回来,没想到突然可以了,啥都没改,估计是域名配置需要时间生效,但之前都没遇到这个问题,有点无语。
微信小程序的域名配置,可能需要一段时间才生效,算一个小坑吧
语音识别:坑少
实现这个 SDK,先要封装通用的录音功能,可以实时将录音转成 PCM 格式,API 比较完善,相对简单
支持实时语音录制
源码位置:ws-tool/pcm-recorder
这一步,让 Windsurf 参考原来基于 Web 实现的 PcmRecorder,然后移除一些没有的功能,比如 AI 降噪相关的、输入设备选项(微信小程序不支持输入设备选项),然后就真的按照我的要求实现了,都几乎不用怎么改,太强了。
我发现, AI 工具的模仿能力超强,让它参考 xxx 的实现,然后就真的实现的 API 一模一样,只是换种语言语言/方案实现,完全都不用自己改
真机测试
录音这个功能只能在真机上测试了,幸好,一切顺利
语音对话:坑多
有了前面的铺垫,本以为这一步就简单了,无非就是好组合 pcm recorder 、 pcm player 和 websocket ,实现实时对话的功能,但事实并非如此,遇到不少问题。
坑一:编译问题
一开始遇到编译问题,由于引入原来 ws tools 的包,而它依赖了一些第三方库,而这些库不支持在微信小程序编译,需要 ignore ,刚开始根据 Windsurf 的建议增加一个配置,但是试了没有效果,编译不报错,但运行报错了,后来就一个个排查,最后发现是引入了一个枚举导致了,试了几种方案都没解决解决,最后只好将这个枚举拷贝一份了,问题就解决了。
小程序要引入一个新的库,真的慎之又慎,稍有不慎就遇到编译的问题,上面的问题,本来还想复用一下代码,最后还是放弃了,还是拷贝吧,少去折腾这些
坑二:音频播放卡顿问题
上面提到的 PcmStreamPlayer,在 IDE 上表现还算正常,但是到了真机上,就出现各种问题,在音频回复过程中,前面的音频会有卡顿的问题,初步排查结果是性能问题,音频的播放流程是这样的:
Base64 Pcm Data -> 转ArrayBuffer -> 重采样ReSample -> 播放onaudioprocess
由于微信小程序是单线程的,如果在处理音频播放的时候没有线程资源,那么就会导致播放卡顿,它用到的 API 是被浏览器淘汰的 API,没有支持新版的 Worker 方案(也即需要和主线程抢时间),所以只能通过代码的角度来优化了。
核心在于确保在处理音频播放的前后,线程能够空闲下来,于是需要对前面收到的 pcm data 进行缓存,只在合适的时机处理,经过不断地优化,现在播放的问题好多了,但在一些低端设备上,还是会有点点问题,这个后续还需要进一步优化。现在的处理流程是这样的: Base64 Pcm Data -> 检查线程是否空闲 -> 空闲 - > 转ArrayBuffer -> 重采样ReSample -> 缓存到内容
Base64 Pcm Data -> 检查线程是否空闲 -> 不空闲 - > 缓存到内容
播放onaudioprocess的时候,直接从缓存中读取数据
时间比较赶,后续应该还可以继续优化
坑三:回声消除问题
RecorderManager 这个录音功能本身是不支持回声消除的,相关问答:RecorderManager录音功能支持回声消除吗? , 感觉是微信故意不支持的,回声消除对于实现实时对话非常关键,如果没有回声消除这个功能,就无法同时使用扬声器和麦克风了,于是我这边做了两种策略,提供实时语音和按键说话的功能,实时语音需要带上耳机来体验,按键说话就跟现在的微信交互是一致的。
真机测试
最终效果如下:
最终结果:还算满意
这个 Demo源码在: coze-js-uniapp
经过 3 天的密集开发,中间还要应付开会、周会等时间,终于把版本发出去,如果没有 AI 工具,这事就不可能这么快完成,也估计不会去支持(因为这不是 OKR 的内容),但作为开源 SDK,如果无法应对社区提出的需求,那么就还不如不开源,既然开源了,还是要维护好社区,尽可能在不耽误工作的前提下。
最终得到用户的正面评价:
