http://www.it165.net/design/html/201406/2651.html
宣传一下自己的qq群:5946699 (暗号:C#交流) 欢迎喜欢C#,热爱C#,正在学习C#,准备学习C#的朋友来这里互相学习交流,共同进步
群刚建,人不多,但是都是真正热爱C#的 我也是热爱C#的 希望大家可以一起交流,共同进步
最近公司需要用到web录音的功能
本人接手了这个任务
在网上找了一些资料
http://www.jsjtt.com/webkaifa/html5/2013-08-28/34.html
http://javascript.ruanyifeng.com/bom/webrtc.html
讲的都差不多
也就是怎么使用 getUserMedia
下载来的栗子也比较简单,可以直接运行
问题1:怎么上传
栗子中最后返回的是Blob数据
1.return new Blob([dataview],
{ type: type })
因为对html5不熟,所以又查了一些数据
原来HTML5中使用FormData这个对象好方便
1.var
fd = new FormData();
2.fd.append('audioData',
blob);
3.var
xhr = new XMLHttpRequest();
4.xhr.open('POST',
url);
5.xhr.send(fd);
在C#服务器端 如下代码就可以接收了
1.public void ProcessRequest(HttpContext
context)
2.{
3.if (context.Request.Files.Count
> 0)
4.{
5.context.Request.Files[0].SaveAs('d:\1.wav');
6.}
7.}
问题2:文件体积太大
是的,使用上面的栗子,直接录音保存后基本上2秒就需要400K,一段20秒的录音就达到了的4M
这样的数据根本无法使用,必须想办法压缩数据
我开始尝试读每一段代码
01.function
encodeWAV(samples){
02.var
buffer = new ArrayBuffer(44 +
samples.length * 2);
03.var
view = new DataView(buffer);
04.
05./*
RIFF identifier */
06.writeString(view, 0, 'RIFF');
07./*
file length */
08.view.setUint32(4, 32 +
samples.length * 2, true);
09./*
RIFF type */
10.writeString(view, 8, 'WAVE');
11./*
format chunk identifier */
12.writeString(view, 12, 'fmt
');
13./*
format chunk length */
14.view.setUint32(16, 16, true);
15./*
sample format (raw) */
16.view.setUint16(20, 1, true);
17./*
channel count */
18.view.setUint16(22, 2, true);
19./*
sample rate */
20.view.setUint32(24,
sampleRate, true);
21./*
byte rate (sample rate * block align) */
22.view.setUint32(28,
sampleRate * 4, true);
23./*
block align (channel count * bytes per sample) */
24.view.setUint16(32, 4, true);
25./*
bits per sample */
26.view.setUint16(34, 16, true);
27./*
data chunk identifier */
28.writeString(view, 36, 'data');
29./*
data chunk length */
30.view.setUint32(40,
samples.length * 2, true);
31.
32.floatTo16BitPCM(view, 44,
samples);
33.
34.return view;
35.}
上面的代码,就是把字节数据格式化成wav的格式的过程
所以我又去查了wav的头文件
要压缩,就要从上面三个红圈的地方入手
最简单的就是把双声道改成单声道的,
在录音的时候只需要记录一个声道就可以了
01.//
创建声音的缓存节点,createJavaScriptNode方法的
02.//
第二个和第三个参数指的是输入和输出都是双声道。
03.//recorder
= context.createJavaScriptNode(bufferSize, 2, 2);
04.recorder
= context.createJavaScriptNode(bufferSize, 1, 1);//这里改成1
05.
06.this.node.onaudioprocess
= function(e){
07.if (!recording) return;
08.worker.postMessage({
09.command: 'record',
10.buffer:
[
11.e.inputBuffer.getChannelData(0)//,
12.//e.inputBuffer.getChannelData(1)//
这里只需要保存一个
13.]
14.});
15.}
16.
17.function
exportWAV(type){
18.var
bufferL = mergeBuffers(recBuffersL, recLength);
19.//var
bufferR = mergeBuffers(recBuffersR, recLength);
20.var
interleaved = interleave(bufferL);//,
bufferR); //合并数据的时候去到对右声道的处理
21.var
dataview = encodeWAV(interleaved);
22.var
audioBlob = new Blob([dataview],
{ type: type });
23.
24.this.postMessage(audioBlob);
25.}
26.
27.function
interleave(inputL){//,
inputR){//混合声道的时候去掉对右声道的处理
28.var
length = inputL.length ;//+
inputR.length;
29.var
result = new Float32Array(length);
30.
31.var
index = 0,
32.inputIndex
= 0;
33.
34.while (index
< length){
35.result[index++]
= inputL[inputIndex];
36.//result[index++]
= inputR[inputIndex];
37.inputIndex++;
38.}
39.return result;
40.}
然后修改一**释,我不喜欢英文的....
01.function
encodeWAV(samples) {
02.var
dataLength = samples.length * 2;
03.var
buffer = new ArrayBuffer(44 +
dataLength);
04.var
view = new DataView(buffer);
05.
06.var
sampleRateTmp = sampleRate;
07.var
sampleBits = 16;
08.var
channelCount = 1;
09.var
offset = 0;
10./*
资源交换文件标识符 */
11.writeString(view,
offset, 'RIFF');
offset += 4;
12./*
下个地址开始到文件尾总字节数,即文件大小-8 */
13.view.setUint32(offset, /*32这里地方栗子中的值错了,但是不知道为什么依然可以运行成功*/ 36 +
dataLength, true);
offset += 4;
14./*
WAV文件标志 */
15.writeString(view,
offset, 'WAVE');
offset += 4;
16./*
波形格式标志 */
17.writeString(view,
offset, 'fmt
');
offset += 4;
18./*
过滤字节,一般为 0x10 = 16 */
19.view.setUint32(offset, 16, true);
offset += 4;
20./*
格式类别 (PCM形式采样数据) */
21.view.setUint16(offset, 1, true);
offset += 2;
22./*
通道数 */
23.view.setUint16(offset,
channelCount, true);
offset += 2;
24./*
采样率,每秒样本数,表示每个通道的播放速度 */
25.view.setUint32(offset,
sampleRateTmp, true);
offset += 4;
26./*
波形数据传输率 (每秒平均字节数) 通道数×每秒数据位数×每样本数据位/8 */
27.view.setUint32(offset,
sampleRateTmp * channelCount * (sampleBits / 8), true);
offset += 4;
28./*
快数据调整数 采样一次占用字节数 通道数×每样本的数据位数/8 */
29.view.setUint16(offset,
channelCount * (sampleBits / 8), true);
offset += 2;
30./*
每样本数据位数 */
31.view.setUint16(offset,
sampleBits, true);
offset += 2;
32./*
数据标识符 */
33.writeString(view,
offset, 'data');
offset += 4;
34./*
采样数据总数,即数据总大小-44 */
35.view.setUint32(offset,
dataLength, true);
offset += 4;
36./*
采样数据 */
37.floatTo16BitPCM(view, 44,
samples);
38.
39.return view;
40.}
一旦把双声道变为单声道,数据直接缩小一半了
但是还不够
继续缩小体积
除了声道以外,还有一个可以缩减的地方就是采样位数 默认是16位的,我们改成8位 又可以减少一半了
01.function
encodeWAV(samples) {
02.var
sampleBits = 8;//16;//这里改成8位
03.var
dataLength = samples.length * (sampleBits / 8);
04.var
buffer = new ArrayBuffer(44 +
dataLength);
05.var
view = new DataView(buffer);
06.
07.var
sampleRateTmp = sampleRate;
08.
09.var
channelCount = 1;
10.var
offset = 0;
11./*
资源交换文件标识符 */
12.writeString(view,
offset, 'RIFF');
offset += 4;
13./*
下个地址开始到文件尾总字节数,即文件大小-8 */
14.view.setUint32(offset, /*32这里地方栗子中的值错了,但是不知道为什么依然可以运行成功*/ 36 +
dataLength, true);
offset += 4;
15./*
WAV文件标志 */
16.writeString(view,
offset, 'WAVE');
offset += 4;
17./*
波形格式标志 */
18.writeString(view,
offset, 'fmt
');
offset += 4;
19./*
过滤字节,一般为 0x10 = 16 */
20.view.setUint32(offset, 16, true);
offset += 4;
21./*
格式类别 (PCM形式采样数据) */
22.view.setUint16(offset, 1, true);
offset += 2;
23./*
通道数 */
24.view.setUint16(offset,
channelCount, true);
offset += 2;
25./*
采样率,每秒样本数,表示每个通道的播放速度 */
26.view.setUint32(offset,
sampleRateTmp, true);
offset += 4;
27./*
波形数据传输率 (每秒平均字节数) 通道数×每秒数据位数×每样本数据位/8 */
28.view.setUint32(offset,
sampleRateTmp * channelCount * (sampleBits / 8), true);
offset += 4;
29./*
快数据调整数 采样一次占用字节数 通道数×每样本的数据位数/8 */
30.view.setUint16(offset,
channelCount * (sampleBits / 8), true);
offset += 2;
31./*
每样本数据位数 */
32.view.setUint16(offset,
sampleBits, true);
offset += 2;
33./*
数据标识符 */
34.writeString(view,
offset, 'data');
offset += 4;
35./*
采样数据总数,即数据总大小-44 */
36.view.setUint32(offset,
dataLength, true);
offset += 4;
37./*
采样数据 */
38.//floatTo16BitPCM(view,
44, samples);
39.floatTo8BitPCM(view, 44,
samples);//这里改为写入8位的数据
40.return view;
41.}
8和16的取值范围不一样
对比一下To8和To16的方法
这里方法是我自己猜的,如果不对还望指出~~~
01.function
floatTo16BitPCM(output, offset, input) {
02.for (var
i = 0;
i < input.length; i++, offset += 2)
{ //因为是int16所以占2个字节,所以偏移量是+2
03.var
s = Math.max(-1,
Math.min(1,
input[i]));
04.output.setInt16(offset,
s < 0 ?
s * 0x8000 :
s * 0x7FFF, true);
05.}
06.}
07.
08.
09.function
floatTo8BitPCM(output, offset, input) {
10.for (var
i = 0;
i < input.length; i++, offset++) { //这里只能加1了
11.var
s = Math.max(-1,
Math.min(1,
input[i]));
12.var
val = s < 0 ?
s * 0x8000 :
s * 0x7FFF;
13.val
= parseInt(255 /
(65535 /
(val + 32768))); //这里有一个转换的代码,这个是我个人猜测的,就是按比例转换
14.output.setInt8(offset,
val, true);
15.}
16.}
怀着忐忑的心情,启动网页...居然听的到声音~居然成功了!!!
经过这样之后又减少了一半大小
最后就是这个采样率了
网页中录音组件的采样率是44100 不知道在哪里改,查询了一些资料,未果...
所以又自己猜测了,是不是我把已经缓存的时候按照比例抛弃一些就可以模拟减少采样率的操作呢?
比如现在已经缓存的数据大小是40960 是不是我直接间隔一位抛弃一次数据,将数据大小变成20480 就可以算是采样率变成22050了呢?
同理,要编程11025只要再抛弃一半的数据?
所以我又做了如下修改
01.function
interleave(inputL, inputR) {
02.var
compression = 44100 / 11025; //计算压缩率
03.var
length = inputL.length / compression;
04.var
result = new Float32Array(length);
05.
06.var
index = 0,
07.inputIndex
= 0;
08.
09.while (index
< length) {
10.result[index]
= inputL[inputIndex];
11.inputIndex
+= compression;//每次都跳过3个数据
12.index++;
13.}
14.return result;
15.}
16.
17.
18.function
encodeWAV(samples) {
19.var
dataLength = samples.length;
20.var
buffer = new ArrayBuffer(44 +
dataLength);
21.var
view = new DataView(buffer);
22.
23.var
sampleRateTmp = 11025 ;//sampleRate;//写入新的采样率
24.var
sampleBits = 8;
25.var
channelCount = 1;
26.var
offset = 0;
27./*
资源交换文件标识符 */
28.writeString(view,
offset, 'RIFF');
offset += 4;
29./*
下个地址开始到文件尾总字节数,即文件大小-8 */
30.view.setUint32(offset, /*32*/ 36 +
dataLength, true);
offset += 4;
31./*
WAV文件标志 */
32.writeString(view,
offset, 'WAVE');
offset += 4;
33./*
波形格式标志 */
34.writeString(view,
offset, 'fmt
');
offset += 4;
35./*
过滤字节,一般为 0x10 = 16 */
36.view.setUint32(offset, 16, true);
offset += 4;
37./*
格式类别 (PCM形式采样数据) */
38.view.setUint16(offset, 1, true);
offset += 2;
39./*
通道数 */
40.view.setUint16(offset,
channelCount, true);
offset += 2;
41./*
采样率,每秒样本数,表示每个通道的播放速度 */
42.view.setUint32(offset,
sampleRateTmp, true);
offset += 4;
43./*
波形数据传输率 (每秒平均字节数) 通道数×每秒数据位数×每样本数据位/8 */
44.view.setUint32(offset,
sampleRateTmp * channelCount * (sampleBits / 8), true);
offset += 4;
45./*
快数据调整数 采样一次占用字节数 通道数×每样本的数据位数/8 */
46.view.setUint16(offset,
channelCount * (sampleBits / 8), true);
offset += 2;
47./*
每样本数据位数 */
48.view.setUint16(offset,
sampleBits, true);
offset += 2;
49./*
数据标识符 */
50.writeString(view,
offset, 'data');
offset += 4;
51./*
采样数据总数,即数据总大小-44 */
52.view.setUint32(offset,
dataLength, true);
offset += 4;
53./*
采样数据 */
54.floatTo16BitPCM(view, 44,
samples);
55.
56.return view;
57.}
再次怀着忐忑的心情,启动网页...居然听的到声音~居然又成功了
最后把之前的代码整理封装一下
001.(function
(window) {
002.//兼容
003.window.URL
= window.URL || window.webkitURL;
004.navigator.getUserMedia
= navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia;
005.
006.var
HZRecorder = function (stream, config) {
007.config
= config || {};
008.config.sampleBits
= config.sampleBits || 8; //采样数位
8, 16
009.config.sampleRate
= config.sampleRate || (44100 / 6); //采样率(1/6
44100)
010.
011.var
context = new webkitAudioContext();
012.var
audioInput = context.createMediaStreamSource(stream);
013.var
recorder = context.createJavaScriptNode(4096, 1, 1);
014.
015.var
audioData = {
016.size: 0 //录音文件长度
017.,
buffer: [] //录音缓存
018.,
inputSampleRate: context.sampleRate //输入采样率
019.,
inputSampleBits: 16 //输入采样数位
8, 16
020.,
outputSampleRate: config.sampleRate //输出采样率
021.,
oututSampleBits: config.sampleBits //输出采样数位
8, 16
022.,
input: function (data) {
023.this.buffer.push(new Float32Array(data));
024.this.size
+= data.length;
025.}
026.,
compress: function () { //合并压缩
027.//合并
028.var
data = new Float32Array(this.size);
029.var
offset = 0;
030.for (var
i = 0;
i < this.buffer.length;
i++) {
031.data.set(this.buffer[i],
offset);
032.offset
+= this.buffer[i].length;
033.}
034.//压缩
035.var
compression = parseInt(this.inputSampleRate
/ this.outputSampleRate);
036.var
length = data.length / compression;
037.var
result = new Float32Array(length);
038.var
index = 0,
j = 0;
039.while (index
< length) {
040.result[index]
= data[j];
041.j
+= compression;
042.index++;
043.}
044.return result;
045.}
046.,
encodeWAV: function () {
047.var
sampleRate = Math.min(this.inputSampleRate, this.outputSampleRate);
048.var
sampleBits = Math.min(this.inputSampleBits, this.oututSampleBits);
049.var
bytes = this.compress();
050.var
dataLength = bytes.length * (sampleBits / 8);
051.var
buffer = new ArrayBuffer(44 +
dataLength);
052.var
data = new DataView(buffer);
053.
054.var
channelCount = 1;//单声道
055.var
offset = 0;
056.
057.var
writeString = function (str) {
058.for (var
i = 0;
i < str.length; i++) {
059.data.setUint8(offset
+ i, str.charCodeAt(i));
060.}
061.}
062.
063.//
资源交换文件标识符
064.writeString('RIFF');
offset += 4;
065.//
下个地址开始到文件尾总字节数,即文件大小-8
066.data.setUint32(offset, 36 +
dataLength, true);
offset += 4;
067.//
WAV文件标志
068.writeString('WAVE');
offset += 4;
069.//
波形格式标志
070.writeString('fmt
');
offset += 4;
071.//
过滤字节,一般为 0x10 = 16
072.data.setUint32(offset, 16, true);
offset += 4;
073.//
格式类别 (PCM形式采样数据)
074.data.setUint16(offset, 1, true);
offset += 2;
075.//
通道数
076.data.setUint16(offset,
channelCount, true);
offset += 2;
077.//
采样率,每秒样本数,表示每个通道的播放速度
078.data.setUint32(offset,
sampleRate, true);
offset += 4;
079.//
波形数据传输率 (每秒平均字节数) 单声道×每秒数据位数×每样本数据位/8
080.data.setUint32(offset,
channelCount * sampleRate * (sampleBits / 8), true);
offset += 4;
081.//
快数据调整数 采样一次占用字节数 单声道×每样本的数据位数/8
082.data.setUint16(offset,
channelCount * (sampleBits / 8), true);
offset += 2;
083.//
每样本数据位数
084.data.setUint16(offset,
sampleBits, true);
offset += 2;
085.//
数据标识符
086.writeString('data');
offset += 4;
087.//
采样数据总数,即数据总大小-44
088.data.setUint32(offset,
dataLength, true);
offset += 4;
089.//
写入采样数据
090.if (sampleBits
=== 8)
{
091.for (var
i = 0;
i < bytes.length; i++, offset++) {
092.var
s = Math.max(-1,
Math.min(1,
bytes[i]));
093.var
val = s < 0 ?
s * 0x8000 :
s * 0x7FFF;
094.val
= parseInt(255 /
(65535 /
(val + 32768)));
095.data.setInt8(offset,
val, true);
096.}
097.} else {
098.for (var
i = 0;
i < bytes.length; i++, offset += 2)
{
099.var
s = Math.max(-1,
Math.min(1,
bytes[i]));
100.data.setInt16(offset,
s < 0 ?
s * 0x8000 :
s * 0x7FFF, true);
101.}
102.}
103.
104.return new Blob([data],
{ type: 'audio/wav' });
105.}
106.};
107.
108.//开始录音
109.this.start
= function () {
110.audioInput.connect(recorder);
111.recorder.connect(context.destination);
112.}
113.
114.//停止
115.this.stop
= function () {
116.recorder.disconnect();
117.}
118.
119.//获取音频文件
120.this.getBlob
= function () {
121.this.stop();
122.return audioData.encodeWAV();
123.}
124.
125.//回放
126.this.play
= function (audio) {
127.audio.src
= window.URL.createObjectURL(this.getBlob());
128.}
129.
130.//上传
131.this.upload
= function (url, callback) {
132.var
fd = new FormData();
133.fd.append('audioData', this.getBlob());
134.var
xhr = new XMLHttpRequest();
135.if (callback)
{
136.xhr.upload.addEventListener('progress',
function (e) {
137.callback('uploading',
e);
138.}, false);
139.xhr.addEventListener('load',
function (e) {
140.callback('ok',
e);
141.}, false);
142.xhr.addEventListener('error',
function (e) {
143.callback('error',
e);
144.}, false);
145.xhr.addEventListener('abort',
function (e) {
146.callback('cancel',
e);
147.}, false);
148.}
149.xhr.open('POST',
url);
150.xhr.send(fd);
151.}
152.
153.//音频采集
154.recorder.onaudioprocess
= function (e) {
155.audioData.input(e.inputBuffer.getChannelData(0));
156.//record(e.inputBuffer.getChannelData(0));
157.}
158.
159.};
160.//抛出异常
161.HZRecorder.throwError
= function (message) {
162.alert(message);
163.throw new function
() { this.toString
= function () { return message;
} }
164.}
165.//是否支持录音
166.HZRecorder.canRecording
= (navigator.getUserMedia != null);
167.//获取录音机
168.HZRecorder.get
= function (callback, config) {
169.if (callback)
{
170.if (navigator.getUserMedia)
{
171.navigator.getUserMedia(
172.{
audio: true } //只启用音频
173.,
function (stream) {
174.var
rec = new HZRecorder(stream,
config);
175.callback(rec);
176.}
177.,
function (error) {
178.switch (error.code
|| error.name) {
179.case 'PERMISSION_DENIED':
180.case 'PermissionDeniedError':
181.HZRecorder.throwError('用户拒绝提供信息。');
182.break;
183.case 'NOT_SUPPORTED_ERROR':
184.case 'NotSupportedError':
185.HZRecorder.throwError('<a
href="http://www.it165.net/edu/ewl/" target="_blank" class="keylink">浏览器</a>不支持硬件设备。');
186.break;
187.case 'MANDATORY_UNSATISFIED_ERROR':
188.case 'MandatoryUnsatisfiedError':
189.HZRecorder.throwError('无法发现指定的硬件设备。');
190.break;
191.default:
192.HZRecorder.throwError('无法打开麦克风。异常信息:' +
(error.code || error.name));
193.break;
194.}
195.});
196.} else {
197.HZRecorder.throwErr('当前<a
href="http://www.it165.net/edu/ewl/" target="_blank" class="keylink">浏览器</a>不支持录音功能。'); return;
198.}
199.}
200.}
201.
202.window.HZRecorder
= HZRecorder;
203.
204.})(window);
01.<!DOCTYPE
html>
02.<html
xmlns='http://www.w3.org/1999/xhtml'>
03.<head>
04.<meta
http-equiv='Content-Type' content='text/html;
charset=utf-8' />
05.<title></title>
06.</head>
07.<body>
08.<div>
09.<audio
controls autoplay></audio>
10.<input
onclick='startRecording()' type='button' value='录音' />
11.<input
onclick='stopRecording()' type='button' value='停止' />
12.<input
onclick='playRecording()' type='button' value='播放' />
13.<input
onclick='uploadAudio()' type='button' value='提交' />
14.</div>
15.
16.<script
type='text/javascript' src='HZRecorder.js'></script>
17.
18.
19.<script>
20.
21.var
recorder;
22.
23.var
audio = document.querySelector('audio');
24.
25.function
startRecording() {
26.HZRecorder.get(function
(rec) {
27.recorder
= rec;
28.recorder.start();
29.});
30.}
31.
32.function
stopRecording() {
33.recorder.stop();
34.}
35.
36.function
playRecording() {
37.recorder.play(audio);
38.}
39.
40.function
uploadAudio() {
41.recorder.upload('Handler1.ashx',
function (state, e) {
42.switch (state)
{
43.case 'uploading':
44.//var
percentComplete = Math.round(e.loaded * 100 / e.total) + '%';
45.break;
46.case 'ok':
47.//alert(e.target.responseText);
48.alert('上传成功');
49.break;
50.case 'error':
51.alert('上传失败');
52.break;
53.case 'cancel':
54.alert('上传被取消');
55.break;
56.}
57.});
58.}
59.
60.</script>
61.
62.</body>
63.</html>
好了 下班了
源码下载: http://www.it165.net/uploadfile/files/2014/0611/RecordingDemo.rar
demo默认采用 44100/6 的采样率 ,原来20秒要4M ,单声道砍一半 2M ,8位砍一半 1M, 6分之一采样率 1M/6 170K左右
微信4秒只有4K是怎么做到的?
宣传一下自己的qq群:5946699 (暗号:C#交流) 欢迎喜欢C#,热爱C#,正在学习C#,准备学习C#的朋友来这里互相学习交流,共同进步
群刚建,人不多,但是都是真正热爱C#的 我也是热爱C#的 希望大家可以一起交流,共同进步