1 | var Sink = this.Sink = function (global) {
|
---|
2 |
|
---|
3 | /**
|
---|
4 | * Creates a Sink according to specified parameters, if possible.
|
---|
5 | *
|
---|
6 | * @class
|
---|
7 | *
|
---|
8 | * @arg =!readFn
|
---|
9 | * @arg =!channelCount
|
---|
10 | * @arg =!bufferSize
|
---|
11 | * @arg =!sampleRate
|
---|
12 | *
|
---|
13 | * @param {Function} readFn A callback to handle the buffer fills.
|
---|
14 | * @param {Number} channelCount Channel count.
|
---|
15 | * @param {Number} bufferSize (Optional) Specifies a pre-buffer size to control the amount of latency.
|
---|
16 | * @param {Number} sampleRate Sample rate (ms).
|
---|
17 | * @param {Number} default=0 writePosition Write position of the sink, as in how many samples have been written per channel.
|
---|
18 | * @param {String} default=async writeMode The default mode of writing to the sink.
|
---|
19 | * @param {String} default=interleaved channelMode The mode in which the sink asks the sample buffers to be channeled in.
|
---|
20 | * @param {Number} default=0 previousHit The previous time of a callback.
|
---|
21 | * @param {Buffer} default=null ringBuffer The ring buffer array of the sink. If null, ring buffering will not be applied.
|
---|
22 | * @param {Number} default=0 ringOffset The current position of the ring buffer.
|
---|
23 | */
|
---|
24 | function Sink (readFn, channelCount, bufferSize, sampleRate) {
|
---|
25 | var sinks = Sink.sinks.list,
|
---|
26 | i;
|
---|
27 | for (i=0; i<sinks.length; i++) {
|
---|
28 | if (sinks[i].enabled) {
|
---|
29 | try {
|
---|
30 | return new sinks[i](readFn, channelCount, bufferSize, sampleRate);
|
---|
31 | } catch(e1){}
|
---|
32 | }
|
---|
33 | }
|
---|
34 |
|
---|
35 | throw Sink.Error(0x02);
|
---|
36 | }
|
---|
37 |
|
---|
38 | function SinkClass () {
|
---|
39 | }
|
---|
40 |
|
---|
41 | Sink.SinkClass = SinkClass;
|
---|
42 |
|
---|
43 | SinkClass.prototype = Sink.prototype = {
|
---|
44 | sampleRate: 44100,
|
---|
45 | channelCount: 2,
|
---|
46 | bufferSize: 4096,
|
---|
47 |
|
---|
48 | writePosition: 0,
|
---|
49 | previousHit: 0,
|
---|
50 | ringOffset: 0,
|
---|
51 |
|
---|
52 | channelMode: 'interleaved',
|
---|
53 | isReady: false,
|
---|
54 |
|
---|
55 | /**
|
---|
56 | * Does the initialization of the sink.
|
---|
57 | * @method Sink
|
---|
58 | */
|
---|
59 | start: function (readFn, channelCount, bufferSize, sampleRate) {
|
---|
60 | this.channelCount = isNaN(channelCount) || channelCount === null ? this.channelCount: channelCount;
|
---|
61 | this.bufferSize = isNaN(bufferSize) || bufferSize === null ? this.bufferSize : bufferSize;
|
---|
62 | this.sampleRate = isNaN(sampleRate) || sampleRate === null ? this.sampleRate : sampleRate;
|
---|
63 | this.readFn = readFn;
|
---|
64 | this.activeRecordings = [];
|
---|
65 | this.previousHit = +new Date();
|
---|
66 | Sink.EventEmitter.call(this);
|
---|
67 | Sink.emit('init', [this].concat([].slice.call(arguments)));
|
---|
68 | },
|
---|
69 | /**
|
---|
70 | * The method which will handle all the different types of processing applied on a callback.
|
---|
71 | * @method Sink
|
---|
72 | */
|
---|
73 | process: function (soundData, channelCount) {
|
---|
74 | this.emit('preprocess', arguments);
|
---|
75 |
|
---|
76 | if (this.ringBuffer) {
|
---|
77 | (this.channelMode === 'interleaved' ? this.ringSpin : this.ringSpinInterleaved).apply(this, arguments);
|
---|
78 | }
|
---|
79 |
|
---|
80 | if (this.channelMode === 'interleaved') {
|
---|
81 | this.emit('audioprocess', arguments);
|
---|
82 |
|
---|
83 | if (this.readFn) {
|
---|
84 | this.readFn.apply(this, arguments);
|
---|
85 | }
|
---|
86 | } else {
|
---|
87 | var soundDataSplit = Sink.deinterleave(soundData, this.channelCount),
|
---|
88 | args = [soundDataSplit].concat([].slice.call(arguments, 1));
|
---|
89 | this.emit('audioprocess', args);
|
---|
90 |
|
---|
91 | if (this.readFn) {
|
---|
92 | this.readFn.apply(this, args);
|
---|
93 | }
|
---|
94 |
|
---|
95 | Sink.interleave(soundDataSplit, this.channelCount, soundData);
|
---|
96 | }
|
---|
97 | this.emit('postprocess', arguments);
|
---|
98 | this.previousHit = +new Date();
|
---|
99 | this.writePosition += soundData.length / channelCount;
|
---|
100 | },
|
---|
101 | /**
|
---|
102 | * Get the current output position, defaults to writePosition - bufferSize.
|
---|
103 | *
|
---|
104 | * @method Sink
|
---|
105 | *
|
---|
106 | * @return {Number} The position of the write head, in samples, per channel.
|
---|
107 | */
|
---|
108 | getPlaybackTime: function () {
|
---|
109 | return this.writePosition - this.bufferSize;
|
---|
110 | },
|
---|
111 | /**
|
---|
112 | * Internal method to send the ready signal if not ready yet.
|
---|
113 | * @method Sink
|
---|
114 | */
|
---|
115 | ready: function () {
|
---|
116 | if (this.isReady) return;
|
---|
117 |
|
---|
118 | this.isReady = true;
|
---|
119 | this.emit('ready', []);
|
---|
120 | }
|
---|
121 | };
|
---|
122 |
|
---|
123 | /**
|
---|
124 | * The container for all the available sinks. Also a decorator function for creating a new Sink class and binding it.
|
---|
125 | *
|
---|
126 | * @method Sink
|
---|
127 | * @static
|
---|
128 | *
|
---|
129 | * @arg {String} type The name / type of the Sink.
|
---|
130 | * @arg {Function} constructor The constructor function for the Sink.
|
---|
131 | * @arg {Object} prototype The prototype of the Sink. (optional)
|
---|
132 | * @arg {Boolean} disabled Whether the Sink should be disabled at first.
|
---|
133 | */
|
---|
134 |
|
---|
135 | function sinks (type, constructor, prototype, disabled, priority) {
|
---|
136 | prototype = prototype || constructor.prototype;
|
---|
137 | constructor.prototype = new Sink.SinkClass();
|
---|
138 | constructor.prototype.type = type;
|
---|
139 | constructor.enabled = !disabled;
|
---|
140 |
|
---|
141 | var k;
|
---|
142 | for (k in prototype) {
|
---|
143 | if (prototype.hasOwnProperty(k)) {
|
---|
144 | constructor.prototype[k] = prototype[k];
|
---|
145 | }
|
---|
146 | }
|
---|
147 |
|
---|
148 | sinks[type] = constructor;
|
---|
149 | sinks.list[priority ? 'unshift' : 'push'](constructor);
|
---|
150 | }
|
---|
151 |
|
---|
152 | Sink.sinks = Sink.devices = sinks;
|
---|
153 | Sink.sinks.list = [];
|
---|
154 |
|
---|
155 | Sink.singleton = function () {
|
---|
156 | var sink = Sink.apply(null, arguments);
|
---|
157 |
|
---|
158 | Sink.singleton = function () {
|
---|
159 | return sink;
|
---|
160 | };
|
---|
161 |
|
---|
162 | return sink;
|
---|
163 | };
|
---|
164 |
|
---|
165 | global.Sink = Sink;
|
---|
166 |
|
---|
167 | return Sink;
|
---|
168 |
|
---|
169 | }(function (){ return this; }());
|
---|
170 | void function (Sink) {
|
---|
171 |
|
---|
172 | /**
|
---|
173 | * A light event emitter.
|
---|
174 | *
|
---|
175 | * @class
|
---|
176 | * @static Sink
|
---|
177 | */
|
---|
178 | function EventEmitter () {
|
---|
179 | var k;
|
---|
180 | for (k in EventEmitter.prototype) {
|
---|
181 | if (EventEmitter.prototype.hasOwnProperty(k)) {
|
---|
182 | this[k] = EventEmitter.prototype[k];
|
---|
183 | }
|
---|
184 | }
|
---|
185 | this._listeners = {};
|
---|
186 | }
|
---|
187 |
|
---|
188 | EventEmitter.prototype = {
|
---|
189 | _listeners: null,
|
---|
190 | /**
|
---|
191 | * Emits an event.
|
---|
192 | *
|
---|
193 | * @method EventEmitter
|
---|
194 | *
|
---|
195 | * @arg {String} name The name of the event to emit.
|
---|
196 | * @arg {Array} args The arguments to pass to the event handlers.
|
---|
197 | */
|
---|
198 | emit: function (name, args) {
|
---|
199 | if (this._listeners[name]) {
|
---|
200 | for (var i=0; i<this._listeners[name].length; i++) {
|
---|
201 | this._listeners[name][i].apply(this, args);
|
---|
202 | }
|
---|
203 | }
|
---|
204 | return this;
|
---|
205 | },
|
---|
206 | /**
|
---|
207 | * Adds an event listener to an event.
|
---|
208 | *
|
---|
209 | * @method EventEmitter
|
---|
210 | *
|
---|
211 | * @arg {String} name The name of the event.
|
---|
212 | * @arg {Function} listener The event listener to attach to the event.
|
---|
213 | */
|
---|
214 | on: function (name, listener) {
|
---|
215 | this._listeners[name] = this._listeners[name] || [];
|
---|
216 | this._listeners[name].push(listener);
|
---|
217 | return this;
|
---|
218 | },
|
---|
219 | /**
|
---|
220 | * Adds an event listener to an event.
|
---|
221 | *
|
---|
222 | * @method EventEmitter
|
---|
223 | *
|
---|
224 | * @arg {String} name The name of the event.
|
---|
225 | * @arg {Function} !listener The event listener to remove from the event. If not specified, will delete all.
|
---|
226 | */
|
---|
227 | off: function (name, listener) {
|
---|
228 | if (this._listeners[name]) {
|
---|
229 | if (!listener) {
|
---|
230 | delete this._listeners[name];
|
---|
231 | return this;
|
---|
232 | }
|
---|
233 |
|
---|
234 | for (var i=0; i<this._listeners[name].length; i++) {
|
---|
235 | if (this._listeners[name][i] === listener) {
|
---|
236 | this._listeners[name].splice(i--, 1);
|
---|
237 | }
|
---|
238 | }
|
---|
239 |
|
---|
240 | if (!this._listeners[name].length) {
|
---|
241 | delete this._listeners[name];
|
---|
242 | }
|
---|
243 | }
|
---|
244 | return this;
|
---|
245 | }
|
---|
246 | };
|
---|
247 |
|
---|
248 | Sink.EventEmitter = EventEmitter;
|
---|
249 |
|
---|
250 | EventEmitter.call(Sink);
|
---|
251 |
|
---|
252 | }(this.Sink);
|
---|
253 | void function (Sink) {
|
---|
254 |
|
---|
255 | /**
|
---|
256 | * Creates a timer with consistent (ie. not clamped) intervals even in background tabs.
|
---|
257 | * Uses inline workers to achieve this. If not available, will revert to regular timers.
|
---|
258 | *
|
---|
259 | * @static Sink
|
---|
260 | * @name doInterval
|
---|
261 | *
|
---|
262 | * @arg {Function} callback The callback to trigger on timer hit.
|
---|
263 | * @arg {Number} timeout The interval between timer hits.
|
---|
264 | *
|
---|
265 | * @return {Function} A function to cancel the timer.
|
---|
266 | */
|
---|
267 |
|
---|
268 | Sink.doInterval = function (callback, timeout) {
|
---|
269 | var timer, kill;
|
---|
270 |
|
---|
271 | function create (noWorker) {
|
---|
272 | if (Sink.inlineWorker.working && !noWorker) {
|
---|
273 | timer = Sink.inlineWorker('setInterval(function (){ postMessage("tic"); }, ' + timeout + ');');
|
---|
274 | timer.onmessage = function (){
|
---|
275 | callback();
|
---|
276 | };
|
---|
277 | kill = function () {
|
---|
278 | timer.terminate();
|
---|
279 | };
|
---|
280 | } else {
|
---|
281 | timer = setInterval(callback, timeout);
|
---|
282 | kill = function (){
|
---|
283 | clearInterval(timer);
|
---|
284 | };
|
---|
285 | }
|
---|
286 | }
|
---|
287 |
|
---|
288 | if (Sink.inlineWorker.ready) {
|
---|
289 | create();
|
---|
290 | } else {
|
---|
291 | Sink.inlineWorker.on('ready', function () {
|
---|
292 | create();
|
---|
293 | });
|
---|
294 | }
|
---|
295 |
|
---|
296 | return function () {
|
---|
297 | if (!kill) {
|
---|
298 | if (!Sink.inlineWorker.ready) {
|
---|
299 | Sink.inlineWorker.on('ready', function () {
|
---|
300 | if (kill) kill();
|
---|
301 | });
|
---|
302 | }
|
---|
303 | } else {
|
---|
304 | kill();
|
---|
305 | }
|
---|
306 | };
|
---|
307 | };
|
---|
308 |
|
---|
309 | }(this.Sink);
|
---|
310 | void function (Sink) {
|
---|
311 |
|
---|
312 | var _Blob, _BlobBuilder, _URL, _btoa;
|
---|
313 |
|
---|
314 | void function (prefixes, urlPrefixes) {
|
---|
315 | function find (name, prefixes) {
|
---|
316 | var b, a = prefixes.slice();
|
---|
317 |
|
---|
318 | for (b=a.shift(); typeof b !== 'undefined'; b=a.shift()) {
|
---|
319 | b = Function('return typeof ' + b + name +
|
---|
320 | '=== "undefined" ? undefined : ' +
|
---|
321 | b + name)();
|
---|
322 |
|
---|
323 | if (b) return b;
|
---|
324 | }
|
---|
325 | }
|
---|
326 |
|
---|
327 | _Blob = find('Blob', prefixes);
|
---|
328 | _BlobBuilder = find('BlobBuilder', prefixes);
|
---|
329 | _URL = find('URL', urlPrefixes);
|
---|
330 | _btoa = find('btoa', ['']);
|
---|
331 | }([
|
---|
332 | '',
|
---|
333 | 'Moz',
|
---|
334 | 'WebKit',
|
---|
335 | 'MS'
|
---|
336 | ], [
|
---|
337 | '',
|
---|
338 | 'webkit'
|
---|
339 | ]);
|
---|
340 |
|
---|
341 | var createBlob = _Blob && _URL && function (content, type) {
|
---|
342 | return _URL.createObjectURL(new _Blob([content], { type: type }));
|
---|
343 | };
|
---|
344 |
|
---|
345 | var createBlobBuilder = _BlobBuilder && _URL && function (content, type) {
|
---|
346 | var bb = new _BlobBuilder();
|
---|
347 | bb.append(content);
|
---|
348 |
|
---|
349 | return _URL.createObjectURL(bb.getBlob(type));
|
---|
350 | };
|
---|
351 |
|
---|
352 | var createData = _btoa && function (content, type) {
|
---|
353 | return 'data:' + type + ';base64,' + _btoa(content);
|
---|
354 | };
|
---|
355 |
|
---|
356 | var createDynURL =
|
---|
357 | createBlob ||
|
---|
358 | createBlobBuilder ||
|
---|
359 | createData;
|
---|
360 |
|
---|
361 | if (!createDynURL) return;
|
---|
362 |
|
---|
363 | if (createBlob) createDynURL.createBlob = createBlob;
|
---|
364 | if (createBlobBuilder) createDynURL.createBlobBuilder = createBlobBuilder;
|
---|
365 | if (createData) createDynURL.createData = createData;
|
---|
366 |
|
---|
367 | if (_Blob) createDynURL.Blob = _Blob;
|
---|
368 | if (_BlobBuilder) createDynURL.BlobBuilder = _BlobBuilder;
|
---|
369 | if (_URL) createDynURL.URL = _URL;
|
---|
370 |
|
---|
371 | Sink.createDynURL = createDynURL;
|
---|
372 |
|
---|
373 | Sink.revokeDynURL = function (url) {
|
---|
374 | if (typeof url === 'string' && url.indexOf('data:') === 0) {
|
---|
375 | return false;
|
---|
376 | } else {
|
---|
377 | return _URL.revokeObjectURL(url);
|
---|
378 | }
|
---|
379 | };
|
---|
380 |
|
---|
381 | }(this.Sink);
|
---|
382 | void function (Sink) {
|
---|
383 |
|
---|
384 | /*
|
---|
385 | * A Sink-specific error class.
|
---|
386 | *
|
---|
387 | * @class
|
---|
388 | * @static Sink
|
---|
389 | * @name Error
|
---|
390 | *
|
---|
391 | * @arg =code
|
---|
392 | *
|
---|
393 | * @param {Number} code The error code.
|
---|
394 | * @param {String} message A brief description of the error.
|
---|
395 | * @param {String} explanation A more verbose explanation of why the error occured and how to fix.
|
---|
396 | */
|
---|
397 |
|
---|
398 | function SinkError(code) {
|
---|
399 | if (!SinkError.hasOwnProperty(code)) throw SinkError(1);
|
---|
400 | if (!(this instanceof SinkError)) return new SinkError(code);
|
---|
401 |
|
---|
402 | var k;
|
---|
403 | for (k in SinkError[code]) {
|
---|
404 | if (SinkError[code].hasOwnProperty(k)) {
|
---|
405 | this[k] = SinkError[code][k];
|
---|
406 | }
|
---|
407 | }
|
---|
408 |
|
---|
409 | this.code = code;
|
---|
410 | }
|
---|
411 |
|
---|
412 | SinkError.prototype = new Error();
|
---|
413 |
|
---|
414 | SinkError.prototype.toString = function () {
|
---|
415 | return 'SinkError 0x' + this.code.toString(16) + ': ' + this.message;
|
---|
416 | };
|
---|
417 |
|
---|
418 | SinkError[0x01] = {
|
---|
419 | message: 'No such error code.',
|
---|
420 | explanation: 'The error code does not exist.'
|
---|
421 | };
|
---|
422 | SinkError[0x02] = {
|
---|
423 | message: 'No audio sink available.',
|
---|
424 | explanation: 'The audio device may be busy, or no supported output API is available for this browser.'
|
---|
425 | };
|
---|
426 |
|
---|
427 | SinkError[0x10] = {
|
---|
428 | message: 'Buffer underflow.',
|
---|
429 | explanation: 'Trying to recover...'
|
---|
430 | };
|
---|
431 | SinkError[0x11] = {
|
---|
432 | message: 'Critical recovery fail.',
|
---|
433 | explanation: 'The buffer underflow has reached a critical point, trying to recover, but will probably fail anyway.'
|
---|
434 | };
|
---|
435 | SinkError[0x12] = {
|
---|
436 | message: 'Buffer size too large.',
|
---|
437 | explanation: 'Unable to allocate the buffer due to excessive length, please try a smaller buffer. Buffer size should probably be smaller than the sample rate.'
|
---|
438 | };
|
---|
439 |
|
---|
440 | Sink.Error = SinkError;
|
---|
441 |
|
---|
442 | }(this.Sink);
|
---|
443 | void function (Sink) {
|
---|
444 |
|
---|
445 | /**
|
---|
446 | * Creates an inline worker using a data/blob URL, if possible.
|
---|
447 | *
|
---|
448 | * @static Sink
|
---|
449 | *
|
---|
450 | * @arg {String} script
|
---|
451 | *
|
---|
452 | * @return {Worker} A web worker, or null if impossible to create.
|
---|
453 | */
|
---|
454 |
|
---|
455 | var define = Object.defineProperty ? function (obj, name, value) {
|
---|
456 | Object.defineProperty(obj, name, {
|
---|
457 | value: value,
|
---|
458 | configurable: true,
|
---|
459 | writable: true
|
---|
460 | });
|
---|
461 | } : function (obj, name, value) {
|
---|
462 | obj[name] = value;
|
---|
463 | };
|
---|
464 |
|
---|
465 | function terminate () {
|
---|
466 | define(this, 'terminate', this._terminate);
|
---|
467 |
|
---|
468 | Sink.revokeDynURL(this._url);
|
---|
469 |
|
---|
470 | delete this._url;
|
---|
471 | delete this._terminate;
|
---|
472 | return this.terminate();
|
---|
473 | }
|
---|
474 |
|
---|
475 | function inlineWorker (script) {
|
---|
476 | function wrap (type, content, typeName) {
|
---|
477 | try {
|
---|
478 | var url = type(content, 'text/javascript');
|
---|
479 | var worker = new Worker(url);
|
---|
480 |
|
---|
481 | define(worker, '_url', url);
|
---|
482 | define(worker, '_terminate', worker.terminate);
|
---|
483 | define(worker, 'terminate', terminate);
|
---|
484 |
|
---|
485 | if (inlineWorker.type) return worker;
|
---|
486 |
|
---|
487 | inlineWorker.type = typeName;
|
---|
488 | inlineWorker.createURL = type;
|
---|
489 |
|
---|
490 | return worker;
|
---|
491 | } catch (e) {
|
---|
492 | return null;
|
---|
493 | }
|
---|
494 | }
|
---|
495 |
|
---|
496 | var createDynURL = Sink.createDynURL;
|
---|
497 |
|
---|
498 | if (!createDynURL) return null;
|
---|
499 |
|
---|
500 | var worker;
|
---|
501 |
|
---|
502 | if (inlineWorker.createURL) {
|
---|
503 | return wrap(inlineWorker.createURL, script, inlineWorker.type);
|
---|
504 | }
|
---|
505 |
|
---|
506 | worker = wrap(createDynURL.createBlob, script, 'blob');
|
---|
507 | if (worker) return worker;
|
---|
508 |
|
---|
509 | worker = wrap(createDynURL.createBlobBuilder, script, 'blobbuilder');
|
---|
510 | if (worker) return worker;
|
---|
511 |
|
---|
512 | worker = wrap(createDynURL.createData, script, 'data');
|
---|
513 |
|
---|
514 | return worker;
|
---|
515 | }
|
---|
516 |
|
---|
517 | Sink.EventEmitter.call(inlineWorker);
|
---|
518 |
|
---|
519 | inlineWorker.test = function () {
|
---|
520 | inlineWorker.ready = inlineWorker.working = false;
|
---|
521 | inlineWorker.type = '';
|
---|
522 | inlineWorker.createURL = null;
|
---|
523 |
|
---|
524 | var worker = inlineWorker('this.onmessage=function(e){postMessage(e.data)}');
|
---|
525 | var data = 'inlineWorker';
|
---|
526 |
|
---|
527 | function ready (success) {
|
---|
528 | if (inlineWorker.ready) return;
|
---|
529 |
|
---|
530 | inlineWorker.ready = true;
|
---|
531 | inlineWorker.working = success;
|
---|
532 | inlineWorker.emit('ready', [success]);
|
---|
533 | inlineWorker.off('ready');
|
---|
534 |
|
---|
535 | if (success && worker) {
|
---|
536 | worker.terminate();
|
---|
537 | }
|
---|
538 |
|
---|
539 | worker = null;
|
---|
540 | }
|
---|
541 |
|
---|
542 | if (!worker) {
|
---|
543 | setTimeout(function () {
|
---|
544 | ready(false);
|
---|
545 | }, 0);
|
---|
546 | } else {
|
---|
547 | worker.onmessage = function (e) {
|
---|
548 | ready(e.data === data);
|
---|
549 | };
|
---|
550 |
|
---|
551 | worker.postMessage(data);
|
---|
552 |
|
---|
553 | setTimeout(function () {
|
---|
554 | ready(false);
|
---|
555 | }, 1000);
|
---|
556 | }
|
---|
557 | };
|
---|
558 |
|
---|
559 | Sink.inlineWorker = inlineWorker;
|
---|
560 |
|
---|
561 | inlineWorker.test();
|
---|
562 |
|
---|
563 | }(this.Sink);
|
---|
564 | (function (sinks, fixChrome82795) {
|
---|
565 |
|
---|
566 | var AudioContext = typeof window === 'undefined' ? null : window.webkitAudioContext || window.AudioContext;
|
---|
567 |
|
---|
568 | /**
|
---|
569 | * A sink class for the Web Audio API
|
---|
570 | */
|
---|
571 |
|
---|
572 | sinks('webaudio', function (readFn, channelCount, bufferSize, sampleRate) {
|
---|
573 | var self = this,
|
---|
574 | context = sinks.webaudio.getContext(),
|
---|
575 | node = null,
|
---|
576 | soundData = null,
|
---|
577 | zeroBuffer = null;
|
---|
578 | self.start.apply(self, arguments);
|
---|
579 | node = context.createJavaScriptNode(self.bufferSize, self.channelCount, self.channelCount);
|
---|
580 |
|
---|
581 | function bufferFill(e) {
|
---|
582 | var outputBuffer = e.outputBuffer,
|
---|
583 | channelCount = outputBuffer.numberOfChannels,
|
---|
584 | i, n, l = outputBuffer.length,
|
---|
585 | size = outputBuffer.size,
|
---|
586 | channels = new Array(channelCount),
|
---|
587 | tail;
|
---|
588 |
|
---|
589 | self.ready();
|
---|
590 |
|
---|
591 | soundData = soundData && soundData.length === l * channelCount ? soundData : new Float32Array(l * channelCount);
|
---|
592 | zeroBuffer = zeroBuffer && zeroBuffer.length === soundData.length ? zeroBuffer : new Float32Array(l * channelCount);
|
---|
593 | soundData.set(zeroBuffer);
|
---|
594 |
|
---|
595 | for (i=0; i<channelCount; i++) {
|
---|
596 | channels[i] = outputBuffer.getChannelData(i);
|
---|
597 | }
|
---|
598 |
|
---|
599 | self.process(soundData, self.channelCount);
|
---|
600 |
|
---|
601 | for (i=0; i<l; i++) {
|
---|
602 | for (n=0; n < channelCount; n++) {
|
---|
603 | channels[n][i] = soundData[i * self.channelCount + n];
|
---|
604 | }
|
---|
605 | }
|
---|
606 | }
|
---|
607 |
|
---|
608 | self.sampleRate = context.sampleRate;
|
---|
609 |
|
---|
610 | node.onaudioprocess = bufferFill;
|
---|
611 | node.connect(context.destination);
|
---|
612 |
|
---|
613 | self._context = context;
|
---|
614 | self._node = node;
|
---|
615 | self._callback = bufferFill;
|
---|
616 | /* Keep references in order to avoid garbage collection removing the listeners, working around http://code.google.com/p/chromium/issues/detail?id=82795 */
|
---|
617 | // Thanks to @baffo32
|
---|
618 | fixChrome82795.push(node);
|
---|
619 | }, {
|
---|
620 | kill: function () {
|
---|
621 | this._node.disconnect(0);
|
---|
622 |
|
---|
623 | for (var i=0; i<fixChrome82795.length; i++) {
|
---|
624 | if (fixChrome82795[i] === this._node) {
|
---|
625 | fixChrome82795.splice(i--, 1);
|
---|
626 | }
|
---|
627 | }
|
---|
628 |
|
---|
629 | this._node = this._context = null;
|
---|
630 | this.emit('kill');
|
---|
631 | },
|
---|
632 |
|
---|
633 | getPlaybackTime: function () {
|
---|
634 | return this._context.currentTime * this.sampleRate;
|
---|
635 | }
|
---|
636 | }, false, true);
|
---|
637 |
|
---|
638 | sinks.webkit = sinks.webaudio;
|
---|
639 |
|
---|
640 | sinks.webaudio.fix82795 = fixChrome82795;
|
---|
641 |
|
---|
642 | sinks.webaudio.getContext = function () {
|
---|
643 | // For now, we have to accept that the AudioContext is at 48000Hz, or whatever it decides.
|
---|
644 | var context = new AudioContext(/*sampleRate*/);
|
---|
645 |
|
---|
646 | sinks.webaudio.getContext = function () {
|
---|
647 | return context;
|
---|
648 | };
|
---|
649 |
|
---|
650 | return context;
|
---|
651 | };
|
---|
652 |
|
---|
653 | }(this.Sink.sinks, []));
|
---|
654 | void function (Sink) {
|
---|
655 |
|
---|
656 | /**
|
---|
657 | * A Sink class for the Mozilla Audio Data API.
|
---|
658 | */
|
---|
659 |
|
---|
660 | Sink.sinks('audiodata', function () {
|
---|
661 | var self = this,
|
---|
662 | currentWritePosition = 0,
|
---|
663 | tail = null,
|
---|
664 | audioDevice = new Audio(),
|
---|
665 | written, currentPosition, available, soundData, prevPos,
|
---|
666 | timer; // Fix for https://bugzilla.mozilla.org/show_bug.cgi?id=630117
|
---|
667 | self.start.apply(self, arguments);
|
---|
668 | self.preBufferSize = isNaN(arguments[4]) || arguments[4] === null ? this.preBufferSize : arguments[4];
|
---|
669 |
|
---|
670 | function bufferFill() {
|
---|
671 | if (tail) {
|
---|
672 | written = audioDevice.mozWriteAudio(tail);
|
---|
673 | currentWritePosition += written;
|
---|
674 | if (written < tail.length){
|
---|
675 | tail = tail.subarray(written);
|
---|
676 | return tail;
|
---|
677 | }
|
---|
678 | tail = null;
|
---|
679 | }
|
---|
680 |
|
---|
681 | currentPosition = audioDevice.mozCurrentSampleOffset();
|
---|
682 | available = Number(currentPosition + (prevPos !== currentPosition ? self.bufferSize : self.preBufferSize) * self.channelCount - currentWritePosition);
|
---|
683 |
|
---|
684 | if (currentPosition === prevPos) {
|
---|
685 | self.emit('error', [Sink.Error(0x10)]);
|
---|
686 | }
|
---|
687 |
|
---|
688 | if (available > 0 || prevPos === currentPosition){
|
---|
689 | self.ready();
|
---|
690 |
|
---|
691 | try {
|
---|
692 | soundData = new Float32Array(prevPos === currentPosition ? self.preBufferSize * self.channelCount :
|
---|
693 | self.forceBufferSize ? available < self.bufferSize * 2 ? self.bufferSize * 2 : available : available);
|
---|
694 | } catch(e) {
|
---|
695 | self.emit('error', [Sink.Error(0x12)]);
|
---|
696 | self.kill();
|
---|
697 | return;
|
---|
698 | }
|
---|
699 | self.process(soundData, self.channelCount);
|
---|
700 | written = self._audio.mozWriteAudio(soundData);
|
---|
701 | if (written < soundData.length){
|
---|
702 | tail = soundData.subarray(written);
|
---|
703 | }
|
---|
704 | currentWritePosition += written;
|
---|
705 | }
|
---|
706 | prevPos = currentPosition;
|
---|
707 | }
|
---|
708 |
|
---|
709 | audioDevice.mozSetup(self.channelCount, self.sampleRate);
|
---|
710 |
|
---|
711 | this._timers = [];
|
---|
712 |
|
---|
713 | this._timers.push(Sink.doInterval(function () {
|
---|
714 | // Check for complete death of the output
|
---|
715 | if (+new Date() - self.previousHit > 2000) {
|
---|
716 | self._audio = audioDevice = new Audio();
|
---|
717 | audioDevice.mozSetup(self.channelCount, self.sampleRate);
|
---|
718 | currentWritePosition = 0;
|
---|
719 | self.emit('error', [Sink.Error(0x11)]);
|
---|
720 | }
|
---|
721 | }, 1000));
|
---|
722 |
|
---|
723 | this._timers.push(Sink.doInterval(bufferFill, self.interval));
|
---|
724 |
|
---|
725 | self._bufferFill = bufferFill;
|
---|
726 | self._audio = audioDevice;
|
---|
727 | }, {
|
---|
728 | // These are somewhat safe values...
|
---|
729 | bufferSize: 24576,
|
---|
730 | preBufferSize: 24576,
|
---|
731 | forceBufferSize: false,
|
---|
732 | interval: 100,
|
---|
733 |
|
---|
734 | kill: function () {
|
---|
735 | while (this._timers.length) {
|
---|
736 | this._timers.shift()();
|
---|
737 | }
|
---|
738 |
|
---|
739 | this.emit('kill');
|
---|
740 | },
|
---|
741 |
|
---|
742 | getPlaybackTime: function () {
|
---|
743 | return this._audio.mozCurrentSampleOffset() / this.channelCount;
|
---|
744 | }
|
---|
745 | }, false, true);
|
---|
746 |
|
---|
747 | Sink.sinks.moz = Sink.sinks.audiodata;
|
---|
748 |
|
---|
749 | }(this.Sink);
|
---|