前言
前段时间,项目中用到html5音频采集,主要是和微信录音一样,流程是按住说话,右侧滑动可以音频转文字,左侧滑动撤销。关于按住说话及左右侧滑动交互,相对简单,主要是运用了onTouchStart,onTouchMove,onTouchEnd三个事件完成。我之前文章也有过模仿微信语音播放效果动画,其中左右侧滑动位置主要是根据 e.targetTouches[0].pageX,和 e.targetTouches[0].pageY来完成的。本文着重讲解一下html5音频采集解决方案。
之所以叫解决方案,因为这里有涉及了音频采集的各种问题,例如音波,格式转换,录制格式,多段音频拼接,多个语音合成等等。语音采集我主要用了。
关于音频采集
音频采集可以利用navigator.mediaDevices.getUserMedia({ audio: true }),来自己手动采集。 判断有无权限及开启权限可以如下代码
export const checkIsOpenPermission = () => {
if (navigator.mediaDevices === undefined) {
navigator.mediaDevices = {}
}
// 一些浏览器部分支持 mediaDevices。我们不能直接给对象设置 getUserMedia
// 因为这样可能会覆盖已有的属性。这里我们只会在没有getUserMedia属性的时候添加它。
if (navigator.mediaDevices.getUserMedia === undefined) {
let getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia
navigator.mediaDevices.getUserMedia = function (constraints) {
// 首先,如果有getUserMedia的话,就获得它
// 一些浏览器根本没实现它 - 那么就返回一个error到promise的reject来保持一个统一的接口
if (!getUserMedia) {
return Promise.reject(new Error('getUserMedia is not implemented in this browser'))
}
// 否则,为老的navigator.getUserMedia方法包裹一个Promise
return new Promise(function (resolve, reject) {
getUserMedia.call(navigator, constraints, resolve, reject)
})
}
}
navigator.mediaDevices
.getUserMedia({ audio: true })
.then(function (stream) {
// 用户成功开启
console.log('user allow audio')
})
.catch(function (error) {
switch (error.code || error.name) {
case 'PERMISSION_DENIED':
case 'PermissionDeniedError':
Toast.show({
style: {
zIndex: 1000000
},
content: '用户拒绝提供信息'
})
break
case 'NOT_SUPPORTED_ERROR':
case 'NotSupportedError':
Toast.show({
style: {
zIndex: 1000000
},
content: '浏览器不支持硬件设备。'
})
break
case 'MANDATORY_UNSATISFIED_ERROR':
case 'MandatoryUnsatisfiedError':
Toast.show({
style: {
zIndex: 1000000
},
content: '无法发现指定的硬件设备。'
})
break
default:
Toast.show({
style: {
zIndex: 1000000
},
content: '无法打开麦克风'
})
break
}
})
}
多个音频的拼接
// 拼接音频的方法
const concatAudio = (arrBufferList) => {
// 获得 AudioBuffer
const audioBufferList = arrBufferList;
// 最大通道数
const maxChannelNumber = Math.max(...audioBufferList.map(audioBuffer => audioBuffer.numberOfChannels));
// 总长度
const totalLength = audioBufferList.map((buffer) => buffer.length).reduce((lenA, lenB) => lenA + lenB, 0);
// 创建一个新的 AudioBuffer
const newAudioBuffer = audioContext.createBuffer(maxChannelNumber, totalLength, audioBufferList[0].sampleRate);
// 将所有的 AudioBuffer 的数据拷贝到新的 AudioBuffer 中
let offset = 0;
audioBufferList.forEach((audioBuffer, index) => {
for (let channel = 0; channel < audioBuffer.numberOfChannels; channel++) {
newAudioBuffer.getChannelData(channel).set(audioBuffer.getChannelData(channel), offset);
}
offset += audioBuffer.length;
});
return newAudioBuffer;
}
// 获取音频AudioBuffer
// AudioContext
const audioContext = new AudioContext();
// 基于src地址获得 AudioBuffer 的方法
const getAudioBuffer = (src) => {
return new Promise((resolve, reject) => {
fetch(src).then(response => response.arrayBuffer()).then(arrayBuffer => {
audioContext.decodeAudioData(arrayBuffer).then(buffer => {
resolve(buffer);
});
})
})
}
多个音频的合并
// 合并音频的方法
const mergeAudio = (arrBufferList) => {
// 获得 AudioBuffer
const audioBufferList = arrBufferList;
// 最大播放时长
const maxDuration = Math.max(...audioBufferList.map(audioBuffer => audioBuffer.duration));
// 最大通道数
const maxChannelNumber = Math.max(...audioBufferList.map(audioBuffer => audioBuffer.numberOfChannels));
// 创建一个新的 AudioBuffer
const newAudioBuffer = audioContext.createBuffer(maxChannelNumber, audioBufferList[0].sampleRate * maxDuration, audioBufferList[0].sampleRate);
// 将所有的 AudioBuffer 的数据合并到新的 AudioBuffer 中
audioBufferList.forEach((audioBuffer, index) => {
for (let channel = 0; channel < audioBuffer.numberOfChannels; channel++) {
const outputData = newAudioBuffer.getChannelData(channel);
const bufferData = audioBuffer.getChannelData(channel);
for (let i = audioBuffer.getChannelData(channel).length - 1; i >= 0; i--) {
outputData[i] += bufferData[i];
}
newAudioBuffer.getChannelData(channel).set(outputData);
}
});
return newAudioBuffer;
}
音量的改变
// 定义一个AudioContext对象
// 因为 Web Audio API都是源自此对象
const audioContext = new AudioContext();
// 创建一个gainNode对象
// gainNode可以对音频输出进行一些控制
const gainNode = audioContext.createGain();
// 音量设置为20%
gainNode.gain.value = 0.2;
// 这个很有必要,建立联系
gainNode.connect(audioContext.destination);
// 创建AudioBufferSourceNode
let source = audioContext.createBufferSource();
// 获取音频资源
fetch('./bgmusic.mp3')
.then(res => res.arrayBuffer())
.then(buffer => audioContext.decodeAudioData(buffer))
.then(audioBuffer => {
source.buffer = audioBuffer;
source.connect(gainNode);
});
// 当需要播放的时候,执行
source.start(0);
第三方音频解决方案
我这边推荐的html5音频处理开源库是https://gitee.com/xiangyuecn/Recorder#%E9%99%84android-hybrid-app---webview%E4%B8%AD%E5%BD%95%E9%9F%B3%E7%A4%BA%E4%BE%8B
一、引入方式
//必须引入的核心,换成require也是一样的。注意:recorder-core会自动往window下挂载名称为Recorder对象,全局可调用window.Recorder,也许可自行调整相关源码清除全局污染
import Recorder from 'recorder-core'
//引入相应格式支持文件;如果需要多个格式支持,把这些格式的编码引擎js文件放到后面统统引入进来即可
import 'recorder-core/src/engine/mp3'
import 'recorder-core/src/engine/mp3-engine' //如果此格式有额外的编码引擎(*-engine.js)的话,必须要加上
//以上三个也可以合并使用压缩好的recorder.xxx.min.js
//比如 import Recorder from 'recorder-core/recorder.mp3.min' //已包含recorder-core和mp3格式支持
//可选的插件支持项
import 'recorder-core/src/extensions/waveview'
//ts import 提示:npm包内已自带了.d.ts声明文件(不过是any类型)
recorder-core库,可以录制各种格式视频,支持格式转换,假如语音转文字,需要pcm格式,这个开源库可以直接录制pcm格式,支持多个pcm格式的拼接。
const pcmMerge = function (fileBytesList, bitRate, sampleRate, True, False) {
//计算所有文件总长度
var size = 0
for (var i = 0; i < fileBytesList.length; i++) {
size += fileBytesList[i].byteLength
}
//全部直接拼接到一起
var fileBytes = new Uint8Array(size)
var pos = 0
for (var i = 0; i < fileBytesList.length; i++) {
var bytes = fileBytesList[i]
fileBytes.set(bytes, pos)
pos += bytes.byteLength
}
//计算合并后的总时长
var duration = Math.round(((size * 8) / bitRate / sampleRate) * 1000)
True(fileBytes, duration, { bitRate: bitRate, sampleRate: sampleRate }, size)
}
二、支持波形播放插件
例如WaveView插件,FrequencyHistogramView插件等等,音频录制波形可视化插件
三、支持音频混音、变速变调音频转换
小结
简单的音频录制需求可以自己实现,但是需要复杂的,音频解决方案,我这边推荐使用recorder-core库,这个库例子比较多,使用相对简单。推荐给大家!