source: gs3-extensions/web-audio/trunk/js-mad/sink.js-master/sink-light.js@ 28388

Last change on this file since 28388 was 28388, checked in by davidb, 11 years ago

Set of JS, CSS, PNG etc web resources to support a mixture of audio player/document display capabilities

File size: 17.9 KB
Line 
1var 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*/
24function 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
38function SinkClass () {
39}
40
41Sink.SinkClass = SinkClass;
42
43SinkClass.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
135function 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
152Sink.sinks = Sink.devices = sinks;
153Sink.sinks.list = [];
154
155Sink.singleton = function () {
156 var sink = Sink.apply(null, arguments);
157
158 Sink.singleton = function () {
159 return sink;
160 };
161
162 return sink;
163};
164
165global.Sink = Sink;
166
167return Sink;
168
169}(function (){ return this; }());
170void function (Sink) {
171
172/**
173 * A light event emitter.
174 *
175 * @class
176 * @static Sink
177*/
178function 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
188EventEmitter.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
248Sink.EventEmitter = EventEmitter;
249
250EventEmitter.call(Sink);
251
252}(this.Sink);
253void 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
268Sink.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);
310void function (Sink) {
311
312var _Blob, _BlobBuilder, _URL, _btoa;
313
314void 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
341var createBlob = _Blob && _URL && function (content, type) {
342 return _URL.createObjectURL(new _Blob([content], { type: type }));
343};
344
345var 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
352var createData = _btoa && function (content, type) {
353 return 'data:' + type + ';base64,' + _btoa(content);
354};
355
356var createDynURL =
357 createBlob ||
358 createBlobBuilder ||
359 createData;
360
361if (!createDynURL) return;
362
363if (createBlob) createDynURL.createBlob = createBlob;
364if (createBlobBuilder) createDynURL.createBlobBuilder = createBlobBuilder;
365if (createData) createDynURL.createData = createData;
366
367if (_Blob) createDynURL.Blob = _Blob;
368if (_BlobBuilder) createDynURL.BlobBuilder = _BlobBuilder;
369if (_URL) createDynURL.URL = _URL;
370
371Sink.createDynURL = createDynURL;
372
373Sink.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);
382void 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
398function 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
412SinkError.prototype = new Error();
413
414SinkError.prototype.toString = function () {
415 return 'SinkError 0x' + this.code.toString(16) + ': ' + this.message;
416};
417
418SinkError[0x01] = {
419 message: 'No such error code.',
420 explanation: 'The error code does not exist.'
421};
422SinkError[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
427SinkError[0x10] = {
428 message: 'Buffer underflow.',
429 explanation: 'Trying to recover...'
430};
431SinkError[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};
435SinkError[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
440Sink.Error = SinkError;
441
442}(this.Sink);
443void 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
455var 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
465function 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
475function 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
517Sink.EventEmitter.call(inlineWorker);
518
519inlineWorker.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
559Sink.inlineWorker = inlineWorker;
560
561inlineWorker.test();
562
563}(this.Sink);
564 (function (sinks, fixChrome82795) {
565
566var AudioContext = typeof window === 'undefined' ? null : window.webkitAudioContext || window.AudioContext;
567
568/**
569 * A sink class for the Web Audio API
570*/
571
572sinks('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
638sinks.webkit = sinks.webaudio;
639
640sinks.webaudio.fix82795 = fixChrome82795;
641
642sinks.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, []));
654void function (Sink) {
655
656/**
657 * A Sink class for the Mozilla Audio Data API.
658*/
659
660Sink.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
747Sink.sinks.moz = Sink.sinks.audiodata;
748
749}(this.Sink);
Note: See TracBrowser for help on using the repository browser.