/*
   SoundManager 2: Javascript Sound for the Web
   --------------------------------------------
   http://schillmania.com/projects/soundmanager2/

   Copyright (c) 2008, Scott Schiller. All rights reserved.
   Code licensed under the BSD License:
   http://schillmania.com/projects/soundmanager2/license.txt

   V2.5b.20080505
*/

function SoundManager(smURL,smID) {
  var self = this;
  this.version = 'V2.5b.20080505';
  this.url = (smURL||'soundmanager25.swf');

  this.debugMode = true;           // enable debugging output (div#soundmanager-debug, OR console if available + configured)
  this.useConsole = true;          // use firebug/safari console.log()-type debug console if available
  this.consoleOnly = false;        // if console is being used, do not create/write to #soundmanager-debug
  this.nullURL = 'data/null.mp3';  // path to "null" (empty) MP3 file, used to unload sounds

  this.defaultOptions = {
    '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)
    'stream': true,                // allows playing before entire file has loaded (recommended)
    'autoPlay': false,             // enable playing of file as soon as possible (much faster if "stream" is true)
    'onid3': null,                 // callback function for "ID3 data is added/available"
    'onload': null,                // callback function for "load finished"
    'whileloading': null,          // callback function for "download progress update" (X of Y bytes received)
    'onplay': null,                // callback for "play" start
    'whileplaying': null,          // callback during play (position update)
    'onstop': null,                // callback for "user stop"
    'onfinish': null,              // callback function for "sound finished playing"
    'onbeforefinish': null,        // callback for "before sound finished playing (at [time])"
    'onbeforefinishtime': 5000,    // offset (milliseconds) before end of sound to trigger beforefinish (eg. 1000 msec = 1 second)
    'onbeforefinishcomplete':null, // function to call when said sound finishes playing
    'onjustbeforefinish':null,     // callback for [n] msec before end of current sound
    'onjustbeforefinishtime':200,  // [n] - if not using, set to 0 (or null handler) and event will not fire.
    'multiShot': true,             // let sounds "restart" or layer on top of each other when played multiple times, rather than one-shot/one at a time
    'position': null,              // offset (milliseconds) to seek to within loaded sound data.
    'pan': 0,                      // "pan" settings, left-to-right, -100 to 100
    'volume': 100                  // self-explanatory. 0-100, the latter being the max.
  };

  this.allowPolling = true;        // allow flash to poll for status update (required for "while playing", "progress" etc. to work.)
  this.swfLoaded = false;
  this.enabled = false;
  this.o = null;
  this.id = (smID||'sm2movie');
  this.oMC = null;
  this.sounds = [];
  this.soundIDs = [];
  this.isIE = (navigator.userAgent.match(/MSIE/));
  this.isSafari = (navigator.userAgent.match(/safari/i));
  this.debugID = 'soundmanager-debug';
  this._debugOpen = true;
  this._didAppend = false;
  this._appendSuccess = false;
  this._didInit = false;
  this._disabled = false;
  this._hasConsole = (typeof console != 'undefined' && typeof console.log != 'undefined');
  this._debugLevels = ['log','info','warn','error'];
  this.sandbox = {
    'type': null,
    'types': {
      'remote': 'remote (domain-based) rules',
	  'localWithFile': 'local with file access (no internet access)',
	  'localWithNetwork': 'local with network (internet access only, no local access)',
	  'localTrusted': 'local, trusted (local + internet access)'
    },
    'description': null,
    'noRemote': null,
    'noLocal': null
  };
  this._overHTTP = (document.location?document.location.protocol.match(/http/i):null);
  this._waitingforEI = false;
  this._initPending = false;
  this._tryInitOnFocus = (this.isSafari && typeof document.hasFocus == 'undefined');
  this._isFocused = (typeof document.hasFocus != 'undefined'?document.hasFocus():null);
  this._okToDisable = !this._tryInitOnFocus;
  var flashCPLink = 'http://www.macromedia.com/support/documentation/en/flashplayer/help/settings_manager04.html';

  // --- public methods ---
  
  this.supported = function() {
    return (self._didInit && !self._disabled);
  };

  this.getMovie = function(smID) {
return self.isIE?window[smID]:(self.isSafari?document.getElementById(smID+'-embed')||document[smID+'-embed']:document.getElementById(smID+'-embed'));
  };

  this.loadFromXML = function(sXmlUrl) {
    try {
      self.o._loadFromXML(sXmlUrl);
    } catch(e) {
      self._failSafely();
      return true;
    };
  };

  this.createSound = function(oOptions) {
    if (!self._didInit) throw new Error('soundManager.createSound(): Not loaded yet - wait for soundManager.onload() before calling sound-related methods');
    if (arguments.length==2) {
      // function overloading in JS! :) ..assume simple createSound(id,url) use case
      oOptions = {'id':arguments[0],'url':arguments[1]};
    };
    var thisOptions = self._mergeObjects(oOptions);
    self._writeDebug('soundManager.createSound(): '+thisOptions.id+' ('+thisOptions.url+')',1);
    if (self._idCheck(thisOptions.id,true)) {
      self._writeDebug('sound '+thisOptions.id+' already defined - exiting',2);
      return self.sounds[thisOptions.id];
    };
    self.sounds[thisOptions.id] = new SMSound(self,thisOptions);
    self.soundIDs[self.soundIDs.length] = thisOptions.id;
    try {
      self.o._createSound(thisOptions.id,thisOptions.onjustbeforefinishtime);
    } catch(e) {
      self._failSafely();
      return true;
    };
    if (thisOptions.autoLoad || thisOptions.autoPlay) self.sounds[thisOptions.id].load(thisOptions);
    if (thisOptions.autoPlay) self.sounds[thisOptions.id].playState = 1; // we can only assume this sound will be playing soon.
    return self.sounds[thisOptions.id];
  };

  this.destroySound = function(sID) {
    // explicitly destroy a sound before normal page unload, etc.
    if (!self._idCheck(sID)) return false;
    for (var i=0; i<self.soundIDs.length; i++) {
      if (self.soundIDs[i] == sID) {
	self.soundIDs.splice(i,1);
        continue;
      };
    };
    self.sounds[sID].unload();
    delete self.sounds[sID];
  };

  this.load = function(sID,oOptions) {
    if (!self._idCheck(sID)) return false;
    self.sounds[sID].load(oOptions);
  };

  this.unload = function(sID) {
    if (!self._idCheck(sID)) return false;
    self.sounds[sID].unload();
  };

  this.play = function(sID,oOptions) {
    if (!self._idCheck(sID)) {
      if (typeof oOptions != 'Object') oOptions = {url:oOptions}; // overloading use case: play('mySound','/path/to/some.mp3');
      if (oOptions && oOptions.url) {
        // overloading use case, creation + playing of sound: .play('someID',{url:'/path/to.mp3'});
        self._writeDebug('soundController.play(): attempting to create "'+sID+'"',1);
        oOptions.id = sID;
        self.createSound(oOptions);
      } else {
        return false;
      };
    };
    self.sounds[sID].play(oOptions);
  };

  this.start = this.play; // just for convenience

  this.setPosition = function(sID,nMsecOffset) {
    if (!self._idCheck(sID)) return false;
    self.sounds[sID].setPosition(nMsecOffset);
  };

  this.stop = function(sID) {
    if (!self._idCheck(sID)) return false;
    self._writeDebug('soundManager.stop('+sID+')',1);
    self.sounds[sID].stop(); 
  };

  this.stopAll = function() {
    self._writeDebug('soundManager.stopAll()',1);
    for (var oSound in self.sounds) {
      if (self.sounds[oSound] instanceof SMSound) self.sounds[oSound].stop(); // apply only to sound objects
    };
  };

  this.pause = function(sID) {
    if (!self._idCheck(sID)) return false;
    self.sounds[sID].pause();
  };

  this.resume = function(sID) {
    if (!self._idCheck(sID)) return false;
    self.sounds[sID].resume();
  };

  this.togglePause = function(sID) {
    if (!self._idCheck(sID)) return false;
    self.sounds[sID].togglePause();
  };

  this.setPan = function(sID,nPan) {
    if (!self._idCheck(sID)) return false;
    self.sounds[sID].setPan(nPan);
  };

  this.setVolume = function(sID,nVol) {
    if (!self._idCheck(sID)) return false;
    self.sounds[sID].setVolume(nVol);
  };

  this.setPolling = function(bPolling) {
    if (!self.o || !self.allowPolling) return false;
    // self._writeDebug('soundManager.setPolling('+bPolling+')');
    self.o._setPolling(bPolling);
  };

  this.disable = function() {
    // destroy all functions
    if (self._disabled) return false;
    self._disabled = true;
    self._writeDebug('soundManager.disable(): Disabling all functions - future calls will return false.',1);
    for (var i=self.soundIDs.length; i--;) {
      self._disableObject(self.sounds[self.soundIDs[i]]);
    };
    self.initComplete(); // fire "complete", despite fail
    self._disableObject(self);
  };

  this.getSoundById = function(sID,suppressDebug) {
    if (!sID) throw new Error('SoundManager.getSoundById(): sID is null/undefined');
    var result = self.sounds[sID];
    if (!result && !suppressDebug) {
      self._writeDebug('"'+sID+'" is an invalid sound ID.',2);
      // soundManager._writeDebug('trace: '+arguments.callee.caller);
    };
    return result;
  };

  this.onload = function() {
    // window.onload() equivalent for SM2, ready to create sounds etc.
    // this is a stub - you can override this in your own external script, eg. soundManager.onload = function() {}
    soundManager._writeDebug('<em>Warning</em>: soundManager.onload() is undefined.',2);
  };

  this.onerror = function() {
    // stub for user handler, called when SM2 fails to load/init
  };

  // --- "private" methods ---

  this._idCheck = this.getSoundById;

  this._disableObject = function(o) {
    for (var oProp in o) {
      if (typeof o[oProp] == 'function' && typeof o[oProp]._protected == 'undefined') o[oProp] = function(){return false;};
    };
    oProp = null;
  };

  this._failSafely = function() {
    // exception handler for "object doesn't support this property or method" or general failure
    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. The latter is probably less-secure.';
    var flashCPL = '<a href="'+flashCPLink+'" title="'+fpgssTitle+'">view/edit</a>';
    var FPGSS = '<a href="'+flashCPLink+'" title="Flash Player Global Security Settings">FPGSS</a>';
    if (!self._disabled) {
      self._writeDebug('soundManager: Failed to initialise.',2);
      self.disable();
    };
  };

  this._createMovie = function(smID,smURL) {
    if (self._didAppend && self._appendSuccess) return false; // ignore if already succeeded
    if (window.location.href.indexOf('debug=1')+1) self.debugMode = true; // allow force of debug mode via URL
    self._didAppend = true;
    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>'];
    var toggleElement = '<div id="'+self.debugID+'-toggle" style="position:fixed;_position:absolute;right:0px;bottom:0px;_top:0px;width:1.2em;height:1.2em;line-height:1.2em;margin:2px;padding:0px;text-align:center;border:1px solid #999;cursor:pointer;background:#fff;color:#333;z-index:706" title="Toggle SM2 debug console" onclick="soundManager._toggleDebug()">-</div>';
    var debugHTML = '<div id="'+self.debugID+'" style="display:'+(self.debugMode && ((!self._hasConsole||!self.useConsole)||(self.useConsole && self._hasConsole && !self.consoleOnly))?'block':'none')+';opacity:0.85"></div>';
    var appXHTML = 'soundManager._createMovie(): appendChild/innerHTML set failed. Serving application/xhtml+xml MIME type? Browser may be enforcing strict rules, not allowing write to innerHTML. (PS: If so, this means your commitment to XML validation is going to break stuff now, because this part isn\'t finished yet. ;))';

    var sHTML = '<div style="position:absolute;left:-256px;top:-256px;width:1px;height:1px" class="movieContainer">'+html[self.isIE?0:1]+'</div>'+(self.debugMode && ((!self._hasConsole||!self.useConsole)||(self.useConsole && self._hasConsole && !self.consoleOnly)) && !document.getElementById(self.debugID)?'x'+debugHTML+toggleElement:'');

    var oTarget = (document.body?document.body:(document.documentElement?document.documentElement:document.getElementsByTagName('div')[0]));
    if (oTarget) {
      self.oMC = document.createElement('div');
      self.oMC.className = 'movieContainer';
      // "hide" flash movie
      self.oMC.style.position = 'absolute';
      self.oMC.style.left = '-256px';
      self.oMC.style.width = '1px';
      self.oMC.style.height = '1px';
      try {
        oTarget.appendChild(self.oMC);
        self.oMC.innerHTML = html[self.isIE?0:1];
        self._appendSuccess = true;
      } catch(e) {
        // may fail under app/xhtml+xml - has yet to be tested
        throw new Error(appXHTML);
      };
      if (!document.getElementById(self.debugID) && ((!self._hasConsole||!self.useConsole)||(self.useConsole && self._hasConsole && !self.consoleOnly))) {
        var oDebug = document.createElement('div');
        oDebug.id = self.debugID;
        oDebug.style.display = (self.debugMode?'block':'none');
        if (self.debugMode) {
          try {
            var oD = document.createElement('div');
            oTarget.appendChild(oD);
            oD.innerHTML = toggleElement;
          } catch(e) {
            throw new Error(appXHTML);
          };
        };
        oTarget.appendChild(oDebug);
      };
      oTarget = null;
    };
    self._writeDebug('-- SoundManager 2 Version '+self.version.substr(1)+' --',1);
    self._writeDebug('soundManager._createMovie(): Trying to load '+smURL,1);
  };

  this._writeDebug = function(sText,sType) {
    if (!self.debugMode) return false;
    if (self._hasConsole && self.useConsole) {
      var sMethod = self._debugLevels[sType];
      if (typeof console[sMethod] != 'undefined') {
        console[sMethod](sText);
      } else {
        console.log(sText);
      };
      if (self.useConsoleOnly) return true;
    };
    var sDID = 'soundmanager-debug';
    try {
      var o = document.getElementById(sDID);
      if (!o) return false;
      var oItem = document.createElement('div');
      sText = sText.replace(/\n/g,'<br />');
      if (typeof sType == 'undefined') {
        var sType = 0;
      } else {
        sType = parseInt(sType);
      };
      oItem.innerHTML = sText;
      if (sType) {
        if (sType >= 2) oItem.style.fontWeight = 'bold';
        if (sType == 3) oItem.style.color = '#ff3333';
      };
      // o.appendChild(oItem); // top-to-bottom
      o.insertBefore(oItem,o.firstChild); // bottom-to-top
    } catch(e) {
      // oh well
    };
    o = null;
  };
  this._writeDebug._protected = true;

  this._writeDebugAlert = function(sText) { alert(sText); };

  if (window.location.href.indexOf('debug=alert')+1 && self.debugMode) {
    self._writeDebug = self._writeDebugAlert;
  };

  this._toggleDebug = function() {
    var o = document.getElementById(self.debugID);
    var oT = document.getElementById(self.debugID+'-toggle');
    if (!o) return false;
    if (self._debugOpen) {
      // minimize
      oT.innerHTML = '+';
      o.style.display = 'none';
    } else {
      oT.innerHTML = '-';
      o.style.display = 'block';
    };
    self._debugOpen = !self._debugOpen;
  };

  this._toggleDebug._protected = true;

  this._debug = function() {
    self._writeDebug('soundManager._debug(): sounds by id/url:',0);
    for (var i=0,j=self.soundIDs.length; i<j; i++) {
      self._writeDebug(self.sounds[self.soundIDs[i]].sID+' | '+self.sounds[self.soundIDs[i]].url,0);
    };
  };

  this._mergeObjects = function(oMain,oAdd) {
    // non-destructive merge
    var o1 = oMain;
    var o2 = (typeof oAdd == 'undefined'?self.defaultOptions:oAdd);
    for (var o in o2) {
      if (typeof o1[o] == 'undefined') o1[o] = o2[o];
    };
    return o1;
  };

  this.createMovie = function(sURL) {
    if (sURL) self.url = sURL;
    self._initMovie();
  };

  this.go = this.createMovie; // nice alias

  this._initMovie = function() {
    // attempt to get, or create, movie
    if (self.o) return false; // pre-init may have fired this function before window.onload(), may already exist
    self.o = self.getMovie(self.id); // try to get flash movie (inline markup)
    if (!self.o) {
      // try to create
      self._createMovie(self.id,self.url);
      self.o = self.getMovie(self.id);
    };
    if (self.o) {
      self._writeDebug('soundManager._initMovie(): Got '+self.o.nodeName+' element ('+(self._didAppend?'created via JS':'static HTML')+')',1);
      self._writeDebug('soundManager._initMovie(): Waiting for ExternalInterface call from Flash..');
    };
  };

  this.waitForExternalInterface = function() {
    if (self._waitingForEI) return false;
    self._waitingForEI = true;
    if (self._tryInitOnFocus && !self._isFocused) {
      self._writeDebug('soundManager: Special case: Flash may not have started due to non-focused tab (Safari is lame), and/or focus cannot be detected. Waiting for focus-related event..');
      return false;
    };
    if (!self._didInit) {
      self._writeDebug('soundManager: Getting impatient, still waiting for Flash.. ;)');
    };
    setTimeout(function() {
      if (!self._didInit) {
        self._writeDebug('soundManager: No Flash response within reasonable time after document load.\nPossible causes: Flash version under 8, no support, or Flash security denying JS-Flash communication.',2);
        if (!self._overHTTP) {
          self._writeDebug('soundManager: Loading this page from local/network file system (not over HTTP?) Flash security likely restricting JS-Flash access. Consider adding current URL to "trusted locations" in the Flash player security settings manager at '+flashCPLink+', or simply serve this content over HTTP.',2);
        };
      };
      // if still not initialized and no other options, give up
      if (!self._didInit && self._okToDisable) self._failSafely();
    },500);
  };

  this.handleFocus = function() {
    if (self._isFocused || !self._tryInitOnFocus) return true;
    self._okToDisable = true;
    self._isFocused = true;
    self._writeDebug('soundManager.handleFocus()');
    if (self._tryInitOnFocus) {
      // giant Safari 3.1 hack - assume window in focus if mouse is moving, since document.hasFocus() not currently implemented.
      window.removeEventListener('mousemove',self.handleFocus,false);
    };
    // allow init to restart
    self._waitingForEI = false;
    setTimeout(self.waitForExternalInterface,500);
    // detach event
    if (window.removeEventListener) {
      window.removeEventListener('focus',self.handleFocus,false);
    } else if (window.detachEvent) {
      window.detachEvent('onfocus',self.handleFocus);
    };
  };

  this.initComplete = function() {
    if (self._didInit) return false;
    self._didInit = true;
    self._writeDebug('-- SoundManager 2 '+(self._disabled?'failed to load':'loaded')+' ('+(self._disabled?'security/load error':'OK')+') --',1);
    if (self._disabled) {
      self._writeDebug('soundManager.initComplete(): calling soundManager.onerror()',1);
      self.onerror.apply(window);
      return false;
    };
    self._writeDebug('soundManager.initComplete(): calling soundManager.onload()',1);
    try {
      // call user-defined "onload", scoped to window
      self.onload.apply(window);
    } catch(e) {
      // something broke (likely JS error in user function)
      self._writeDebug('soundManager.onload() threw an exception: '+e.message,2);
      throw e; // (so browser/console gets message) - TODO: Doesn't seem to cascade down, probably due to nested try..catch blocks.
   };
    self._writeDebug('soundManager.onload() complete',1);
  };

  this.init = function() {
    self._writeDebug('-- soundManager.init() --');
    // called after onload()
    self._initMovie();
    if (self._didInit) {
      self._writeDebug('soundManager.init(): Already called?');
      return false;
    };
    // event cleanup
    if (window.removeEventListener) {
      window.removeEventListener('load',self.beginDelayedInit,false);
    } else if (window.detachEvent) {
      window.detachEvent('onload',self.beginDelayedInit);
    };
    try {
      self._writeDebug('Attempting to call JS-Flash..');
      self.o._externalInterfaceTest(); // attempt to talk to Flash
      // self._writeDebug('Flash ExternalInterface call (JS-Flash) OK.',1);
      if (!self.allowPolling) self._writeDebug('Polling (whileloading/whileplaying support) is disabled.',1);
      self.setPolling(true);
      self.enabled = true;
    } catch(e) {
      self._writeDebug('init exception: '+e.message);
      self._failSafely();
      self.initComplete();
      return false;
    };
    self.initComplete();
  };

  this.beginDelayedInit = function() {
    self._writeDebug('soundManager.beginDelayedInit(): Document loaded');
    setTimeout(self.waitForExternalInterface,500);
    setTimeout(self.beginInit,20);
  };

  this.beginInit = function() {
    if (self._initPending) return false;
    self.createMovie(); // ensure creation if not already done
    self._initMovie();
    self._initPending = true;
    return true;
  };

  this.domContentLoaded = function() {
    self._writeDebug('soundManager.domContentLoaded()');
    if (document.removeEventListener) document.removeEventListener('DOMContentLoaded',self.domContentLoaded,false);
    self.go();
  }

  this._externalInterfaceOK = function() {
    // callback from flash for confirming that movie loaded, EI is working etc.
    if (self.swfLoaded) return false;
    self._writeDebug('soundManager._externalInterfaceOK()');
    self.swfLoaded = true;
    self._tryInitOnFocus = false;
    if (self.isIE) {
      // IE needs a timeout OR delay until window.onload - may need TODO: investigating
	  setTimeout(self.init,100);
    } else {
      self.init();
    };
  };

  this._setSandboxType = function(sandboxType) {
    var sb = self.sandbox;
    sb.type = sandboxType;
    sb.description = sb.types[(typeof sb.types[sandboxType] != 'undefined'?sandboxType:'unknown')];
    self._writeDebug('Flash security sandbox type: '+sb.type);
    if (sb.type == 'localWithFile') {
      sb.noRemote = true;
      sb.noLocal = false;
      self._writeDebug('Flash security note: Network/internet URLs will not load due to security restrictions. Access can be configured via Flash Player Global Security Settings Page: http://www.macromedia.com/support/documentation/en/flashplayer/help/settings_manager04.html',2);
      } else if (sb.type == 'localWithNetwork') {
        sb.noRemote = false;
        sb.noLocal = true;
      } else if (sb.type == 'localTrusted') {
        sb.noRemote = false;
        sb.noLocal = false;
      };
  };

  this.destruct = function() {
    self._writeDebug('soundManager.destruct()');
    if (self.isSafari) {
      /* --
        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
       (Apparently related to ExternalInterface making calls to an unloading/unloaded page?)
       Unloading sounds (detaching handlers and so on) may help to prevent this
      -- */
      for (var i=self.soundIDs.length; i--;) {
        if (self.sounds[self.soundIDs[i]].readyState == 1) self.sounds[self.soundIDs[i]].unload();
      };
    };
    self.disable();
  };
  
  // SMSound (sound object)
  
  function SMSound(oSM,oOptions) {
  var self = this;
  var sm = oSM;
  this.sID = oOptions.id;
  this.url = oOptions.url;
  this.options = sm._mergeObjects(oOptions);
  if (sm.debugMode) {
    var stuff = null;
    var msg = [];
    var sF = null;
    var sfBracket = null;
    var maxLength = 64; // # of characters of function code to show before truncating
    for (stuff in this.options) {
      if (this.options[stuff] != null) {
        if (this.options[stuff] instanceof Function) {
	  // handle functions specially
	  sF = this.options[stuff].toString();
	  sF = sF.replace(/\s\s+/g,' '); // normalize spaces
	  sfBracket = sF.indexOf('{');
	  msg[msg.length] = ' '+stuff+': {'+sF.substr(sfBracket+1,(Math.min(Math.max(sF.indexOf('\n')-1,maxLength),maxLength))).replace(/\n/g,'')+'... }';
	} else {
	  msg[msg.length] = ' '+stuff+': '+this.options[stuff];
	};
      };
    };
    sm._writeDebug('SMSound() merged options: {\n'+msg.join(', \n')+'\n}');
  };

  this.id3 = {
   /* 
    Name/value pairs set via Flash when available - see reference for names:
    http://livedocs.macromedia.com/flash/8/main/wwhelp/wwhimpl/common/html/wwhelp.htm?context=LiveDocs_Parts&file=00001567.html
    (eg., this.id3.songname or this.id3['songname'])
   */
  };

  self.resetProperties = function(bLoaded) {
    self.bytesLoaded = null;
    self.bytesTotal = null;
    self.position = null;
    self.duration = null;
    self.durationEstimate = null;
    self.loaded = false;
    self.loadSuccess = null;
    self.playState = 0;
    self.paused = false;
    self.readyState = 0; // 0 = uninitialised, 1 = loading, 2 = failed/error, 3 = loaded/success
    self.didBeforeFinish = false;
    self.didJustBeforeFinish = false;
  };

  self.resetProperties();

  // --- public methods ---

  this.load = function(oOptions) {
    self.loaded = false;
    self.loadSuccess = null;
    self.readyState = 1;
    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?)
    var thisOptions = sm._mergeObjects(oOptions);
    if (typeof thisOptions.url == 'undefined') thisOptions.url = self.url;
    try {
      sm._writeDebug('loading '+thisOptions.url,1);
      sm.o._load(self.sID,thisOptions.url,thisOptions.stream,thisOptions.autoPlay,(thisOptions.whileloading?1:0));
    } catch(e) {
      sm._writeDebug('SMSound().load(): JS-Flash communication failed.',2);
    };
  };

  this.unload = function() {
    // Flash 8/AS2 can't "close" a stream - fake it by loading an empty MP3
    sm._writeDebug('SMSound().unload(): "'+self.sID+'"');
    self.setPosition(0); // reset current sound positioning
    sm.o._unload(self.sID,sm.nullURL);
    // reset load/status flags
    self.resetProperties();
  };

  this.play = function(oOptions) {
    if (!oOptions) oOptions = {};

    var thisOptions = sm._mergeObjects(oOptions, self.options); // inherit default createSound()-level options
    thisOptions = sm._mergeObjects(thisOptions); // merge with default SM2 options

    if (self.playState == 1) {
      var allowMulti = thisOptions.multiShot;
      if (!allowMulti) {
        sm._writeDebug('SMSound.play(): "'+self.sID+'" already playing? (one-shot)',1);
        return false;
      } else {
        sm._writeDebug('SMSound.play(): "'+self.sID+'" already playing (multi-shot)',1);
      };
    };
    if (!self.loaded) {
      if (self.readyState == 0) {
        sm._writeDebug('SMSound.play(): .play() before load request. Attempting to load "'+self.sID+'"',1);
        // try to get this sound playing ASAP
        thisOptions.stream = true;
        thisOptions.autoPlay = true;
        // TODO: need to investigate when false, double-playing
        // if (typeof oOptions.autoPlay=='undefined') thisOptions.autoPlay = true; // only set autoPlay if unspecified here
        self.load(thisOptions); // try to get this sound playing ASAP
      } else if (self.readyState == 2) {
        sm._writeDebug('SMSound.play(): Could not load "'+self.sID+'" - exiting',2);
        return false;
      } else {
        sm._writeDebug('SMSound.play(): "'+self.sID+'" is loading - attempting to play..',1);
      };
    } else {
      sm._writeDebug('SMSound.play(): "'+self.sID+'"');
    };
    if (self.paused) {
      self.resume();
    } else {
      self.playState = 1;
      self.position = (typeof thisOptions.position != 'undefined' && !isNaN(thisOptions.position)?thisOptions.position/1000:0);
      if (thisOptions.onplay) thisOptions.onplay.apply(self);
      self.setVolume(thisOptions.volume);
      self.setPan(thisOptions.pan);
      if (!thisOptions.autoPlay) {
        // sm._writeDebug('starting sound '+self.sID);
        sm.o._start(self.sID,thisOptions.loop||1,self.position); // TODO: verify !autoPlay doesn't cause issue
      };
    };
  };

  this.start = this.play; // just for convenience

  this.stop = function(bAll) {
    if (self.playState == 1) {
      self.playState = 0;
      self.paused = false;
      // if (sm.defaultOptions.onstop) sm.defaultOptions.onstop.apply(self);
      if (self.options.onstop) self.options.onstop.apply(self);
      sm.o._stop(self.sID);
    };
  };

  this.setPosition = function(nMsecOffset) {
    // sm._writeDebug('setPosition('+nMsecOffset+')');
    self.options.position = nMsecOffset; // update local options
    sm.o._setPosition(self.sID,nMsecOffset/1000,self.paused||!self.playState); // if paused or not playing, will not resume (by playing)
  };

  this.pause = function() {
    if (self.paused) return false;
    sm._writeDebug('SMSound.pause()');
    self.paused = true;
    sm.o._pause(self.sID);
  };

  this.resume = function() {
    if (!self.paused) return false;
    sm._writeDebug('SMSound.resume()');
    self.paused = false;
    sm.o._pause(self.sID); // flash method is toggle-based (pause/resume)
  };

  this.togglePause = function() {
    sm._writeDebug('SMSound.togglePause()');
    if (!self.playState) {
      self.play({position:self.position/1000});
      return false;
    };
    if (self.paused) {
      sm._writeDebug('SMSound.togglePause(): resuming..');
      self.resume();
    } else {
      sm._writeDebug('SMSound.togglePause(): pausing..');
      self.pause();
    };
  };

  this.setPan = function(nPan) {
    if (typeof nPan == 'undefined') nPan = 0;
    sm.o._setPan(self.sID,nPan);
    self.options.pan = nPan;
  };

  this.setVolume = function(nVol) {
    if (typeof nVol == 'undefined') nVol = 100;
    sm.o._setVolume(self.sID,nVol);
    self.options.volume = nVol;
  };

  // --- "private" methods called by Flash ---

  this._whileloading = function(nBytesLoaded,nBytesTotal,nDuration) {
    self.bytesLoaded = nBytesLoaded;
    self.bytesTotal = nBytesTotal;
    self.duration = nDuration;
    self.durationEstimate = parseInt((self.bytesTotal/self.bytesLoaded)*self.duration); // estimate total time (will only be accurate with CBR MP3s.)
    if (self.readyState != 3 && self.options.whileloading) self.options.whileloading.apply(self);
    // soundManager._writeDebug('duration/durationEst: '+self.duration+' / '+self.durationEstimate);
  };

  this._onid3 = function(oID3PropNames,oID3Data) {
    // oID3PropNames: string array (names)
    // ID3Data: string array (data)
    sm._writeDebug('SMSound()._onid3(): "'+this.sID+'" ID3 data received.');
    var oData = [];
    for (var i=0,j=oID3PropNames.length; i<j; i++) {
      oData[oID3PropNames[i]] = oID3Data[i];
      // sm._writeDebug(oID3PropNames[i]+': '+oID3Data[i]);
    };
    self.id3 = sm._mergeObjects(self.id3,oData);
    if (self.options.onid3) self.options.onid3.apply(self);
  };

  this._whileplaying = function(nPosition) {
    if (isNaN(nPosition) || nPosition == null) return false; // Flash may return NaN at times
    self.position = nPosition;
    if (self.playState == 1) {
      if (self.options.whileplaying) self.options.whileplaying.apply(self); // flash may call after actual finish
      if (self.loaded && self.options.onbeforefinish && self.options.onbeforefinishtime && !self.didBeforeFinish && self.duration-self.position <= self.options.onbeforefinishtime) {
        sm._writeDebug('duration-position &lt;= onbeforefinishtime: '+self.duration+' - '+self.position+' &lt= '+self.options.onbeforefinishtime+' ('+(self.duration-self.position)+')');
        self._onbeforefinish();
      };
    };
  };

  this._onload = function(bSuccess) {
    bSuccess = (bSuccess==1?true:false);
    sm._writeDebug('SMSound._onload(): "'+self.sID+'"'+(bSuccess?' loaded.':' failed to load? - '+self.url));
    if (!bSuccess) {
      if (sm.sandbox.noRemote == true) {
        sm._writeDebug('SMSound._onload(): Reminder: Flash security is denying network/internet access',1);
      };
      if (sm.sandbox.noLocal == true) {
        sm._writeDebug('SMSound._onload() Reminder: Flash security is denying local access',1);
      };
    };
    self.loaded = bSuccess;
    self.loadSuccess = bSuccess;
    self.readyState = bSuccess?3:2;
    if (self.options.onload) self.options.onload.apply(self);
  };

  this._onbeforefinish = function() {
    if (!self.didBeforeFinish) {
      self.didBeforeFinish = true;
      if (self.options.onbeforefinish) self.options.onbeforefinish.apply(self);
    };
  };

  this._onjustbeforefinish = function(msOffset) {
    // msOffset: "end of sound" delay actual value (eg. 200 msec, value at event fire time was 187)
    if (!self.didJustBeforeFinish) {
      self.didJustBeforeFinish = true;
      // soundManager._writeDebug('SMSound._onjustbeforefinish()');
      if (self.options.onjustbeforefinish) self.options.onjustbeforefinish.apply(self);;
    };
  };

  this._onfinish = function() {
    // sound has finished playing
    sm._writeDebug('SMSound._onfinish(): "'+self.sID+'"');
    self.playState = 0;
    self.paused = false;
    if (self.options.onfinish) self.options.onfinish.apply(self);
    if (self.options.onbeforefinishcomplete) self.options.onbeforefinishcomplete.apply(self);
    // reset some state items
    self.setPosition(0);
    self.didBeforeFinish = false;
    self.didJustBeforeFinish = false;
  };

  }; // SMSound()

  // register a few event handlers
  if (window.addEventListener) {
    window.addEventListener('focus',self.handleFocus,false);
    window.addEventListener('load',self.beginDelayedInit,false);
    window.addEventListener('beforeunload',self.destruct,false);
    if (self._tryInitOnFocus) window.addEventListener('mousemove',self.handleFocus,false); // massive Safari focus hack
  } else if (window.attachEvent) {
    window.attachEvent('onfocus',self.handleFocus);
    window.attachEvent('onload',self.beginDelayedInit);
    window.attachEvent('beforeunload',self.destruct);
  } else {
    // no add/attachevent support - safe to assume no JS -> Flash either.
    soundManager.onerror();
    soundManager.disable();
  };

  if (document.addEventListener) document.addEventListener('DOMContentLoaded',self.domContentLoaded,false);

}; // SoundManager()

var soundManager = new SoundManager();


/* *******************************************************

 jsAMP - V0.9a.20071010 - "Technology Preview" :D
 ---------------------------------------------------------
 An MP3 player implementation using the SoundManager 2 API

 http://www.schillmania.com/projects/soundmanager2/

 *********************************************************

 GENERAL DISCLAIMER: This isn't finished yet.
 --------------------------------------------------------
 In the meantime, check the SM2 project at the above URL.

*/

function SMUtils() {
  var self = this;
  this.isSafari = navigator.userAgent.match(/safari/);
  this.isMac = navigator.platform.match(/mac/);
  this.isIE = (navigator.appVersion.match(/MSIE/) && !navigator.userAgent.match(/Opera/));
  this.isNewIE = (this.isIE && !this.isMac && (!navigator.userAgent.match(/MSIE (5|6)/)));
  this.isOldIE = (this.isIE && !this.isNewIE);

  this.$ = function(sID) {
    return document.getElementById(sID);
  }

  this.isChildOf = function(oChild,oParent) {
    while (oChild.parentNode && oChild != oParent) {
      oChild = oChild.parentNode;
    }
    return (oChild == oParent);
  }

  this.addEventHandler = function(o,evtName,evtHandler) {
    typeof(attachEvent)=='undefined'?o.addEventListener(evtName,evtHandler,false):o.attachEvent('on'+evtName,evtHandler);
  }

  this.removeEventHandler = function(o,evtName,evtHandler) {
    typeof(attachEvent)=='undefined'?o.removeEventListener(evtName,evtHandler,false):o.detachEvent('on'+evtName,evtHandler);
  }

  this.classContains = function(o,cStr) {
    return (typeof(o.className)!='undefined'?o.className.indexOf(cStr)+1:false);
  }

  this.addClass = function(o,cStr) {
    if (!o) return false; // safety net
    if (self.classContains(o,cStr)) return false;
    o.className = (o.className?o.className+' ':'')+cStr;
  }

  this.removeClass = function(o,cStr) {
    if (!o) return false; // safety net
    if (!self.classContains(o,cStr)) return false;
    o.className = o.className.replace(new RegExp('( '+cStr+')|('+cStr+')','g'),'');
  }

  this.getElementsByClassName = function(className,tagNames,oParent) {
    var doc = (oParent||document);
    var matches = [];
    var i,j;
    var nodes = [];
    if (typeof(tagNames)!='undefined' && typeof(tagNames)!='string') {
      for (i=tagNames.length; i--;) {
        if (!nodes || !nodes[tagNames[i]]) {
          nodes[tagNames[i]] = doc.getElementsByTagName(tagNames[i]);
        }
      }
    } else if (tagNames) {
      nodes = doc.getElementsByTagName(tagNames);
    } else {
      nodes = doc.all||doc.getElementsByTagName('*');
    }
    if (typeof(tagNames)!='string') {
      for (i=tagNames.length; i--;) {
        for (j=nodes[tagNames[i]].length; j--;) {
          if (self.classContains(nodes[tagNames[i]][j],className)) {
            matches[matches.length] = nodes[tagNames[i]][j];
          }
        }
      }
    } else {
      for (i=0; i<nodes.length; i++) {
        if (self.classContains(nodes[i],className)) {
          matches[matches.length] = nodes[i];
        }
      }
    }
    return matches;
  }

  this.getOffX = function(o) {
    // http://www.xs4all.nl/~ppk/js/findpos.html
    var curleft = 0;
    if (o.offsetParent) {
      while (o.offsetParent) {
        curleft += o.offsetLeft;
        o = o.offsetParent;
      }
    }
    else if (o.x) curleft += o.x;
    return curleft;
  }

  this.getOffY = function(o) {
    // http://www.xs4all.nl/~ppk/js/findpos.html
    var curtop = 0;
    if (!o) return false;
    if (o.offsetParent) {
      while (o.offsetParent) {
       curtop += o.offsetTop;
       o = o.offsetParent;
      }
    }
    else if (o.y) curtop += o.y;
    return curtop;
  }

  this.setOpacity = this.isIE?function(o,nOpacity) {
    o.style.filter = 'alpha(opacity='+nOpacity+')';
  }:function(o,nOpacity) {
    o.style.opacity = nOpacity/100;
  }

  this.copy = function(oArray) {
    // there *must* be a cleaner way to do this..
    var o2 = [];
    for (var i=0,j=oArray.length; i<j; i++) {
      o2[i] = oArray[i];
    }
    return o2;
  }

};

var smUtils = new SMUtils();

function SMPlayer(oSoundPlayer) {
  var self = this;
  this.oParent = oSoundPlayer;
  var u = smUtils; // alias
  var getEBCN = u.getElementsByClassName;
  this.oMain = u.$('player-template');
  this.o = this.oMain.getElementsByTagName('div')[0];
  this.oLeft = getEBCN('left','div',this.o)[0];
  this.oBar = getEBCN('mid','div',this.o)[0];
  this.oSlider = getEBCN('slider','a',this.o)[0];
  this.oSlider.style.display = 'block';
  this.oTitle = getEBCN('caption','span',this.oBar)[0];
  this.oSeek = getEBCN('seek','div',this.oBar)[0];
  this.oDivider = getEBCN('divider','div',this.oBar)[0];
  this.sFormat = (this.oTitle.innerHTML||'%artist - %title');
  this.sFormatSeek = (this.oSeek.innerHTML||'%{time1}/%{time2} (%{percent}%)');
  this.oProgress = getEBCN('progress','div',this.oBar)[0];
  this.oRight = getEBCN('right','div',this.o)[0];
  this.oTime = getEBCN('time','div',this.o)[0];
  this.oShuffle = getEBCN('shuffle','a',this.o)[0];
  this.oRepeat = getEBCN('loop','a',this.o)[0];
  this.oMute = getEBCN('mute','a',this.o)[0];
  this.oVolume = getEBCN('volume','a',this.o)[0];
  this.lastTime = 0;
  this.scale = 100;
  this.percentLoaded = 0;
  this.gotTimeEstimate = 0;
  this.offX = 0;
  this.x = 0;
  this.xMin = 0;
  this.barWidth = self.oBar.offsetWidth;
  this.xMax = self.barWidth-self.oSlider.offsetWidth;
  this.xMaxLoaded = 0;
  // this.value = 0;
  this.timer = null;
  this._className = this.oBar.className;
  this.tween = [];
  this.frame = 0;
  this.playState = 0;
  this.busy = false; // when being dragged/animated/moved by user
  this.maxOpacity = 100; // barber pole opacity (when animating in)
  this.didDrag = false;
  this.coords = {
   'x': 0,
   'y': 0,
   'offX':0,
   'offY':0,
   'titleWidth': 0
  }
  this.muted = false;
  this.volume = soundManager.defaultOptions.volume;

  var useAltFont = u.isMac; // specific letter-spacing CSS tweak (OSX has better-kerned/tighter spacing)

  // set default caption
  try {
    // normalize some wackiness betweeen Firefox and Safari in true/strict XHTML mode (writing to innerHTML should probably fail, but FF allows it.)
    this.oTitle.innerHTML = getEBCN('default','div',this.oBar)[0].innerHTML;
  } catch(e) {
    this.oTitle.innerText = getEBCN('default','div',this.oBar)[0].innerText;
  }
  this.oTitle.style.visibility = 'visible';

  this.over = function() {
    this.className = self._className+' hover';
    event.cancelBubble=true;
    return false;
  }

  this.out = function() {
    this.className = self._className;
    event.cancelBubble=true;
    return false;
  }

  this.down = function(e) {
    if (!self.oParent.currentSound) return false;
    self.didDrag = false;
    var e = e?e:event;
    self.offX = e.clientX-(u.getOffX(self.oSlider)-u.getOffX(self.oBar));
    self.busy = true;
    u.addClass(self.oSlider,'active');
    self.refreshSeek();
    self.setSeekVisibility(1);
    u.addEventHandler(document,'mousemove',self.move);
    u.addEventHandler(document,'mouseup',self.up);
    e.stopPropgation?e.stopPropagation():e.cancelBubble=true;
    return false;
  }

  this.barDown = function(e) {
    var e=e?e:event;
    self.didDrag = false;
    self.coords.x = e.clientX;
    self.coords.y = e.clientY;
    self.coords.offX = e.clientX-u.getOffX(self.oMain)+parseInt(((window.innerWidth?window.innerWidth:document.documentElement.clientWidth||document.body.clientWidth||document.body.scrollWidth)-$('nav').offsetWidth)/2);
    self.coords.offY = e.clientY-u.getOffY(self.oMain);
    u.addEventHandler(document,'mousemove',self.barMove);
    u.addEventHandler(document,'mouseup',self.barUp);
    return false;
  }

  this.barMove = function(e) {
    var e=e?e:event;
    if (!self.didDrag) {
      if (Math.abs(e.clientX-self.coords.x)<3 && Math.abs(e.clientY-self.coords.y)<3) {
        // drag threshold
        return false;
      } else {
        self.didDrag = true;
      }
    }
    self.oMain.style.left = (e.clientX-self.coords.offX)+'px';
    self.oMain.style.top = (e.clientY-self.coords.offY)+'px';
    e.stopPropgation?e.stopPropagation():e.cancelBubble=true;
    return false;
  }

  this.barUp = function(e) {
    u.removeEventHandler(document,'mousemove',self.barMove);
    u.removeEventHandler(document,'mouseup',self.barUp);
  }

  this.barClick = function(e) {
    if (!self.oParent.currentSound) return false;
    if (self.didDrag) return false;
    var tgt = (e?e.target:event.srcElement);
    var e=e?e:event;
    if (tgt.tagName.toLowerCase()=='a') return false; // ignore clicks on links (eg. dragging slider)
    var xNew = Math.min(e.clientX-u.getOffX(self.oBar),self.xMaxLoaded);
    self.slide(self.x,xNew);
  }

  // volume x/y offsets
  this.volumeX = 0;
  this.volumeWidth = 0;

  this.volumeDown = function(e) {
    // set initial volume based on offset?
    self.volumeX = u.getOffX(self.oVolume);
    self.volumeWidth = parseInt(self.oVolume.offsetWidth);
    soundManager._writeDebug('offsets: '+self.volumeX+', '+self.volumeWidth);
    document.onmousemove = self.volumeMove;
    document.onmouseup = self.volumeUp;
    self.volumeMove(e);
    return false;
  }

  this.volumeMove = function(e) {
    // set volume based on position
    var e = e?e:event;
    var vol = ((e.clientX-self.volumeX)/(self.volumeWidth));
    vol = Math.min(1,Math.max(0,vol));
    self.setVolume(vol*100);
    return false;
  }

  this.volumeUp = function(e) {
    var e = e?e:event;
    document.onmousemove = null;
    document.onmouseup = null;
    return false;
  }

  this.setVolume = function(nVol) {
    if (!self.oParent.currentSound || self.volume == nVol) return false;
    soundManager.defaultOptions.volume = nVol;
    soundManager._writeDebug('soundManager.setVolume('+nVol+')');
    self.volume = nVol;
    if (!self.muted) soundManager.setVolume(self.oParent.currentSound,nVol);
    u.setOpacity(self.oVolume,nVol);
  }

  this.move = function(e) {
    var e=e?e:event;
    var x = e.clientX-self.offX;
    if (x>self.xMaxLoaded) {
      x = self.xMaxLoaded;
    } else if (x<self.xMin) {
      x = self.xMin;
    }
    if (x != self.x) {
      self.moveTo(x);
      if (self.oParent.options.allowScrub) self.doScrub();
      self.refreshSeek();
    }
    e.stopPropgation?e.stopPropagation():e.cancelBubble=true;
    return false;
  }

  this.up = function(e) {
    u.removeEventHandler(document,'mousemove',self.move);
    u.removeEventHandler(document,'mouseup',self.up);
    u.removeClass(self.oSlider,'active');
    self.busy = false;
    if (!self.oParent.options.allowScrub || self.oParent.paused) self.oParent.onUserSetSlideValue(self.x); // notify parent of update
    self.setSeekVisibility();
    return false;
  }

  this.slide = function(x0,x1) {
    self.tween = animator.createTween(x0,x1);
    self.busy = true;
    self.slideLastExec = new Date();
    animator.addMethod(self.animate,self.animateComplete);
    animator.start();
  }

  this.refreshSeek = function() {
    var sData = self.sFormatSeek;
    var oSound = soundManager.getSoundById(self.oParent.currentSound);
// soundManager._writeDebug('oSound.duration: '+oSound.duration);
// soundManager._writeDebug(self.x+','+self.xMaxLoaded+','+oSound.duration+','+oSound.durationEstimate);
    var sliderMSec = self.x/self.xMaxLoaded*oSound.duration;
    var attrs = {
      'time1': self.getTime(sliderMSec,true),
      'time2': (!oSound.loaded?'~':'')+self.getTime(oSound.durationEstimate,true),
      'percent': Math.floor(sliderMSec/oSound.durationEstimate*100)
    }
    // soundManager._writeDebug(attrs.time1+' / '+attrs.time2+' / '+attrs.percent);
    for (var attr in attrs) {
      data = attrs[attr];
      if (self.isEmpty(data)) data = '!null!';
      sData = sData.replace('\%\{'+attr+'\}',data);
    }
    // remove any empty/null fields
    var aData = sData.split(' ');
    for (var i=aData.length; i--;) {
      if (aData[i].indexOf('!null!')+1) aData[i] = null;
    }
    self.oSeek.innerHTML = aData.join(' ');
  }

  this.setSeekVisibility = function(bVisible) {
    self.oTitle.style.visibility = bVisible?'hidden':'visible';
    self.oSeek.style.display = bVisible?'block':'none';
  }

  this.animateComplete = function() {
    self.busy = false;
    // set sound position, if needed
    if (self.oParent) self.oParent.onUserSetSlideValue(self.x);
  }

  this.moveTo = function(x) {
    self.x = x;
    self.oSlider.style.marginLeft = (Math.floor(x)+1)+'px'; // 1 offset
  }

  this.moveToEnd = function() {
    self.moveTo(self.xMax);
  }

  this.slideLastExec = new Date();

  this.animate = function() {
    self.moveTo(self.tween[self.frame]);
    self.frame = Math.max(++self.frame,animator.determineFrame(self.slideLastExec,40));
    // if (self.frame++>=self.tween.length-1) {
    if (self.frame>=self.tween.length-1) {
      self.active = false;
      self.frame = 0;
      if (self._oncomplete) self._oncomplete();
      return false;
    }
    return true;
  }

  this.doScrub = function(t) {
    if (self.oParent.paused) return false;
    if (self.oParent.options.scrubThrottle) {
      if (!self.timer) self.timer = setTimeout(self.scrub,t||20);
    } else {
      self.scrub();
    }
  }

  this.scrub = function() {
    self.timer = null;
    self.oParent.onUserSetSlideValue(self.x)
  }

  this.randomize = function() {
    self.slide(self.x,parseInt(Math.random()*self.xMax));
  }

  this.getTimeEstimate = function(oSound) {
    // try to estimate song length within first 128 KB (or total bytes), updating n times
    var byteCeiling = Math.min(1048576||oSound.bytes);
    var samples = (byteCeiling==oSound.bytes?2:4);
    var milestone = Math.floor(oSound.bytesLoaded/byteCeiling*samples);
    if (oSound.bytesLoaded>byteCeiling && self.gotTimeEstimate>0) return false;
    if (self.gotTimeEstimate == milestone) return false;
    self.gotTimeEstimate = milestone;
    self.setMetaData(oSound);
  }

  this.getTime = function(nMSec,bAsString) {
    // convert milliseconds to mm:ss, return as object literal or string
    var nSec = Math.floor(nMSec/1000);
    var min = Math.floor(nSec/60);
    var sec = nSec-(min*60);
    if (min == 0 && sec == 0) return null; // return 0:00 as null
    return (bAsString?(min+':'+(sec<10?'0'+sec:sec)):{'min':min,'sec':sec});
  }

  this.updateTime = function(nMSec) {
    // update "current playing" time
    self.lastTime = nMSec;
    self.oTime.innerHTML = (self.getTime(nMSec,true)||'0:00');
  }

  this.setTitle= function(sTitle) {
    // used in the absence of ID3 info
    self.oTitle.innerHTML = unescape(sTitle);
    self.titleString = unescape(sTitle);
    self.refreshScroll();
  }

  this.isEmpty = function(o) {
    return (typeof o == 'undefined' || o == null || o == 'null' || (typeof o == 'string' && o.toLowerCase() == 'n/a' || o.toLowerCase == 'undefined'));
  }

  self.setMetaData = function(oSound) {
    // get id3 data and populate according to formatting string (%artist - %title [%album] etc.)
    var friendlyAttrs = {
     // ID3V1 inherits from ID3V2 if populated
     'title': 'songname', // songname/TIT2
     'artist': 'artist', // artist/TPE1
     'album': 'album', // album/TALB
     'track': 'track', // track/TRCK
     'year': 'year', // year/TYER
     'genre': 'genre', // genre/TCON
     'comment': 'comment', // comment/COMM
     'url': 'WXXX'
    }
    var sTime = self.getTime(oSound.durationEstimate,true);
    sTime = (sTime && !oSound.loaded?'~':'')+sTime;
    var metaAttrs = {
      // custom attributes taken directly from sound data
      'time': sTime // get time as mm:ss
    }
    // get normalised data, build string, replace
    var sData = self.sFormat; // eg. %{artist} - %{title}
    var data = null;
    var useID3 = (!self.isEmpty(oSound.id3.songname) && !self.isEmpty(oSound.id3.artist)); // artist & title must be present to consider using ID3
    for (var attr in friendlyAttrs) {
      data = oSound.id3[friendlyAttrs[attr]];
      if (self.isEmpty(data)) data = '!null!';
      sData = sData.replace('\%\{'+attr+'\}',data);
    }
    for (var attr in metaAttrs) {
      data = metaAttrs[attr];
      if (self.isEmpty(data)) data = '!null!';
      sData = sData.replace('\%\{'+attr+'\}',data);
    }
    // remove any empty/null fields
    var aData = sData.split(' ');
    for (var i=aData.length; i--;) {
      if (aData[i].indexOf('!null!')+1) aData[i] = null;
    }
    var sMetaData = (useID3?unescape(aData.join(' ')):unescape(self.oParent.oPlaylist.getCurrentItem().userTitle)+(!self.isEmpty(metaAttrs.time)?' ('+metaAttrs.time+')':'')).replace(/\s+/g,' ');
    self.oTitle.innerHTML = sMetaData;
    self.titleString = sMetaData;
    self.oParent.oPlaylist.getCurrentItem().setTooltip(sMetaData);
    self.refreshScroll();
  }

  this.setLoadingProgress = function(nPercentage) {
// soundManager._writeDebug('setLoadingProgress(): '+nPercentage);
    self.percentLoaded = nPercentage;
    self.xMaxLoaded = self.percentLoaded*self.xMax;
    self.oProgress.style.width = parseInt(nPercentage*self.barWidth)+'px';
  }

  this.setLoading = function(bLoading) {
    if (self.isLoading == bLoading) return false;
    self.isLoading = bLoading;
    var f = bLoading?u.addClass:u.removeClass;
    f(self.oProgress,'loading');
    self.setLoadingAnimation(bLoading);
  }

  this.setLoadingAnimation = function(bLoading) {
    soundManager._writeDebug('setLoadingAnimation(): '+bLoading);
    if (bLoading) {
      self.loadingTween = self.loadingTweens[0];
      animator.addMethod(self.loadingAnimate);
      animator.addMethod(self.loadingAnimateSlide,self.loadingAnimateSlideComplete);
      animator.start();
    } else {
      self.loadingTween = self.loadingTweens[1];
      if (self.loadingAnimateFrame>0) {
        // reverse animation while active
        // self.loadingTween.reverse();
        self.loadingAnimateFrame = (self.loadingTween.length-self.loadingAnimateFrame);
      } else {
        self.loadingTween = self.loadingTweens[1];
        animator.addMethod(self.loadingAnimateSlide,self.loadingAnimateSlideComplete);
      }
    }
  }

  this.loadingAnimate = function() {
    var d = new Date();
    if (d-self.loadingLastExec<50) return true; // throttle fps
    self.loadingLastExec = d;
    self.loadingX--;
    self.oProgress.style.backgroundPosition = self.loadingX+'px '+self.loadingY+'px';
    return self.isLoading;
  }

  this.loadingLastExec = new Date();
  this.loadingTweens = [animator.createTween(0,self.maxOpacity),animator.createTween(self.maxOpacity,0)];
  this.loadingDirection = 0;
  this.loadingTween = this.loadingTweens[this.loadingDirection];
  this.loadingAnimateFrame = 0;

  this.loadingAnimateSlide = function() {
    var d = new Date();
    if (d-self.loadingLastExec<50) return true; // throttle to 20fps
    u.setOpacity(self.oProgress,self.loadingTween[self.loadingAnimateFrame++]);
    if (!self.isLoading) self.loadingAnimate(); // show update if not actively loading
    self.loadingLastExec = d; // updates time, prevents loadingAnimate()
    return (++self.loadingAnimateFrame<self.loadingTweens[0].length);
  }

  this.loadingAnimateSlideComplete = function() {
    soundManager._writeDebug('loadingAnimateSlideComplete()');
    self.loadingAnimateFrame = 0;
    // self.loadingDirection = !self.loadingDirection;
    self.loadingX = 0;
  }

  this.isLoading = false;
  this.loadingTimer = null;
  this.loadingX = 0;
  this.loadingY = 0;

  this.setPlayState = function(bPlayState) {
    soundManager._writeDebug('SMPlayer.setPlayState('+bPlayState+')');
    self.playState = bPlayState;
    self.oLeft.getElementsByTagName('span')[0].className = (self.playState?'playing':'');
  }

  this.togglePause = function() {
    soundManager._writeDebug('togglePause()');
    if (self.oParent.currentSound) {
      soundManager.togglePause(self.oParent.currentSound);
    } else {
      self.oParent.oPlaylist.playNextItem();
    }
    var isPaused = soundManager.getSoundById(self.oParent.currentSound).paused;
    self.oParent.paused = isPaused;
    self.setPlayState(!isPaused);
  }

  this.toggleShuffle = function() {
    soundManager._writeDebug('SMPlayer.toggleShuffle()');
    self.oParent.oPlaylist.toggleShuffle();
    self.setShuffle(self.oParent.oPlaylist.doShuffle);
  }

  this.toggleRepeat = function() {
    soundManager._writeDebug('SMPlayer.toggleRepeat()');
    self.oParent.oPlaylist.toggleRepeat();
    self.setRepeat(self.oParent.oPlaylist.doRepeat);
  }

  this.toggleMute = function() {
    soundManager._writeDebug('SMPlayer.toggleMute()');
    self.muted = !self.muted;
    var nVol = self.muted?0:self.volume;
    if (self.oParent.currentSound) soundManager.setVolume(self.oParent.currentSound,nVol);
    soundManager.defaultOptions.volume = nVol; // update global volume
    self.setMute(self.muted);
  }

  this.togglePlaylist = function() {
    // show UI changes here in main player?
    soundManager._writeDebug('SMPlayer.togglePlaylist()');
  }

  this.setShuffle = function(bShuffle) {
    var f = (bShuffle?u.addClass:u.removeClass);
    f(self.oShuffle,'active');
  }

  this.setRepeat = function(bRepeat) {
    var f = (bRepeat?u.addClass:u.removeClass);
    f(self.oRepeat,'active');
  }

  this.setMute = function(bMute) {
    var f = (bMute?u.addClass:u.removeClass);
    f(self.oMute,'active');
  }

  this.scrollOffset = 0;
  this.scrollOffsetMax = self.oBar.offsetWidth;
  this.scrollInterval = 100;
  this.scrollAmount = 2; // pixels
  this.scrollLastExec = new Date();
  this.scrollTimer = null;
  this.isScrolling = null;

  this.scrollTo = function(nOffset) {
    self.oTitle.style.marginLeft = (nOffset*-1)+'px';
    // soundManager._writeDebug('scrollTo(): '+nOffset);
    self.refreshDocumentTitle();
  }

  var tmp = document.createElement('p');
  tmp.innerHTML = ' '; // &nbsp;
  var nbsp = tmp.innerHTML;

  this.refreshDocumentTitle = function(nOffset) {
    var offset = (typeof nOffset != 'undefined'?nOffset:null);
    var str = (self.titleString).substr(nOffset != null?nOffset:Math.max(self.scrollOffset-self.scrollAmount,0));
    str = str.replace(/ /i,' ');
    if (self.oParent.options.usePageTitle) {
      try {
        document.title = str; // str.replace(/&nbsp;/i,' ');
      } catch(e) {
        // oh well
      }
    }
  }

  this.doScroll = function() {
    var d = new Date();
    if (d-self.scrollLastExec<self.scrollInterval) return true; // throttle
    self.scrollLastExec = d;
    self.scrollOffset += self.scrollAmount;
    if (self.scrollOffset>self.coords.titleWidth) {
      // soundManager._writeDebug('wrapping around');
      self.scrollOffset = (smUtils.isIE?5:1);
    }
    self.scrollTo(self.scrollOffset);
    return self.isScrolling;
  }

  this.resetScroll = function() {
    soundManager._writeDebug('resetScroll()');
    self.scrollOffset = 0;
    self.scrollTo(self.scrollOffset);
    self.refreshDocumentTitle(0);
  }

  this.setScroll = function(bScroll) {
    soundManager._writeDebug('setScroll('+bScroll+')');
    if (bScroll && !self.isScrolling) {
      soundManager._writeDebug('starting scroll');
      self.isScrolling = true;
      animator.addMethod(self.doScroll,self.resetScroll);
      animator.start();
    }
    if (!bScroll && self.isScrolling) {
      soundManager._writeDebug('stopping scroll');
      self.isScrolling = false;
    }
  }

  this.titleString = ''; // for document title

  this.refreshScroll = function() {
    // self.scrollOffsetMax = 25; // self.oTitle.innerHTML.length;
    // soundManager._writeDebug('refreshScroll(): '+self.scrollOffsetMax);
    self.coords.titleWidth = self.oTitle.offsetWidth;
    var doScroll = (self.coords.titleWidth>self.scrollOffsetMax);
    if (doScroll) {
      var sHTML = self.oTitle.innerHTML;
      var dHTML = self.oDivider.innerHTML; // heh
      self.oTitle.innerHTML = sHTML+dHTML;
      self.coords.titleWidth = self.oTitle.offsetWidth;
      self.setScroll(doScroll);
      self.titleString = sHTML;
      self.oTitle.innerHTML = sHTML+dHTML+sHTML;
    } else {
      self.setScroll(doScroll);
      self.titleString = self.oTitle.innerHTML;
    }    
    // if (doScroll) self.oTitle.innerHTML = (self.oTitle.innerHTML+' *** '+self.oTitle.innerHTML); // fake the "repeat"
  }

  this.reset = function() {
    soundManager._writeDebug('SMPlayer.reset()');
    if (self.x != 0) self.moveTo(0);
    self.setLoadingProgress(0);
    self.gotTimeEstimate = 0;
    self.updateTime(0);
    self.resetScroll();
  }

  this.destructor = function() {
    self.oBar.onmouseover = null;
    self.oBar.onmouseout = null;
    self.o.onmousedown = null;
    self.o.ondblclick = null;
    self.o = null;
    self.oV = null;
    self.oB.onclick = null;
    self.oB = null;
  }

  if (u.isIE) {
    // IE is lame, no :hover
    this.oBar.onmouseover = this.over;
    this.oBar.onmouseout = this.out;
  }

  if (u.isSafari) u.addClass(this.oMain,'noOpacity'); // stupid transparency tweak
  if (useAltFont) u.addClass(this.oMain,'altFont');

  // this.setScroll(true); // test

  this.oSlider.onmousedown = this.down;
  this.oBar.onmousedown = this.barDown;
  this.oBar.onclick = this.barClick;
  this.oBar.ondblclick = this.oParent.togglePlaylist;
//  self.update();

  // start scrolling, if needed
  self.refreshScroll();

}

function Animator() {
  var self = this;
  this.timer = null;
  this.active = null;
  this.methods = [];
  this.tweenStep = [1,2,3,4,5,6,7,8,9,10,9,8,7,6,5,4,3,2];
  this.frameCount = this.tweenStep.length;
  // this.lastExec = new Date();
  
  this.start = function() {
    if (self.active==true) return false;
    self.active = true;
    self.timer = window.setInterval(self.animate,20);
  }

  this.stop = function() {
    if (self.timer) {
      window.clearInterval(self.timer);
      self.timer = null;
      self.active = false;
    }
  }

  this.reset = function() {
    self.methods = [];
  }

  this.addMethod = function(oMethod,oncomplete) {
    for (var i=self.methods.length; i--;) {
      if (self.methods[i] == oMethod) {
        if (oncomplete) {
          self.methods[i]._oncomplete = oncomplete;
        }
        return false;
      }
    }
    self.methods[self.methods.length] = oMethod;
    self.methods[self.methods.length-1]._oncomplete = oncomplete||null;
  }

  this.createTween = function(start,end) {
    var start = parseInt(start);
    var end = parseInt(end);
    var tweenStepData = self.tweenStep;
    var tween = [start];
    var tmp = start;
    var diff = end-start;
    var j = tweenStepData.length;
    var isAscending = end>start;
    for (var i=0; i<j; i++) {
      tmp += diff*tweenStepData[i]*0.01;
      tween[i] = parseInt(tmp);
      // floor/ceiling checks (rounding errors?)
      if (isAscending) {
        if (tween[i]>end) tween[i] = end;
      } else {
        if (tween[i]<end) tween[i] = end;
      }
    }
    if (tween[i] != end) tween[i] = end;
    return tween;
  }

  this.determineFrame = function(tStart,nInterval) {
    var d = new Date();
    // var tElapsed = (new Date()-tStart);
    // determine current frame, including lag

    return Math.min(self.frameCount,Math.floor(self.frameCount*((new Date()-tStart)/(nInterval*self.frameCount))));
  }
  
  this.animate = function(e) { 
    if (!self.active) return false;
    /*
    var now = new Date();
    if (now-self.lastExec<50) return false; // no more than 20 fps
    self.lastExec = now;
    */
    var active = false;
    for (var i=self.methods.length; i--;) {
      if (self.methods[i]) {
        if (self.methods[i]()) {
          active = true;
        } else {
          if (self.methods[i]._oncomplete) {
            self.methods[i]._oncomplete();
            self.methods[i]._oncomplete = null;
          }
          self.methods[i] = null;
        }
      }
    }
    if (!active) {
      self.stop();
      self.reset();
    }
  }

}

var animator = new Animator();

function SPPlaylist(oSoundPlayer,oPlaylist) {
  var self = this;
  var oParent = oSoundPlayer;
  this.o = null;
  this.links = [];
  this.items = [];
  this.playlistItems = []; // pointer
  this.playlistItemsUnsorted = [];
  this.playlistItemsShuffled = [];
  this.index = -1;
  this.lastIndex = null;
  this.o = oPlaylist; // containing element
  this.history = [];
  this.isVisible = false;
  this.doShuffle = false;
  this.doRepeat = false;
  this._ignoreCurrentSound = false;

  var seamlessDelay = 0; // offset for justBeforeFinish

  this.findURL = function(sURL) {
    for (var i=self.items.length; i--;) {
      if (self.items[i].url == sURL) return true;
    }
    return false;
  }

  this.addItem = function(oOptions) {
    // oOptions = {url:string,name:string}
    var sURL = oOptions.url||null;
    var sName = oOptions.name||null;
    if (!sURL || self.findURL(sURL)) return false;
    self.items[self.items.length] = {
      url: sURL,
      name: (sName||sURL.substr(sURL.lastIndexOf('/')+1))
    }
    soundManager._writeDebug('SPPlaylist().addItem('+self.items[self.items.length-1].url+')');
  }

  this.getItem = function(sURL) {
    for (var i=self.items.length; i--;) {
      if (self.items[i].url == sURL) return self.items[i];
    }
    return null;
  }

  this.getCurrentItem = function() {
    return self.playlistItems[self.index];
  }

  this.getRandomItem = function() {
    return parseInt(Math.random()*self.items.length);
  }

  this.calcNextItem = function() {
    var nextItem = self.index+1;
    if (nextItem >= self.items.length) nextItem = -1;
    return nextItem;
  }

  this.getNextItem = function() {
    self.index++;
    if (self.index>=self.items.length) {
      self.index = -1; // reset
      return false;
    }
    return true;
  }

  this.calcPreviousItem = function() {
    var prevItem = self.index-1;
    if (prevItem <0) prevItem = self.items.length-1;
    return prevItem;
  }

  this.getPreviousItem = function() {
    // self.index--;
    if (--self.index<0) {
      self.index = self.items.length-1;
      return false;
    }
    return true;
  }

  this.playNextItem = function() {
    // call getNextItem, decide what to do based on repeat/random state etc.
    soundManager._writeDebug('SPPlaylist.playNextItem()');

    if (self.getNextItem() || self.doRepeat) {
      if (self.doRepeat && self.index == -1) {
        // did loop
        soundManager._writeDebug('did loop - restarting playlist');
        self.index = 0;
      }
      self.play(self.index);
      self.setHighlight(self.index);
    } else {
      soundManager._writeDebug('SPPlaylist.playNextItem(): finished?');
      // finished
      self.index = self.items.length-1;
      if (!oParent.playState) {
        self.play(self.index); // only start playing if currently stopped
      }
      // self.setHighlight(self.index);
    }
  }

  this.playPreviousItem = function() {
    // call getPreviousItem, decide what to do
    soundManager._writeDebug('SPPlaylist.playPreviousItem()');
    if (self.getPreviousItem() || self.doRepeat) {
      // self.play(self.playlistItems[self.index].index);
      self.play(self.index);
      self.setHighlight(self.index);
    } else {
      // soundManager._writeDebug('SPPlaylist.playPreviousItem(): finished?');
      self.index = 0;
      // if (!oParent.playState) self.play(self.playlistItems[self.index].index); // only start playing if currently stopped
      if (!oParent.playState) self.play(self.index); // only start playing if currently stopped
      self.setHighlight(self.index);
    }
  }

  this.setHighlight = function(i) {
    if (self.playlistItems[i]) self.playlistItems[i].setHighlight();
    // self.index = i;
    if (self.lastIndex != null && self.lastIndex != i) self.removeHighlight(self.lastIndex);
    self.lastIndex = i;
  }

  this.removeHighlight = function(i) {
    if (self.playlistItems[i]) self.playlistItems[i].removeHighlight();
  }

  this.selectItem = function(i) {
    self.index = i;
    self.setHighlight(i);
  }

  this.onItemBeforeFinish = function() {
    // NOTE: This could be inconsistent across systems and is not guaranteed (it's JS-based timing.)
    if (oParent.oSMPlayer.busy) return false; // ignore if user is scrubbing
    // setTimeout(self.onItemJustBeforeFinish,4800);
    soundManager._writeDebug('SPPlaylist.onItemBeforeFinish()');
    // start preloading next track
    var nextItem = self.calcNextItem();
    self.load(self.playlistItems[nextItem].index);
  }

  this.onItemJustBeforeFinish = function() {
    // compensate for JS/Flash lag to attempt seamless audio. (woah.)
    soundManager._writeDebug('SPPlaylist.onItemJustBeforeFinish()');
    // soundManager.getSoundById(oParent.currentSound)._ignoreOnFinish = true; // prevent this sound's onfinish() from triggering next load, etc.
    soundManager.getSoundById(this.sID)._ignoreOnFinish = true; // prevent this sound's onfinish() from triggering next load, etc.
    if (this.sID == oParent.currentSound) { // just in case this method fires too late (next song already playing..)
      self._ignoreCurrentSound = true; // prevent current track from stopping
      self.playNextItem(); 
    }
  }

  this.onItemBeforeFinishComplete = function() {
    // TODO: Make getting SID reference cleaner (scope to playlist item)
    soundManager._writeDebug('onItemBeforeFinishComplete()');
    // soundManager.stop(oParent.lastSound);
    // soundManager.unload(oParent.lastSound);
  }

  this.onItemFinish = function() {
    soundManager._writeDebug('SPPlaylist.onItemFinish()');
    if (this._ignoreOnFinish) {
      // special case for seamless playback - don't trigger next track, already done
      soundManager._writeDebug('sound '+this.sID+' ended with ._ignoreOnFinish=true');
      this._ignoreOnFinish = false; // reset for next use
      return false;
    }
    oParent.setPlayState(false); // stop
    if (!self.getNextItem()) {
      self.onfinish();
    } else {
      // self.play(self.playlistItems[self.index].index); // not needed?
      self.play(self.index); // not needed?
      self.setHighlight(self.index);
    }
  }

  this.onfinish = function() {
    // end of playlist
    soundManager._writeDebug('SPPlaylist.onfinish()');
    oParent.onfinish();
    // hacks: reset scroll and index
    oParent.x = 0; // haaack 
    oParent.lastSound = oParent.currentSound;
    oParent.currentSound = null;
    self.removeHighlight(self.index); // reset highlight
    self.index = -1; // haaack
//    self.reset();

    // if repeat mode, start playing next song
    if (self.doRepeat) self.playNextItem();

  }

  this.show = function() {
    self.setDisplay(true);
  }

  this.hide = function() {
    self.setDisplay();
  }

  this.toggleShuffle = function() {
    soundManager._writeDebug('SPPlaylist.toggleShuffle()');
    self.doShuffle = !self.doShuffle;
    soundManager._writeDebug('shuffle: '+self.doShuffle);
    if (self.doShuffle) {
      // undo current highlight
      self.removeHighlight(self.index);
      self.shufflePlaylist();
      self.playlistItems = self.playlistItemsShuffled;
      self.index = 0; // self.playlistItems[0].index;
      self.setHighlight(0);
      self.play(0);
    } else {
      self.index = self.playlistItems[self.index].origIndex; // restore to last unsorted position
      self.lastIndex = self.playlistItems[self.lastIndex].origIndex; // map new lastIndex
      self.playlistItems = self.playlistItemsUnsorted;
    }
  }

  this.toggleRepeat = function() {
    soundManager._writeDebug('SPPlaylist.toggleRepeat()');
    self.doRepeat = !self.doRepeat;
    soundManager._writeDebug('repeat: '+self.doRepeat);
  }

  this.shufflePlaylist = function() {
    soundManager._writeDebug('SPPlaylist.shufflePlaylist()');
    var p = self.playlistItemsShuffled, j = null, tmp = null, newIndex = null;
    for (var i=p.length; i--;) {
      j = parseInt(Math.random()*p.length);
      tmp = p[j];
      p[j] = p[i];
      p[i] = tmp;
    }
  }
  
  this.displayTweens = null;
  this.opacityTweens = [animator.createTween(90,0),animator.createTween(0,90)];
  this.displayTween = null;
  this.opacityTween = null;
  this.widthTweens = null;
  this.widthTween = null;

  this.frame = 0;

  this.setOpacity = function(nOpacity) {
    // soundManager._writeDebug('spPlaylist.setOpacity('+nOpacity+')');
    // u.setOpacity(self.o,nOpacity);
  }

  this.createTweens = function() {
    // calculate tweens
    var base = (smUtils.isOldIE?16:0); // IE<7 needs vertical offset for playlist.
    self.displayTweens = [animator.createTween(base,self.o.offsetHeight),animator.createTween(self.o.offsetHeight,base)];
    self.widthTweens = [animator.createTween(self.o.offsetWidth,1),animator.createTween(1,self.o.offsetWidth)];
  }

  this.setCoords = function(nHeight,nOpacity,nWidth) {
    self.o.style.marginTop = -nHeight+'px';
    if (!smUtils.isIE) smUtils.setOpacity(self.o,nOpacity);
    // self.o.style.width = nWidth+'px';
    // self.o.style.marginLeft = (parseInt((self.widthTweens[0][0]-nWidth)/2)+1)+'px';
  }

  this.animate = function() {
    self.frame = Math.max(++self.frame,animator.determineFrame(self.displayLastExec,35));
    // self.frame++;
    self.setCoords(self.displayTween[self.frame],self.opacityTween[self.frame],self.widthTween[self.frame]);
    // self.playlistItems[self.frame].doAnimate(1);
    if (self.frame>=self.displayTween.length-1) {
      // self.active = false;
      self.frame = 0;
      return false;
    }
    return true;
  }

  this.displayLastExec = new Date();

  this.setDisplay = function(bDisplay) {
    soundManager._writeDebug('setDisplay()');
    self.displayTween = self.displayTweens[self.isVisible?1:0];
    self.opacityTween = self.opacityTweens[self.isVisible?1:0];
    self.widthTween = self.widthTweens[self.isVisible?1:0];
    if (self.frame>0) self.frame = self.displayTweens[0].length-self.frame;
    self.displayLastExec = new Date();
    animator.addMethod(self.animate,self.animateComplete);
    animator.start();
  }

  this.animateComplete = function() {
    // soundManager._writeDebug('spPlaylist.animateComplete()');
    // if (self.isVisible) self.o.style.display = 'none';
  }

  this.toggleDisplay = function() {
    self.isVisible = !self.isVisible;
    if (!self.isVisible) self.o.style.display = 'block';
    self.setDisplay(self.isVisible);
  }

  this.createPlaylist = function() {
    for (var i=0,j=self.items.length; i<j; i++) {
      self.playlistItems[i] = new SPPLaylistItem(self.links[i],self,i);
    }
    // assign copies
    self.playlistItemsUnsorted = smUtils.copy(self.playlistItems);
    self.playlistItemsShuffled = smUtils.copy(self.playlistItems);
  }

  this.searchForSoundLinks = function(oContainer) {
    soundManager._writeDebug('SPPlaylist.searchForSoundLinks()');
    var o = oContainer||document.documentElement;
    if (!o) return false;
    self.links = [];
    var items = o.getElementsByTagName('a');
    for (var i=0,j=items.length; i<j; i++) {
      try {
        // if [object], then ignore??
        if (items[i].href.toString().indexOf('.mp3')+1) {
          self.links[self.links.length] = items[i];
          self.addItem({url:items[i].href.toString()});
        }
      } catch(e) {
        // error may be thrown by funny characters in URL such as % for some reason under IE 7 (eg. "100% pure love" seemed to fail - odd.)
        soundManager._writeDebug('<b>SPPlaylist.searchForSoundLinks(): Error at link index '+i+'</b> - may be caused by funny characters in URL');
        // return false;
      }
    }
  }

  this.load = function(i) {
    soundManager._writeDebug('SPPlaylist.load('+i+')');
    // start preloading a sound
    var sID = 'spsound'+i;
    var s = soundManager.getSoundById(sID,true);
    if (s) {
      // reload (preload) existing sound
      soundManager._writeDebug('reloading existing sound');
      var thisOptions = {
        'autoPlay': false,
        'url': s.url, // reload original URL (assuming currently "unloaded" state)
        'stream': true
      }
      s.load(thisOptions);
    } else {
      soundManager._writeDebug('preloading new sound');
      soundManager.createSound({
       'id': sID,
       'url': self.items[i].url,
       // 'onload': self.onload,
       'onload': oParent.onload,
       'stream': true,
       'autoLoad': true,
       'autoPlay': false,
       'onid3': oParent.onid3,
       'onplay': oParent.onplay,
       'whileloading': oParent.whileloading,
       'whileplaying': oParent.whileplaying,
       'onbeforefinish': self.onItemBeforeFinish,
       'onbeforefinishcomplete': self.onItemBeforeFinishComplete,
       'onbeforefinishtime': 5000,
       'onjustbeforefinish': self.onItemJustBeforeFinish,
       'onjustbeforefinishtime':seamlessDelay , // 0 = do not call
       'onfinish': self.onItemFinish,
       'multiShot': false
      });
      // s = soundManager.getSoundById(sID);
      // soundManager._writeDebug('<b>preloaded sound load state: '+s.loaded+'</b>');
      // soundManager.getSoundById(sID).disableEvents(); // prevent UI calls etc., just preload
      // self.setMetaData(soundManager.getSoundById(sID));
    }
  }

  this.play = function(i) {
    // scoped to playlistItem instance
    if (!self.items[i]) return false;
    soundManager._writeDebug('SPPlaylist.play()');
    // var sID = 'spsound'+self.index;
    // if (i==-1) i=0; // safeguard
    if (self.doShuffle) i = self.playlistItems[i].index; // if shuffle enabled, map to proper sound
    var sID = 'spsound'+i;
    var exists = false;
    if (oParent.currentSound) {
      if (!self._ignoreCurrentSound) {
        soundManager._writeDebug('stopping current sound');
        soundManager.stop(oParent.currentSound);
        soundManager.unload(oParent.currentSound);
      } else {
        soundManager._writeDebug('allowing current sound to finish');
        self._ignoreCurrentSound = false;
      }
    }
    if (!soundManager.getSoundById(sID,true)) {
      soundManager._writeDebug('creating sound '+sID);
      soundManager.createSound({
       'id': sID,
       'url': self.items[i].url,
       // 'onload': self.onload,
       'onload': oParent.onload,
       'stream': true,
       'autoPlay': false,
       'onid3': oParent.onid3,
       'onplay': oParent.onplay,
       'whileloading': oParent.whileloading,
       'whileplaying': oParent.whileplaying,
       'onbeforefinish': self.onItemBeforeFinish,
       'onbeforefinishcomplete': self.onItemBeforeFinishComplete,
       'onbeforefinishtime': 5000,
       'onjustbeforefinish': self.onItemJustBeforeFinish,
       'onjustbeforefinishtime':seamlessDelay,
       'onfinish': self.onItemFinish,
       'multiShot': false
      });
    } else {
      // sound already exists - preload or replay use cases
      exists = true;
      soundManager._writeDebug('sound id '+sID+' already exists (preload/reuse case)');
    }

    soundManager._writeDebug('Refreshing sound details');
    oParent.refreshDetails(sID);
    oParent.lastSound = oParent.currentSound;
    oParent.currentSound = sID;
    oParent.reset(); // ensure slider starts at 0
    oParent.setLoading(true);
    soundManager.play(sID);
    oParent.setPlayState(true);

    // apply URL hash
    if (oParent.options.allowBookmarking) window.location.hash = 'track='+encodeURI(self.items[i].url.substr(self.items[i].url.lastIndexOf('/')+1));

    if (exists) {
      var s = soundManager.getSoundById(sID);
      oParent.setMetaData(s);
      if (s.loaded) {
        // already loaded before playing started - calculate time estimates, re-call onload() now
        oParent.onload.apply(s);
      }
    }

  }

  this.init = function() {
    self.o = smUtils.$('playlist-template');
    // set width to parent
    self.o.style.width = (parseInt(oParent.oSMPlayer.oMain.offsetWidth)-2)+'px';
    // smUtils.getElementsByClassName('sm2playlist-box','div',self.o)[0].style.width = '100px';
  }

  this.loadFromHash = function() {
    // given a hash, match an MP3 URL and play it.
    if (!oParent.options.allowBookmarking) return false;
    var hash = oParent.options.hashPrefix;
    var hashOffset = hash.length+1;
    var i = (window.location.hash.indexOf(hash));
    if (i==-1) return false;
    var url = decodeURI(window.location.hash.substr(hashOffset));
    soundManager._writeDebug('loadFromHasn(): searching for '+url);
    var index = self.findItemByURL(encodeURI(url));
    if (index == -1) {
      soundManager._writeDebug('trying alternate search..');
      index = self.findItemByURL(escape(url));
    }
    if (index != -1) {
      soundManager._writeDebug('loadFromHash(): found index '+index+' ('+url+')');
      self.selectItem(index);
      self.play(index);
      smUtils.addEventHandler(window,'beforeunload',self.removeHash);
    } else {
      soundManager._writeDebug('loadFromHash(): no match found');
    }
  }

  this.removeHash = function() {
    // experimental - probably won't work in any good browsers (eg. Firefox)
    try {
      window.location.hash = ''; // prevent reload from maintaining current state
    } catch(e) {
      // oh well
    }
  }

  this.findItemByURL = function(sURL) {
    for (var i=self.items.length; i--;) {
      if (self.items[i].url.indexOf(sURL)!=-1) {
        return i;
      }
    }
    return -1;
  }

  this.init();

}

function SPPLaylistItem(oLink,oPlaylist,nIndex) {
  var self = this;
  var oParent = oPlaylist;
  this.index = nIndex;
  this.origIndex = nIndex;
  this.userTitle = oLink.innerHTML;
  var sURL = oParent.items[this.index].url;
  this.o = document.createElement('li');
  if (nIndex%2==0) this.o.className = 'alt'; // striping
  var a = document.createElement('a');
  a.href = sURL;
  var s = document.createElement('span');
  s.innerHTML = this.userTitle;
  a.appendChild(s);
  this.o.appendChild(a);
  // this.o.innerHTML = '<a href="'+sURL+'"><span></span></a>';
  // this.o.getElementsByTagName('span')[0].innerHTML = this.userTitle;

  this.setHighlight = function() {
    smUtils.addClass(self.o,'highlight');
  }

  this.removeHighlight = function() {
    smUtils.removeClass(self.o,'highlight');
  }

  this.setTooltip = function(sHTML) {
    self.o.title = sHTML;
  }

  this.onclick = function() {
    if (oParent.doShuffle) soundPlayer.toggleShuffle(); // disable shuffle, if on (should be oParent.oParent too, ideally)
    oParent.selectItem(self.index);
    oParent.play(self.index);
    return false;
  }

  this.init = function() {
    // append self.o somewhere
    // oParent.o.appendChild(self.o);
    document.getElementById('playlist-template').getElementsByTagName('ul')[0].appendChild(self.o);
    self.o.onclick = self.onclick;
  }

  this.init();

}


function SoundPlayer() {
  var self = this;
  this.urls = []; // will get from somewhere..
  this.currentSound = null; // current sound ID (offset/count)
  this.lastSound = null;
  this.oPlaylist = null;
  this.oSMPlayer = null;
  this.playState = 0;
  this.paused = false;
  this.options = {
    allowScrub: true,       // let sound play when possible while user is dragging the slider (seeking)
    scrubThrottle: false,   // prevent scrub update call on every mouse move while dragging - "true" may be nicer on CPU, but track will update less
    allowBookmarking: false,// change URL to reflect currently-playing MP3
    usePageTitle: false,    // change document.title (window title) to reflect track data
    hashPrefix: 'track='    // eg. #track=foo%20bar.mp3
  }
  var u = smUtils; // alias

  this.reset = function() {
    // this.sliderPosition = 0;
    self.oSMPlayer.reset();
  }

  this.whileloading = function() {
    if (this.sID != self.currentSound) return false;
    // "this" scoped to soundManager SMSound object instance
    // this.sID, this.bytesLoaded, this.bytesTotal
    // soundManager._writeDebug('whileLoading: '+parseInt(this.bytesLoaded/this.bytesTotal*100)+' %');
    self.oSMPlayer.setLoadingProgress(Math.max(0,this.bytesLoaded/this.bytesTotal));
    self.oSMPlayer.getTimeEstimate(this);
  }

  this.onload = function() {
    if (this.sID != self.currentSound) return false;
    // force slider calculation (position) update?
    // soundManager._writeDebug('<b>time, estimate: '+this.duration+', '+this.durationEstimate+'</b>');
    soundManager._writeDebug('soundPlayer.onload()');
    self.oSMPlayer.setLoadingProgress(1); // ensure complete
    self.setMetaData(this);
    self.oSMPlayer.setLoading(false);

  }

  this.onid3 = function() {
    if (this.sID != self.currentSound) return false;
    soundManager._writeDebug('SoundPlayer.onid3()');
    // update with received ID3 data
    self.setMetaData(this);
  }

  this.onfinish = function() {
    // sound reached end - reset controls, stop?
    // $('controls').getElementsByTagName('dd')[0].innerHTML = 'Finished playing.';
    soundManager._writeDebug('SoundPlayer.onfinish()');
    self.oSMPlayer.moveToEnd();
    self.setPlayState(false);
  }

  this.onplay = function() {
    // started playing?
    soundManager._writeDebug('.onplay!');
  }

  this.whileplaying = function() {
    if (this.sID != self.currentSound) return false;
    // this.sID, this.position, this.duration
    // with short MP3s when loading for >1st time, this.duration can be null??
    var duration = (!this.loaded?this.durationEstimate:this.duration); // use estimated duration until completely loaded
    if (this.position>duration) return false; // can happen when resuming from an unloaded state?
    var newPos = Math.floor(this.position/duration*self.oSMPlayer.xMax);
    if (newPos != self.oSMPlayer.x) { // newPos > self.oSMPlayer.x
      if (!self.oSMPlayer.busy) {
        self.oSMPlayer.moveTo(newPos);
        self.oSMPlayer.update();
      }
    }
    // soundManager._writeDebug(Math.abs(this.position-self.oSMPlayer.lastTime));
    if (Math.abs(this.position-self.oSMPlayer.lastTime)>1000) self.oSMPlayer.updateTime(this.position);
  }

  this.onUserSetSlideValue = function(nX) {
    // called from slider control
    var x = parseInt(nX);
    // soundManager._writeDebug('onUserSetSlideValue('+x+')');
    // play sound at this position
    var s = soundManager.sounds[self.currentSound];
    if (!s) return false;
    var nMsecOffset = Math.floor(x/self.oSMPlayer.xMax*s.durationEstimate);
    soundManager.setPosition(self.currentSound,nMsecOffset);
  }

  this.setTitle = function(sTitle) {
    var title = (typeof sTitle == 'undefined'?'Untitled':sTitle);
    self.oSMPlayer.setTitle(title);
    self.oSMPlayer.refreshScroll();
  }

  this.setMetaData = function(oSound) {
    // pass sound to oSMPlayer
    self.oSMPlayer.setMetaData(oSound);
  }

  this.setLoading = function(bLoading) {
    self.oSMPlayer.setLoading(bLoading);
  }

  this.setPlayState = function(bPlaying) {
    self.playState = bPlaying;
    self.oSMPlayer.setPlayState(bPlaying);
  }

  this.refreshDetails = function(sID) {
    var id = (sID||self.currentSound);
    if (!id) return false;
    var s = soundManager.getSoundById(id);
    if (!s) return false;
    soundManager._writeDebug('refreshDetails(): got sound: '+s);
    // in absence of ID3, use user-provided data (URL or link text?)
    self.setTitle(self.oPlaylist.getCurrentItem().userTitle);
  }

  this.volumeDown = function(e) {
    self.oSMPlayer.volumeDown(e);
  }

  this.togglePause = function() {
    self.oSMPlayer.togglePause();
  }

  this.toggleShuffle = function() {
    self.oSMPlayer.toggleShuffle();
  }

  this.toggleRepeat = function() {
    self.oSMPlayer.toggleRepeat();
  }

  this.toggleMute = function() {
    // soundManager._writeDebug('soundPlayer.toggleMute()');
    self.oSMPlayer.toggleMute();
  }

  this.togglePlaylist = function() {
    soundManager._writeDebug('soundPlayer.togglePlaylist()');
    self.oPlaylist.toggleDisplay();

    self.oSMPlayer.togglePlaylist();
    // instance-custom implementation
    if ($('nav')) YAHOO.util.Dom[(self.oPlaylist.isVisible?'addClass':'removeClass')]($('nav'),'disabled'); // toggle nav, schillmania 2007
  }

  this.init = function() {
    self.oSMPlayer = new SMPlayer(self);
  }

}

var soundPlayer = null;

// soundPlayer = new SoundPlayer();

function initSPStuff() {
  soundPlayer = new SoundPlayer();
  soundPlayer.init(); // load mp3, etc.
  setTimeout(initOtherSPStuff,20);
}

function initOtherSPStuff() {
  soundPlayer.oPlaylist = new SPPlaylist(soundPlayer,null);
  soundPlayer.oPlaylist.searchForSoundLinks();
  soundPlayer.oPlaylist.createPlaylist();
  soundPlayer.oPlaylist.createTweens(); // make tweens for playlist
  soundPlayer.oPlaylist.loadFromHash();
}

soundManager.debugMode = (window.location.toString().match(/debug=1/)?true:false); // set debug mode
soundManager.defaultOptions.multiShot = false;

/*
soundManager.onload = function() {
  // called after window.onload() + SoundManager is loaded
  soundManager._writeDebug('<b><a href="http://www.schillmania.com/projects/soundmanager2/">www.schillmania.com/projects/soundmanager2/</a></b>');
  soundManager._writeDebug('<b>-- jsAMP v0.99a.20071010 --</b>',1);
  initStuff();
}
*/

