背景
老板要做录音体验优化,有一条实现和app中一样的音频强度波纹曲线输出。因为之前在web端实现过,所以我想应该不难吧,无非就是把曲线代码用微信小程序重写一遍?于是赶紧搞参考之前的曲线生成代码搞了一份,上体验,完美。我以为就这么简单就去搞别的优化了,结果临近deadline再回来搞他,好家伙,噩梦来了——这个强度值搞不到。
爬坑
-
微信小程序提供了录音 Api —— wx.getRecorderManager()。正常是没有实时信息输出的,前几年微信提供了onFrameRecorded,如果设置了format为mp3或pcm,就可以设置frameSize,然后回调就会输出相应frameSize大小的帧文件ArrayBuffer。这也使得很多功能得以实现,比如驰声先声的实时打分、科大讯飞的实时语音转文字等。
-
正常思路是我们拿到了这个ArrayBuffer,然后处理这个文件就行,可以这个ArrayBuffer就是字节流,根本不能直接看出什么门道,于是开始百度,也知道了如果我们录的是mp3,就需要将mp3先转换为pcm,然后在通过一定的解析算法对pcm进行解析,然后在通过一定算法计算出音频强度,至于为啥一定要mp3,是因为之前的业务都必须要mp3格式的音频才能走下去。
-
那如何解析mp3呢,网上说要用到一个库js-mp3,于是搞下来,一调用发现没任何反应,网上说要使用指定的采样率,指定的编码码率,指定的frameSize,但是一一尝试并没有任何反应。也许之前可以,但是可能微信做了调整,现在不可以。
-
然后有同事建议说使用ffmpeg,这更离谱了,了解ffmpeg的都知道,这个要求的成本更高。
-
最后,很多测试分贝的微信小程序都使用的是服务端计算的方案,基于websocket,但是这个成本也比较高,而且性能也折扣很大,小程序那边 10 个测试分贝的,也就 1-2 个能够真实测试,大部分都是假的效果,或者没反应。本来就是做用户体验优化,所以这个也pass了。
念念不忘,又有一村
由于时间的原因,就先做了一版假的动效上线,但是老板说这效果必须搞出来。于是又开始研究之前web端那边的库源码,web端那边的音频强度是基于AudioContext进行读取的,这边放一下两段代码:
1、获取强度算法
r.prototype.getFrequencyData = function a(e) {
var t = this.__analyser
var n = null
var r = 0
var s = 0
var i = 0
var o = function o() {
try {
n = new Uint8Array(t.frequencyBinCount)
t.getByteFrequencyData(n)
} catch (a) {
console.error(a)
}
r = Math.max.apply(null, n)
s = Math.min.apply(null, n)
i = (r - s) / 128
i = Math.round(i * 100 / 2)
i = i > 100 ? 100 : i
if (typeof e === 'function') {
e({
soundIntensity: i
})
}
setTimeout(o, 167)
}
o()
}
// 创建 __analyser
var e = window.AudioContext || window.webkitAudioContext || window.mozAudioContext || window.msAudioContext
this.context = new e()
var n = 0
var t = D.buff_size()
var s = this.context.createMediaStreamSource(a)
this.inputPoint = this.context.createGain()
this.audioInput = s
this.audioInput.connect(this.inputPoint)
this.analyserNode = this.context.createAnalyser()
this.analyserNode.fftSize = 2048
this.inputPoint.connect(this.analyserNode)
于是就搜索了createAnalyser相关的内容,忽然发现微信小程序文档也有这个方法,原来微信小程序提供了一个兼容web端AudioContext的音频播放器WebAudioContext,也给出了两段代码:
- 播放示例:
const audioCtx = wx.createWebAudioContext()
const loadAudio = (url) => {
return new Promise((resolve) => {
wx.request({
url,
responseType: 'arraybuffer',
success: res => {
console.log('res.data', res.data)
audioCtx.decodeAudioData(res.data, buffer => {
resolve(buffer)
},
err => {
console.error('decodeAudioData fail', err)
reject()
})
},
fail: res => {
console.error('request fail', res)
reject()
}
})
})
}
const play = () => {
loadAudio('xxx-test.mp3').then(buffer => {
let source = audioCtx.createBufferSource()
source.buffer = buffer
source.connect(audioCtx.destination)
source.start()
}).catch(() => {
console.log('fail')
})
}
play()
- 获取音频强度示例:
const audioCtx = wx.createWebAudioContext();
const analyser = audioCtx.createAnalyser();
analyser.fftSize = 2048;
const bufferLength = analyser.fftSize;
const dataArray = new Uint8Array(bufferLength);
analyser.getByteTimeDomainData(dataArray);
其中第一段示例可以看出这个音频播放器是可以直接播放mp3的ArrayBuffer的。第二段可以发现就和web端的那个示例很像了。当时只是感觉这两个示例应该可以搞出音频强度,但是官方也没有直接把饭喂到嘴里,还得自己试。
实现
-
最开始是将两段代码整合,将音频帧的ArrayBuffer通过createWebAudioContext实例的decodeAudioData直接转换为可播放buffer试了一下播放,确实可以在录音时边录边播放,而且播放的是有损的音频,但是可以听出强度大小。顿时感觉这玩意靠谱但又不知道该怎么搞,边录边播放总是不行的。
-
结果又是一天快过去了,直到第二天,忽然搜到了抖音小程序那边给了一个音频强度绘制的示例,代码如下:
const ctx = tt.getAudioContext();
const audio = ctx.createAudio();
audio.src = "xxxx.mp3";
audio.oncanplay = () => {
audio.play();
};
const source = ctx.createMediaElementSource(audio);
const analyser = ctx.createAnalyser();
source.connect(analyser);
var bufferLength = analyser.frequencyBinCount;
var array = new Uint8Array(bufferLength);
analyser.getByteFrequencyData(array);
let values = 0;
const arrlength = array.length;
// 获取频率振幅
for (let i = 0; i < arrlength; i++) {
values += array[i];
}
//通过平均值去到当前音量数据
const average = values / arrlength;
const data = average;
- 仔细看发现BufferSourceNode链接的都是AnalyserNode,和web端保持一致,会不会是微信小程序那边也可以,然后又专门看了BufferSourceNode的connect方法的参数类型:AudioNode|AudioParam destination。似乎不支持AnalyserNode,但是也没办法试一试吧。结果完了,在微信开发者工具上直接运行都不运行了。本以为要绝望了,忽然抖了个机灵,在真机上试了一下,卧槽,成了。
代码
let recorder = wx.getRecorderManager()
const audioCtx = wx.createWebAudioContext()
const analyser = audioCtx.createAnalyser();
recorder.onFrameRecorded(listener => {
if (listener.isLastFrame) {
console.log('soundIntensity',0)
} else {
audioCtx.decodeAudioData(listener.frameBuffer, buffer => {
let source = audioCtx.createBufferSource()
source.buffer = buffer
source.connect(analyser)
source.start()
let n = new Uint8Array(analyser.frequencyBinCount)
analyser.getByteTimeDomainData(n)
let i = 0, r = 0, s = 0
r = Math.max.apply(null, n)
s = Math.min.apply(null, n)
i = (r - s) / 128
i = Math.round(i * 100 / 2)
i = i > 100 ? 100 : i
console.log('soundIntensity', listener.isLastFrame ? 0 : i)
}, err => {
console.error('decodeAudioData fail', err)
})
}
})
recorder.start({
duration: 1000,
sampleRate: 16000, //采样率
numberOfChannels: 1, //录音通道数
encodeBitRate: 96000, //编码码率
format: 'mp3', //音频格式,有效值 aac/mp3
frameSize: 1
})
总结
探索的过程总是很有趣。在群里问了好久,都是以前做过,但是忘了。或者就是网上不是有很多现成的。很受打击,在微信小程序交流社区也是一堆人年年天天在催官方给出示例的,但是似乎都没有结果。好在网上是开放的,在诸多的帮助下,终于搞出来,特写这篇文章分享给大家,希望需要的同学不至于像我一般搞了这么多天。另外吐槽一下掘金真的是微信小程序的荒漠,啥有效内容都搜索不到。感谢阅读,为了方便搜索就不做标题党了。
原文链接:https://juejin.cn/post/7325246251460460596 作者:CodePlayer