1 /**
  2  * Tutorials:
  3  * http://www.html5rocks.com/en/tutorials/webaudio/games/
  4  * http://www.html5rocks.com/en/tutorials/webaudio/positional_audio/ <- +1 as it is three.js
  5  * http://www.html5rocks.com/en/tutorials/webaudio/intro/
  6  *
  7  * Spec:
  8  * https://dvcs.w3.org/hg/audio/raw-file/tip/webaudio/specification.html
  9  *
 10  * Chromium Demo:
 11  * http://chromium.googlecode.com/svn/trunk/samples/audio/index.html  <- running page
 12  * http://code.google.com/p/chromium/source/browse/trunk/samples/audio/ <- source
 13 */
 14 
 15 
 16 
 17 
 18 //////////////////////////////////////////////////////////////////////////////////
 19 //////////////////////////////////////////////////////////////////////////////////
 20 //////////////////////////////////////////////////////////////////////////////////
 21 //		tQuery.World.* WebAudio						//
 22 //////////////////////////////////////////////////////////////////////////////////
 23 //////////////////////////////////////////////////////////////////////////////////
 24 //////////////////////////////////////////////////////////////////////////////////
 25 
 26 
 27 
 28 tQuery.World.register('enableWebAudio', function(){
 29 	// sanity check
 30 	console.assert( this.hasWebAudio() === false, "there is already a webaudio" );
 31 	// intenciate a tQuery.World.WebAudio
 32 	var webaudio	= new tQuery.WebAudio(this);
 33 	// store webaudio in the world
 34 	tQuery.data(this, "webaudio", webaudio);
 35 	// for chained API
 36 	return this;
 37 });
 38 
 39 tQuery.World.register('disabledWebAudio', function(){
 40 	if( this.hasWebAudio() === false )	return this;
 41 	var webaudio	= tQuery.data(this, "webaudio");
 42 	webaudio.destroy();
 43 	tQuery.removeData(this, "webaudio");
 44 	return this;	// for chained API
 45 });
 46 
 47 tQuery.World.register('getWebAudio', function(){
 48 	var webaudio	= tQuery.data(this, "webaudio");
 49 	return webaudio;
 50 });
 51 
 52 tQuery.World.register('hasWebAudio', function(){
 53 	var webaudio	= tQuery.data(this, "webaudio");
 54 	return webaudio ? true : false;
 55 });
 56 
 57 tQuery.World.register('supportWebAudio', function(){
 58 	return tQuery.WebAudio.isAvailable;
 59 });
 60 
 61 
 62 //////////////////////////////////////////////////////////////////////////////////
 63 //////////////////////////////////////////////////////////////////////////////////
 64 //////////////////////////////////////////////////////////////////////////////////
 65 //		tQuery.WebAudio							//
 66 //////////////////////////////////////////////////////////////////////////////////
 67 //////////////////////////////////////////////////////////////////////////////////
 68 //////////////////////////////////////////////////////////////////////////////////
 69 
 70 
 71 /**
 72  * Main class to handle webkit audio
 73  * 
 74  * TODO make the clip detector from http://www.html5rocks.com/en/tutorials/webaudio/games/
 75  *
 76  * @class Handle webkit audio API
 77  *
 78  * @param {tQuery.World} [world] the world on which to run 
 79 */
 80 tQuery.WebAudio	= function(world){
 81 	// sanity check - the api MUST be available
 82 	console.assert(tQuery.WebAudio.isAvailable === true, 'webkitAudioContext isnt available on your browser');
 83 	
 84 	// handle parameter
 85 	this._world	= world ? world : tQuery.world;
 86 
 87 	// hook this._updateCb to this.world.loop()
 88 	this._updateCb	= function(deltaTime){
 89 		this.updateListener(this._world.camera(), deltaTime)
 90 	}.bind(this);
 91 	this._world.loop().hook(this._updateCb);
 92 
 93 	// create the context
 94 	this._ctx	= new webkitAudioContext();
 95 
 96 	// setup the end of the node chain
 97 	// TODO later code the clipping detection from http://www.html5rocks.com/en/tutorials/webaudio/games/ 
 98 	this._gainNode	= this._ctx.createGainNode();
 99 	this._compressor= this._ctx.createDynamicsCompressor();
100 	this._gainNode.connect( this._compressor );
101 	this._compressor.connect( this._ctx.destination );	
102 };
103 
104 tQuery.WebAudio.prototype.destroy	= function(){
105 	// unhook this._updateCb from this.world.loop()
106 	this._world.loop().unhook(this._updateCb);
107 	this._updateCb	= null;
108 };
109 
110 /**
111  * 
112  *
113  * @return {Boolean} true if it is available or not
114 */
115 tQuery.WebAudio.isAvailable	= window.webkitAudioContext ? true : false;
116 
117 //////////////////////////////////////////////////////////////////////////////////
118 //										//
119 //////////////////////////////////////////////////////////////////////////////////
120 
121 /**
122  * get the audio context
123  *
124  * @returns {AudioContext} the audio context
125 */
126 tQuery.WebAudio.prototype.context	= function(){
127 	return this._ctx;
128 };
129 
130 /**
131  * return the entry node in the master node chains
132 */
133 tQuery.WebAudio.prototype._entryNode	= function(){
134 	//return this._ctx.destination;
135 	return this._gainNode;
136 }
137 
138 /**
139  * getter/setter on the volume
140  * 
141 */
142 tQuery.WebAudio.prototype.world		= function(value){
143 	if( value === undefined )	return this._world;
144 	this._world	= value;
145 	return this;
146 };
147 
148 /**
149  * getter/setter on the volume
150 */
151 tQuery.WebAudio.prototype.volume	= function(value){
152 	if( value === undefined )	return this._gainNode.gain.value;
153 	this._gainNode.gain.value	= value;
154 	return this;
155 };
156 
157 tQuery.WebAudio.prototype.updateListener	= function(object3d, deltaTime){
158 	var context	= this._ctx;
159 	// sanity check on parameters
160 	console.assert( object3d instanceof THREE.Object3D );
161 	console.assert( typeof(deltaTime) === 'number' );
162 
163 	// ensure object3d.matrixWorld is up to date
164 	object3d.updateMatrixWorld();
165 	
166 	////////////////////////////////////////////////////////////////////////
167 	// set position
168 	var position	= object3d.matrixWorld.getPosition();
169 	context.listener.setPosition(position.x, position.y, position.z);
170 
171 	////////////////////////////////////////////////////////////////////////
172 	// set orientation
173 	var mOrientation= object3d.matrixWorld.clone();
174 	// zero the translation
175 	mOrientation.setPosition({x : 0, y: 0, z: 0});
176 	// Compute Front vector: Multiply the 0,0,1 vector by the world matrix and normalize the result.
177 	var vFront= new THREE.Vector3(0,0,1);
178 	mOrientation.multiplyVector3(vFront);
179 	vFront.normalize();
180 	// Compute UP vector: Multiply the 0,-1,0 vector by the world matrix and normalize the result.
181 	var vUp= new THREE.Vector3(0,-1, 0);
182 	mOrientation.multiplyVector3(vUp);
183 	vUp.normalize();
184 	// Set panner orientation
185 	context.listener.setOrientation(vFront.x, vFront.y, vFront.z, vUp.x, vUp.y, vUp.z);
186 
187 	////////////////////////////////////////////////////////////////////////
188 	// set velocity
189 	if( this._prevPos === undefined ){
190 		this._prevPos	= object3d.matrixWorld.getPosition().clone();
191 	}else{
192 		var position	= object3d.matrixWorld.getPosition();
193 		var velocity	= position.clone().subSelf(this._prevPos).divideScalar(deltaTime);
194 		this._prevPos	= object3d.matrixWorld.getPosition().clone();
195 		context.listener.setVelocity(velocity.x, velocity.y, velocity.z);
196 	}
197 }
198 
199 
200 
201 //////////////////////////////////////////////////////////////////////////////////
202 //////////////////////////////////////////////////////////////////////////////////
203 //////////////////////////////////////////////////////////////////////////////////
204 //		tQuery.WebAudio.NodeChainBuilder				//
205 //////////////////////////////////////////////////////////////////////////////////
206 //////////////////////////////////////////////////////////////////////////////////
207 //////////////////////////////////////////////////////////////////////////////////
208 
209 /**
210  * Constructor
211  *
212  * @class builder to generate nodes chains. Used in tQuery.WebAudio.Sound
213  * @param {webkitAudioContext} audioContext the audio context
214 */
215 tQuery.WebAudio.NodeChainBuilder	= function(audioContext){
216 	console.assert( audioContext instanceof webkitAudioContext );
217 	this._context	= audioContext;
218 	this._firstNode	= null;
219 	this._lastNode	= null;
220 	this._nodes	= {};
221 };
222 
223 /**
224  * destructor
225 */
226 tQuery.WebAudio.NodeChainBuilder.prototype.destroy	= function(){
227 };
228 
229 /**
230  * getter for the nodes
231 */
232 tQuery.WebAudio.NodeChainBuilder.prototype.nodes	= function(){
233 	return this._nodes;
234 }
235 
236 /**
237  * @returns the first node of the chain
238 */
239 tQuery.WebAudio.NodeChainBuilder.prototype.first	= function(){
240 	return this._firstNode;
241 }
242 
243 /**
244  * @returns the last node of the chain
245 */
246 tQuery.WebAudio.NodeChainBuilder.prototype.last	= function(){
247 	return this._lastNode;
248 }
249 
250 tQuery.WebAudio.NodeChainBuilder.prototype._addNode	= function(node, properties)
251 {
252 	// update this._bufferSourceDst - needed for .cloneBufferSource()
253 	var lastIsBufferSource	= this._lastNode && ('playbackRate' in this._lastNode) ? true : false;
254 	if( lastIsBufferSource )	this._bufferSourceDst	= node;
255 
256 	// connect this._lastNode to node if suitable
257 	if( this._lastNode !== null )	this._lastNode.connect(node);
258 	
259 	// update this._firstNode && this._lastNode
260 	if( this._firstNode === null )	this._firstNode	= node;
261 	this._lastNode	= node;
262 		
263 	// apply properties to the node
264 	for( var property in properties ){
265 		node[property]	= properties[property];
266 	}
267 
268 	// for chained API
269 	return this;
270 };
271 
272 
273 /**
274  * Clone the bufferSource. Used just before playing a sound
275  * @returns {AudioBufferSourceNode} the clone AudioBufferSourceNode
276 */
277 tQuery.WebAudio.NodeChainBuilder.prototype.cloneBufferSource	= function(){
278 	console.assert(this._nodes.bufferSource, "no buffersource presents. Add one.");
279 	var orig	= this._nodes.bufferSource;
280 	var clone	= this._context.createBufferSource()
281 	clone.buffer		= orig.buffer;
282 	clone.playbackRate	= orig.playbackRate;
283 	clone.loop		= orig.loop;
284 	clone.connect(this._bufferSourceDst);
285 	return clone;
286 }
287 
288 /**
289  * add a bufferSource
290  *
291  * @param {Object} [properties] properties to set in the created node
292 */
293 tQuery.WebAudio.NodeChainBuilder.prototype.bufferSource	= function(properties){
294 	var node		= this._context.createBufferSource()
295 	this._nodes.bufferSource= node;
296 	return this._addNode(node, properties)
297 };
298 
299 /**
300  * add a panner
301  * 
302  * @param {Object} [properties] properties to set in the created node
303 */
304 tQuery.WebAudio.NodeChainBuilder.prototype.panner	= function(properties){
305 	var node		= this._context.createPanner()
306 	this._nodes.panner	= node;
307 	return this._addNode(node, properties)
308 };
309 
310 /**
311  * add a analyser
312  *
313  * @param {Object} [properties] properties to set in the created node
314 */
315 tQuery.WebAudio.NodeChainBuilder.prototype.analyser	= function(properties){
316 	var node		= this._context.createAnalyser()
317 	this._nodes.analyser	= node;
318 	return this._addNode(node, properties)
319 };
320 
321 /**
322  * add a gainNode
323  *
324  * @param {Object} [properties] properties to set in the created node
325 */
326 tQuery.WebAudio.NodeChainBuilder.prototype.gainNode	= function(properties){
327 	var node		= this._context.createGainNode()
328 	this._nodes.gainNode	= node;
329 	return this._addNode(node, properties)
330 };
331 
332 
333 //////////////////////////////////////////////////////////////////////////////////
334 //////////////////////////////////////////////////////////////////////////////////
335 //////////////////////////////////////////////////////////////////////////////////
336 //		tQuery.WebAudio.Sound						//
337 //////////////////////////////////////////////////////////////////////////////////
338 //////////////////////////////////////////////////////////////////////////////////
339 //////////////////////////////////////////////////////////////////////////////////
340 
341 tQuery.register('createSound', function(world, nodeChain){
342 	return new tQuery.WebAudio.Sound(world, nodeChain);
343 });
344 
345 /**
346  * sound instance
347  *
348  * @class Handle one sound for tQuery.WebAudio
349  *
350  * @param {tQuery.World} [world] the world on which to run
351  * @param {tQuery.WebAudio.NodeChainBuilder} [nodeChain] the nodeChain to use
352 */
353 tQuery.WebAudio.Sound	= function(world, nodeChain){
354 	this._world	= world ? world	: tQuery.world;
355 	this._webaudio	= this._world.getWebAudio();
356 	this._context	= this._webaudio.context();
357 
358 	console.assert( this._world instanceof tQuery.World );
359 	
360 	// create a default NodeChainBuilder if needed
361 	if( nodeChain === undefined ){
362 		nodeChain	= new tQuery.WebAudio.NodeChainBuilder(this._context)
363 					.bufferSource().gainNode().analyser().panner();
364 	}
365 	// setup this._chain
366 	console.assert( nodeChain instanceof tQuery.WebAudio.NodeChainBuilder );
367 	this._chain	= nodeChain;
368 
369 	// connect this._chain.last() node to this._webaudio._entryNode()
370 	this._chain.last().connect( this._webaudio._entryNode() );
371 	
372 	// create some alias
373 	this._source	= this._chain.nodes().bufferSource;
374 	this._gainNode	= this._chain.nodes().gainNode;
375 	this._analyser	= this._chain.nodes().analyser;
376 	this._panner	= this._chain.nodes().panner;
377 	
378 	// sanity check
379 	console.assert(this._source	, "no bufferSource: not yet supported")
380 	console.assert(this._gainNode	, "no gainNode: not yet supported")
381 	console.assert(this._analyser	, "no analyser: not yet supported")
382 	console.assert(this._panner	, "no panner: not yet supported")
383 };
384 
385 /**
386  * destructor
387 */
388 tQuery.WebAudio.Sound.prototype.destroy	= function(){
389 	// disconnect from this._webaudio
390 	this._chain.last().disconnect();
391 	// destroy this._chain
392 	this._chain.destroy();
393 	this._chain	= null;
394 };
395 
396 //////////////////////////////////////////////////////////////////////////////////
397 //		TODO put that in a plugins					//
398 //////////////////////////////////////////////////////////////////////////////////
399 
400 /**
401  * follow a object3D
402 */
403 tQuery.WebAudio.Sound.prototype.follow	= function(object3d){
404 	console.assert( this.isFollowing() === false );
405 	// handle parameter
406 	if( object3d instanceof tQuery.Object3D ){
407 		console.assert(object3d.length === 1)
408 		object3d	= object3d.get(0);
409 	}
410 	// sanity check on parameters
411 	console.assert( object3d instanceof THREE.Object3D );
412 
413 	// hook the world loop
414 	this._followCb		= function(deltaTime){
415 		this.updateWithObject3d(object3d, deltaTime);
416 	}.bind(this);
417 	this._world.loop().hook(this._followCb);
418 	// for chained API
419 	return this;
420 }
421 
422 /**
423  * unfollow the object3D if any
424 */
425 tQuery.WebAudio.Sound.prototype.unfollow	= function(){
426 	this._world.loop().unhook(this._followCb);
427 	this._followCb		= null;
428 	// for chained API
429 	return this;
430 }
431 
432 /**
433  * @returns {Boolean} true if this sound is following a object3d, false overwise
434 */
435 tQuery.WebAudio.Sound.prototype.isFollowing	= function(){
436 	return this._followCb ? true : false;
437 	// for chained API
438 	return this;
439 }
440 
441 //////////////////////////////////////////////////////////////////////////////////
442 //										//
443 //////////////////////////////////////////////////////////////////////////////////
444 
445 /**
446  * getter of the chain nodes
447 */
448 tQuery.WebAudio.Sound.prototype.nodes	= function(){
449 	return this._chain.nodes();
450 };
451 
452 /**
453  * @returns {Boolean} true if the sound is playable, false otherwise
454 */
455 tQuery.WebAudio.Sound.prototype.isPlayable	= function(){
456 	return this._source.buffer ? true : false;
457 };
458 
459 /**
460  * play the sound
461  *
462  * @param {Number} [time] time when to play the sound
463 */
464 tQuery.WebAudio.Sound.prototype.play		= function(time){
465 	if( time ===  undefined )	time	= 0;
466 	// clone the bufferSource
467 	var clonedNode	= this._chain.cloneBufferSource();
468 	// set the noteOn
469 	clonedNode.noteOn(time);
470 	// create the source object
471 	var source	= {
472 		node	: clonedNode,
473 		stop	: function(time){
474 			if( time ===  undefined )	time	= 0;
475 			this.node.noteOff(time);
476 			return source;	// for chained API
477 		}
478 	}
479 	// return it
480 	return source;
481 };
482 
483 /**
484  * getter/setter on the volume
485  *
486  * @param {Number} [value] the value to set, if not provided, get current value
487 */
488 tQuery.WebAudio.Sound.prototype.volume	= function(value){
489 	if( value === undefined )	return this._gainNode.gain.value;
490 	this._gainNode.gain.value	= value;
491 	return this;	// for chained API
492 };
493 
494 
495 /**
496  * getter/setter on the loop
497  * 
498  * @param {Number} [value] the value to set, if not provided, get current value
499 */
500 tQuery.WebAudio.Sound.prototype.loop	= function(value){
501 	if( value === undefined )	return this._source.loop;
502 	this._source.loop	= value;
503 	return this;	// for chained API
504 };
505 
506 /**
507  * Set parameter for the pannerCone
508  *
509  * @param {Number} innerAngle the inner cone hangle in radian
510  * @param {Number} outerAngle the outer cone hangle in radian
511  * @param {Number} outerGain the gain to apply when in the outerCone
512 */
513 tQuery.WebAudio.Sound.prototype.pannerCone	= function(innerAngle, outerAngle, outerGain)
514 {
515 	this._panner.coneInnerAngle	= innerAngle * 180 / Math.PI;
516 	this._panner.coneOuterAngle	= outerAngle * 180 / Math.PI;
517 	this._panner.coneOuterGain	= outerGain;
518 	return this;	// for chained API
519 };
520 
521 /**
522  * getter/setter on the pannerConeInnerAngle
523  * 
524  * @param {Number} value the angle in radian
525 */
526 tQuery.WebAudio.Sound.prototype.pannerConeInnerAngle	= function(value){
527 	if( value === undefined )	return this._panner.coneInnerAngle / 180 * Math.PI;
528 	this._panner.coneInnerAngle	= value * 180 / Math.PI;
529 	return this;	// for chained API
530 };
531 
532 /**
533  * getter/setter on the pannerConeOuterAngle
534  *
535  * @param {Number} value the angle in radian
536 */
537 tQuery.WebAudio.Sound.prototype.pannerConeOuterAngle	= function(value){
538 	if( value === undefined )	return this._panner.coneOuterAngle / 180 * Math.PI;
539 	this._panner.coneOuterAngle	= value * 180 / Math.PI;
540 	return this;	// for chained API
541 };
542 
543 /**
544  * getter/setter on the pannerConeOuterGain
545  * 
546  * @param {Number} value the value
547 */
548 tQuery.WebAudio.Sound.prototype.pannerConeOuterGain	= function(value){
549 	if( value === undefined )	return this._panner.coneOuterGain;
550 	this._panner.coneOuterGain	= value;
551 	return this;	// for chained API
552 };
553 
554 /**
555  * compute the amplitude of the sound (not sure at all it is the proper term)
556  *
557  * @param {Number} width the number of frequencyBin to take into account
558  * @returns {Number} return the amplitude of the sound
559 */
560 tQuery.WebAudio.Sound.prototype.amplitude	= function(width)
561 {
562 	// handle paramerter
563 	width		= width !== undefined ? width : 2;
564 	// inint variable
565 	var analyser	= this._analyser;
566 	var freqByte	= new Uint8Array(analyser.frequencyBinCount);
567 	// get the frequency data
568 	analyser.getByteFrequencyData(freqByte);
569 	// compute the sum
570 	var sum	= 0;
571 	for(var i = 0; i < width; i++){
572 		sum	+= freqByte[i];
573 	}
574 	// complute the amplitude
575 	var amplitude	= sum / (width*256-1);
576 	// return ampliture
577 	return amplitude;
578 }
579 
580 //////////////////////////////////////////////////////////////////////////////////
581 //		TODO put that in a plugin					//
582 //////////////////////////////////////////////////////////////////////////////////
583 
584 /**
585  * Update the source with object3d. usefull for positional sounds
586  * 
587  * @param {THREE.Object3D} object3d the object which originate the source
588  * @param {Number} deltaTime the number of seconds since last update
589 */
590 tQuery.WebAudio.Sound.prototype.updateWithObject3d	= function(object3d, deltaTime){
591 	// sanity check on parameters
592 	console.assert( object3d instanceof THREE.Object3D );
593 	console.assert( typeof(deltaTime) === 'number' );
594 
595 	// ensure object3d.matrixWorld is up to date
596 	object3d.updateMatrixWorld();
597 	
598 	this.updateWithMatrix4(object3d.matrixWorld, deltaTime);
599 	
600 	return this;	// for chained API
601 }
602 
603 /**
604  * Update the source with a matrixWorld. usefull for positional sounds
605  * 
606  * @param {THREE.Matrix4} matrixWorld the matrixWorld describing the position of the sound
607  * @param {Number} deltaTime the number of seconds since last update
608 */
609 tQuery.WebAudio.Sound.prototype.updateWithMatrix4	= function(matrixWorld, deltaTime){
610 	// sanity check on parameters
611 	console.assert( matrixWorld instanceof THREE.Matrix4 );
612 	console.assert( typeof(deltaTime) === 'number' );
613 
614 	////////////////////////////////////////////////////////////////////////
615 	// set position
616 	var position	= matrixWorld.getPosition();
617 	this._panner.setPosition(position.x, position.y, position.z);
618 
619 	////////////////////////////////////////////////////////////////////////
620 	// set orientation
621 	var vOrientation= new THREE.Vector3(0,0,1);
622 	var mOrientation= matrixWorld.clone();
623 	// zero the translation
624 	mOrientation.setPosition({x : 0, y: 0, z: 0});
625 	// Multiply the 0,0,1 vector by the world matrix and normalize the result.
626 	mOrientation.multiplyVector3(vOrientation);
627 	vOrientation.normalize();
628 	// Set panner orientation
629 	this._panner.setOrientation(vOrientation.x, vOrientation.y, vOrientation.z);
630 	
631 	////////////////////////////////////////////////////////////////////////
632 	// set velocity
633 	if( this._prevPos === undefined ){
634 		this._prevPos	= matrixWorld.getPosition().clone();
635 	}else{
636 		var position	= matrixWorld.getPosition();
637 		var velocity	= position.clone().subSelf(this._prevPos).divideScalar(deltaTime);
638 		this._prevPos	= matrixWorld.getPosition().clone();
639 		this._panner.setVelocity(velocity.x, velocity.y, velocity.z);
640 	}
641 }
642 
643 //////////////////////////////////////////////////////////////////////////////////
644 //										//
645 //////////////////////////////////////////////////////////////////////////////////
646 
647 /**
648  * Load a sound
649  *
650  * @param {String} url the url of the sound to load
651  * @param {Function} callback function to notify once the url is loaded (optional)
652 */
653 tQuery.WebAudio.Sound.prototype.load = function(url, callback){
654 	this._loadAndDecodeSound(url, function(buffer){
655 		this._source.buffer	= buffer;
656 		callback && callback(this);
657 	}.bind(this), function(){
658 		console.warn("unable to load sound "+url);
659 	});
660 	return this;	// for chained API
661 };
662 
663 /**
664  * Load and decode a sound
665  *
666  * @param {String} url the url where to get the sound
667  * @param {Function} onLoad the function called when the sound is loaded and decoded (optional)
668  * @param {Function} onError the function called when an error occured (optional)
669 */
670 tQuery.WebAudio.Sound.prototype._loadAndDecodeSound	= function(url, onLoad, onError){
671 	var context	= this._context;
672 	var request	= new XMLHttpRequest();
673 	request.open('GET', url, true);
674 	request.responseType	= 'arraybuffer';
675 	// Decode asynchronously
676 	request.onload	= function() {
677 		context.decodeAudioData(request.response, function(buffer) {
678 			onLoad && onLoad(buffer);
679 		}, function(){
680 			onError && onError();
681 		});
682 	};
683 	// actually start the request
684 	request.send();
685 }
686