├── .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 |
--------------------------------------------------------------------------------