1/*
2   SoundManager 2: Javascript Sound for the Web.
3   --------------------------------------------------
4   http://www.schillmania.com/projects/soundmanager2/
5
6   Copyright (c) 2007, Scott Schiller. All rights reserved.
7   Code licensed under the BSD License:
8   http://www.schillmania.com/projects/soundmanager2/license.txt
9
10   Beta V2.0b.20070118
11*/
12
13function SoundManager(smURL,smID) {
14  var self = this;
15  this.enabled = false;
16  this.o = null;
17  this.url = (smURL||s5Path+'ui/audio_support/soundmanager2.swf');
18  this.id = (smID||'sm2movie');
19  this.oMC = null;
20  this.sounds = [];
21  this.soundIDs = [];
22  this.allowPolling = true; // allow flash to poll for status update (required for "while playing", "progress" etc. to work.)
23  this.isIE = (navigator.userAgent.match(/MSIE/));
24  this.isSafari = (navigator.userAgent.match(/safari/i));
25  this._didAppend = false;
26  this._didInit = false;
27  this._disabled = false;
28  this.version = 'V2.0b.20070118';
29
30  this.defaultOptions = {
31    // -----------------
32    'debugMode': false,            // enable debug/status messaging, handy for development/troubleshooting - will be written to a DIV with an ID of "soundmanager-debug", you can use CSS to make it pretty
33    'autoLoad': false,             // enable automatic loading (otherwise .load() will be called on demand with .play(), the latter being nicer on bandwidth - if you want to .load yourself, you also can)
34    'stream': true,                // allows playing before entire file has loaded (recommended)
35    'autoPlay': false,             // enable playing of file as soon as possible (much faster if "stream" is true)
36    'onid3': null,                 // callback function for "ID3 data is added/available"
37    'onload': null,                // callback function for "load finished"
38    'whileloading': null,          // callback function for "download progress update" (X of Y bytes received)
39    'onplay': null,                // callback for "play" start
40    'whileplaying': null,          // callback during play (position update)
41    'onstop': null,                // callback for "user stop"
42    'onfinish': null,              // callback function for "sound finished playing"
43    'onbeforefinish': null,        // callback for "before sound finished playing (at [time])"
44    'onbeforefinishtime': 2000,    // offset (milliseconds) before end of sound to trigger beforefinish (eg. 1000 msec = 1 second)
45    'onbeforefinishcomplete':null, // function to call when said sound finishes playing
46    'onjustbeforefinish':null,     // callback for [n] msec before end of current sound
47    'onjustbeforefinishtime':200,  // [n] - if not using, set to 0 (or null handler) and event will not fire.
48    'multiShot': true,             // let sounds "restart" or layer on top of each other when played multiple times, rather than one-shot/one at a time
49    'pan': 0,                      // "pan" settings, left-to-right, -100 to 100
50    'volume': 100,                 // self-explanatory. 0-100, the latter being the max.
51    // -----------------
52    'foo': 'bar' // you don't need to change this one - it's actually an intentional filler.
53  }
54
55  // --- public methods ---
56
57  this.getMovie = function(smID) {
58    // return self.isIE?window[smID]:document[smID];
59    return self.isIE?window[smID]:(self.isSafari?document[smID+'-embed']:document.getElementById(smID+'-embed'));
60  }
61
62  this.loadFromXML = function(sXmlUrl) {
63    try {
64      self.o._loadFromXML(sXmlUrl);
65    } catch(e) {
66      self._failSafely();
67      return true;
68    }
69  }
70
71  this.createSound = function(oOptions) {
72    if (!self._didInit) throw new Error('soundManager.createSound(): Not loaded yet - wait for soundManager.onload() before calling sound-related methods');
73    if (arguments.length==2) {
74      // function overloading in JS! :) ..assume simple createSound(id,url) use case
75      oOptions = {'id':arguments[0],'url':arguments[1]}
76    }
77    var thisOptions = self._mergeObjects(oOptions);
78    self._writeDebug('soundManager.createSound(): "<a href="#" onclick="soundManager.play(\''+thisOptions.id+'\');return false" title="play this sound">'+thisOptions.id+'</a>" ('+thisOptions.url+')');
79    if (self._idCheck(thisOptions.id,true)) {
80      self._writeDebug('sound '+thisOptions.id+' already defined - exiting');
81      return false;
82    }
83    self.sounds[thisOptions.id] = new SMSound(self,thisOptions);
84    self.soundIDs[self.soundIDs.length] = thisOptions.id;
85    try {
86      self.o._createSound(thisOptions.id,thisOptions.onjustbeforefinishtime);
87    } catch(e) {
88      self._failSafely();
89      return true;
90    }
91    if (thisOptions.autoLoad || thisOptions.autoPlay) self.sounds[thisOptions.id].load(thisOptions);
92    if (thisOptions.autoPlay) self.sounds[thisOptions.id].playState = 1; // we can only assume this sound will be playing soon.
93  }
94
95  this.load = function(sID,oOptions) {
96    if (!self._idCheck(sID)) return false;
97    self.sounds[sID].load(oOptions);
98  }
99
100  this.unload = function(sID) {
101    if (!self._idCheck(sID)) return false;
102    self.sounds[sID].unload();
103  }
104
105  this.play = function(sID,oOptions) {
106    if (!self._idCheck(sID)) {
107      if (typeof oOptions != 'Object') oOptions = {url:oOptions}; // overloading use case: play('mySound','/path/to/some.mp3');
108      if (oOptions && oOptions.url) {
109        // overloading use case, creation + playing of sound: .play('someID',{url:'/path/to.mp3'});
110        self._writeDebug('soundController.play(): attempting to create "'+sID+'"');
111        oOptions.id = sID;
112        self.createSound(oOptions);
113      } else {
114        return false;
115      }
116    }
117    self.sounds[sID].play(oOptions);
118  }
119
120  this.start = this.play; // just for convenience
121
122  this.setPosition = function(sID,nMsecOffset) {
123    if (!self._idCheck(sID)) return false;
124    self.sounds[sID].setPosition(nMsecOffset);
125  }
126
127  this.stop = function(sID) {
128    if (!self._idCheck(sID)) return false;
129    self._writeDebug('soundManager.stop('+sID+')');
130    self.sounds[sID].stop();
131  }
132
133  this.stopAll = function() {
134    for (var oSound in self.sounds) {
135      if (oSound instanceof SMSound) oSound.stop(); // apply only to sound objects
136    }
137  }
138
139  this.pause = function(sID) {
140    if (!self._idCheck(sID)) return false;
141    self.sounds[sID].pause();
142  }
143
144  this.resume = function(sID) {
145    if (!self._idCheck(sID)) return false;
146    self.sounds[sID].resume();
147  }
148
149  this.togglePause = function(sID) {
150    if (!self._idCheck(sID)) return false;
151    self.sounds[sID].togglePause();
152  }
153
154  this.setPan = function(sID,nPan) {
155    if (!self._idCheck(sID)) return false;
156    self.sounds[sID].setPan(nPan);
157  }
158
159  this.setVolume = function(sID,nVol) {
160    if (!self._idCheck(sID)) return false;
161    self.sounds[sID].setVolume(nVol);
162  }
163
164  this.setPolling = function(bPolling) {
165    if (!self.o || !self.allowPolling) return false;
166    self._writeDebug('soundManager.setPolling('+bPolling+')');
167    self.o._setPolling(bPolling);
168  }
169
170  this.disable = function() {
171    // destroy all functions
172    if (self._disabled) return false;
173    self._disabled = true;
174    self._writeDebug('soundManager.disable(): Disabling all functions - future calls will return false.');
175    for (var i=self.soundIDs.length; i--;) {
176      self._disableObject(self.sounds[self.soundIDs[i]]);
177    }
178    self.initComplete(); // fire "complete", despite fail
179    self._disableObject(self);
180  }
181
182  this.getSoundById = function(sID,suppressDebug) {
183    if (!sID) throw new Error('SoundManager.getSoundById(): sID is null/undefined');
184    var result = self.sounds[sID];
185    if (!result && !suppressDebug) {
186      self._writeDebug('"'+sID+'" is an invalid sound ID.');
187      // soundManager._writeDebug('trace: '+arguments.callee.caller);
188    }
189    return result;
190  }
191
192  this.onload = function() {
193  	onloadSM2();
194    // window.onload() equivalent for SM2, ready to create sounds etc.
195    // this is a stub - you can override this in your own external script, eg. soundManager.onload = function() {}
196    // soundManager._writeDebug('<em>Warning</em>: soundManager.onload() is undefined.');
197  }
198
199  this.onerror = function() {
200  	onerrorSM2();
201    // stub for user handler, called when SM2 fails to load/init
202  }
203
204  // --- "private" methods ---
205
206  this._idCheck = this.getSoundById;
207
208  this._disableObject = function(o) {
209    for (var oProp in o) {
210      if (typeof o[oProp] == 'function') o[oProp] = function(){return false;}
211    }
212    oProp = null;
213  }
214
215  this._failSafely = function() {
216    // exception handler for "object doesn't support this property or method"
217    var flashCPLink = 'http://www.macromedia.com/support/documentation/en/flashplayer/help/settings_manager04.html';
218    var fpgssTitle = 'You may need to whitelist this location/domain eg. file://C:/ or C:/ or mysite.com, or set ALWAYS ALLOW under the Flash Player Global Security Settings page. Note that this seems to apply only to file system viewing.';
219    var flashCPL = '<a href="'+flashCPLink+'" title="'+fpgssTitle+'">view/edit</a>';
220    var FPGSS = '<a href="'+flashCPLink+'" title="Flash Player Global Security Settings">FPGSS</a>';
221    if (!self._disabled) {
222      self._writeDebug('soundManager: JS-&gt;Flash communication failed. Possible causes: flash/browser security restrictions ('+flashCPL+'), insufficient browser/plugin support, or .swf not found');
223      self._writeDebug('Verify that the movie path of <em>'+self.url+'</em> is correct (<a href="'+self.url+'" title="If you get a 404/not found, fix it!">test link</a>)');
224      if (self._didAppend) {
225        if (!document.domain) {
226          self._writeDebug('Loading from local file system? (document.domain appears to be null, this location may need whitelisting by '+FPGSS+')');
227          self._writeDebug('Possible security/domain restrictions ('+flashCPL+'), should work when served by http on same domain');
228        }
229        // self._writeDebug('Note: Movie added via JS method, static object/embed in-page markup method may work instead.');
230      }
231      self.disable();
232    }
233  }
234
235  this._createMovie = function(smID,smURL) {
236    var useDOM = !self.isIE; // IE needs document.write() to work, long story short - lame
237    if (window.location.href.indexOf('debug=1')+1) self.defaultOptions.debugMode = true; // allow force of debug mode via URL
238    var html = ['<object classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000" codebase="http://fpdownload.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=8,0,0,0" width="16" height="16" id="'+smID+'"><param name="movie" value="'+smURL+'"><param name="quality" value="high"><param name="allowScriptAccess" value="always" /></object>','<embed name="'+smID+'-embed" id="'+smID+'-embed" src="'+smURL+'" width="1" height="1" quality="high" allowScriptAccess="always" pluginspage="http://www.macromedia.com/go/getflashplayer" type="application/x-shockwave-flash"></embed>'];
239    var debugID = 'soundmanager-debug';
240    var debugHTML = '<div id="'+debugID+'" style="display:'+(self.defaultOptions.debugMode?'block':'none')+'"></div>';
241    if (useDOM) {
242      self.oMC = document.createElement('div');
243      self.oMC.className = 'movieContainer';
244      // "hide" flash movie
245      self.oMC.style.position = 'absolute';
246      self.oMC.style.left = '-256px';
247      self.oMC.style.width = '1px';
248      self.oMC.style.height = '1px';
249      self.oMC.innerHTML = html[self.isIE?0:1];
250      var oTarget = (!self.isIE && document.body?document.body:document.getElementsByTagName('div')[0]);
251      oTarget.appendChild(self.oMC); // append to body here can throw "operation aborted" under IE
252      if (!document.getElementById(debugID)) {
253        var oDebug = document.createElement('div');
254        oDebug.id = debugID;
255        oDebug.style.display = (self.defaultOptions.debugMode?'block':'none');
256        oTarget.appendChild(oDebug);
257      }
258       oTarget = null;
259    } else {
260      // IE method - local file system, may cause strange JS error at line 53?
261      // I hate this method, but it appears to be the only one that will work (createElement works, but JS->Flash communication oddly fails when viewing locally.)
262      // Finally, IE doesn't do application/xhtml+xml yet (thus document.write() is still minimally acceptable here.)
263      if (document.getElementById(debugID)) debugHTML = ''; // avoid overwriting
264      document.write('<div style="position:absolute;left:-256px;top:-256px;width:1px;height:1px" class="movieContainer">'+html[self.isIE?0:1]+'</div>'+debugHTML);
265    }
266    self._didAppend = true;
267    self._writeDebug('-- SoundManager 2 Version '+self.version.substr(1)+' --');
268    self._writeDebug('soundManager._createMovie(): trying to load <a href="'+smURL+'" title="Test this link (404=bad)">'+smURL+'</a>');
269  }
270
271  this._writeDebug = function(sText) {
272    if (!self.defaultOptions.debugMode) return false;
273    var sDID = 'soundmanager-debug';
274    try {
275      var o = document.getElementById(sDID);
276      if (!o) {
277        // attempt to create soundManager debug element
278        var oNew = document.createElement('div');
279        oNew.id = sDID;
280        // o = document.body.appendChild(oNew);
281        o = null;
282        if (!o) return false;
283      }
284      var p = document.createElement('div');
285      p.innerHTML = sText;
286      // o.appendChild(p); // top-to-bottom
287      o.insertBefore(p,o.firstChild); // bottom-to-top
288    } catch(e) {
289      // oh well
290    }
291    o = null;
292  }
293
294  this._debug = function() {
295    self._writeDebug('soundManager._debug(): sounds by id/url:');
296    for (var i=0,j=self.soundIDs.length; i<j; i++) {
297      self._writeDebug(self.sounds[self.soundIDs[i]].sID+' | '+self.sounds[self.soundIDs[i]].url);
298    }
299  }
300
301  this._mergeObjects = function(oMain,oAdd) {
302    // non-destructive merge
303    var o1 = oMain;
304    var o2 = (typeof oAdd == 'undefined'?self.defaultOptions:oAdd);
305    for (var o in o2) {
306      if (typeof o1[o] == 'undefined') o1[o] = o2[o];
307    }
308    return o1;
309  }
310
311  this.createMovie = function(sURL) {
312    self._writeDebug('soundManager.createMovie('+(sURL||'')+')');
313    if (sURL) self.url = sURL;
314    self._initMovie();
315  }
316
317  this._initMovie = function() {
318    // attempt to get, or create, movie
319    if (self.o) return false; // pre-init may have fired this function before window.onload(), may already exist
320    self.o = self.getMovie(self.id); // try to get flash movie (inline markup)
321    if (!self.o) {
322      // try to create
323      self._createMovie(self.id,self.url);
324      self.o = self.getMovie(self.id);
325    }
326    if (!self.o) {
327      self._writeDebug('SoundManager(): Could not find object/embed element. Sound will be disabled.');
328      self.disable();
329    } else {
330      self._writeDebug('SoundManager(): Got '+self.o.nodeName+' element ('+(self._didAppend?'created via JS':'static HTML')+')');
331    }
332  }
333
334  this.initComplete = function() {
335    if (self._didInit) return false;
336    self._didInit = true;
337    self._writeDebug('-- SoundManager 2 '+(self._disabled?'failed to load':'loaded')+' ('+(self._disabled?'security/load error':'OK')+') --');
338    if (self._disabled) {
339      self._writeDebug('soundManager.initComplete(): calling soundManager.onerror()');
340      self.onerror.apply(window);
341      return false;
342    }
343    self._writeDebug('soundManager.initComplete(): calling soundManager.onload()');
344    try {
345      // call user-defined "onload", scoped to window
346      self.onload.apply(window);
347    } catch(e) {
348      // something broke (likely JS error in user function)
349      self._writeDebug('soundManager.onload() threw an exception: '+e.message);
350      throw e; // (so browser/console gets message)
351    }
352    self._writeDebug('soundManager.onload() complete');
353  }
354
355  this.init = function() {
356    // called after onload()
357    self._initMovie();
358    // event cleanup
359    if (window.removeEventListener) {
360      window.removeEventListener('load',self.beginInit,false);
361    } else if (window.detachEvent) {
362      window.detachEvent('onload',self.beginInit);
363    }
364    try {
365      self.o._externalInterfaceTest(); // attempt to talk to Flash
366      self._writeDebug('Flash ExternalInterface call (JS -&gt; Flash) succeeded.');
367      if (!self.allowPolling) self._writeDebug('Polling (whileloading/whileplaying support) is disabled.');
368      self.setPolling(true);
369      self.enabled = true;
370    }  catch(e) {
371      self._failSafely();
372      self.initComplete();
373      return false;
374    }
375    self.initComplete();
376  }
377
378  this.beginInit = function() {
379    self._initMovie();
380    setTimeout(self.init,1000); // some delay required, otherwise JS<->Flash/ExternalInterface communication fails under non-IE (?!)
381  }
382
383  this.destruct = function() {
384    if (self.isSafari) {
385      /* --
386        Safari 1.3.2 (v312.6)/OSX 10.3.9 and perhaps newer will crash if a sound is actively loading when user exits/refreshes/leaves page
387       (Apparently related to ExternalInterface making calls to an unloading/unloaded page?)
388       Unloading sounds (detaching handlers and so on) may help to prevent this
389      -- */
390      for (var i=self.soundIDs.length; i--;) {
391        if (self.sounds[self.soundIDs[i]].readyState == 1) self.sounds[self.soundIDs[i]].unload();
392      }
393    }
394    self.disable();
395    // self.o = null;
396    // self.oMC = null;
397  }
398
399}
400
401function SMSound(oSM,oOptions) {
402  var self = this;
403  var sm = oSM;
404  this.sID = oOptions.id;
405  this.url = oOptions.url;
406  this.options = sm._mergeObjects(oOptions);
407  this.id3 = {
408   /*
409    Name/value pairs set via Flash when available - see reference for names:
410    http://livedocs.macromedia.com/flash/8/main/wwhelp/wwhimpl/common/html/wwhelp.htm?context=LiveDocs_Parts&file=00001567.html
411    (eg., this.id3.songname or this.id3['songname'])
412   */
413  }
414
415  self.resetProperties = function(bLoaded) {
416    self.bytesLoaded = null;
417    self.bytesTotal = null;
418    self.position = null;
419    self.duration = null;
420    self.durationEstimate = null;
421    self.loaded = false;
422    self.loadSuccess = null;
423    self.playState = 0;
424    self.paused = false;
425    self.readyState = 0; // 0 = uninitialised, 1 = loading, 2 = failed/error, 3 = loaded/success
426    self.didBeforeFinish = false;
427    self.didJustBeforeFinish = false;
428  }
429
430  self.resetProperties();
431
432  // --- public methods ---
433
434  this.load = function(oOptions) {
435    self.loaded = false;
436    self.loadSuccess = null;
437    self.readyState = 1;
438    self.playState = (oOptions.autoPlay||false); // if autoPlay, assume "playing" is true (no way to detect when it actually starts in Flash unless onPlay is watched?)
439    var thisOptions = sm._mergeObjects(oOptions);
440    if (typeof thisOptions.url == 'undefined') thisOptions.url = self.url;
441    try {
442      sm._writeDebug('loading '+thisOptions.url);
443      sm.o._load(self.sID,thisOptions.url,thisOptions.stream,thisOptions.autoPlay,thisOptions.whileloading?1:0);
444    } catch(e) {
445      sm._writeDebug('SMSound().load(): JS-&gt;Flash communication failed.');
446    }
447  }
448
449  this.unload = function() {
450    // Flash 8/AS2 can't "close" a stream - fake it by loading an empty MP3
451    sm._writeDebug('SMSound().unload()');
452    self.setPosition(0); // reset current sound positioning
453    sm.o._unload(self.sID,s5Path+'ui/audio_support/null.mp3');
454    // reset load/status flags
455    self.resetProperties();
456  }
457
458  this.play = function(oOptions) {
459    if (!oOptions) oOptions = {};
460
461    // --- TODO: make event handlers specified via oOptions only apply to this instance of play() (eg. onfinish applies but will reset to default on finish)
462    if (oOptions.onfinish) self.options.onfinish = oOptions.onfinish;
463    if (oOptions.onbeforefinish) self.options.onbeforefinish = oOptions.onbeforefinish;
464    if (oOptions.onjustbeforefinish) self.options.onjustbeforefinish = oOptions.onjustbeforefinish;
465    // ---
466
467    var thisOptions = sm._mergeObjects(oOptions);
468    if (self.playState == 1) {
469      // var allowMulti = typeof oOptions.multiShot!='undefined'?oOptions.multiShot:sm.defaultOptions.multiShot;
470      var allowMulti = thisOptions.multiShot;
471      if (!allowMulti) {
472        sm._writeDebug('SMSound.play(): "'+self.sID+'" already playing? (one-shot)');
473        return false;
474      } else {
475        sm._writeDebug('SMSound.play(): "'+self.sID+'" already playing (multi-shot)');
476      }
477    }
478    if (!self.loaded) {
479      if (self.readyState == 0) {
480        sm._writeDebug('SMSound.play(): .play() before load request. Attempting to load "'+self.sID+'"');
481        // try to get this sound playing ASAP
482        thisOptions.stream = true;
483        thisOptions.autoPlay = true;
484        // TODO: need to investigate when false, double-playing
485        // if (typeof oOptions.autoPlay=='undefined') thisOptions.autoPlay = true; // only set autoPlay if unspecified here
486        self.load(thisOptions); // try to get this sound playing ASAP
487      } else if (self.readyState == 2) {
488        sm._writeDebug('SMSound.play(): Could not load "'+self.sID+'" - exiting');
489        return false;
490      } else {
491        sm._writeDebug('SMSound.play(): "'+self.sID+'" is loading - attempting to play..');
492      }
493    } else {
494      sm._writeDebug('SMSound.play(): "'+self.sID+'"');
495    }
496    if (self.paused) {
497      self.resume();
498    } else {
499      self.playState = 1;
500      self.position = (thisOptions.offset||0);
501      if (thisOptions.onplay) thisOptions.onplay.apply(self);
502      self.setVolume(thisOptions.volume);
503      self.setPan(thisOptions.pan);
504      if (!thisOptions.autoPlay) {
505        sm._writeDebug('starting sound '+self.sID);
506        sm.o._start(self.sID,thisOptions.loop||1,self.position); // TODO: verify !autoPlay doesn't cause issue
507      }
508    }
509  }
510
511  this.start = this.play; // just for convenience
512
513  this.stop = function(bAll) {
514    if (self.playState == 1) {
515      self.playState = 0;
516      self.paused = false;
517      if (sm.defaultOptions.onstop) sm.defaultOptions.onstop.apply(self);
518      sm.o._stop(self.sID);
519    }
520  }
521
522  this.setPosition = function(nMsecOffset) {
523    // sm._writeDebug('setPosition('+nMsecOffset+')');
524    sm.o._setPosition(self.sID,nMsecOffset/1000,self.paused||!self.playState); // if paused or not playing, will not resume (by playing)
525  }
526
527  this.pause = function() {
528    if (self.paused) return false;
529    sm._writeDebug('SMSound.pause()');
530    self.paused = true;
531    sm.o._pause(self.sID);
532  }
533
534  this.resume = function() {
535    if (!self.paused) return false;
536    sm._writeDebug('SMSound.resume()');
537    self.paused = false;
538    sm.o._pause(self.sID); // flash method is toggle-based (pause/resume)
539  }
540
541  this.togglePause = function() {
542    // if playing, pauses - if paused, resumes playing.
543    sm._writeDebug('SMSound.togglePause()');
544    if (!self.playState) {
545      // self.setPosition();
546      self.play({offset:self.position/1000});
547      return false;
548    }
549    if (self.paused) {
550      sm._writeDebug('SMSound.togglePause(): resuming..');
551      self.resume();
552    } else {
553      sm._writeDebug('SMSound.togglePause(): pausing..');
554      self.pause();
555    }
556  }
557
558  this.setPan = function(nPan) {
559    if (typeof nPan == 'undefined') nPan = 0;
560    sm.o._setPan(self.sID,nPan);
561    self.options.pan = nPan;
562  }
563
564  this.setVolume = function(nVol) {
565    if (typeof nVol == 'undefined') nVol = 100;
566    sm.o._setVolume(self.sID,nVol);
567    self.options.volume = nVol;
568  }
569
570  // --- "private" methods called by Flash ---
571
572  this._whileloading = function(nBytesLoaded,nBytesTotal,nDuration) {
573    self.bytesLoaded = nBytesLoaded;
574    self.bytesTotal = nBytesTotal;
575    self.duration = nDuration;
576    self.durationEstimate = parseInt((self.bytesTotal/self.bytesLoaded)*self.duration); // estimate total time (will only be accurate with CBR MP3s.)
577    if (self.readyState != 3 && self.options.whileloading) self.options.whileloading.apply(self);
578    // soundManager._writeDebug('duration/durationEst: '+self.duration+' / '+self.durationEstimate);
579  }
580
581  this._onid3 = function(oID3PropNames,oID3Data) {
582    // oID3PropNames: string array (names)
583    // ID3Data: string array (data)
584    sm._writeDebug('SMSound()._onid3(): "'+this.sID+'" ID3 data received.');
585    var oData = [];
586    for (var i=0,j=oID3PropNames.length; i<j; i++) {
587      oData[oID3PropNames[i]] = oID3Data[i];
588      // sm._writeDebug(oID3PropNames[i]+': '+oID3Data[i]);
589    }
590    self.id3 = sm._mergeObjects(self.id3,oData);
591    if (self.options.onid3) self.options.onid3.apply(self);
592  }
593
594  this._whileplaying = function(nPosition) {
595    if (isNaN(nPosition) || nPosition == null) return false; // Flash may return NaN at times
596    self.position = nPosition;
597    if (self.playState == 1) {
598      if (self.options.whileplaying) self.options.whileplaying.apply(self); // flash may call after actual finish
599      if (self.loaded && self.options.onbeforefinish && self.options.onbeforefinishtime && !self.didBeforeFinish && self.duration-self.position <= self.options.onbeforefinishtime) {
600        sm._writeDebug('duration-position &lt;= onbeforefinishtime: '+self.duration+' - '+self.position+' &lt= '+self.options.onbeforefinishtime+' ('+(self.duration-self.position)+')');
601        self._onbeforefinish();
602      }
603    }
604  }
605
606  this._onload = function(bSuccess) {
607    bSuccess = (bSuccess==1?true:false);
608    sm._writeDebug('SMSound._onload(): "'+self.sID+'"'+(bSuccess?' loaded.':' failed to load (or loaded from cache - weird bug) - [<a href="'+self.url+'">test URL</a>]'));
609    self.loaded = bSuccess;
610    self.loadSuccess = bSuccess;
611    self.readyState = bSuccess?3:2;
612    if (self.options.onload) self.options.onload.apply(self);
613  }
614
615  this._onbeforefinish = function() {
616    if (!self.didBeforeFinish) {
617      self.didBeforeFinish = true;
618      if (self.options.onbeforefinish) self.options.onbeforefinish.apply(self);
619    }
620  }
621
622  this._onjustbeforefinish = function(msOffset) {
623    // msOffset: "end of sound" delay actual value (eg. 200 msec, value at event fire time was 187)
624    if (!self.didJustBeforeFinish) {
625      self.didJustBeforeFinish = true;
626      soundManager._writeDebug('SMSound._onjustbeforefinish()');
627      if (self.options.onjustbeforefinish) self.options.onjustbeforefinish.apply(self);;
628    }
629  }
630
631  this._onfinish = function() {
632    // sound has finished playing
633    sm._writeDebug('SMSound._onfinish(): "'+self.sID+'" finished playing');
634    self.playState = 0;
635    self.paused = false;
636    if (self.options.onfinish) self.options.onfinish.apply(self);
637    if (self.options.onbeforefinishcomplete) self.options.onbeforefinishcomplete.apply(self);
638    // reset some state items
639    self.setPosition(0);
640    self.didBeforeFinish = false;
641    self.didJustBeforeFinish = false;
642  }
643
644}
645
646var soundManager = new SoundManager();
647
648// attach onload handler
649if (window.addEventListener) {
650  window.addEventListener('load',soundManager.beginInit,false);
651  window.addEventListener('beforeunload',soundManager.destruct,false);
652} else if (window.attachEvent) {
653  window.attachEvent('onload',soundManager.beginInit);
654  window.attachEvent('beforeunload',soundManager.destruct);
655} else {
656  // no add/attachevent support - safe to assume no JS->Flash either.
657  soundManager.disable();
658}