微信小程序录音、获取录音音量大小并绘制录音波形图,以及将PCM转MP3
前言
微信小程序自己提供了录音相关的接口:wx.getRecorderManager(),想要获取录音时的音量大小,则主要是通过录音指定帧回调来计算。
对于绘制录音的波形图,博主目前有两种方式:一种是使用微信自己的 Canvas 2d 进行绘图;另一种则是通过得到的波形图数组,直通过wx:for来遍历,每个波形数据对应一个元素标签view。不过从博主自己的实践中,感觉第二种方法好一些。
首先Canvas 2d 绘制,在 Android 端从视觉上来看,基本上算是正常的,但在 IOS 端就有些问题了,如会每一次绘制都会闪烁之类的,也可能是博主自己不太会用吧。其次,便是Canvas的绘制宽度,是存在限制的,而如果想要更精细的获取音量大小数据,那么录音的指定帧大小就要更小,获取的分段和波形数据就会更多,导致宽度越来越长,Canvas无法承载更多的数据。
关于为什么有将PCM格式转换为MP3,是因为录音时选择的是PCM格式,计算音量大小的方法,也只适用WAV与PCM格式,不适用与MP3格式,博主本人没有找到可以适用MP3的音量大小计算方法。同时,微信小程序的录音接口,指定帧回调,无法获取WAV格式的回调(文档是这么说的,不过博主实测在Android端,WAV格式可以获取回调,但IOS端不行)
基本参数
// 此处实际上并不是马上赋值的,只是将代码拆解出来,方便理解
// 录音接口
let recorderManager = null;
// 录音接口
recorderManager = wx.getRecorderManager()
// 录音启动
recorderManager.start(this.data.options);
// 文件管理接口
let fs = null
// 文件管理接口
fs = wx.getFileSystemManager()
// 录音参数 data内部的数据
options: {
// 录音的时长 1min
duration: 60 * 1000,
// 采样率 22050 24000
sampleRate: 12000,
// 录音通道数
numberOfChannels: 1,
// 编码码率 39000 40000
encodeBitRate: 64000,
// 音频格式
format: 'pcm',
// 指定帧大小回调 KB
frameSize: 0.8,
// 指定录音的音频输入源
audioSource: 'auto'
},
// 波形数据
waveData: [],
// 临时录音路径
tempRecordPath: '',
// 临时文件数据
tempRecordBuffer: null,
录音操作
在开始录音之前,需要先初始化一些操作,如录音监听的一系列回调:
监听录音开始:recorderManager.onStart()
在得知录音开始后,便可以根据自己的需求,设计相关操作,如录音计时等。
监听已录制完指定帧大小的文件事件:recorderManager.onFrameRecorded()
此回调为获取波形的一个关键步骤,通过监听已录制完指定帧大小的文件事件,获取得到的录音分片数据,需要注意的是,本人获取的回调中,前几段数据是空的(大概是前6条?具体情况具体分析叭,鸿蒙的部分机型前几段数据是正常的,Android与IOS测试的几台设备前6条回调都是空的),需要过滤一下。
具体原因尚不明确,没有积极去了解,可能是录音没加载好,启动的前一小段时间还早准备中,导致数据无法计算?
以及,原本是想获取WAV格式的录音回调,但发现在Android端可以拿到回调,但IOS却不行。官方文档目前也只明确说只能拿到PCM与MP3格式的回调。所以最后还是选择的PCM格式。
监听录音暂停:recorderManager.onPause()
此方法在录音被暂停时被调用,可以用来处理一些操作,如计时暂停等。
监听录音结束事件:recorderManager.onStop()
此回调则是监听录音结束,用来获取结束后的录音文件相关信息,如临时文件地址。通过这些信息,可以将文件转化为其他格式,以及保存文件等。
// 初始化录音相关监听
initRecord() {
console.log('触发initRecord')
// 监听录音开始事件
recorderManager.onStart(() => {
console.log('录音开始 recorder start')
this.startCountdown()
})
// 监听已录制完指定帧大小的文件事件
recorderManager.onFrameRecorded((res) => {
const { frameBuffer } = res
// console.log('录音指定帧 frameBuffer', frameBuffer, frameBuffer.byteLength)
if (res.frameBuffer && res.frameBuffer.byteLength) {
console.warn('指定帧回调,计算volume')
// 自定义RMS计算
const volume = calculateVolume(res.frameBuffer)
// console.log('volume:', volume)
// 初始会得到几个为0的回调数据,过滤掉
if (volume) {
// 将得到的波形数据保存
this.data.waveData.push(volume.toFixed(2))
}
// 更新波形图数据
this.setData({
waveData: this.data.waveData
})
// console.log('自定义计算:', volume)
}
})
// 监听录音暂停
recorderManager.onPause(() => {
console.warn('录音暂停')
})
// 监听录音结束事件
recorderManager.onStop((res) => {
console.log('录音结束 recorder stop', res)
// 显示加载弹窗
this.showLoading();
new Promise((resolve, reject) => {
try {
// const result = convertWavToMp3(res.tempFilePath);
// pcm 转 mp3 格式
const result = convertPcmToMp3(res.tempFilePath, this.data.options.sampleRate, this.data.options.numberOfChannels);
if (result) {
resolve(result);
} else {
reject(new Error('音频转换失败,返回结果为空'));
}
} catch (error) {
reject(error);
}
})
// 处理得到的 mp3 数据
.then(({ mp3FilePath, mp3Buffer }) => {
console.log('拿到的mp3FilePath:', mp3FilePath);
console.log('mp3Buffer:', mp3Buffer);
// 设置录音临时路径
this.setData({
tempRecordPath: mp3FilePath,
tempRecordBuffer: mp3Buffer.buffer,
recordState: 'stopPlay',
});
})
.catch(error => {
console.error('音频处理失败:', error);
wx.showToast({
title: '音频处理失败',
icon: 'none',
duration: 1000,
});
// 关闭蒙版
this.onClickHide()
try {
// 删除文件
setTimeout(() => {
console.log('准备删除文件')
fs.unlinkSync(this.data.tempRecordPath)
this.setData({
tempRecordPath: ''
})
}, 500)
} catch (e) {
console.warn(e)
}
})
.finally(() => {
// 无论成功或失败都隐藏加载弹窗
this.hideLoading();
});
})
},
计算音量大小
通过回调拿到的分段数据,计算此分段的录音大小calculateVolume()。
计算方法博主在网络上查找了许多内容,但不太适用,此计算方法为本人问AI的,测试了几次,符合我的预期,便直接使用了。
不过,部分机型,似乎不怎么会录入环境的声音,不说话的话,得到的回调就是0了,因此录音回调中的volume为0过滤的操作,还是需要优化一下的,可以将前几个0过滤掉(回调计算结果为0),随后,如果后续通过calculateVolume计算得到的音量大小是0的话,可以将手动将音量大小设置为1这样的(也可以默认将最小值设置为1这样的,就不会出现空数据了)。

// 计算录音数据对应的音量大小
function calculateVolume(pcmData) {
// 将ArrayBuffer转换为16位有符号整数数组
const int16Array = new Int16Array(pcmData);
// console.log('转换后的int16Array:', int16Array)
// 计算RMS (Root Mean Square)
let sum = 0;
for (let i = 0; i < int16Array.length; i++) {
const sample = int16Array[i] / 32768.0; // 归一化到[-1, 1]
sum += sample * sample;
}
// console.log('计算后的sum:', sum)
const rms = Math.sqrt(sum / int16Array.length);
// console.log('初始的rms:', rms)
// 转换为音量大小 (避免log10(0)的情况)
const db = 20 * Math.log10(Math.max(rms, 0.0001));
// console.log('转换为音量大小', db)
// 转换为0-100的线性值 (假设-60dB是静音,0dB是最大值)
// let volume = Math.max(0, (db + 60) / 60 * 100);
// 因得到为0的情况非常少见,因此这里将最小值0改为1 具体怎么改看个人情况
let volume = Math.max(1, (db + 60) / 60 * 100);
// console.log('转换为0-100的线性值', volume)
volume = Math.min(100, volume); // 限制在0-100
return volume;
}
最后返回的 volume则是当前回调对应的音量大小,将其保存至waveData数组内,用来绘制波形图。只要拿到了数据,怎么使用便是随便了,使用Canvas也好,元素标签遍历也好,看各自的喜好就行。
PCM转MP3
PCM数据转MP3的方法,需要借助外部库实现,这里是用的是lamejs
这里博主是直接下载的lame.all.js文件,保存在本地直接使用的

需要注意的是,如果是本地引用的话,需要在文件末尾加上以下代码将lamejs暴露出来:

代码:
import lamejs from '/utils/lame/lame.all'
const lame = lamejs
// 录音转MP3的核心方法
function convertPcmToMp3(pcmFilePath, sampleRate = 12000, channels = 1, bitRate = 32) {
try {
// 1. 读取PCM文件内容
const pcmBuffer = fs.readFileSync(pcmFilePath);
// console.log('读取PCM文件内容', pcmBuffer)
const pcmData = pcmBuffer
// console.log('提取PCM数据', pcmData)
// 1. 确保PCM数据是Int16Array格式
let samples;
if (pcmData instanceof ArrayBuffer) {
samples = new Int16Array(pcmData);
} else if (ArrayBuffer.isView(pcmData)) {
samples = new Int16Array(pcmData.buffer);
} else {
throw new Error('不支持的PCM数据格式');
}
// console.log('转换后的Int16Array:', samples);
// 2. 创建LAME编码器实例
const mp3Encoder = new lame.Mp3Encoder(
channels, // 声道数 (直接传入)
sampleRate, // 采样率 (直接传入)
bitRate // 比特率(kbps)
);
// console.log('编码器配置:', { channels, sampleRate, bitRate });
// 3. 编码为MP3
const mp3Buffer = mp3Encoder.encodeBuffer(samples);
// console.log('编码后的MP3数据:', mp3Buffer);
// 4. 生成MP3文件路径
const mp3FilePath = `${wx.env.USER_DATA_PATH}/Record_${Date.now()}.mp3`;
console.log('生成的MP3文件路径:', mp3FilePath);
// 5. 将MP3数据写入文件
fs.writeFileSync(mp3FilePath, mp3Buffer.buffer, 'binary');
const fileStats = fs.statSync(mp3FilePath);
console.log(`文件大小:${fileStats.size} 字节`);
console.log(`文件大小:${(fileStats.size / 1024).toFixed(2)} KB`);
return {
mp3FilePath,
mp3Buffer: new Uint8Array(mp3Buffer) // 返回Uint8Array格式
};
} catch (error) {
console.error('PCM转MP3失败:', error);
return null;
}
}
附带WAV格式转MP3
实际上WAV比PCM只是多了44字节的头部信息,其实转化操作基本上是一样的
// 录音转MP3的核心方法
function convertWavToMp3(wavFilePath) {
try {
// 1. 读取WAV文件内容
const wavBuffer = fs.readFileSync(wavFilePath);
console.log('读取WAV文件内容', wavBuffer)
// 2. 解析WAV文件头(获取采样率、声道数等信息)
const wavHeader = parseWavHeader(wavBuffer);
console.log('解析WAV文件头(获取采样率、声道数等信息)', wavHeader)
// 3. 提取PCM数据(WAV文件头之后的数据部分)
const pcmData = wavBuffer.slice(44); // WAV文件头通常为44字节
console.log('提取PCM数据(WAV文件头之后的数据部分)', pcmData)
// 4. 创建LAME编码器实例
const mp3Encoder = new lame.Mp3Encoder(
wavHeader.channels, // 声道数
wavHeader.sampleRate, // 采样率
32 // 比特率(kbps)默认 128
);
// 5. 将PCM数据转换为Int16Array(LAME需要的格式)
const samples = new Int16Array(pcmData);
// 6. 编码为MP3
const mp3Buffer = mp3Encoder.encodeBuffer(samples);
console.log('编码为MP3', mp3Buffer)
// 8. 生成MP3文件路径
const mp3FilePath = `${wx.env.USER_DATA_PATH}/recording_${Date.now()}.mp3`;
console.log('生成MP3文件路径', mp3FilePath)
// 9. 将MP3数据写入文件
fs.writeFileSync(mp3FilePath, mp3Buffer.buffer, 'binary')
const aaa = fs.readFileSync(mp3FilePath)
const bbb = fs.statSync(mp3FilePath)
// console.log('MP3转换成功:', mp3FilePath, aaa, bbb);
console.log(`文件大小:${bbb.size} 字节`);
console.log(`文件大小:${(bbb.size / 1024).toFixed(2)} KB`);
console.log(`文件大小:${(bbb.size / 1024 / 1024).toFixed(2)} MB`);
return { mp3FilePath, mp3Buffer };
} catch (error) {
console.error('MP3转换失败:', error);
return null;
}
}
// 解析WAV文件头
function parseWavHeader(buffer) {
const view = new DataView(buffer);
// 确保是WAV文件
const riff = String.fromCharCode(view.getUint8(0), view.getUint8(1), view.getUint8(2), view.getUint8(3));
const wave = String.fromCharCode(view.getUint8(8), view.getUint8(9), view.getUint8(10), view.getUint8(11));
if (riff !== 'RIFF' || wave !== 'WAVE') {
throw new Error('不是有效的WAV文件');
}
// 提取关键信息
const sampleRate = view.getUint32(24, true);
const channels = view.getUint16(22, true);
const bitDepth = view.getUint16(34, true);
return {
sampleRate,
channels,
bitDepth,
dataOffset: view.getUint32(40, true) // 数据起始位置
};
}
波形图绘制
博主Canvas 2d的实现方式其实是比较复杂的,备份的代码不太完全,整理起来也有些麻烦,因此就偷懒不做展示了,这边只展示通过wx:for遍历元素的方法:
<scroll-view
class="wave-container"
scroll-x="true"
scroll-left="{{scrollLeft}}"
enhanced
bounces="{{true}}"
show-scrollbar="{{false}}"
enable-flex
>
<view
class="wave-wrapper"
style="width: {{waveData.length * 5}}px; margin-left: {{marginLeft}}px;"
>
<view
class="wave-item"
wx:for="{{waveData}}"
wx:key="item"
style="height: {{item / 100 * 75}}px;"
></view>
</view>
</scroll-view>
Component({
properties: {
waveData: {
type: Array,
value: [],
observer(newVal) {
// console.log('得到的新波形数据:', newVal.at(-1), newVal)
if (newVal && newVal.length > 0) {
let marginLeft = 0;
const waveWidth = newVal.length * 5
if (waveWidth < this.data.containerWidth) {
marginLeft = this.data.containerWidth - waveWidth;
}
this.setData({
marginLeft: marginLeft,
scrollLeft: waveWidth - this.data.containerWidth
})
}
}
},
},
data: {
containerWidth: 0,
containerHeight: 0,
scrollLeft: 0,
marginLeft: 0,
},
lifetimes: {
attached() {
this.getContainerRect()
},
detached() {
}
},
methods: {
// 获取容器尺寸
getContainerRect() {
return new Promise((resolve) => {
wx.createSelectorQuery()
.in(this)
.select('.wave-container')
.boundingClientRect(rect => {
console.log('获取容器尺寸')
if (rect) {
this.setData({
containerWidth: rect.width,
containerHeight: rect.height
}, resolve);
console.log('尺寸:', this.data.containerWidth, this.data.containerHeight)
} else {
// 添加重试机制
setTimeout(() => {
console.log('重试获取尺寸')
this.getContainerRect().then(resolve);
}, 100);
}
})
.exec();
});
},
}
});
.wave-container {
width: 100%;
height: 75px;
overflow-x: auto;
background-color: #654321;
position: relative;
}
.wave-wrapper {
height: 75px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-end;
}
.wave-item {
width: 1px;
height: 0px;
margin: 2px;
border-radius: 5px;
background-color: #123456;
}
/*偷偷改个颜色awa*/
总结
从实践来看,微信小程序录音方面的一些接口,Android端与IOS端的一些效果是不太一样的,比如录音回调和错误回调等,以及两端录音所需要设置的参数、音频播放的相关效果也有些不太相同,存在兼容性问题。录音波形这一块的后续操作也是折煞我也,太菜了是这样的QwQ。
更多推荐

所有评论(0)