├── .gitignore ├── LICENSE.txt ├── README.md ├── img ├── mic.svg ├── mic128.png └── save.svg ├── index.html └── js ├── audiodisplay.js ├── main.js └── recorderjs ├── recorder.js └── recorderWorker.js /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Chris Wilson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Audio Recorder 2 | 3 | This is a code snippet/example for using RecorderJS with the web audio input feature to record audio from 4 | [Web Audio API](https://dvcs.w3.org/hg/audio/raw-file/tip/webaudio/specification.html). 5 | 6 | Hosted live on [Web Audio Demos](http://webaudiodemos.appspot.com/AudioRecorder/index.html). 7 | Check it out, feel free to fork, submit pull requests, etc. 8 | 9 | -Chris 10 | -------------------------------------------------------------------------------- /img/mic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /img/mic128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwilso/AudioRecorder/88e7d9e10d5d59253c0c28fcbdb8cb91349d555a/img/mic128.png -------------------------------------------------------------------------------- /img/save.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 19 | 21 | 24 | 28 | 32 | 33 | 36 | 40 | 44 | 45 | 48 | 52 | 56 | 57 | 68 | 71 | 75 | 79 | 80 | 90 | 93 | 97 | 101 | 102 | 113 | 115 | 119 | 123 | 127 | 131 | 135 | 136 | 138 | 142 | 146 | 147 | 150 | 154 | 158 | 159 | 162 | 166 | 170 | 171 | 173 | 177 | 181 | 182 | 185 | 189 | 193 | 194 | 196 | 200 | 204 | 205 | 207 | 211 | 215 | 216 | 226 | 236 | 246 | 257 | 267 | 278 | 288 | 297 | 306 | 315 | 316 | 337 | 339 | 340 | 342 | image/svg+xml 343 | 345 | Save 346 | 347 | 348 | Jakub Steiner 349 | 350 | 351 | 352 | 353 | hdd 354 | hard drive 355 | save 356 | io 357 | store 358 | 359 | 360 | 362 | 363 | http://jimmac.musichall.cz 364 | 365 | 367 | 369 | 371 | 373 | 375 | 377 | 379 | 380 | 381 | 382 | 386 | 396 | 401 | 406 | 411 | 418 | 423 | 428 | 432 | 442 | 452 | 457 | 461 | 465 | 469 | 473 | 477 | 481 | 485 | 489 | 493 | 497 | 501 | 505 | 515 | 516 | 520 | 530 | 535 | 540 | 545 | 546 | 547 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Audio Recorder 6 | 7 | 8 | 9 | 10 | 61 | 62 | 63 |
64 | 65 | 66 |
67 |
68 | 69 | 70 |
71 | 72 | -------------------------------------------------------------------------------- /js/audiodisplay.js: -------------------------------------------------------------------------------- 1 | function drawBuffer( width, height, context, data ) { 2 | var step = Math.ceil( data.length / width ); 3 | var amp = height / 2; 4 | context.fillStyle = "silver"; 5 | context.clearRect(0,0,width,height); 6 | for(var i=0; i < width; i++){ 7 | var min = 1.0; 8 | var max = -1.0; 9 | for (j=0; j max) 14 | max = datum; 15 | } 16 | context.fillRect(i,(1+min)*amp,1,Math.max(1,(max-min)*amp)); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /js/main.js: -------------------------------------------------------------------------------- 1 | /* Copyright 2013 Chris Wilson 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | window.AudioContext = window.AudioContext || window.webkitAudioContext; 17 | 18 | var audioContext = new AudioContext(); 19 | var audioInput = null, 20 | realAudioInput = null, 21 | inputPoint = null, 22 | audioRecorder = null; 23 | var rafID = null; 24 | var analyserContext = null; 25 | var canvasWidth, canvasHeight; 26 | var recIndex = 0; 27 | 28 | /* TODO: 29 | 30 | - offer mono option 31 | - "Monitor input" switch 32 | */ 33 | 34 | function saveAudio() { 35 | audioRecorder.exportWAV( doneEncoding ); 36 | // could get mono instead by saying 37 | // audioRecorder.exportMonoWAV( doneEncoding ); 38 | } 39 | 40 | function gotBuffers( buffers ) { 41 | var canvas = document.getElementById( "wavedisplay" ); 42 | 43 | drawBuffer( canvas.width, canvas.height, canvas.getContext('2d'), buffers[0] ); 44 | 45 | // the ONLY time gotBuffers is called is right after a new recording is completed - 46 | // so here's where we should set up the download. 47 | audioRecorder.exportWAV( doneEncoding ); 48 | } 49 | 50 | function doneEncoding( blob ) { 51 | Recorder.setupDownload( blob, "myRecording" + ((recIndex<10)?"0":"") + recIndex + ".wav" ); 52 | recIndex++; 53 | } 54 | 55 | function toggleRecording( e ) { 56 | if (e.classList.contains("recording")) { 57 | // stop recording 58 | audioRecorder.stop(); 59 | e.classList.remove("recording"); 60 | audioRecorder.getBuffers( gotBuffers ); 61 | } else { 62 | // start recording 63 | if (!audioRecorder) 64 | return; 65 | e.classList.add("recording"); 66 | audioRecorder.clear(); 67 | audioRecorder.record(); 68 | } 69 | } 70 | 71 | function convertToMono( input ) { 72 | var splitter = audioContext.createChannelSplitter(2); 73 | var merger = audioContext.createChannelMerger(2); 74 | 75 | input.connect( splitter ); 76 | splitter.connect( merger, 0, 0 ); 77 | splitter.connect( merger, 0, 1 ); 78 | return merger; 79 | } 80 | 81 | function cancelAnalyserUpdates() { 82 | window.cancelAnimationFrame( rafID ); 83 | rafID = null; 84 | } 85 | 86 | function updateAnalysers(time) { 87 | if (!analyserContext) { 88 | var canvas = document.getElementById("analyser"); 89 | canvasWidth = canvas.width; 90 | canvasHeight = canvas.height; 91 | analyserContext = canvas.getContext('2d'); 92 | } 93 | 94 | // analyzer draw code here 95 | { 96 | var SPACING = 3; 97 | var BAR_WIDTH = 1; 98 | var numBars = Math.round(canvasWidth / SPACING); 99 | var freqByteData = new Uint8Array(analyserNode.frequencyBinCount); 100 | 101 | analyserNode.getByteFrequencyData(freqByteData); 102 | 103 | analyserContext.clearRect(0, 0, canvasWidth, canvasHeight); 104 | analyserContext.fillStyle = '#F6D565'; 105 | analyserContext.lineCap = 'round'; 106 | var multiplier = analyserNode.frequencyBinCount / numBars; 107 | 108 | // Draw rectangle for each frequency bin. 109 | for (var i = 0; i < numBars; ++i) { 110 | var magnitude = 0; 111 | var offset = Math.floor( i * multiplier ); 112 | // gotta sum/average the block, or we miss narrow-bandwidth spikes 113 | for (var j = 0; j< multiplier; j++) 114 | magnitude += freqByteData[offset + j]; 115 | magnitude = magnitude / multiplier; 116 | var magnitude2 = freqByteData[i * multiplier]; 117 | analyserContext.fillStyle = "hsl( " + Math.round((i*360)/numBars) + ", 100%, 50%)"; 118 | analyserContext.fillRect(i * SPACING, canvasHeight, BAR_WIDTH, -magnitude); 119 | } 120 | } 121 | 122 | rafID = window.requestAnimationFrame( updateAnalysers ); 123 | } 124 | 125 | function toggleMono() { 126 | if (audioInput != realAudioInput) { 127 | audioInput.disconnect(); 128 | realAudioInput.disconnect(); 129 | audioInput = realAudioInput; 130 | } else { 131 | realAudioInput.disconnect(); 132 | audioInput = convertToMono( realAudioInput ); 133 | } 134 | 135 | audioInput.connect(inputPoint); 136 | } 137 | 138 | function gotStream(stream) { 139 | inputPoint = audioContext.createGain(); 140 | 141 | // Create an AudioNode from the stream. 142 | realAudioInput = audioContext.createMediaStreamSource(stream); 143 | audioInput = realAudioInput; 144 | audioInput.connect(inputPoint); 145 | 146 | // audioInput = convertToMono( input ); 147 | 148 | analyserNode = audioContext.createAnalyser(); 149 | analyserNode.fftSize = 2048; 150 | inputPoint.connect( analyserNode ); 151 | 152 | audioRecorder = new Recorder( inputPoint ); 153 | 154 | zeroGain = audioContext.createGain(); 155 | zeroGain.gain.value = 0.0; 156 | inputPoint.connect( zeroGain ); 157 | zeroGain.connect( audioContext.destination ); 158 | updateAnalysers(); 159 | } 160 | 161 | function initAudio() { 162 | if (!navigator.getUserMedia) 163 | navigator.getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia; 164 | if (!navigator.cancelAnimationFrame) 165 | navigator.cancelAnimationFrame = navigator.webkitCancelAnimationFrame || navigator.mozCancelAnimationFrame; 166 | if (!navigator.requestAnimationFrame) 167 | navigator.requestAnimationFrame = navigator.webkitRequestAnimationFrame || navigator.mozRequestAnimationFrame; 168 | 169 | navigator.getUserMedia( 170 | { 171 | "audio": { 172 | "mandatory": { 173 | "googEchoCancellation": "false", 174 | "googAutoGainControl": "false", 175 | "googNoiseSuppression": "false", 176 | "googHighpassFilter": "false" 177 | }, 178 | "optional": [] 179 | }, 180 | }, gotStream, function(e) { 181 | alert('Error getting audio'); 182 | console.log(e); 183 | }); 184 | } 185 | 186 | window.addEventListener('load', initAudio ); 187 | -------------------------------------------------------------------------------- /js/recorderjs/recorder.js: -------------------------------------------------------------------------------- 1 | /*License (MIT) 2 | 3 | Copyright © 2013 Matt Diamond 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 6 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 7 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 8 | to permit persons to whom the Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of 11 | the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO 14 | THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 15 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 16 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 17 | DEALINGS IN THE SOFTWARE. 18 | */ 19 | 20 | (function(window){ 21 | 22 | var WORKER_PATH = 'js/recorderjs/recorderWorker.js'; 23 | 24 | var Recorder = function(source, cfg){ 25 | var config = cfg || {}; 26 | var bufferLen = config.bufferLen || 4096; 27 | this.context = source.context; 28 | if(!this.context.createScriptProcessor){ 29 | this.node = this.context.createJavaScriptNode(bufferLen, 2, 2); 30 | } else { 31 | this.node = this.context.createScriptProcessor(bufferLen, 2, 2); 32 | } 33 | 34 | var worker = new Worker(config.workerPath || WORKER_PATH); 35 | worker.postMessage({ 36 | command: 'init', 37 | config: { 38 | sampleRate: this.context.sampleRate 39 | } 40 | }); 41 | var recording = false, 42 | currCallback; 43 | 44 | this.node.onaudioprocess = function(e){ 45 | if (!recording) return; 46 | worker.postMessage({ 47 | command: 'record', 48 | buffer: [ 49 | e.inputBuffer.getChannelData(0), 50 | e.inputBuffer.getChannelData(1) 51 | ] 52 | }); 53 | } 54 | 55 | this.configure = function(cfg){ 56 | for (var prop in cfg){ 57 | if (cfg.hasOwnProperty(prop)){ 58 | config[prop] = cfg[prop]; 59 | } 60 | } 61 | } 62 | 63 | this.record = function(){ 64 | recording = true; 65 | } 66 | 67 | this.stop = function(){ 68 | recording = false; 69 | } 70 | 71 | this.clear = function(){ 72 | worker.postMessage({ command: 'clear' }); 73 | } 74 | 75 | this.getBuffers = function(cb) { 76 | currCallback = cb || config.callback; 77 | worker.postMessage({ command: 'getBuffers' }) 78 | } 79 | 80 | this.exportWAV = function(cb, type){ 81 | currCallback = cb || config.callback; 82 | type = type || config.type || 'audio/wav'; 83 | if (!currCallback) throw new Error('Callback not set'); 84 | worker.postMessage({ 85 | command: 'exportWAV', 86 | type: type 87 | }); 88 | } 89 | 90 | this.exportMonoWAV = function(cb, type){ 91 | currCallback = cb || config.callback; 92 | type = type || config.type || 'audio/wav'; 93 | if (!currCallback) throw new Error('Callback not set'); 94 | worker.postMessage({ 95 | command: 'exportMonoWAV', 96 | type: type 97 | }); 98 | } 99 | 100 | worker.onmessage = function(e){ 101 | var blob = e.data; 102 | currCallback(blob); 103 | } 104 | 105 | source.connect(this.node); 106 | this.node.connect(this.context.destination); // if the script node is not connected to an output the "onaudioprocess" event is not triggered in chrome. 107 | }; 108 | 109 | Recorder.setupDownload = function(blob, filename){ 110 | var url = (window.URL || window.webkitURL).createObjectURL(blob); 111 | var link = document.getElementById("save"); 112 | link.href = url; 113 | link.download = filename || 'output.wav'; 114 | } 115 | 116 | window.Recorder = Recorder; 117 | 118 | })(window); 119 | -------------------------------------------------------------------------------- /js/recorderjs/recorderWorker.js: -------------------------------------------------------------------------------- 1 | /*License (MIT) 2 | 3 | Copyright © 2013 Matt Diamond 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 6 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 7 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 8 | to permit persons to whom the Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of 11 | the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO 14 | THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 15 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 16 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 17 | DEALINGS IN THE SOFTWARE. 18 | */ 19 | 20 | var recLength = 0, 21 | recBuffersL = [], 22 | recBuffersR = [], 23 | sampleRate; 24 | 25 | this.onmessage = function(e){ 26 | switch(e.data.command){ 27 | case 'init': 28 | init(e.data.config); 29 | break; 30 | case 'record': 31 | record(e.data.buffer); 32 | break; 33 | case 'exportWAV': 34 | exportWAV(e.data.type); 35 | break; 36 | case 'exportMonoWAV': 37 | exportMonoWAV(e.data.type); 38 | break; 39 | case 'getBuffers': 40 | getBuffers(); 41 | break; 42 | case 'clear': 43 | clear(); 44 | break; 45 | } 46 | }; 47 | 48 | function init(config){ 49 | sampleRate = config.sampleRate; 50 | } 51 | 52 | function record(inputBuffer){ 53 | recBuffersL.push(inputBuffer[0]); 54 | recBuffersR.push(inputBuffer[1]); 55 | recLength += inputBuffer[0].length; 56 | } 57 | 58 | function exportWAV(type){ 59 | var bufferL = mergeBuffers(recBuffersL, recLength); 60 | var bufferR = mergeBuffers(recBuffersR, recLength); 61 | var interleaved = interleave(bufferL, bufferR); 62 | var dataview = encodeWAV(interleaved); 63 | var audioBlob = new Blob([dataview], { type: type }); 64 | 65 | this.postMessage(audioBlob); 66 | } 67 | 68 | function exportMonoWAV(type){ 69 | var bufferL = mergeBuffers(recBuffersL, recLength); 70 | var dataview = encodeWAV(bufferL, true); 71 | var audioBlob = new Blob([dataview], { type: type }); 72 | 73 | this.postMessage(audioBlob); 74 | } 75 | 76 | function getBuffers() { 77 | var buffers = []; 78 | buffers.push( mergeBuffers(recBuffersL, recLength) ); 79 | buffers.push( mergeBuffers(recBuffersR, recLength) ); 80 | this.postMessage(buffers); 81 | } 82 | 83 | function clear(){ 84 | recLength = 0; 85 | recBuffersL = []; 86 | recBuffersR = []; 87 | } 88 | 89 | function mergeBuffers(recBuffers, recLength){ 90 | var result = new Float32Array(recLength); 91 | var offset = 0; 92 | for (var i = 0; i < recBuffers.length; i++){ 93 | result.set(recBuffers[i], offset); 94 | offset += recBuffers[i].length; 95 | } 96 | return result; 97 | } 98 | 99 | function interleave(inputL, inputR){ 100 | var length = inputL.length + inputR.length; 101 | var result = new Float32Array(length); 102 | 103 | var index = 0, 104 | inputIndex = 0; 105 | 106 | while (index < length){ 107 | result[index++] = inputL[inputIndex]; 108 | result[index++] = inputR[inputIndex]; 109 | inputIndex++; 110 | } 111 | return result; 112 | } 113 | 114 | function floatTo16BitPCM(output, offset, input){ 115 | for (var i = 0; i < input.length; i++, offset+=2){ 116 | var s = Math.max(-1, Math.min(1, input[i])); 117 | output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true); 118 | } 119 | } 120 | 121 | function writeString(view, offset, string){ 122 | for (var i = 0; i < string.length; i++){ 123 | view.setUint8(offset + i, string.charCodeAt(i)); 124 | } 125 | } 126 | 127 | function encodeWAV(samples, mono){ 128 | var buffer = new ArrayBuffer(44 + samples.length * 2); 129 | var view = new DataView(buffer); 130 | 131 | /* RIFF identifier */ 132 | writeString(view, 0, 'RIFF'); 133 | /* file length */ 134 | view.setUint32(4, 32 + samples.length * 2, true); 135 | /* RIFF type */ 136 | writeString(view, 8, 'WAVE'); 137 | /* format chunk identifier */ 138 | writeString(view, 12, 'fmt '); 139 | /* format chunk length */ 140 | view.setUint32(16, 16, true); 141 | /* sample format (raw) */ 142 | view.setUint16(20, 1, true); 143 | /* channel count */ 144 | view.setUint16(22, mono ? 1 : 2, true); 145 | /* sample rate */ 146 | view.setUint32(24, sampleRate, true); 147 | /* byte rate (sample rate * channels * bytes per sample) */ 148 | view.setUint32(28, sampleRate * (mono ? 1 : 2) * 2, true); 149 | /* block align (channel count * bytes per sample) */ 150 | view.setUint16(32, (mono ? 1 : 2) * 2, true); 151 | /* bits per sample */ 152 | view.setUint16(34, 16, true); 153 | /* data chunk identifier */ 154 | writeString(view, 36, 'data'); 155 | /* data chunk length */ 156 | view.setUint32(40, samples.length * 2, true); 157 | 158 | floatTo16BitPCM(view, 44, samples); 159 | 160 | return view; 161 | } 162 | --------------------------------------------------------------------------------