关于录音 Recorder.js
看,学,做
关于目录
- 关于关键 Api
- 关于关键代码实现
- 关于 audio 标签的细节
关于实现所需 API
- navigator.mediaDevices.getUserMedia() 获取客户端录音流
- AudioContext 音频处理对象
- AudioContext.createMediaStreamSource(stream) 关联录音流
- AudioContext.createScriptProcessor(option) 获取音频流
关于关键代码实现
首要必须是获取客户端录音权限,并给予一些错误提示
/*
@type constraints = { audio: true, video: true }
@description 一个 MediaStreamConstraints 对象,
指定了请求的媒体类型和相对应的参数。
*/
navigator.mediaDevices.getUserMedia([constraints])
.(function(stream) {
/* 使用这个 stream stream */
const context = new AudioContext({ sampleRate: 48000 });
const audioInput = context.createMediaStreamSource(stream);
const recorder = context.createScriptProcessor(4096, 1, 1);
})
.catch(function(err) {
/*
处理error
------------
可能有的错误
AbortError [中止错误]
NotAllowedError [拒绝错误]
NotFoundError [找不到错误]
NotReadableError [无法读取错误]
OverConstrainedError [无法满足要求错误]
SecurityError [安全错误]
TypeError [类型错误]
*/
switch(err.message || err.name){
case 'PERMISSION_DENIED':
case 'PermissionDeniedError':
alert('用户拒绝提供信息。');
break;
case 'NOT_SUPPORTED_ERROR':
case 'NotSupportedError':
alert('浏览器不支持硬件设备。');
break;
case 'MANDATORY_UNSATISFIED_ERROR':
case 'MandatoryUnsatisfiedError':
alert('无法发现指定的硬件设备。');
break;
default:
alert('无法打开麦克风。异常信息:' + (err.code || err.name));
break;
}
});
有权限录音流后,目前所见的常用做法,是放入一个对象内,
对象有些根据所传入的配置要求初始化,并初始化缓存数据,
最后暴露出简单的方法
对象初始化
class Recorder{
// 初始化 config 与需要的数据流
constructor(stream, config, callback){
// 配置初始化
this.callback = callback
this.sampleBits = config.sampleBits || 16
this.sampleRate = config.sampleRate || 16000
this.context = new AudioContext({ sampleRate: 48000 })
this.audioInput = this.context.createMediaStreamSource(stream)
this.recorder = this.context.createScriptProcessor(4096,1,1)
// Audio Source 数据缓存
this.size = 0
this.buffer = []
this.inputSampleRate = 48000
this.inputSampleBits = 16
this.outputSampleRate = this.sampleRate
this.outputSampleBits = this.sampleBits
this.objUrl = null
}
}
Tip:如果 AudioContext 对象无配置采样率,则会根据系统变化,已知 Mac 默认是 44100, Window 默认是 48000
编写内置工具函数
内置需要一些压缩与输出的算法工具函数
- clear() 清空缓存数据 or 上一次数据
- input() 存储过程数据
- compress() 压缩算法
- encode() 输出格式,可自行百度获取
class Recorder{
/* ... */
/* 清空数据 */
claer(){
this.buffer = [];
this.size = 0;
}
/* 数据缓存 */
input(inputBuffer){
this.buffer.push(new Float32Array(inputBuffer));
this.size += inputBuffer.length;
}
/* 数据合并压缩 */
compress(){
// 合并
const data = new Float32Array(this.size);
for (let i = 0, offset = 0; i < this.buffer.length; i++) {
data.set(this.buffer[i], offset);
offset += this.buffer[i].length;
}
//压缩
const compression = this.inputSampleRate / this.outputSampleRate;
const length = data.length / compression;
const result = new Float32Array(length);
for (let i = 0; i < length; i += compression) {
result.push(this.buffer[i]);
}
return result
}
}
encode()
方法有几种,主要是为了输出不同的格式,以满足不同的需求,
这里只描绘一下 Wav
这里也是网上拿下来的
class Recorder {
encodeWAV(){
const sampleRate = Math.min(this.inputSampleRate, this.outputSampleRate);
const sampleBits = Math.min(this.inputSampleBits, this.outputSampleBits);
const bytes = this.compress();
const dataLength = bytes.length * (sampleBits / 8);
const buffer = new ArrayBuffer(44 + dataLength);
const data = new DataView(buffer);
const channelCount = 1; //单声道
let offset = 0;
const writeString = function (str: string) {
for (let i = 0; i < str.length; i++) {
data.setUint8(offset + i, str.charCodeAt(i));
}
};
// 资源交换文件标识符
writeString('RIFF');
offset += 4;
// 下个地址开始到文件尾总字节数,即文件大小-8
data.setUint32(offset, 36 + dataLength, true);
offset += 4;
// WAV文件标志
writeString('WAVE');
offset += 4;
// 波形格式标志
writeString('fmt ');
offset += 4;
// 过滤字节,一般为 0x10 = 16
data.setUint32(offset, 16, true);
offset += 4;
// 格式类别 (PCM形式采样数据)
data.setUint16(offset, 1, true);
offset += 2;
// 通道数
data.setUint16(offset, channelCount, true);
offset += 2;
// 采样率,每秒样本数,表示每个通道的播放速度
data.setUint32(offset, sampleRate, true);
offset += 4;
// 波形数据传输率 (每秒平均字节数) 单声道×每秒数据位数×每样本数据位/8
data.setUint32(offset, channelCount * sampleRate * (sampleBits / 8), true);
offset += 4;
// 快数据调整数 采样一次占用字节数 单声道×每样本的数据位数/8
data.setUint16(offset, channelCount * (sampleBits / 8), true);
offset += 2;
// 每样本数据位数
data.setUint16(offset, sampleBits, true);
offset += 2;
// 数据标识符
writeString('data');
offset += 4;
// 采样数据总数,即数据总大小-44
data.setUint32(offset, dataLength, true);
offset += 4;
// 写入采样数据
if (sampleBits === 8) {
for (let i = 0; i < bytes.length; i++, offset++) {
const s = Math.max(-1, Math.min(1, Number(bytes[i])));
const val = s < 0 ? s * 0x8000 : s * 0x7fff;
val = 255 / (65535 / (val + 32768));
data.setInt8(offset, val);
}
} else {
for (let i = 0; i < bytes.length; i++, offset += 2) {
const s = Math.max(-1, Math.min(1, Number(bytes[i])));
data.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7fff, true);
}
}
return new Blob([data], { type: 'audio/wav' });
}
}
编写可暴露的数据
- start() 开始记录录音数据
- stop() 停止记录录音数据
- getAudioUrl() 获取录音数据
class Recorder {
/* ... */
// 开始
start(){
console.log('recorder start');
this.audioInput.connect(this.recorder);
this.recorder.connect(this.context.destination);
this.recorder.onaudioprocess = e => {
const inputBuffer = e.inputBuffer.getChannelData(0);
this.input(inputBuffer);
};
}
// 停止
stop(){
this.recorder.disconnect();
}
// 获取可播放 url
getAudioUrl(){
const url = this.encodeWAV();
if (this.objUrl) {
URL.revokeObjectURL(this.objUrl);
}
this.objUrl = URL.createObjectURL(wav);
return this.objUrl
}
}
实时传送录音数据流
- 需要进行一次压缩
- 需要一个回调函数
class Recorder {
/* ... */
sendData(){
const compression = this.inputSampleRate / this.outputSampleRate;
const length = inputBuffer.length / compression;
const result = new Float32Array(length);
let index = 0,
j = 0;
while (index < length) {
result[index] = inputBuffer[j];
j += compression;
index++;
}
const dataLength = result.length * (16 / 8);
const buffer = new ArrayBuffer(dataLength);
const blockData = new Uint8Array(buffer);
blockData && this.callback && this.callback(blockData)
}
}
关于小结
尚未解决的问题
createScriptProcessor 已经不推荐使用,然在大部分浏览器内尚可运行,
且新的录音 Api 尚未有新鲜可循的 blog 文教授
本文大致才是参照,网上诸多老旧 blog 以及已有的插件组成的,大致可以看作是一片缝合水文,
本文目的在于记录自己开发前置需求中的一些思考,以及找寻到的 blog
感谢阅读
参照文章
- [jqpeng 的技术记事本][https://www.cnblogs.com/xiaoqi/p/6993912.html]
- [Recorder 用于 html5 录音][https://github.com/xiangyuecn/Recorder]
- [使用 html5 在网页录音和保存][http://luoma.pro/Content/Detail/503?parentId=1]