HTML5操做麦克风获取音频数据(WAV)的一些基础技能

基于HTML5的新特性,操做其实思路很简单。javascript

首先经过navigator获取设备,而后经过设备监听语音数据,进行原始数据采集。 相关的案例比较多,最典型的就是连接:https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_APIphp

 

第一部分: 代码案例css

 

下面,我这里是基于一个Github上的例子,作了些许调整,为了本身的项目作准备的。这里,重点不是说如何经过H5获取Audio数据,重点是说这个过程当中涉及的坑或者技术元素知识。直接上代码!html

1. HTML测试页面前端

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta name="apple-mobile-web-capable" content="yes">
    <title>语音转写</title>    
    <link rel="stylesheet" type="text/css" href="css/style.css"/>
</head>
<body>
<div id="container">
    <div id="player">
        <h1>Voice Robot</h1>        
        <button id="btn-start-recording" onclick="startRecording();">录音</button>
        <button id="btn-stop-recording" disabled onclick="stopRecording();">转写</button>
        <button id="btn-start-palying" disabled onclick="playRecording();">播放</button>                        
        <div id="inbo">
            <div id="change"></div>        
        </div>
        <input type="hidden" id="audiolength"> 
        <hr>
        <audio id="audioSave" controls autoplay></audio>        
        <textarea id="btn-text-content" class="text-content">你好啊</textarea>        
    </div>    
</div>
<script type="text/javascript" src="js/jquery-1.11.1.js"></script>
<script type="text/javascript" src="js/HZRecorder.js"></script>
<script src="js/main.js"></script>
</body>
</html>

页面效果以下:java

2. JS代码(分为两个部分,main.js,以及recorder.js)jquery

2.1 main.jsweb

//=======================================================================
//author: shihuc
//date: 2018-09-19
//动态获取服务地址
//=======================================================================
var protocol = window.location.protocol;
var baseService = window.location.host;
var pathName = window.location.pathname;
var projectName = pathName.substring(0,pathName.substr(1).indexOf('/')+1);

var protocolStr = document.location.protocol;
var baseHttpProtocol = "http://";
if(protocolStr == "https:") {  
  baseHttpProtocol = "https://";
}
var svrUrl =  baseHttpProtocol + baseService + projectName + "/audio/trans";
//=========================================================================
  
var recorder = null;
var startButton = document.getElementById('btn-start-recording');
var stopButton = document.getElementById('btn-stop-recording');
var playButton = document.getElementById('btn-start-palying');

//var audio = document.querySelector('audio');
var audio = document.getElementById('audioSave');

function startRecording() {
    if(recorder != null) {
        recorder.close();
    }
    Recorder.get(function (rec) {
        recorder = rec;
        recorder.start();
    });
    stopButton.disabled = false;    
    playButton.disabled = false;
}

function stopRecording() {
    recorder.stop();    
    recorder.trans(svrUrl, function(res, errcode){
      if(errcode != 500){
        alert(res);
      }
    });
}

function playRecording() {
    recorder.play(audio);
}

 

2.2 reocrder.jsajax

(function (window) {  
    //兼容  
    window.URL = window.URL || window.webkitURL;  
    //请求麦克风
    navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia;  
  
    var Recorder = function (stream, config) {  
        //建立一个音频环境对象  
        audioContext = window.AudioContext || window.webkitAudioContext;  
        var context = new audioContext();  
        
        config = config || {};  
        config.channelCount = 1;
        config.numberOfInputChannels = config.channelCount;
        config.numberOfOutputChannels = config.channelCount;
        config.sampleBits = config.sampleBits || 16;      //采样数位 8, 16  
        //config.sampleRate = config.sampleRate || (context.sampleRate / 6);   //采样率(1/6 44100)
        config.sampleRate = config.sampleRate || 8000;   //采样率16K
        //建立缓存,用来缓存声音  
        config.bufferSize = 4096;
        
        //将声音输入这个对像  
        var audioInput = context.createMediaStreamSource(stream);  
          
        //设置音量节点  
        var volume = context.createGain();
        audioInput.connect(volume);  
  
        // 建立声音的缓存节点,createScriptProcessor方法的  
        // 第二个和第三个参数指的是输入和输出都是声道数。
        var recorder = context.createScriptProcessor(config.bufferSize, config.channelCount, config.channelCount); 
         
        //用来储存读出的麦克风数据,和压缩这些数据,将这些数据转换为WAV文件的格式
        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));  //Float32Array
                this.size += data.length;  
            }  
            , getRawData: 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 getRawDataion = parseInt(this.inputSampleRate / this.outputSampleRate);  
                var length = data.length / getRawDataion;  
                var result = new Float32Array(length);  
                var index = 0, j = 0;  
                while (index < length) {  
                    result[index] = data[j];  
                    j += getRawDataion;  
                    index++;  
                }  
                return result;
            }             
            ,getFullWavData: function(){
              var sampleRate = Math.min(this.inputSampleRate, this.outputSampleRate);  
              var sampleBits = Math.min(this.inputSampleBits, this.oututSampleBits);  
              var bytes = this.getRawData();  
              var dataLength = bytes.length * (sampleBits / 8);  
              var buffer = new ArrayBuffer(44 + dataLength);  
              var data = new DataView(buffer);  
              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, config.channelCount, true); offset += 2;  
              // 采样率,每秒样本数,表示每一个通道的播放速度   
              data.setUint32(offset, sampleRate, true); offset += 4;  
              // 波形数据传输率 (每秒平均字节数) 单声道×每秒数据位数×每样本数据位/8   
              data.setUint32(offset, config.channelCount * sampleRate * (sampleBits / 8), true); offset += 4;  
              // 快数据调整数 采样一次占用字节数 单声道×每样本的数据位数/8   
              data.setUint16(offset, config.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; 
              // 写入采样数据   
              data = this.reshapeWavData(sampleBits, offset, bytes, data);
// var wavd = new Int8Array(data.buffer.byteLength); // var pos = 0; // for (var i = 0; i < data.buffer.byteLength; i++, pos++) { // wavd[i] = data.getInt8(pos); // } 
// return wavd;

return new Blob([data], { type: 'audio/wav' }); } ,closeContext:function(){ context.close(); //关闭AudioContext不然录音屡次会报错。 } ,getPureWavData: function(offset) { var sampleBits = Math.min(this.inputSampleBits, this.oututSampleBits) var bytes = this.getRawData(); var dataLength = bytes.length * (sampleBits / 8); var buffer = new ArrayBuffer(dataLength); var data = new DataView(buffer); data = this.reshapeWavData(sampleBits, offset, bytes, data); // var wavd = new Int8Array(data.buffer.byteLength); // var pos = 0; // for (var i = 0; i < data.buffer.byteLength; i++, pos++) { // wavd[i] = data.getInt8(pos); // }
// return wavd;
                  return new Blob([data], { type: 'audio/wav' });

} ,reshapeWavData: function(sampleBits, offset, iBytes, oData) { if (sampleBits === 8) { for (var i = 0; i < iBytes.length; i++, offset++) { var s = Math.max(-1, Math.min(1, iBytes[i])); var val = s < 0 ? s * 0x8000 : s * 0x7FFF; val = parseInt(255 / (65535 / (val + 32768))); oData.setInt8(offset, val, true); } } else { for (var i = 0; i < iBytes.length; i++, offset += 2) { var s = Math.max(-1, Math.min(1, iBytes[i])); oData.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true); } } return oData; } }; //开始录音 this.start = function () { audioInput.connect(recorder); recorder.connect(context.destination); }; //中止 this.stop = function () { recorder.disconnect(); }; //获取音频文件 this.getBlob = function () { this.stop(); return audioData.getFullWavData(); }; //回放 this.play = function (audio) { audio.src = window.URL.createObjectURL(this.getBlob()); audio.onended = function() { $('#play').text("Play"); }; }; //中止播放 this.stopPlay=function(audio){ audio.pause(); } this.close=function(){ audioData.closeContext(); } //上传 this.upload = function (url, pdata, callback) { var fd = new FormData(); fd.append('file', this.getBlob()); var xhr = new XMLHttpRequest(); for (var e in pdata) fd.append(e, pdata[e]); 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); }; this.trans = function (url, callback) { var fd = new FormData(); var buffer = audioData.getPureWavData(0); fd.set('wavData', buffer); fd.set('wavSize', buffer.size); console.log("wavSize: " + buffer.size); document.getElementById('btn-text-content').value = "当前录音长度为:" + buffer.size; var xhr = new XMLHttpRequest(); xhr.open('POST', url, false); //async=false,采用同步方式处理 xhr.onreadystatechange = function(){ if (xhr.readyState == 4) { //响应数据接收完毕 callback(xhr.responseText, xhr.status); } } xhr.send(fd); }; var $bo=$("#inbo"); var $change=$("#change"); var width=$bo.width(); //音频采集 recorder.onaudioprocess = function (e) { audioData.input(e.inputBuffer.getChannelData(0)); //获取输入和输出的数据缓冲区 var input = e.inputBuffer.getChannelData(0); //绘制条形波动图 for(i=0;i<width;i++){ var changeWidth=width/2*input[input.length*i/width|0]; $change.width(changeWidth); } var timeHidden=document.getElementById('audiolength'); timeHidden.Value=e.playbackTime; console.log(timeHidden.Value); if(timeHidden.Value>=60){ recorder.disconnect(); setTimeout(saveAudio(),500); } }; }; //抛出异常 Recorder.throwError = function (message) { throw new function () { this.toString = function () { return message; };}; }; //是否支持录音 Recorder.canRecording = (navigator.getUserMedia != null); //获取录音机 Recorder.get = function (callback, config) { if (callback) { if (navigator.getUserMedia) { navigator.getUserMedia( { audio: true } //只启用音频 A , function (stream) { //stream这个参数是麦克风的输入流,将这个流传递给Recorder var rec = new Recorder(stream, config); callback(rec); } , function (error) { switch (error.code || error.name) { case 'PERMISSION_DENIED': case 'PermissionDeniedError': Recorder.throwError('用户拒绝提供信息。'); break; case 'NOT_SUPPORTED_ERROR': case 'NotSupportedError': Recorder.throwError('浏览器不支持硬件设备。'); break; case 'MANDATORY_UNSATISFIED_ERROR': case 'MandatoryUnsatisfiedError': Recorder.throwError('没法发现指定的硬件设备。'); break; default: Recorder.throwError('没法打开麦克风。异常信息:' + (error.code || error.name)); break; } }); } else { Recorder.throwErr('当前浏览器不支持录音功能。'); return; } } }; window.Recorder = Recorder; })(window);

2.3 CSSspring

body {
    margin: 0;
    background: #f0f0f0;
    font-family:  'Roboto', Helvetica, Arial, sans-serif;
}

#container {
    margin-top: 30px;
}

h1 {
    margin: 0;
}

button {
    padding: 10px;
    background: #eee;
    border: none;
    border-radius: 3px;
    color: #ffffff;
    font-family: inherit;
    font-size: 16px;
    outline: none !important;
    cursor: pointer;
}

button[disabled] {
    background: #aaa !important;
    cursor: default;
}

#btn-start-recording {
    background: #5db85c;
}

#btn-stop-recording {
    background: #d95450;
}

#btn-start-palying {
    background: #d95450;
}

#btn-start-saving {
    background: #d95450;
}

#player {
    max-width: 600px;
    margin: 0 auto;
    padding: 20px 20px;
    border: 1px solid #ddd;
    background: #ffffff;
}

.text-content {    
    margin: 20px auto;
    resize:none;
    background: #dbdbdb;
    width: 100%;
    font-size: 14px;
    padding:5px 5px;
    border-radius: 5px;
    min-height: 100px;
    box-sizing: border-box;
}

audio {
    width: 100%;
}

#inbo{
    width: 100%;
    height: 20px;
    border: 1px solid #ccc;
    margin-top: 20px;
}
#change{
    height: 20px;
    width: 0;
    background-color: #009933;
}
View Code

 

小结: 仅仅就这个案例来看,须要注意几点

A. 这个例子将采集的数据WAV格式,传递到服务端(http),浏览器需求是要用HTTPS的协议

B. 传递数据,若直接用上面JS文件中红色部分代码进行传递,而不是用基于Blob的数据进行传,会出现数据转码错误,这个错误是逻辑错误,不会遇到exception。 所谓逻辑错误,是原始的数据,被转换成了ASCII码了,即被当字符串信息了。参照下面的这个截图:

49实际上是ASCII的1,50实际上是ASCII的2,依次类推,之因此发现这个问题,是由于研究数据长度,即JS前端显示的A长度,可是服务端显示的缺是比A长不少的值,可是baos.toString显示的内容和JS前端显示的数字内容同样。。。仔细一研究,发现上述总结的问题。后面还会介绍,XMLHttpRequest传递数据给后台时,Blob相关的数据其妙效果!!!

 

下面进入第二部分:知识点的总结

1. FormData

FormData对象用以将数据编译成键值对,以便用XMLHttpRequest来发送数据。其主要用于发送表单数据,但亦可用于发送带键数据(keyed data),而独立于表单使用。若是表单enctype属性设为multipart/form-data ,则会使用表单的submit()方法来发送数据,从而,发送数据具备一样形式。

语法:
var formData = new FormData(form)
参数form是Optional

An HTML <form> element — when specified, the FormData object will be populated with the form's current keys/values using the name property of each element for the keys and their submitted value for the values. It will also encode file input content.

建立一个新的表单对象,其中form来源于一个html的form标签,这个form参数,能够非必填。
关于FormData类型的方法,能够参照下面的链接https://developer.mozilla.org/zh-CN/docs/Web/API/FormData自行阅读,很是清楚。

 

一般状况下,FormData的建立,有两种形式:
1》从零开始建立FormData对象
你能够本身建立一个FormData对象,而后调用它的append()方法来添加字段,像这样:

var formData = new FormData();
formData.append("username", "Groucho");
formData.append("accountnum", 123456); //数字123456会被当即转换成字符串 "123456"
// HTML 文件类型input,由用户选择
formData.append("userfile", fileInputElement.files[0]);
// JavaScript file-like 对象
var content = '<a id="a"><b id="b">hey!</b></a>'; // 新文件的正文...
var blob = new Blob([content], { type: "text/xml"});
formData.append("webmasterfile", blob);
var request = new XMLHttpRequest();
request.open("POST", "http://foo.com/submitform.php");
request.send(formData);

注意:

A> 字段 "userfile" 和 "webmasterfile" 都包含一个文件. 字段 "accountnum" 是数字类型,它将被FormData.append()方法转换成字符串类型(FormData 对象的字段类型能够是 Blob, File, 或者 string: 若是它的字段类型不是Blob也不是File,则会被转换成字符串类)
B> 一个 Blob对象表示一个不可变的, 原始数据的相似文件对象。Blob表示的数据不必定是一个JavaScript原生格式。 File 接口基于Blob,继承 blob功能并将其扩展为支持用户系统上的文件。你能够经过 Blob() 构造函数建立一个Blob对象

 

2》经过HTML表单建立FormData对象
想要构造一个包含Form表单数据的FormData对象,须要在建立FormData对象时指定表单的元素。

<form id="myForm" action="" method="post" enctype="multipart/form-data">
<input type="text" name="param1">参数1
<input type="text" name="param2">参数2 
<input type="file" name="param3">参数3 
</form>

而后看如何操做表单form元素构建FormData:

var formElement = document.getElementById("myForm");;
var request = new XMLHttpRequest();
request.open("POST", svrUrl);
var formData = new FormData(formElement);
request.send(formData);

注意:

A> 这里基于表单元素form进行构建FormData对象,而后提交了带有文件类型的数据到后台,这里enctype,必须是multipart/form-data,表单必须指定。enctype属性规定在将表单数据发送到服务器以前如何对其进行编码
B>form标签中,只有method="post"时才使用enctype属性。enctype常见的类型值

application/x-www-form-urlencoded          默认。在发送前对全部字符进行编码(将空格转换为 "+" 符号,特殊字符转换为 ASCII HEX 值)。


multipart/form-data              不对字符编码。当使用有文件上传控件的表单时,该值是必需的。


text/plain                  将空格转换为 "+" 符号,但不编码特殊字符。

C>若是FormData对象是经过表单建立的,则表单中指定的请求方式会被应用到方法open()中
D>你还能够直接向FormData对象附加File或Blob类型的文件,以下所示:
formData.append("myfile", myBlob, "filename.txt");
使用append()方法时,能够经过第三个可选参数设置发送请求的头 Content-Disposition 指定文件名。若是不指定文件名(或者不支持该参数时),将使用名字“blob”。
若是你设置正确的配置项,你也能够经过jQuery来使用FormData对象:

var fd = new FormData(document.querySelector("form"));
fd.append("CustomField", "This is some extra data");
$.ajax({
   url: "stash.php",
   type: "POST",
   data: fd,
   processData: false, // 不处理数据
   contentType: false // 不设置内容类型
});

E>经过AJAX提交表单和上传文件能够不使用FormData对象

 

 

 

2. XMLHttpRequest请求

下面看看官方文档的描述:

Use XMLHttpRequest (XHR) objects to interact with servers. You can retrieve data from a URL without having to do a full page refresh. This enables a Web page to update just part of a page without disrupting what the user is doing. XMLHttpRequest is used heavily in Ajax programming.
Despite its name, XMLHttpRequest can be used to retrieve any type of data, not just XML, and it supports protocols other than HTTP (including file and ftp).
If your communication needs involve receiving event or message data from the server, consider using server-sent events through the EventSource interface. For full-duplex communication, WebSockets may be a better choice.

相应的详细描述,请参考连接https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest

1》下面说说经常使用的几个函数:
a. onreadystatechange
XMLHttpRequest.onreadystatechange = callback;
下面看看例子:

var xhr = new XMLHttpRequest(), method = "GET", url = "https://developer.mozilla.org/";
xhr.open(method, url, true);
xhr.onreadystatechange = function () {
   if(xhr.readyState === 4 && xhr.status === 200) {
       console.log(xhr.responseText);
   }
};
xhr.send();

注意:服务端怎么能给出responseText呢?或者其余响应参数。其实,仍是蛮简单的,只要搞清楚http的工做流程,不要受到springMVC或者Jersey等MVC框架迷惑,其实这些高端框架,也是对http的数据流进行了封装,由于HTTP流程,数据都是有一个请求HttpServletRequest和一个HttpServletResponse对应的,一个对应请求一个对应响应。响应就是服务端给到客户端的应答,因此,咱们给输出的时候,必定要注意,直接操做HttpServletResponse时,是进行数据流操做。相似下面的一段例子:

PrintWriter out = response.getWriter();
out.print(text);
out.flush();//必定要有这个操做,不然数据不会发出去,停留在buffer中。

 

b. open

The XMLHttpRequest method open() initializes a newly-created request, or re-initializes an existing one.

注意:Calling this method for an already active request (one for which open() has already been called) is the equivalent of calling abort(). 意思是说,对一个已经开启的request,在没有结束时,再次调用open,等效于调用abort进行中断了

语法:

XMLHttpRequest.open(method, url)
XMLHttpRequest.open(method, url, async)
XMLHttpRequest.open(method, url, async, user)
XMLHttpRequest.open(method, url, async, user, password)

说明: 

I) The HTTP request method to use, such as "GET", "POST", "PUT", "DELETE", etc. Ignored for non-HTTP(S) URLs. 注意,只支持HTTP系列请求,其余将被忽视掉
II) method和url是必填项,async是可选的,默认是true,表示open启动的请求默认是异步的

 

c. send

The XMLHttpRequest method send() sends the request to the server. If the request is asynchronous (which is the default), this method returns as soon as the request is sent and the result is delivered using events. If the request is synchronous, this method doesn't return until the response has arrived.
send() accepts an optional parameter which lets you specify the request's body; this is primarily used for requests such as PUT. If the request method is GET or HEAD, the body parameter is ignored and the request body is set to null.
If no Accept header has been set using the setRequestHeader(), an Accept header with the type "*/*" (any type) is sent.

语法:

XMLHttpRequest.send(body)

注意:The best way to send binary content (e.g. in file uploads) is by using an ArrayBufferView or Blob in conjunction with the send() method.

下面看看ArrayBufferView对应的内容:

I) ArrayBufferView is a helper type representing any of the following JavaScript TypedArray types:

Int8Array,
Uint8Array,
Uint8ClampedArray,
Int16Array,
Uint16Array,
Int32Array,
Uint32Array,
Float32Array,
Float64Array or
DataView.

个人项目经验告知我,用ArrayBufferView传递数据的话,基于FormData传递,会存在将原始数据转成字符串的效果,这个也是符合FormData技术介绍的,如前面的注意事项内容。 因此,为了方便,强烈建议数据(二进制)文件的传递,用Blob类型,保持原始数据格式,不会转码

II)看看Blob的内容

A Blob object represents a file-like object of immutable, raw data. Blobs represent data that isn't necessarily in a JavaScript-native format. The File interface is based on Blob, inheriting blob functionality and expanding it to support files on the user's system.
To construct a Blob from other non-blob objects and data, use the Blob() constructor. To create a blob that contains a subset of another blob's data, use the slice() method. To obtain a Blob object for a file on the user's file system, see the File documentation.

 

2》获取响应

responseText: 得到字符串形式的响应数据 
responseXML: 得到XML形式的响应数据(用的较少,大多数状况用JSON) 
status, statusText: 以数字和文本形式返回HTTP状态码 
getAllResponseHeader(): 获取全部的响应报头 
getResponseHeader(参数): 查询响应中某个字段的值

 

3》属性

readyState: 响应是否成功 
0:请求为初始化,open尚未调用 
1:服务器链接已创建,open已经调用了 
2:请求已接收,接收到头信息了 
3:请求处理中,接收到响应主题了 
4:请求已完成,且响应已就绪,也就是响应完成了 

 

4》另附http请求相应代码

200 请求成功
202 请求被接受但处理未完成
204 接收到空的响应
400 错误请求
404 请求资源未找到
500 内部服务器错误
相关文章
相关标签/搜索