小程序踩坑系列(一)
录音API简介
- 通过
wx.getRecorderManager()
获取全局唯一的录音管理器。 - 支持的事件监听包括录音开始、暂停、继续、结束、错误、帧录制完成,以及录音中断的开始和结束。
分贝解析
录音格式format需要指定为pcm
开始录音API RecorderManager.start(Object object)
可指定录音文件的格式,默认格式为aac
重点:需要指定format为pcm(无损格式)
- 'aac' 格式 :
- 压缩格式: AAC(Advanced Audio Codec)是一种有损压缩格式,主要用于减少音频文件的大小。
- 编码特性: 由于 AAC 是一种压缩格式,它在编码过程中会丢失一些音频信息,包括可能用于计算分贝的精细音频数据。
- 分贝解析: 因为 AAC 压缩会丢失一些音频数据,因此无法直接从 AAC 文件中提取出精确的分贝信息。
- 'pcm' 格式 :
- 未压缩格式: PCM(Pulse Code Modulation)是一种未压缩的音频格式,保留了音频信号的完整信息。
- 编码特性: PCM 直接记录了音频信号的波形数据,保留了所有的音频细节。
- 分贝解析: 由于 PCM 保留了完整的音频数据,可以直接访问波形数据,从而能够准确计算和解析分贝信息。
总结来说,AAC 格式由于其压缩特性,会丢失一些用于解析分贝的音频细节,而 PCM 格式保留了完整的音频信息,因此可以进行分贝解析。对于需要分析音频特征(如分贝、频谱等)的应用场景,PCM 是更合适的选择。
获取录音分片
RecorderManager.onFrameRecorded(function listener)
监听已录制完指定帧大小的文件事件。如果设置了 frameSize,则会回调此事件。
参数
function listener
已录制完指定帧大小的文件事件的监听函数
监听函数参数
Object res
属性 | 类型 | 说明 |
---|---|---|
frameBuffer | ArrayBuffer | 录音分片数据 |
isLastFrame | boolean | 当前帧是否正常录音结束前的最后一帧 |
当我们使用这个API注册了监听函数之后了,小程序会在录音开始后,每录满指定的文件大小,就触发一次此监听,将这一段录制内容的分片数据作为入参回传给我们。
这个【指定的文件大小】,是在每次调用RecorderManager.start(Object object)
时,作为可选配置项的frameSize
传入的。
一个简单的示例:
jsx
// 其他属性先忽略,这里我们重点关注frameSize
const options = {
duration: 60000 * 3,
sampleRate: 44100,
numberOfChannels: 1,
encodeBitRate: 192000,
format: 'pcm',
frameSize: 50 // 每帧音频数据的大小
}
const recorderManager = wx.getRecorderManager();
recorderManager.onFrameRecorded((res) => {
const {
frameBuffer
} = res
console.log('frameBuffer.byteLength', frameBuffer.byteLength)
const audioData = new Int16Array(frameBuffer); // 将音频数据转换为 16 位整数
// 计算 RMS 值
let sum = 0;
for (let i = 0; i < audioData.length; i++) {
sum += audioData[i] * audioData[i]; // 平方和
}
const rms = Math.sqrt(sum / audioData.length); // 计算 RMS
// 将 RMS 转换为分贝
const db = 20 * Math.log10(rms);
console.log('当前音量(分贝):', db);
return db;
})
// 开始录制
recorderManager.start(options);
分贝解析算法
拿到每一帧的录音文件数据后,我们就可以对其进行解析了。
具体的计算原理就不展开解释了(实则我也不懂),网上可以搜到的算法大差不差都能用,自己多试试就行,我这边给出一个我们项目实际在用的分贝解析算法以供参考。
jsx
// 解析每一帧的buffer并返回当前片段的分贝
const getDb = (frameBuffer) => {
const audioData = new Int16Array(frameBuffer); // 将音频数据转换为 16 位整数
// 计算 RMS 值
let sum = 0;
for (let i = 0; i < audioData.length; i++) {
sum += audioData[i] * audioData[i]; // 平方和
}
const rms = Math.sqrt(sum / audioData.length); // 计算 RMS
// 将 RMS 转换为分贝
const db = 20 * Math.log10(rms);
console.log('当前音量(分贝):', db);
return db;
}
完整录音&分贝解析代码
综合以上的录音配置、帧回调和分贝解析算法,我们总结一下,完整的代码如下
jsx
// 解析每一帧的buffer并返回当前片段的分贝
const getDb = (frameBuffer) => {
const audioData = new Int16Array(frameBuffer); // 将音频数据转换为 16 位整数
// 计算 RMS 值
let sum = 0;
for (let i = 0; i < audioData.length; i++) {
sum += audioData[i] * audioData[i]; // 平方和
}
const rms = Math.sqrt(sum / audioData.length); // 计算 RMS
// 将 RMS 转换为分贝
const db = 20 * Math.log10(rms);
console.log('当前音量(分贝):', db);
return db;
}
const options = {
duration: 60000 * 3, // 录音时长,单位 ms
sampleRate: 44100, // 采样率
numberOfChannels: 1, // 录音通道数
encodeBitRate: 192000, // 编码码率
format: 'pcm', // 音频格式
frameSize: 50 // 每帧音频数据的大小
}
const recorderManager = wx.getRecorderManager()
recorderManager.onFrameRecorded((res) => {
const {
frameBuffer
} = res
console.log('frameBuffer.byteLength', frameBuffer.byteLength)
const db = getDb(frameBuffer)
console.log('当前音量(分贝):', db);
})
// 开始录制
recorderManager.start(options);
来电检测 & 异常处理
小程序录音动作会被各种异常场景暂停或者中断,例如接入电话、隐藏小程序、下拉通知界面等等。我们需要根据业务场景,对这些异常边界进行恰当的处理。
方法一:强制中断
适用场景:对最终的录音文件完整性、连贯性要求较高
核心逻辑:监听onPause事件,强制中断当前录音并提示重新开始
先上代码
jsx
const recorderManager = wx.getRecorderManager();
recorderManager.onPause(() => {
// 检测到录音被意外暂停,说明可能出现来电、隐藏小程序等边界场景
// 直接强制结束本次录音
recorderManager.stop();
// 提示用户麦克风被占用,需要重新录音
wx.showToast({
title: '克风被占用,需要重新录音',
duration: 2000,
});
})
有的朋友可能会有疑问,这个逻辑怎么这么简单粗暴,我只能说,这都是经历无数边界场景折磨之后得出的最优解。基本上,只要出现意外的暂停,很难保证用户的录音能正常恢复,即使恢复了,最终拿到的录音文件,也很可能是不连续的,或者存在其他无法预知的问题。
对于录音文件完整性要求高的业务场景,直接强制结束这次录音,让用户重新录制是最好的选择。
这个方案的难点在于如何说服业务和产品(逃
方法二:暂停&恢复
适用场景:对于录音的完整性要求没有那么高,或者只需要在录音过程中进行实时分析,不需要最终的录音文件
核心逻辑:在录音被暂停、恢复时给用户提示,不进行主动的干预,重点在于让用户知道自己当前是否处于正常录音状态
小程序录音API自带微信语音/视频来电的监听功能,按照一个常见的接电话然后挂断场景下,触发监听的顺序:
onInterruptionBegin
:监听录音因为受到系统占用而被中断开始事件。以下场景会触发此事件:微信语音聊天、微信视频聊天。此事件触发后,录音会被暂停。pause 事件在此事件后触发
onPause
:监听录音暂停事件
onInterruptionEnd
:监听录音中断结束事件。在收到 interruptionBegin 事件之后,小程序内所有录音会暂停,收到此事件之后才可再次录音成功
onResume
:监听录音继续事件
要注意onInterruptionBegin
/ onInterruptionEnd
这对API仅在微信语音和视频的来电时会触发,如果是移动电话接入,是不会有反应的。
并且,如果移动电话接入,而用户又没有连接Wi-Fi,则大概率在来电的时候用户的手机会进入断网状态。即使用户直接挂断了电话,对于小程序来说,网络恢复也需要几秒到几十秒不等的时间。这段时间内,不但网络请求可能发送失败,小程序内置的一些API、组件初始化也可能会失败(微信小程序严重依赖网络能力)。
注意事项
兜底处理
小程序开发中,任何关键的API调用都需要有完整的兜底逻辑。
要做好这样的准备:
当你调用某个API时,它可能会直接失败;
如果这个API是异步的,它的所有回调(success、fail、complete)都有可能不会触发
清除监听
开头我们提到过,小程序的录音管理器RecorderManager
是全局唯一的,也就是说,我们注册过的监听,无论在哪个页面使用录音都会被触发。如果监听里有一些副作用,在其他页面执行了,可能导致意想不到的结果。
所以最好在每个使用RecorderManager
的页面销毁时,清除注册的监听。
由于RecorderManager
的监听API并未显式地提供对应的取消监听API(例如有onFrameRecorded
,但是没有offFrameRecorded
),因此目前比较可行的清除监听的方式,是用一个空函数去覆盖原有的监听:
jsx
Page({
onLoad() {
this.recorderManager = wx.getRecorderManager();
// 注册监听器
this.recorderManager.onFrameRecorded(this.onFrameRecorded);
this.recorderManager.onPause(this.onPause);
},
onFrameRecorded(res) {
const { frameBuffer } = res;
const audioData = new Int16Array(frameBuffer);
let sum = 0;
for (let i = 0; i < audioData.length; i++) {
sum += audioData[i] * audioData[i];
}
const rms = Math.sqrt(sum / audioData.length);
const db = 20 * Math.log10(rms);
console.log('当前音量(分贝):', db);
},
onPause() {
console.warn('录音被暂停');
wx.showToast({
title: '录音被中断',
icon: 'none'
});
},
onUnload() {
// 用空函数覆盖监听,达到"取消监听"的目的
this.recorderManager.onFrameRecorded(() => {});
this.recorderManager.onPause(() => {});
console.log('监听已清除');
}
});