缘起
因为新项目必须,大家要在web端完成音频作用。1刚开始,寻找的计划方案有两个,1个是根据iframe,1个是html5的getUserMedia api。因为大家的音频作用不必须适配IE访问器,因此绝不迟疑的挑选了html5出示的getUserMedia去完成。基础思路是参照了官方的api文本文档和在网上搜索的1些计划方案做融合做出了合适新项目必须的计划方案。但因为大家务必确保这个音频作用可以另外在pad端、pc端都可以以开启,因此在其中也踩了1些坑。下列为全过程复原。
流程1
因为新的api是根据navigator.mediaDevices.getUserMedia,且回到1个promise。
而旧的api是navigator.getUserMedia,因而做了1个适配性。编码以下:
// 老的访问器将会压根沒有完成 mediaDevices,因此大家能够先设定1个空的目标 if (navigator.mediaDevices === undefined) { navigator.mediaDevices = {}; } // 1些访问器一部分适用 mediaDevices。大家不可以立即给目标设定 getUserMedia // 由于这样将会会遮盖已有的特性。这里大家只会在沒有getUserMedia特性的情况下加上它。 if (navigator.mediaDevices.getUserMedia === undefined) { let getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia; navigator.mediaDevices.getUserMedia = function(constraints) { // 最先,假如有getUserMedia的话,就得到它 // 1些访问器压根没完成它 - 那末就回到1个error到promise的reject来维持1个统1的插口 if (!getUserMedia) { return Promise.reject(new Error('getUserMedia is not implemented in this browser')); } // 不然,为老的navigator.getUserMedia方式包裹1个Promise return new Promise(function(resolve, reject) { getUserMedia.call(navigator, constraints, resolve, reject); }); };
流程2
这是在网上存在的1个方式,封裝了1个HZRecorder。基础上引入了这个方式。启用HZRecorder.get便可以调起音频插口,这个方式传入1个callback涵数,new HZRecorder后实行callback涵数且传入1个实体线化后的HZRecorder目标。能够根据该目标的方式完成刚开始音频、中止、终止、播发等作用。
var HZRecorder = function (stream, config) { config = config || {}; config.sampleBits = config.sampleBits || 8; //取样多位 8, 16 config.sampleRate = config.sampleRate || (44100 / 6); //取样率(1/6 44100) //建立1个声频自然环境目标 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方式的 // 第2个和第3个主要参数指的是键入和輸出全是双声道。 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; // 下个详细地址刚开始到文档尾总字节数,即文档尺寸⑻ data.setUint32(offset, 36 + dataLength, true); offset += 4; // WAV文档标示 writeString('WAVE'); offset += 4; // 波形文件格式标示 writeString('fmt '); offset += 4; // 过虑字节,1般为 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; // 快数据信息调剂数 取样1次占有字节数 单声道×每样版的数据信息位数/8 data.setUint16(offset, channelCount * (sampleBits / 8), true); offset += 2; // 每样版数据信息位数 data.setUint16(offset, sampleBits, true); offset += 2; // 数据信息标志符 writeString('data'); offset += 4; // 取样数据信息总数,即数据信息总尺寸⑷4 data.setUint32(offset, dataLength, true); offset += 4; // 写入取样数据信息 if (sampleBits === 8) { for (var i = 0; i < bytes.length; i++, offset++) { var s = Math.max(⑴, 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(⑴, 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目标编码。并引进在网上1位高手的1个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(⑴, 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; // 下个详细地址刚开始到文档尾总字节数,即文档尺寸⑻ data.setUint32(offset, 36 + dataLength, true); offset += 4; // WAV文档标示 writeString('WAVE'); offset += 4; // 波形文件格式标示 writeString('fmt '); offset += 4; // 过虑字节,1般为 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; // 快数据信息调剂数 取样1次占有字节数 单声道×每样版的数据信息位数/8 data.setUint16(offset, channelCount * (sampleBits / 8), true); offset += 2; // 每样版数据信息位数 data.setUint16(offset, sampleBits, true); offset += 2; // 数据信息标志符 writeString('data'); offset += 4; // 取样数据信息总数,即数据信息总尺寸⑷4 data.setUint32(offset, dataLength, true); offset += 4; // 写入取样数据信息 if (sampleBits === 8) { for (let i = 0; i < bytes.length; i++, offset++) { const s = Math.max(⑴, 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(⑴, 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
这里关键详细介绍如何做音频时的动效。大家的1个动效要求为:
依据传入的声音尺寸,做1个圆弧动态性拓展。
// 建立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, ⑶0, 30); drawArc(ctx, colors[1], center.x, center.y, 52 + 16, 150, 210); drawArc(ctx, colors[2], center.x, center.y, 52 + 32, ⑵2.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, ⑴3, 13); drawArc(ctx, colors[5], center.x, center.y, 52 + 48, 167, 193); } // 恳求下1帧 requestAnimationFrame(drawSpectrum); })();
缘尽
至此,1个详细的html5音频作用计划方案早已进行。有甚么必须填补,不符合理的地区的欢迎留言。
ps:lamejs可参照这个github
以上便是本文的所有內容,期待对大伙儿的学习培训有一定的协助,也期待大伙儿多多适用脚本制作之家。