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

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

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

第一部分: 代码案例

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

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>

页面效果如下:

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

2.1 main.js

//=======================================================================
//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.js

(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 CSS

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 内部服务器错误

猜你喜欢

转载自www.cnblogs.com/shihuc/p/9703508.html