├── .gitignore ├── server.js ├── package.json ├── style.css ├── index.html ├── LICENSE ├── cert.pem ├── key.pem ├── README.md └── AudioRecorder.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .idea/ 3 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var bs = require("browser-sync").create(); 2 | 3 | bs.init({ 4 | server: "./", 5 | https: { 6 | key: "key.pem", 7 | cert: "cert.pem" 8 | }, 9 | files: ["*.*"] 10 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "safari-audio-recording", 3 | "version": "1.0.0", 4 | "description": "A simple audio recorder which works in Safari 11", 5 | "main": "AudioRecorder.js", 6 | "dependencies": {}, 7 | "devDependencies": { 8 | "browser-sync": "^2.18.13" 9 | }, 10 | "scripts": { 11 | "test": "echo \"Error: no test specified\" && exit 1", 12 | "start": "node server.js" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://danstorey@bitbucket.org/danstorey/danstorey.bitbucket.io.git" 17 | }, 18 | "author": "Dan Storey ", 19 | "license": "MIT", 20 | "homepage": "https://github.com/danielstorey/webrtc-audio-recording.git" 21 | } 22 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | background: #f0f0f0; 4 | font-family: 'Roboto', Helvetica, Arial, sans-serif; 5 | } 6 | 7 | #container { 8 | margin-top: 30px; 9 | } 10 | 11 | h1 { 12 | margin: 0; 13 | } 14 | 15 | button { 16 | padding: 10px; 17 | background: #eee; 18 | border: none; 19 | border-radius: 3px; 20 | color: #ffffff; 21 | font-family: inherit; 22 | font-size: 16px; 23 | outline: none !important; 24 | cursor: pointer; 25 | } 26 | 27 | button[disabled] { 28 | background: #aaa !important; 29 | cursor: default; 30 | } 31 | 32 | #btn-start-recording { 33 | background: #5db85c; 34 | } 35 | 36 | #btn-stop-recording { 37 | background: #d95450; 38 | } 39 | 40 | #player { 41 | max-width: 600px; 42 | margin: 0 auto; 43 | padding: 20px; 44 | border: 1px solid #ddd; 45 | background: #ffffff; 46 | } 47 | 48 | audio { 49 | width: 100%; 50 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | WebRTC Safari Test 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 |
16 | 17 |

Audio Recording using RecordRTC

18 |

Tested in Chrome, Firefox and Safari 11 (Mac and IOS)

19 | 20 |
21 | 22 | 23 | 24 |
25 | 26 |
27 |
28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 [Daniel Storey](https://github.com/danielstorey/webrtc-audio-recording) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDnDCCAoSgAwIBAgIJAMksBtcDaAGFMA0GCSqGSIb3DQEBCwUAMGMxCzAJBgNV 3 | BAYTAlVLMRQwEgYDVQQIDAtOb3J0aGFtcHRvbjEUMBIGA1UEBwwLTm9ydGhhbXB0 4 | b24xFzAVBgNVBAoMDlN0b3JleSBTdHVkaW9zMQ8wDQYDVQQDDAZzZXJ2ZXIwHhcN 5 | MTcwOTI3MTUyNTQ2WhcNMTgwOTI3MTUyNTQ2WjBjMQswCQYDVQQGEwJVSzEUMBIG 6 | A1UECAwLTm9ydGhhbXB0b24xFDASBgNVBAcMC05vcnRoYW1wdG9uMRcwFQYDVQQK 7 | DA5TdG9yZXkgU3R1ZGlvczEPMA0GA1UEAwwGc2VydmVyMIIBIjANBgkqhkiG9w0B 8 | AQEFAAOCAQ8AMIIBCgKCAQEAv6dYzmVQczq/L+kx3gLg356BvwV89OJWHQP8YWQK 9 | 6U7O6uJ3Qft/WuQCiausl64RchbZz8VQmOCIbrIm1obQ+klQYwPHs0CkmcB5eEn8 10 | ABQsvbXHtxuB0y9akgdpYkqiQ+Rm1PMYb2FMF2cVvTekaTXpzz3N55zWyG8JtVWQ 11 | Q3I8pqtgUpgPH6KXKLcVjIrcikgUhHA6fX2CL5wzlVEtJLkeksOL+0FKfchJKviy 12 | UNBXSNLMM+AC8HteAfevBMAaWLfuvfWMhEFHDIJvztEmHlK1VWIax+UFJJDw7Obj 13 | YN9PrtHigOBf1E+BHnfRhfjTGXHfLHr80EKbRX8cr56kSwIDAQABo1MwUTAdBgNV 14 | HQ4EFgQU/kKjj+Yo+xdubUhJh6OE+xRhpNMwHwYDVR0jBBgwFoAU/kKjj+Yo+xdu 15 | bUhJh6OE+xRhpNMwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEA 16 | RULRbqHgFOdsRuUJSvbJcVO5GWw3YFOJRIVmZWcAYlSzJ8UVKbmyzvy+UEqB12RR 17 | gbDSoGRfPdS+FS+aaIFY1dVTBqVzf9AAUaM3poXpBf68Xo9RZqrA3m5GmfmicJV9 18 | Ak+kx4GhsFDNIFoqFUQPzvOkFPsJLFDA0YprkhWN38NkD9B+UG6SB80nueCAIpMO 19 | x/tbogLRE72xSVH5A1G5X7NuMHVXMEPjmrV9/n8Y8PP5kL7FR+A3K05goXSVBFfC 20 | HKeyx6RAJ4JsFXvdLbqQ0zyL+Z3nzF0gsgwWBlx69WhOYkZRNkj1pp8HEcMIv+N+ 21 | r4+2LYz1YyRcUXWt0Q4lYw== 22 | -----END CERTIFICATE----- 23 | -------------------------------------------------------------------------------- /key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAv6dYzmVQczq/L+kx3gLg356BvwV89OJWHQP8YWQK6U7O6uJ3 3 | Qft/WuQCiausl64RchbZz8VQmOCIbrIm1obQ+klQYwPHs0CkmcB5eEn8ABQsvbXH 4 | txuB0y9akgdpYkqiQ+Rm1PMYb2FMF2cVvTekaTXpzz3N55zWyG8JtVWQQ3I8pqtg 5 | UpgPH6KXKLcVjIrcikgUhHA6fX2CL5wzlVEtJLkeksOL+0FKfchJKviyUNBXSNLM 6 | M+AC8HteAfevBMAaWLfuvfWMhEFHDIJvztEmHlK1VWIax+UFJJDw7ObjYN9PrtHi 7 | gOBf1E+BHnfRhfjTGXHfLHr80EKbRX8cr56kSwIDAQABAoIBABR+cKChdNjdj1Qw 8 | O4TiCenfqUAj9hi5mzu/EV3YtNH9+2JilR4ZJqUxfJP8J5iQpIzupJvzHYs7d1te 9 | 8Yg/IlmhPVdZMCGa5/8I9fRG9QuNHLuslI6gAmHyzlyPLlW0ZWWdJuHenN5MKTbX 10 | bxust4gIm9ftY/dAbjdAqGkbnTGY9iNieXq4+GiF//NnZfeEVzuvYgQgeNJS2HDd 11 | 57yFJk8QDirthwAC96q2Bnn14IsMhc2iq/RKHnR7H5E+w9rnaRXACgYsOKXPLor+ 12 | cTwIrAdL6mFdmAWozFI85kgqgIndUMHu7h6+ZsrXAhXYlqWa/ho8/a6bP9fGwlOf 13 | 4x5RlQECgYEA7mGSQZW9pIeK+4jroYizLJgQKYfycHyDMDVFRaJ+tpVra1Qzgcgc 14 | 1a/UVJ/+0rX4I1MYRg9ZKtoj3V59XY5z832nFgKpOrRCtiBHYTkPR5hrIPNVnQfA 15 | ZKbrYMA+GG2I52gV4h9HIyd/7zmtQZXjTn7nzhxW45wqiajnyRiR/q8CgYEAzdGk 16 | VvMer4HwuRbCV//1ionwsin9UOpXcWdnkKLc4V1kAuqON0tThKuNoPanFCx8aMap 17 | OVJ6eEjXWsfGsrdY3VDL4wZ3nSK0LcZZlmKLzZMOrZ1fumrHn4DRFmsrZa1HaE3C 18 | bcNVPFZjjerUoF59a0e5HqqzWfg8hYr520D1uyUCgYAssTBZiXhvo1XkSMxckaN+ 19 | Bdhp4OoMOtvhqusc3hVBqAvmqHerqlf1nCyD3SdKXAF0pfyUDgaaqSE2PKPmaXHF 20 | wdYUo1UVA3zKZozbZnY95w6Ws+hmM3DXrg/NKN27eLXFJNeNeG2+4oXy1O5tsGtY 21 | aSSOmPOVYs761ib2pduhUwKBgGkxtSUbrZtWdoqjL0F6+SyNxA/LlkU9AORdTXmH 22 | RA2LhgpXh8iLH2y3ofObHVoaQpvqraM2nJHN6QPlB5FgVHMJUKwAKjKOAjlDH5bV 23 | V08C0oW54auN1+mWFUe4Dr4xCkYtOCqRo4brQIbQd0xf/wpN5jfeVzysu4Ilvf5p 24 | /S5hAoGBALY72xn1drlatuL3tVoJcRP3oj8J40y+T+wKADUGpghry7eqrqjqcSiy 25 | xApF4dSF8ljomqeivkdG/BYVQulFOCO0D7f6IEodew/I4ZPe/fnt+tV2uj4UL1Vu 26 | +um8+SRQlSvmei13ovRAdFChQsNCqErRo468i/93vC+j3uVIx04h 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Audio Recording Using WebRTC 2 | 3 | ### **** Now deprecated. **** 4 | 5 | For an easy to impliment reusable plugin please see 6 | 7 | ## [https://github.com/danielstorey/WebAudioTrack](https://github.com/danielstorey/WebAudioTrack) 8 | 9 | This is a proof of concept demonstrating how it is possible to record and playback audio in most modern browsers, and most notably - Safari 11 on both Mac and IOS. This repo will not be updated any further but the demo will remain here as well as the notes below, containing crucial information for anyone wanting to work with the WebRTC API. 10 | 11 | #### [Click here](https://danielstorey.github.io/webrtc-audio-recording/) for the demo 12 | 13 |
14 | 15 | You can also serve and work on the files locally but please note this will not work unless the site is hosted using SSL (https://) 16 | 17 | I have included Open SSL certificates for convenience and once you have installed the necessary modules via `npm install` you can simply run `node server` and open `https://localhost:3000` in your browser to view. 18 | 19 |
20 | 21 | The most important thing to note about getting this to work in IOS is that the `AudioContext` must be created within an event handler and not within an automatically running script or a Promise. This is to do with the measures that Apple has put in place to prevent audio from automatically playing when you visit a site. So as long as you call `new AudioContext()` or `new webkitAudioContext()` from within an event handler you'll be fine. 22 | 23 | The other thing to watch out for is to make sure that this context not 'garbage collected', otherwise the audio processor will stop with no error or warning. In this example I have created a `Storage` object in the global scope and assigned the `AudioContext` as a property of this object. 24 | 25 |
26 | 27 | Finally I just want to say that much of my work on this has been based on, (and copied!) from [Muaz Khan's](https://github.com/muaz-khan) rather brilliant [RecordRTC](https://github.com/muaz-khan/RecordRTC) library, which includes video and gif recording as well. 28 | 29 | This is simply a lightweight example of how recording audio can work and not necessarily a reusable library in its current state. 30 | -------------------------------------------------------------------------------- /AudioRecorder.js: -------------------------------------------------------------------------------- 1 | // Based on Muaz Khan's RecordRTC Repository 2 | // 3 | // https://github.com/muaz-khan/RecordRTC 4 | // 5 | // www.MuazKhan.com 6 | 7 | var Storage = {}; 8 | var AudioContext = window.AudioContext || window.webkitAudioContext; 9 | var recorder = new AudioRecorder(); 10 | 11 | var startButton = document.getElementById('btn-start-recording'); 12 | var stopButton = document.getElementById('btn-stop-recording'); 13 | 14 | startButton.onclick = recorder.start; 15 | stopButton.onclick = recorder.stop; 16 | 17 | function AudioRecorder(config) { 18 | 19 | config = config || {}; 20 | 21 | var self = this; 22 | var mediaStream; 23 | var audioInput; 24 | var jsAudioNode; 25 | var bufferSize = config.bufferSize || 4096; 26 | var sampleRate = config.sampleRate || 44100; 27 | var numberOfAudioChannels = config.numberOfAudioChannels || 2; 28 | var leftChannel = []; 29 | var rightChannel = []; 30 | var recording = false; 31 | var recordingLength = 0; 32 | var isPaused = false; 33 | var isAudioProcessStarted = false; 34 | 35 | this.start = function() { 36 | setupStorage(); 37 | 38 | navigator.mediaDevices.getUserMedia({audio: true}) 39 | .then(onMicrophoneCaptured) 40 | .catch(onMicrophoneCaptureError); 41 | }; 42 | 43 | this.stop = function() { 44 | stopRecording(function(blob) { 45 | startButton.disabled = false; 46 | stopButton.disabled = true; 47 | 48 | var url = URL.createObjectURL(blob); 49 | var audio = document.querySelector("audio"); 50 | audio.src = url; 51 | }); 52 | }; 53 | 54 | function stopRecording(callback) { 55 | 56 | // stop recording 57 | recording = false; 58 | 59 | // to make sure onaudioprocess stops firing 60 | audioInput.disconnect(); 61 | jsAudioNode.disconnect(); 62 | 63 | mergeLeftRightBuffers({ 64 | sampleRate: sampleRate, 65 | numberOfAudioChannels: numberOfAudioChannels, 66 | internalInterleavedLength: recordingLength, 67 | leftBuffers: leftChannel, 68 | rightBuffers: numberOfAudioChannels === 1 ? [] : rightChannel 69 | }, function(buffer, view) { 70 | 71 | self.blob = new Blob([view], { 72 | type: 'audio/wav' 73 | }); 74 | 75 | self.buffer = new ArrayBuffer(view.buffer.byteLength); 76 | self.view = view; 77 | self.sampleRate = sampleRate; 78 | self.bufferSize = bufferSize; 79 | self.length = recordingLength; 80 | 81 | callback && callback(self.blob); 82 | 83 | clearRecordedData(); 84 | 85 | isAudioProcessStarted = false; 86 | }); 87 | } 88 | 89 | function clearRecordedData() { 90 | leftChannel = rightChannel = []; 91 | recordingLength = 0; 92 | isAudioProcessStarted = false; 93 | recording = false; 94 | isPaused = false; 95 | } 96 | 97 | function setupStorage() { 98 | Storage.ctx = new AudioContext(); 99 | 100 | if (Storage.ctx.createJavaScriptNode) { 101 | jsAudioNode = Storage.ctx.createJavaScriptNode(bufferSize, numberOfAudioChannels, numberOfAudioChannels); 102 | } else if (Storage.ctx.createScriptProcessor) { 103 | jsAudioNode = Storage.ctx.createScriptProcessor(bufferSize, numberOfAudioChannels, numberOfAudioChannels); 104 | } else { 105 | throw 'WebAudio API has no support on this browser.'; 106 | } 107 | 108 | jsAudioNode.connect(Storage.ctx.destination); 109 | } 110 | 111 | function onMicrophoneCaptured(microphone) { 112 | startButton.disabled = true; 113 | stopButton.disabled = false; 114 | 115 | mediaStream = microphone; 116 | 117 | audioInput = Storage.ctx.createMediaStreamSource(microphone); 118 | audioInput.connect(jsAudioNode); 119 | 120 | jsAudioNode.onaudioprocess = onAudioProcess; 121 | 122 | recording = true; 123 | } 124 | 125 | function onMicrophoneCaptureError() { 126 | console.log("There was an error accessing the microphone. You may need to allow the browser access"); 127 | } 128 | 129 | function onAudioProcess(e) { 130 | 131 | if (isPaused) { 132 | return; 133 | } 134 | 135 | if (isMediaStreamActive() === false) { 136 | if (!config.disableLogs) { 137 | console.log('MediaStream seems stopped.'); 138 | } 139 | } 140 | 141 | if (!recording) { 142 | return; 143 | } 144 | 145 | if (!isAudioProcessStarted) { 146 | isAudioProcessStarted = true; 147 | if (config.onAudioProcessStarted) { 148 | config.onAudioProcessStarted(); 149 | } 150 | 151 | if (config.initCallback) { 152 | config.initCallback(); 153 | } 154 | } 155 | 156 | var left = e.inputBuffer.getChannelData(0); 157 | 158 | // we clone the samples 159 | leftChannel.push(new Float32Array(left)); 160 | 161 | if (numberOfAudioChannels === 2) { 162 | var right = e.inputBuffer.getChannelData(1); 163 | rightChannel.push(new Float32Array(right)); 164 | } 165 | 166 | recordingLength += bufferSize; 167 | 168 | // export raw PCM 169 | self.recordingLength = recordingLength; 170 | } 171 | 172 | function isMediaStreamActive() { 173 | if (config.checkForInactiveTracks === false) { 174 | // always return "true" 175 | return true; 176 | } 177 | 178 | if ('active' in mediaStream) { 179 | if (!mediaStream.active) { 180 | return false; 181 | } 182 | } else if ('ended' in mediaStream) { // old hack 183 | if (mediaStream.ended) { 184 | return false; 185 | } 186 | } 187 | return true; 188 | } 189 | 190 | function mergeLeftRightBuffers(config, callback) { 191 | function mergeAudioBuffers(config, cb) { 192 | var numberOfAudioChannels = config.numberOfAudioChannels; 193 | 194 | // todo: "slice(0)" --- is it causes loop? Should be removed? 195 | var leftBuffers = config.leftBuffers.slice(0); 196 | var rightBuffers = config.rightBuffers.slice(0); 197 | var sampleRate = config.sampleRate; 198 | var internalInterleavedLength = config.internalInterleavedLength; 199 | var desiredSampRate = config.desiredSampRate; 200 | 201 | if (numberOfAudioChannels === 2) { 202 | leftBuffers = mergeBuffers(leftBuffers, internalInterleavedLength); 203 | rightBuffers = mergeBuffers(rightBuffers, internalInterleavedLength); 204 | if (desiredSampRate) { 205 | leftBuffers = interpolateArray(leftBuffers, desiredSampRate, sampleRate); 206 | rightBuffers = interpolateArray(rightBuffers, desiredSampRate, sampleRate); 207 | } 208 | } 209 | 210 | if (numberOfAudioChannels === 1) { 211 | leftBuffers = mergeBuffers(leftBuffers, internalInterleavedLength); 212 | if (desiredSampRate) { 213 | leftBuffers = interpolateArray(leftBuffers, desiredSampRate, sampleRate); 214 | } 215 | } 216 | 217 | // set sample rate as desired sample rate 218 | if (desiredSampRate) { 219 | sampleRate = desiredSampRate; 220 | } 221 | 222 | // for changing the sampling rate, reference: 223 | // http://stackoverflow.com/a/28977136/552182 224 | function interpolateArray(data, newSampleRate, oldSampleRate) { 225 | var fitCount = Math.round(data.length * (newSampleRate / oldSampleRate)); 226 | //var newData = new Array(); 227 | var newData = []; 228 | //var springFactor = new Number((data.length - 1) / (fitCount - 1)); 229 | var springFactor = Number((data.length - 1) / (fitCount - 1)); 230 | newData[0] = data[0]; // for new allocation 231 | for (var i = 1; i < fitCount - 1; i++) { 232 | var tmp = i * springFactor; 233 | //var before = new Number(Math.floor(tmp)).toFixed(); 234 | //var after = new Number(Math.ceil(tmp)).toFixed(); 235 | var before = Number(Math.floor(tmp)).toFixed(); 236 | var after = Number(Math.ceil(tmp)).toFixed(); 237 | var atPoint = tmp - before; 238 | newData[i] = linearInterpolate(data[before], data[after], atPoint); 239 | } 240 | newData[fitCount - 1] = data[data.length - 1]; // for new allocation 241 | return newData; 242 | } 243 | 244 | function linearInterpolate(before, after, atPoint) { 245 | return before + (after - before) * atPoint; 246 | } 247 | 248 | function mergeBuffers(channelBuffer, rLength) { 249 | var result = new Float64Array(rLength); 250 | var offset = 0; 251 | var lng = channelBuffer.length; 252 | 253 | for (var i = 0; i < lng; i++) { 254 | var buffer = channelBuffer[i]; 255 | result.set(buffer, offset); 256 | offset += buffer.length; 257 | } 258 | 259 | return result; 260 | } 261 | 262 | function interleave(leftChannel, rightChannel) { 263 | var length = leftChannel.length + rightChannel.length; 264 | 265 | var result = new Float64Array(length); 266 | 267 | var inputIndex = 0; 268 | 269 | for (var index = 0; index < length;) { 270 | result[index++] = leftChannel[inputIndex]; 271 | result[index++] = rightChannel[inputIndex]; 272 | inputIndex++; 273 | } 274 | return result; 275 | } 276 | 277 | function writeUTFBytes(view, offset, string) { 278 | var lng = string.length; 279 | for (var i = 0; i < lng; i++) { 280 | view.setUint8(offset + i, string.charCodeAt(i)); 281 | } 282 | } 283 | 284 | // interleave both channels together 285 | var interleaved; 286 | 287 | if (numberOfAudioChannels === 2) { 288 | interleaved = interleave(leftBuffers, rightBuffers); 289 | } 290 | 291 | if (numberOfAudioChannels === 1) { 292 | interleaved = leftBuffers; 293 | } 294 | 295 | var interleavedLength = interleaved.length; 296 | 297 | // create wav file 298 | var resultingBufferLength = 44 + interleavedLength * 2; 299 | 300 | var buffer = new ArrayBuffer(resultingBufferLength); 301 | 302 | var view = new DataView(buffer); 303 | 304 | // RIFF chunk descriptor/identifier 305 | writeUTFBytes(view, 0, 'RIFF'); 306 | 307 | // RIFF chunk length 308 | view.setUint32(4, 44 + interleavedLength * 2, true); 309 | 310 | // RIFF type 311 | writeUTFBytes(view, 8, 'WAVE'); 312 | 313 | // format chunk identifier 314 | // FMT sub-chunk 315 | writeUTFBytes(view, 12, 'fmt '); 316 | 317 | // format chunk length 318 | view.setUint32(16, 16, true); 319 | 320 | // sample format (raw) 321 | view.setUint16(20, 1, true); 322 | 323 | // stereo (2 channels) 324 | view.setUint16(22, numberOfAudioChannels, true); 325 | 326 | // sample rate 327 | view.setUint32(24, sampleRate, true); 328 | 329 | // byte rate (sample rate * block align) 330 | view.setUint32(28, sampleRate * 2, true); 331 | 332 | // block align (channel count * bytes per sample) 333 | view.setUint16(32, numberOfAudioChannels * 2, true); 334 | 335 | // bits per sample 336 | view.setUint16(34, 16, true); 337 | 338 | // data sub-chunk 339 | // data chunk identifier 340 | writeUTFBytes(view, 36, 'data'); 341 | 342 | // data chunk length 343 | view.setUint32(40, interleavedLength * 2, true); 344 | 345 | // write the PCM samples 346 | var lng = interleavedLength; 347 | var index = 44; 348 | var volume = 1; 349 | for (var i = 0; i < lng; i++) { 350 | view.setInt16(index, interleaved[i] * (0x7FFF * volume), true); 351 | index += 2; 352 | } 353 | 354 | if (cb) { 355 | return cb({ 356 | buffer: buffer, 357 | view: view 358 | }); 359 | } 360 | 361 | postMessage({ 362 | buffer: buffer, 363 | view: view 364 | }); 365 | } 366 | 367 | var webWorker = processInWebWorker(mergeAudioBuffers); 368 | 369 | webWorker.onmessage = function(event) { 370 | callback(event.data.buffer, event.data.view); 371 | 372 | // release memory 373 | URL.revokeObjectURL(webWorker.workerURL); 374 | }; 375 | 376 | webWorker.postMessage(config); 377 | } 378 | 379 | function processInWebWorker(_function) { 380 | var workerURL = URL.createObjectURL(new Blob([_function.toString(), 381 | ';this.onmessage = function (e) {' + _function.name + '(e.data);}' 382 | ], { 383 | type: 'application/javascript' 384 | })); 385 | 386 | var worker = new Worker(workerURL); 387 | worker.workerURL = workerURL; 388 | return worker; 389 | } 390 | } --------------------------------------------------------------------------------