html5录音功能实战示例
缘起
由于项目需要,我们要在web端实现录音功能。一开始,找到的方案有两个,一个是通过iframe,一个是html5的getusermedia api。由于我们的录音功能不需要兼容ie浏览器,所以毫不犹豫的选择了html5提供的getusermedia去实现。基本思路是参考了官方的api文档以及网上查找的一些方案做结合做出了适合项目需要的方案。但由于我们必须保证这个录音功能能够同时在pad端、pc端都可以打开,所以其中也踩了一些坑。以下为过程还原。
步骤1
由于新的api是通过navigator.mediadevices.getusermedia,且返回一个promise。
而旧的api是navigator.getusermedia,于是做了一个兼容性。代码如下:
// 老的浏览器可能根本没有实现 mediadevices,所以我们可以先设置一个空的对象 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); }); };
步骤2
这是网上存在的一个方法,封装了一个hzrecorder。基本上引用了这个方法。调用hzrecorder.get就可以调起录音接口,这个方法传入一个callback函数,new hzrecorder后执行callback函数且传入一个实体化后的hzrecorder对象。可以通过该对象的方法实现开始录音、暂停、停止、播放等功能。
var hzrecorder = function (stream, config) { config = config || {}; config.samplebits = config.samplebits || 8; //采样数位 8, 16 config.samplerate = config.samplerate || (44100 / 6); //采样率(1/6 44100) //创建一个音频环境对象 audiocontext = window.audiocontext || window.webkitaudiocontext; var context = new audiocontext(); //将声音输入这个对像 var audioinput = context.createmediastreamsource(stream); //设置音量节点 var volume = context.creategain(); audioinput.connect(volume); //创建缓存,用来缓存声音 var buffersize = 4096; // 创建声音的缓存节点,createscriptprocessor方法的 // 第二个和第三个参数指的是输入和输出都是双声道。 var recorder = context.createscriptprocessor(buffersize, 2, 2); var audiodata = { size: 0 //录音文件长度 , buffer: [] //录音缓存 , inputsamplerate: context.samplerate //输入采样率 , inputsamplebits: 16 //输入采样数位 8, 16 , outputsamplerate: config.samplerate //输出采样率 , oututsamplebits: config.samplebits //输出采样数位 8, 16 , input: function (data) { this.buffer.push(new float32array(data)); this.size += data.length; } , compress: function () { //合并压缩 //合并 var data = new float32array(this.size); var offset = 0; for (var i = 0; i < this.buffer.length; i++) { data.set(this.buffer[i], offset); offset += this.buffer[i].length; } //压缩 var compression = parseint(this.inputsamplerate / this.outputsamplerate); var length = data.length / compression; var result = new float32array(length); var index = 0, j = 0; while (index < length) { result[index] = data[j]; j += compression; index++; } return result; } , encodewav: function () { var samplerate = math.min(this.inputsamplerate, this.outputsamplerate); var samplebits = math.min(this.inputsamplebits, this.oututsamplebits); var bytes = this.compress(); var datalength = bytes.length * (samplebits / 8); var buffer = new arraybuffer(44 + datalength); var data = new dataview(buffer); var channelcount = 1;//单声道 var offset = 0; var writestring = function (str) { for (var 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 (var i = 0; i < bytes.length; i++, offset++) { var s = math.max(-1, math.min(1, bytes[i])); var val = s < 0 ? s * 0x8000 : s * 0x7fff; val = parseint(255 / (65535 / (val + 32768))); data.setint8(offset, val, true); } } else { for (var i = 0; i < bytes.length; i++, offset += 2) { var s = math.max(-1, math.min(1, bytes[i])); data.setint16(offset, s < 0 ? s * 0x8000 : s * 0x7fff, true); } } return new blob([data], { type: 'audio/wav' }); } }; //开始录音 this.start = function () { audioinput.connect(recorder); recorder.connect(context.destination); }; //停止 this.stop = function () { recorder.disconnect(); }; // 结束 this.end = function() { context.close(); }; // 继续 this.again = function() { recorder.connect(context.destination); }; //获取音频文件 this.getblob = function () { this.stop(); return audiodata.encodewav(); }; //回放 this.play = function (audio) { audio.src = window.url.createobjecturl(this.getblob()); }; //上传 this.upload = function (url, callback) { var fd = new formdata(); fd.append('audiodata', this.getblob()); var xhr = new xmlhttprequest(); if (callback) { xhr.upload.addeventlistener('progress', function (e) { callback('uploading', e); }, false); xhr.addeventlistener('load', function (e) { callback('ok', e); }, false); xhr.addeventlistener('error', function (e) { callback('error', e); }, false); xhr.addeventlistener('abort', function (e) { callback('cancel', e); }, false); } xhr.open('post', url); xhr.send(fd); }; //音频采集 recorder.onaudioprocess = function (e) { audiodata.input(e.inputbuffer.getchanneldata(0)); //record(e.inputbuffer.getchanneldata(0)); }; }; //抛出异常 hzrecorder.throwerror = function (message) { throw new function () { this.tostring = function () { return message; };}; }; //是否支持录音 hzrecorder.canrecording = (navigator.getusermedia != null); //获取录音机 hzrecorder.get = function (callback, config) { if (callback) { navigator.mediadevices .getusermedia({ audio: true }) .then(function(stream) { let rec = new hzrecorder(stream, config); callback(rec); }) .catch(function(error) { hzrecorder.throwerror('无法录音,请检查设备状态'); }); } }; window.hzrecorder = hzrecorder;
以上,已经可以满足大部分的需求。但是我们要兼容pad端。我们的pad有几个问题必须解决。
- 录音格式必须是mp3才能播放
- window.url.createobjecturl传入blob数据在pad端报错,转不了
以下为解决这两个问题的方案。
步骤3
以下为我实现 录音格式为mp3 和 window.url.createobjecturl传入blob数据在pad端报错 的方案。
1、修改hzrecorder里的audiodata对象代码。并引入网上一位大神的一个js文件lamejs.js
const lame = new lamejs(); let audiodata = { samplesmono: null, maxsamples: 1152, mp3encoder: new lame.mp3encoder(1, context.samplerate || 44100, config.bitrate || 128), databuffer: [], size: 0, // 录音文件长度 buffer: [], // 录音缓存 inputsamplerate: context.samplerate, // 输入采样率 inputsamplebits: 16, // 输入采样数位 8, 16 outputsamplerate: config.samplerate, // 输出采样率 oututsamplebits: config.samplebits, // 输出采样数位 8, 16 convertbuffer: function(arraybuffer) { let data = new float32array(arraybuffer); let out = new int16array(arraybuffer.length); this.floatto16bitpcm(data, out); return out; }, floatto16bitpcm: function(input, output) { for (let i = 0; i < input.length; i++) { let s = math.max(-1, math.min(1, input[i])); output[i] = s < 0 ? s * 0x8000 : s * 0x7fff; } }, appendtobuffer: function(mp3buf) { this.databuffer.push(new int8array(mp3buf)); }, encode: function(arraybuffer) { this.samplesmono = this.convertbuffer(arraybuffer); let remaining = this.samplesmono.length; for (let i = 0; remaining >= 0; i += this.maxsamples) { let left = this.samplesmono.subarray(i, i + this.maxsamples); let mp3buf = this.mp3encoder.encodebuffer(left); this.appendtobuffer(mp3buf); remaining -= this.maxsamples; } }, finish: function() { this.appendtobuffer(this.mp3encoder.flush()); return new blob(this.databuffer, { type: 'audio/mp3' }); }, input: function(data) { this.buffer.push(new float32array(data)); this.size += data.length; }, compress: function() { // 合并压缩 // 合并 let data = new float32array(this.size); let offset = 0; for (let i = 0; i < this.buffer.length; i++) { data.set(this.buffer[i], offset); offset += this.buffer[i].length; } // 压缩 let compression = parseint(this.inputsamplerate / this.outputsamplerate, 10); let length = data.length / compression; let result = new float32array(length); let index = 0; let j = 0; while (index < length) { result[index] = data[j]; j += compression; index++; } return result; }, encodewav: function() { let samplerate = math.min(this.inputsamplerate, this.outputsamplerate); let samplebits = math.min(this.inputsamplebits, this.oututsamplebits); let bytes = this.compress(); let datalength = bytes.length * (samplebits / 8); let buffer = new arraybuffer(44 + datalength); let data = new dataview(buffer); let channelcount = 1; // 单声道 let offset = 0; let writestring = function(str) { 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, bytes[i])); let val = s < 0 ? s * 0x8000 : s * 0x7fff; val = parseint(255 / (65535 / (val + 32768)), 10); data.setint8(offset, val, true); } } else { for (let i = 0; i < bytes.length; i++, offset += 2) { const s = math.max(-1, math.min(1, bytes[i])); data.setint16(offset, s < 0 ? s * 0x8000 : s * 0x7fff, true); } } return new blob([data], { type: 'audio/wav' }); } };
2、修改hzrecord的音频采集的调用方法。
// 音频采集 recorder.onaudioprocess = function(e) { audiodata.encode(e.inputbuffer.getchanneldata(0)); };
3、hzrecord的getblob方法。
this.getblob = function() { this.stop(); return audiodata.finish(); };
4、hzrecord的play方法。把blob转base64url。
this.play = function(func) { readblobasdataurl(this.getblob(), func); }; function readblobasdataurl(data, callback) { let filereader = new filereader(); filereader.onload = function(e) { callback(e.target.result); }; filereader.readasdataurl(data); }
至此,已经解决以上两个问题。
步骤4
这里主要介绍怎么做录音时的动效。我们的一个动效需求为:
根据传入的音量大小,做一个圆弧动态扩展。
// 创建analyser节点,获取音频时间和频率数据 const analyser = context.createanalyser(); audioinput.connect(analyser); const inputanalyser = new uint8array(1); const wrapele = $this.refs['wrap']; let ctx = wrapele.getcontext('2d'); const width = wrapele.width; const height = wrapele.height; const center = { x: width / 2, y: height / 2 }; function drawarc(ctx, color, x, y, radius, beginangle, endangle) { ctx.beginpath(); ctx.linewidth = 1; ctx.strokestyle = color; ctx.arc(x, y, radius, (math.pi * beginangle) / 180, (math.pi * endangle) / 180); ctx.stroke(); } (function drawspectrum() { analyser.getbytefrequencydata(inputanalyser); // 获取频域数据 ctx.clearrect(0, 0, width, height); // 画线条 for (let i = 0; i < 1; i++) { let value = inputanalyser[i] / 3; // <===获取数据 let colors = []; if (value <= 16) { colors = ['#f5a631', '#f5a631', '#e4e4e4', '#e4e4e4', '#e4e4e4', '#e4e4e4']; } else if (value <= 32) { colors = ['#f5a631', '#f5a631', '#f5a631', '#f5a631', '#e4e4e4', '#e4e4e4']; } else { colors = ['#f5a631', '#f5a631', '#f5a631', '#f5a631', '#f5a631', '#f5a631']; } drawarc(ctx, colors[0], center.x, center.y, 52 + 16, -30, 30); drawarc(ctx, colors[1], center.x, center.y, 52 + 16, 150, 210); drawarc(ctx, colors[2], center.x, center.y, 52 + 32, -22.5, 22.5); drawarc(ctx, colors[3], center.x, center.y, 52 + 32, 157.5, 202.5); drawarc(ctx, colors[4], center.x, center.y, 52 + 48, -13, 13); drawarc(ctx, colors[5], center.x, center.y, 52 + 48, 167, 193); } // 请求下一帧 requestanimationframe(drawspectrum); })();
缘尽
至此,一个完整的html5录音功能方案已经完成。有什么需要补充,不合理的地方的欢迎留言。
ps:lamejs可参考这个
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
下一篇: 孙皓是什么样的人?真是遗臭万年