HTML5網頁錄音和壓縮

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(432 + samples.length * 2true);
09. /* RIFF type */
10. writeString(view, 8'WAVE');
11. /* format chunk identifier */
12. writeString(view, 12'fmt ');
13. /* format chunk length */
14. view.setUint32(1616true);
15. /* sample format (raw) */
16. view.setUint16(201true);
17. /* channel count */
18. view.setUint16(222true);
19. /* sample rate */
20. view.setUint32(24, sampleRate, true);
21. /* byte rate (sample rate * block align) */
22. view.setUint32(28, sampleRate * 4true);
23. /* block align (channel count * bytes per sample) */
24. view.setUint16(324true);
25. /* bits per sample */
26. view.setUint16(3416true);
27. /* data chunk identifier */
28. writeString(view, 36'data');
29. /* data chunk length */
30. view.setUint32(40, samples.length * 2true);
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, 11);//這裏改成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, 16true); offset += 4;
20. /* 格式類別 (PCM形式採樣數據) */
21. view.setUint16(offset, 1true); 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, 16true); offset += 4;
21. /* 格式類別 (PCM形式採樣數據) */
22. view.setUint16(offset, 1true); 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 * 0x7FFFtrue);
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, 16true); offset += 4;
37. /* 格式類別 (PCM形式採樣數據) */
38. view.setUint16(offset, 1true); 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(409611);
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, 16true); offset += 4;
073. // 格式類別 (PCM形式採樣數據)
074. data.setUint16(offset, 1true); 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 * 0x7FFFtrue);
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);
js
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.
相關文章
相關標籤/搜索