最近有個在線招聘錄音的開發需求,需要在招聘網站上讓招聘者上傳錄音和視頻。
找到兩個不錯的javascript開源,可以在除了IE以外的瀏覽器運行。
https://github.com/mattdiamond/Recorderjs
https://github.com/muaz-khan/RecordRTC
核心算法如下:
Bit rate = (sampling rate) × (bit depth) × (number of channels)
舉例說明,CD音質的Bit rate就是:
sampling rate =44.1 kHz
bit depth =16bit
通道(左聲道+右聲道) = 2
44100 × 16 × 2 = 1411200 bits per second = 1411.2 kbit/s
注意,以上單位是位!不是字節!
開發中有一個坑點,就是音頻的采樣率比較高,造成音頻的比特率較大,文件隨之很大,找了一些資料,只能減少一半:
http://stackoverflow.com/questions/16296645/decrease-bitrate-on-wav-file-created-with-recorderjs/26245260#26245260
Webrtc的音頻編解碼采用iLIBC/iSAC/G722/PCM16/RED/AVT編解碼技術
在iSAC編碼下,采樣率是16khz,24khz,32khz;(默認為16khz)
在iLBC編碼(Internet Low Bitrate Codec)下,采樣頻率:8khz;20ms幀比特率為15.2kbps
在iLBC下能最大化的減少比特率.但在這兩個js庫中查不到使用了什么編碼。
嘗試1:
window.AudioContext = window.AudioContext || window.webkitAudioContext; var audioContext = new AudioContext(); audioContext.sampleRate = 16000; //發現是無法設置AudioContext 的采樣率,這是一個操作系統指定的不可改屬性 console.log(audioContext.sampleRate);
嘗試2:成功。方法是通過將wav轉為mp3:
https://github.com/remusnegrota/Recorderjs/tree/Recorder.js-For-Mp3
使用Recorderjs的例子在:examples/example_simple_exportwav
MVC工程是使用RecordRTC。
一些預備知識:
碼率:Bit Rate,指視頻或音頻文件在單位時間內使用的數據流量,該參數的單位通常是Kbps,也就是千比特每秒。通常2000kbps~3000kbps就已經足以將畫質效果表現到極致了。碼率參數與視頻文件最終體積大小有直接性的關系。 (編碼碼率---軟件)
混合碼率:Overall Bit Rate,指視頻文件中視頻和音頻混合后的整體平均碼率。一般描述一個視頻文件的碼率都是指OBR,如新浪播客允許的OBR上限為523Kbps。
固定碼率:Constant Bit Rate,指的是編碼器的輸出碼率(或者解碼器的輸入碼率)應該是固定制(常數)。CBR不適合高清晰度視頻的編碼,因為CBR將導致沒有足夠的碼率應對復雜多變內容部分進行編碼(從而導致畫質下降),同時在簡單的內容部分會浪費一些碼率。
可變碼率:Variable Bit Rate,編碼器的輸出碼率(或者解碼器的輸入碼率)可以根據編碼器的輸入源信號的負責度自適應的調整,目的是達到保持輸出質量保持不變而不是保持輸出碼率保持不變。VBR編碼會消耗較多的計算時間,但可以更好的利用有限的存儲空間:用比較多的碼率對復雜度高的段進行編碼,用比較少的碼率對復雜度低的段進行編碼。總之需要清晰度高且體積小的視頻,選擇VBR是明智的選擇。
平均碼率:Average Bit Rate,指音頻或視頻的平均碼率,可以簡單的認為等於文件大小除以播放時間。在音頻編碼方面與CBR基本相同,會按照設定的目標碼率進行編碼。但當編碼器認為“適當”的時候,會使用高於目標碼率的數值來進行編碼以保證更好的質量。
幀率:Frame Rate,是用於測量畫面顯示幀數的量度。所謂的測量單位為每秒顯示幀數(Frames per Second,縮寫:FPS)。如電影的幀率一般是25fps和29.97fps,而第一人稱射擊游戲等要求畫面極為順暢的特殊場合,則需要30fps以上的效果,高於60fps就沒有必要了。
采樣率:每秒從連續信號中提取並組成離散信號的采樣個數,它用赫茲(Hz)來表示。一般音樂CD的采樣率是44100Hz,所以視頻編碼中的音頻采樣率保持在這個級別就完全足夠了,通常視頻轉換器也將這個采樣率作為默認設置。(芯片采樣次數---硬件,得到的是原始波形文件pcm)
Single Pass:在編碼的時候只進行一次運算,直接生成經過編碼的視頻文件。
Two Pass:需要運算兩次,可以理解為先進行一次全局的計算,收集畫面信息,並將這些信息記錄到信息文件。第二次才根據采集的信息,正式進行壓縮,生成壓縮文件。
Single pass模式編碼較簡單,編碼速度較快,但是最終質量不如Two pass模式好,對於視頻源本身畫質就不佳的編碼過程可以采用。Two pass通過第一次運算的信息采集,可以讓需要高碼率的運動畫面可以分配更的碼率來保證畫面質量。而對於不包含太多運動信息的靜態畫面,則可以消減分配的碼率。Twopass模式可以在影片容量與畫面質量之間找到最佳平衡點。所以要求畫面清晰的視頻,肯定要選擇Two Pass,只是編碼速度慘不忍睹。
封裝格式:多媒體封裝格式也稱多媒體容器 (Multimedia Container),它不同於H.264、 AAC這類編碼格式,它只是為多媒體編碼提供了一個“外殼”,也就是所謂的視頻格式。如MP4、AVI、MKV、FLV、WMA等。
畫面比例:Aspect Ratio,指視頻畫面寬和高的比例。常見的比例有16:9和4:3。電視媒體有嚴格的視頻制式要求,視頻比例和幀數都是固定的,而網絡傳播的視頻比例則較為自由。一般DVD和BD電影的視頻比例大多是寬屏或者超寬屏。在視頻編碼過程中一定要注意畫面比例是否正確,不然就會出現畫面拉伸變形。
分辨率:指視頻寬高的像素數值,單位為Px。通常視頻分辨率的數值寬高比要等於畫面比例,不然視頻文件就會產生黑邊。標准1080P的分辨率為1920×1080,幀率為60fps,也就是真高清。而最常見的網絡傳播的1080P高清片幀率通常為23.976 fps。
什么是 采樣率 和 比特率? 16bit/44.1kHz、24bit/48kHz、24bit/192kHz 分別代表什么?
|
簡單來講,采樣率和比特率就像是坐標軸上的橫縱坐標。 橫坐標的采樣率表示了每秒鍾的采樣次數。而聲音的位數就表示每個取樣的數據量,數據量越大,回放的聲音越准確。
簡單來講,采樣率和比特率就像是坐標軸上的橫縱坐標。 以電話為例,每秒3000次取樣,每個取樣是7比特,那么電話的比特率是21000。而CD是每秒44100次取樣,兩個聲道,每個取樣是13位PCM編碼,所以CD的比特率是44100*2*13=1146600。
1G容量用480Mbps傳有多快,一想,這還不簡單,480Mbps多快,用1024M除下不就得了,后來發現這么做不對,我將"480Mbps"誤解為480兆/秒。事實上"480MBPS"應為480兆比特/秒或480兆位/秒,它等於60兆/秒.要是傳1G容量應該是1024M/60=17秒。
采樣率 采樣率實際上是指當將聲音儲存至計算機中,必須經過一個錄音轉換的過程,轉換些什么呢?就是把聲音這種模擬信號轉成計算機可以辨識的數字信號,在轉換過程中將聲波的波形以微分方式切開成許多單位,再把每個切開的聲波以一個數值來代表該單位的一個量,以此方式完成采樣的工作,而在單位時間內切開的數量便是所謂的采樣頻率,說明白些,就是模擬轉數字時每秒對聲波采樣的數量,像是CD音樂的標准采樣頻率為44.1KHz,這也是目前聲卡與計算機作業間最常用的采樣頻率。 另外,在單位時間內采樣的數量越多就會越接近原始的模擬信號,在將數字信號還原成模擬信號時也就越能接近真實的原始聲音;相對的越高的采樣率,資料的大小就越大,反之則越小,當然也就越不真實了。數字數據量的大小與聲道數、采樣率、音質分辨率有着密不可分的關系。 前面提到CD音樂的采樣率為44.1KHz,而在計算機上的DVD音效則為48KHz (經聲卡轉換) ,一般的電台FM廣播為32KHz,其它的音效則因不同的應用有不同的采樣率,像是以網絡會議之類的應用就不要使用高的采樣率,否則在傳遞這些聲音數據時會是一件十分痛苦的事。 當然,目前比較盛行的高清碟的采樣率就相當的高,達到了192kHz。而目前的聲卡,絕大多數都可以支持44.1kHz、48kHz、96kHz,高端產品可支持192kHz甚至更高。
![]() 上圖中 16bits 對應 2^16 = 65536個電平, 20*log10(65536) = 96.3296dB
比特率 聲波在轉為數字的過程中不是只有采樣率會影響原始聲音的完整性,另一個亦具有舉足輕重的參數——量化精度(比特率),也是相當的重要。一般來說,音質分辨率就是大家常說的bit數。目前,絕大多數的聲卡都已經可以支持24bit的量化精度。 那么,什么是量化精度呢?前面曾說明采樣頻率,它是針對每秒鍾所采樣的數量,而量化精度則是對於聲波的“振幅”進行切割,形成類似階梯的度量單位。所以,如果說采樣頻率是對聲波水平進行的X軸切割,那么量化精度則是對Y軸的切割,切割的數量是以最大振幅切成2的n次方計算,n就是bit數。 舉個例子,如果是8bit,那么在振幅方面的采樣就有256階,若是16bit,則振幅的計量單位便會成為65536階,越多的階數就越能精確描述每個采樣的振幅高度。如此,也就越接近原始聲波的“能量”,在還原的過程序也就越接近原始的聲音了。 另外,bit的數目還決定了聲波振幅的范圍(即動態范圍,最大音量與最小音量的差距)。如果這個位數越大,則能夠表示的數值越大,描述波形更精確。每一個Bit的數據可以記錄約等於6dB動態的信號。一般來說,16Bit可以提供最大96dB的動態范圍(加高頻顫動后只有92dB)。每增加一個Bit的量化精度,這個值就增加6dB。因此,我們可以推斷出20Bit可以達到120dB的動態范圍,而24Bit則可以提供高達144dB的動態范圍。 那么,動態范圍大了,會有什么好處呢?動態范圍是指系統的輸出噪音功率和最大不失真音量功率的比值,這個值越大,則系統可以承受很高的動態。比如1812序曲中的炮聲,如果系統動態過小,高於動態范圍的信號將被削波(Clipping, 高於0dB的溢出信號將被砍掉,會導致噼里啪啦的聲音)。 |
視圖代碼:
@{ ViewBag.Title = "Index"; Layout = null; } <!DOCTYPE html> <html lang="en"> <head> <title>錄像並上傳</title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> <link rel="stylesheet" href="/css/style.css"> <style> audio { vertical-align: bottom; width: 10em; } video { vertical-align: top;max-width: 100%; } input { border: 1px solid #d9d9d9; border-radius: 1px; font-size: 2em; margin: .2em; width: 30%; } p, .inner { padding: 1em; } li { border-bottom: 1px solid rgb(189, 189, 189); border-left: 1px solid rgb(189, 189, 189); padding: .5em; } label { display: inline-block; width: 8em; } </style> <script> document.createElement('article'); document.createElement('footer'); </script> <!-- script used for audio/video/gif recording --> <script src="/js/RecordRTC.js"> </script> </head> <body> <article> <section class="experiment"> <p style="text-align:center;"> <video id="preview" controls style="border: 1px solid rgb(15, 158, 238); height: 240px; width: 320px;"></video> </p> <hr /> <button id="record">錄像</button> <button id="stop" disabled>停止並上傳</button> <button id="delete" disabled>從服務器刪除錄像</button> <div id="container" style="padding:1em 2em;"></div> </section> <script> // PostBlob方法使用 XHR2 和 FormData 來發送提交視頻文件 ,IE不支持 // 錄制blob並發送到服務器 function PostBlob(blob, fileType, fileName) { // FormData var formData = new FormData(); //文件名鍵值對 formData.append(fileType + '-filename', fileName); //blob鍵值對 formData.append(fileType + '-blob', blob); // progress-bar 進度條 var hr = document.createElement('hr'); container.appendChild(hr); var strPercentage = document.createElement('strPercentage'); strPercentage.id = 'percentage'; strPercentage.innerHTML = fileType + ' 上傳進度: '; container.appendChild(strPercentage); var progress = document.createElement('progress'); container.appendChild(progress); // POST the Blob using XHR2 xhr('/Home/PostRecordedAudioVideo', formData, progress, percentage, function (fName) //回調 { container.appendChild(document.createElement('hr')); var mediaElement = document.createElement(fileType); var source = document.createElement('source'); source.src = location.href + 'uploads/' + fName.replace(/"/g, ''); if (fileType == 'video') source.type = 'video/webm; codecs="vp8, vorbis"'; if (fileType == 'audio') source.type = !!navigator.mozGetUserMedia ? 'audio/ogg' : 'audio/wav'; mediaElement.appendChild(source); mediaElement.controls = true; container.appendChild(mediaElement); mediaElement.play(); progress.parentNode.removeChild(progress); strPercentage.parentNode.removeChild(strPercentage); hr.parentNode.removeChild(hr); } ); //xhr 結束 }//PostBlob 方法結束 var record = document.getElementById('record'); var stop = document.getElementById('stop'); var deleteFiles = document.getElementById('delete'); var audio = document.querySelector('audio'); var recordVideo = document.getElementById('record-video'); var preview = document.getElementById('preview'); var container = document.getElementById('container'); // 如果只想用chrome錄制音頻,可以設置 // "isFirefox=true" var isFirefox = !!navigator.mozGetUserMedia; //******** 錄音函數 開始 ******** var recordAudio, recordVideo; record.onclick = function () { record.disabled = true; //提示用戶需要權限去使用像攝像頭或麥克風之類的媒體設備.如果用戶提供了這個權限 //successCallback函數會被調用,且接收一個LocalMediaStream 對象作為參數. //語法:navigator.getUserMedia ( constraints, successCallback, errorCallback ); var mediaConstraints = { audio: { mandatory: { echoCancellation: true, googAutoGainControl: false, googNoiseSuppression: false, googHighpassFilter: false }, optional: [{ googAudioMirroring: false }] }, video: true }; navigator.getUserMedia( mediaConstraints, //successCallback函數被調用 function (stream) { preview.src = window.URL.createObjectURL(stream); preview.muted = true;//如果正在錄制的時候進行預覽不靜音播放會產生循環回音 preview.play(); // var legalBufferValues = [256, 512, 1024, 2048, 4096, 8192, 16384]; // sample-rates in at least the range 22050 to 96000. recordAudio = RecordRTC(stream, { //bufferSize: 256, //sampleRate: 22050, //設置22050后,音軌長度增加一倍 //audioBitsPerSecond: 21000,//無效 //bitsPerSecond: 64000 , //無效 //recorderType: StereoAudioRecorder, numberOfAudioChannels: 1, //單聲道(左聲道)這個將減少wav文件一半大小 onAudioProcessStarted: function () { if (!isFirefox) { recordVideo.startRecording(); } } }); if (isFirefox) { recordAudio.startRecording(); } if (!isFirefox) { recordVideo = RecordRTC(stream, { //recordVideo = WhammyRecorder(stream, { type: 'video' }); recordAudio.startRecording(); } stop.disabled = false; }, //errorCallback函數被調用 function (error) { alert(JSON.stringify(error, null, '\t')); }); }; //******** 錄音函數 結束 ******** //******** 結束錄音函數 開始 ******** var fileName; stop.onclick = function () { record.disabled = false; stop.disabled = true; preview.src = ''; fileName = Math.round(Math.random() * 99999999) + 99999999; if (!isFirefox) { recordAudio.stopRecording(function () { PostBlob(recordAudio.getBlob(), 'audio', fileName + '.wav'); }); } else { recordAudio.stopRecording(function (url) { preview.src = url; PostBlob(recordAudio.getBlob(), 'video', fileName + '.webm'); }); } if (!isFirefox) { recordVideo.stopRecording(function () { PostBlob(recordVideo.getBlob(), 'video', fileName + '.webm'); }); } deleteFiles.disabled = false; }; //******** 結束錄音函數 結束 ******** deleteFiles.onclick = function () { deleteAudioVideoFiles(); }; function deleteAudioVideoFiles() { deleteFiles.disabled = true; if (!fileName) return; var formData = new FormData(); formData.append('delete-file', fileName); xhr('/Home/DeleteFile', formData, null, null, function (response) { console.log(response); }); fileName = null; container.innerHTML = ''; } function xhr(url, data, progress, percentage, callback) { var request = new XMLHttpRequest(); request.onreadystatechange = function () { if (request.readyState == 4 && request.status == 200) { callback(request.responseText); } }; if (url.indexOf('/Home/DeleteFile') == -1) { request.upload.onloadstart = function () { percentage.innerHTML = 'Upload started...'; }; request.upload.onprogress = function (event) { progress.max = event.total; progress.value = event.loaded; percentage.innerHTML = 'Upload Progress ' + Math.round(event.loaded / event.total * 100) + "%"; }; request.upload.onload = function () { percentage.innerHTML = 'Saved!'; }; } request.open('POST', url); request.send(data); } window.onbeforeunload = function () { if (!!fileName) { deleteAudioVideoFiles(); return 'It seems that you\'ve not deleted audio/video files from the server.'; } }; </script> </article> <footer> <p> </p> </footer> <!-- commits.js is useless for you! --> <script src="/js/commits.js" async> </script> </body> </html>
控制器:
using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Web; using System.Web.Mvc; namespace WebTalk.Controllers { public class HomeController : Controller { public ActionResult Index() { return View(); } [HttpPost] public ActionResult PostRecordedAudioVideo() { foreach (string upload in Request.Files) { var path = AppDomain.CurrentDomain.BaseDirectory + "uploads/"; var file = Request.Files[upload]; if (file == null) continue; file.SaveAs(Path.Combine(path, Request.Form[0])); } return Json(Request.Form[0]); } [HttpPost] public ActionResult DeleteFile() { var fileUrl = AppDomain.CurrentDomain.BaseDirectory + "uploads/" + Request.Form["delete-file"]; new FileInfo(fileUrl + ".wav").Delete(); new FileInfo(fileUrl + ".webm").Delete(); return Json(true); } public ActionResult About() { return View(); } } }
一個優化文件大小的做法:
http://www.cnblogs.com/blqw/p/3782420.html
修改后js

//兼容 window.URL = window.URL || window.webkitURL; navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia; var Storage = {}; if (typeof AudioContext !== 'undefined') { Storage.AudioContext = AudioContext; } else if (typeof webkitAudioContext !== 'undefined') { Storage.AudioContext = webkitAudioContext; } function HZRecorder(stream, config) { config = config || {}; config.sampleBits = config.sampleBits || 8; config.sampleRate = config.sampleRate || (44100 / 6); var channelCount = 1;//單聲道 var self = this; if (!Storage.AudioContextConstructor) { Storage.AudioContextConstructor = new Storage.AudioContext(); } var context = Storage.AudioContextConstructor; var audioInput = context.createMediaStreamSource(stream); var recorder; if (context.createJavaScriptNode) { recorder = context.createJavaScriptNode(4096, channelCount, channelCount); } else if (context.createScriptProcessor) { recorder = context.createScriptProcessor(4096, channelCount, channelCount); } else { throw 'WebAudio API has no support on this browser.'; } recorder.onaudioprocess = function (e) { audioData.input(e.inputBuffer.getChannelData(0)); } var audioData = { size: 0, buffer: [], inputSampleRate: context.sampleRate, inputSampleBit: 16, outputSampleRate: config.sampleRate, outputSampleBit: config.sampleBits, 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++; } console.log(this.inputSampleRate); console.log(this.outputSampleRate); console.log(this.compression); return result; }, encodeWAV: function () { var sampleRate = Math.min(this.inputSampleRate, this.outputSampleRate); var sampleBits = Math.min(this.inputSampleBit, this.outputSampleBit); var bytes = this.compress(); var dataLength = bytes.length * (sampleBits / 8); var buffer = new ArrayBuffer(44 + dataLength); var data = new DataView(buffer); var writeString = function (offset, str) { for (var i = 0; i < str.length; i++) { data.setUint8(offset + i, str.charCodeAt(i)); } } //資源交換文件標識符 writeString(0, 'RIFF'); //下個地址開始到文件尾總字節數 data.setUint32(4, 36 + dataLength, true); //WAV文件標識 writeString(8, 'WAVE'); //波形格式標識 writeString(12, 'fmt '); //過濾字節,一般為0x10=16 data.setUint32(16, 16, true); //格式類別(PCM形式采樣數據) data.setUint16(20, 1, true); //通道數 data.setUint16(22, channelCount, true); //采樣率,每秒樣本數,表示每個通達奧的播放速度 data.setUint32(24, sampleRate, true); //波形數據傳輸率(每秒平均字節數) data.setUint32(28, channelCount * sampleRate * (sampleBits / 8), true); //快數據調整數 采樣一次占用字節數 data.setUint16(32, channelCount * (sampleBits / 8), true); //每樣本數據位數 data.setUint16(34, sampleBits, true); //數據標識符 writeString(36, 'data'); //采樣數據總數 data.setUint32(40, dataLength, true); var offset = 44; if (sampleBits === 8) { for (var i = 0; i < bytes.length; i++, offset++) { var s = Math.max(-1, 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(-1, Math.min(1, bytes[i])); data.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true); } } return new Blob([data], { type: 'audio/wavv' }); } }; this.start = function () { audioInput.connect(recorder); recorder.connect(context.destination); } this.stop = function () { recorder.disconnect(); } this.getBlob = function () { this.stop(); return audioData.encodeWAV(); } var returnObject = { start: start, stop: stop, getBlob: getBlob, blob: null, bufferSize: 0, sampleRate: 0, buffer: null, view: null }; return returnObject; }
完成代碼下載: source code download