Jump To …

page-player.js

SoundManager 2 Demo: “Page as playlist” UI

http://schillmania.com/projects/soundmanager2/

An example of a Muxtape.com-style UI, where an unordered list of MP3 links becomes a playlist

Flash 9 “MovieStar” edition supports MPEG4 audio as well.

Requires SoundManager 2 Javascript API.

/*jslint white: false, onevar: true, undef: true, nomen: false, eqeqeq: true, plusplus: false, bitwise: true, newcap: true, immed: true */
/*global soundManager, window, document, navigator, setTimeout, attachEvent, Metadata, PP_CONFIG */

var pagePlayer = null;

function PagePlayer() {

  var self = this,
      pl = this,
      sm = soundManager, // soundManager instance
      _event,
      vuDataCanvas = null,
      controlTemplate = null,
      _head = document.getElementsByTagName('head')[0],
      spectrumContainer = null,

sniffing for favicon stuff, IE workarounds and touchy-feely devices

      ua = navigator.userAgent,
      supportsFavicon = (ua.match(/(opera|firefox)/i)),
      isTouchDevice = (ua.match(/ipad|ipod|iphone/i)),
      cleanup;

configuration options note that if Flash 9 is required, you must set soundManager.flashVersion = 9 in your script before this point.

  this.config = {
    usePeakData: false,     // [Flash 9 only]: show peak data
    useWaveformData: false, // [Flash 9 only]: enable sound spectrum (raw waveform data) - WARNING: CPU-INTENSIVE: may set CPUs on fire.
    useEQData: false,       // [Flash 9 only]: enable sound EQ (frequency spectrum data) - WARNING: Also CPU-intensive.
    fillGraph: false,       // [Flash 9 only]: draw full lines instead of only top (peak) spectrum points
    allowRightClick: true,  // let users right-click MP3 links ("save as...", etc.) or discourage (can't prevent.)
    useThrottling: true,    // try to rate-limit potentially-expensive calls (eg. dragging position around)
    autoStart: false,       // begin playing first sound when page loads
    playNext: true,         // stop after one sound, or play through list until end
    updatePageTitle: true,  // change the page title while playing sounds
    emptyTime: '-:--',      // null/undefined timer values (before data is available)
    useFavIcon: false       // try to show peakData in address bar (Firefox + Opera) - may be too CPU heavy
  };

  this.css = {              // CSS class names appended to link during various states
    sDefault: 'sm2_link',   // default state
    sLoading: 'sm2_loading',
    sPlaying: 'sm2_playing',
    sPaused: 'sm2_paused'
  };

  this.sounds = [];
  this.soundsByObject = [];
  this.lastSound = null;
  this.soundCount = 0;
  this.strings = [];
  this.dragActive = false;
  this.dragExec = new Date();
  this.dragTimer = null;
  this.pageTitle = document.title;
  this.lastWPExec = new Date();
  this.lastWLExec = new Date();
  this.vuMeterData = [];
  this.oControls = null;

  this._mergeObjects = function(oMain,oAdd) {

non-destructive merge

    var o1 = {}, o2, i, o; // clone o1
    for (i in oMain) {
      if (oMain.hasOwnProperty(i)) {
        o1[i] = oMain[i];
      }
    }
    o2 = (typeof oAdd === 'undefined'?{}:oAdd);
    for (o in o2) {
      if (typeof o1[o] === 'undefined') {
        o1[o] = o2[o];
      }
    }
    return o1;
  };

  _event = (function() {

    var old = (window.attachEvent && !window.addEventListener),
    _slice = Array.prototype.slice,
    evt = {
      add: (old?'attachEvent':'addEventListener'),
      remove: (old?'detachEvent':'removeEventListener')
    };

    function getArgs(oArgs) {
      var args = _slice.call(oArgs), len = args.length;
      if (old) {
        args[1] = 'on' + args[1]; // prefix
        if (len > 3) {
          args.pop(); // no capture
        }
      } else if (len === 3) {
        args.push(false);
      }
      return args;
    }

    function apply(args, sType) {
      var element = args.shift(),
          method = [evt[sType]];
      if (old) {
        element[method](args[0], args[1]);
      } else {
        element[method].apply(element, args);
      }
    }

    function add() {
      apply(getArgs(arguments), 'add');
    }

    function remove() {
      apply(getArgs(arguments), 'remove');
    }

    return {
      'add': add,
      'remove': remove
    };

  }());

event + DOM utilities

  this.hasClass = function(o, cStr) {
    return (typeof(o.className)!=='undefined'?new RegExp('(^|\\s)'+cStr+'(\\s|$)').test(o.className):false);
  };

  this.addClass = function(o, cStr) {
    if (!o || !cStr || self.hasClass(o,cStr)) {
      return false; // safety net
    }
    o.className = (o.className?o.className+' ':'')+cStr;
  };

  this.removeClass = function(o, cStr) {
    if (!o || !cStr || !self.hasClass(o,cStr)) {
      return false;
    }
    o.className = o.className.replace(new RegExp('( '+cStr+')|('+cStr+')','g'),'');
  };

  this.select = function(className, oParent) {
    var result = self.getByClassName(className, 'div', oParent||null);
    return (result ? result[0] : null);
  };

  this.getByClassName = (document.querySelectorAll ? function(className, tagNames, oParent) { // tagNames: string or ['div', 'p'] etc.

    var pattern = ('.'+className), qs;
    if (tagNames) {
      tagNames = tagNames.split(' ');
    }
    qs = (tagNames.length > 1 ? tagNames.join(pattern+', ') : tagNames[0]+pattern);
    return (oParent?oParent:document).querySelectorAll(qs);

  } : function(className, tagNames, oParent) {

    var node = (oParent?oParent:document), matches = [], i, j, nodes = [];
    if (tagNames) {
      tagNames = tagNames.split(' ');
    }
    if (tagNames instanceof Array) {
      for (i=tagNames.length; i--;) {
        if (!nodes || !nodes[tagNames[i]]) {
          nodes[tagNames[i]] = node.getElementsByTagName(tagNames[i]);
        }
      }
      for (i=tagNames.length; i--;) {
        for (j=nodes[tagNames[i]].length; j--;) {
          if (self.hasClass(nodes[tagNames[i]][j], className)) {
            matches.push(nodes[tagNames[i]][j]);
          }
        }
      }
    } else {
      nodes = node.all||node.getElementsByTagName('*');
      for (i=0, j=nodes.length; i<j; i++) {
        if (self.hasClass(nodes[i],className)) {
          matches.push(nodes[i]);
        }
      }
    }
    return matches;

  });
  
  this.isChildOfClass = function(oChild, oClass) {
    if (!oChild || !oClass) {
      return false;
    }
    while (oChild.parentNode && !self.hasClass(oChild,oClass)) {
      oChild = oChild.parentNode;
    }
    return (self.hasClass(oChild,oClass));
  };

  this.getParentByNodeName = function(oChild, sParentNodeName) {
    if (!oChild || !sParentNodeName) {
      return false;
    }
    sParentNodeName = sParentNodeName.toLowerCase();
    while (oChild.parentNode && sParentNodeName !== oChild.parentNode.nodeName.toLowerCase()) {
      oChild = oChild.parentNode;
    }
    return (oChild.parentNode && sParentNodeName === oChild.parentNode.nodeName.toLowerCase()?oChild.parentNode:null);
  };

  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.getTime = function(nMSec, bAsString) {

convert milliseconds to mm:ss, return as object literal or string

    var nSec = Math.floor(nMSec/1000),
        min = Math.floor(nSec/60),
        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.getSoundByObject = function(o) {
    return (typeof self.soundsByObject[o.id] !== 'undefined'?self.soundsByObject[o.id]:null);
  };

  this.getPreviousItem = function(o) {

given

  • playlist item, find previous
  • and then

  •     if (o.previousElementSibling) {
          o = o.previousElementSibling;
        } else {
          o = o.previousSibling; // move from original node..
          while (o && o.previousSibling && o.previousSibling.nodeType !== 1) {
            o = o.previousSibling;
          }
        }
        if (o.nodeName.toLowerCase() !== 'li') {
          return null;
        } else {
          return o.getElementsByTagName('a')[0];
        }
      };
    
      this.playPrevious = function(oSound) {
        if (!oSound) {
          oSound = self.lastSound;
        }
        if (!oSound) {
          return false;
        }
        var previousItem = self.getPreviousItem(oSound._data.oLI);
        if (previousItem) {
          pl.handleClick({target:previousItem}); // fake a click event - aren't we sneaky. ;)
        }
        return previousItem;
      };
    
      this.getNextItem = function(o) {

    given

  • playlist item, find next
  • and then

  •     if (o.nextElementSibling) {
          o = o.nextElementSibling;
        } else {
          o = o.nextSibling; // move from original node..
          while (o && o.nextSibling && o.nextSibling.nodeType !== 1) {
            o = o.nextSibling;
          }
        }
        if (o.nodeName.toLowerCase() !== 'li') {
          return null;
        } else {
          return o.getElementsByTagName('a')[0];
        }
      };
    
      this.playNext = function(oSound) {
        if (!oSound) {
          oSound = self.lastSound;
        }
        if (!oSound) {
          return false;
        }
        var nextItem = self.getNextItem(oSound._data.oLI);
        if (nextItem) {
          pl.handleClick({target:nextItem}); // fake a click event - aren't we sneaky. ;)
        }
        return nextItem;
      };
    
      this.setPageTitle = function(sTitle) {
        if (!self.config.updatePageTitle) {
          return false;
        }
        try {
          document.title = (sTitle?sTitle+' - ':'')+self.pageTitle;
        } catch(e) {

    oh well

          self.setPageTitle = function() {
            return false;
          };
        }
      };
    
      this.events = {

    handlers for sound events as they’re started/stopped/played

        play: function() {
          pl.removeClass(this._data.oLI,this._data.className);
          this._data.className = pl.css.sPlaying;
          pl.addClass(this._data.oLI,this._data.className);
          self.setPageTitle(this._data.originalTitle);
        },
    
        stop: function() {
          pl.removeClass(this._data.oLI,this._data.className);
          this._data.className = '';
          this._data.oPosition.style.width = '0px';
          self.setPageTitle();
          self.resetPageIcon();
        },
    
        pause: function() {
          if (pl.dragActive) {
            return false;
          }
          pl.removeClass(this._data.oLI,this._data.className);
          this._data.className = pl.css.sPaused;
          pl.addClass(this._data.oLI,this._data.className);
          self.setPageTitle();
          self.resetPageIcon();
        },
    
        resume: function() {
          if (pl.dragActive) {
            return false;
          }
          pl.removeClass(this._data.oLI,this._data.className);
          this._data.className = pl.css.sPlaying;
          pl.addClass(this._data.oLI,this._data.className);
        },
    
        finish: function() {
          pl.removeClass(this._data.oLI,this._data.className);
          this._data.className = '';
          this._data.oPosition.style.width = '0px';

    play next if applicable

          if (self.config.playNext) {
            pl.playNext(this);
          } else {
            self.setPageTitle();
            self.resetPageIcon();
          }
        },
    
        whileloading: function() {
          function doWork() {
            this._data.oLoading.style.width = (((this.bytesLoaded/this.bytesTotal)*100)+'%'); // theoretically, this should work.
            if (!this._data.didRefresh && this._data.metadata) {
              this._data.didRefresh = true;
              this._data.metadata.refresh();
            }
          }
          if (!pl.config.useThrottling) {
            doWork.apply(this);
          } else {
            var d = new Date();
            if (d && d-self.lastWLExec > 50 || this.bytesLoaded === this.bytesTotal) {
              doWork.apply(this);
              self.lastWLExec = d;
            }
          }
    
        },
    
        onload: function() {
          if (!this.loaded) {
            var oTemp = this._data.oLI.getElementsByTagName('a')[0],
                oString = oTemp.innerHTML,
                oThis = this;
            oTemp.innerHTML = oString+' <span style="font-size:0.5em"> | Load failed, d\'oh! '+(sm.sandbox.noRemote?' Possible cause: Flash sandbox is denying remote URL access.':(sm.sandbox.noLocal?'Flash denying local filesystem access':'404?'))+'</span>';
            setTimeout(function(){
              oTemp.innerHTML = oString;

    pl.events.finish.apply(oThis); // load next

            },5000);
          } else {
            if (this._data.metadata) {
              this._data.metadata.refresh();
            }
          }
        },
    
        whileplaying: function() {
          var d = null;
          if (pl.dragActive || !pl.config.useThrottling) {
            self.updateTime.apply(this);
            if (sm.flashVersion >= 9) {
              if (pl.config.usePeakData && this.instanceOptions.usePeakData) {
                self.updatePeaks.apply(this);
              }
              if (pl.config.useWaveformData && this.instanceOptions.useWaveformData || pl.config.useEQData && this.instanceOptions.useEQData) {
                self.updateGraph.apply(this);
              }
            }
            if (this._data.metadata) {
              d = new Date();
              if (d && d-self.lastWPExec>500) {
                this._data.metadata.refreshMetadata(this);
                self.lastWPExec = d;
              }
            }
            this._data.oPosition.style.width = (((this.position/self.getDurationEstimate(this))*100)+'%');
          } else {
            d = new Date();
            if (d-self.lastWPExec>30) {
              self.updateTime.apply(this);
              if (sm.flashVersion >= 9) {
                if (pl.config.usePeakData && this.instanceOptions.usePeakData) {
                  self.updatePeaks.apply(this);
                }
                if (pl.config.useWaveformData && this.instanceOptions.useWaveformData || pl.config.useEQData && this.instanceOptions.useEQData) {
                  self.updateGraph.apply(this);
                }
              }
              if (this._data.metadata) {
                this._data.metadata.refreshMetadata(this);
              }
              this._data.oPosition.style.width = (((this.position/self.getDurationEstimate(this))*100)+'%');
              self.lastWPExec = d;
            }
          }
        }
    
      }; // events{}
    
      this.setPageIcon = function(sDataURL) {
        if (!self.config.useFavIcon || !self.config.usePeakData || !sDataURL) {
          return false;
        }
        var link = document.getElementById('sm2-favicon');
        if (link) {
          _head.removeChild(link);
          link = null;
        }
        if (!link) {
          link = document.createElement('link');
          link.id = 'sm2-favicon';
          link.rel = 'shortcut icon';
          link.type = 'image/png';
          link.href = sDataURL;
          document.getElementsByTagName('head')[0].appendChild(link);
        }
      };
    
      this.resetPageIcon = function() {
        if (!self.config.useFavIcon) {
          return false;
        }
        var link = document.getElementById('favicon');
        if (link) {
          link.href = '/favicon.ico';
        }
      };
    
      this.updatePeaks = function() {
        var o = this._data.oPeak,
            oSpan = o.getElementsByTagName('span');
        oSpan[0].style.marginTop = (13-(Math.floor(15*this.peakData.left))+'px');
        oSpan[1].style.marginTop = (13-(Math.floor(15*this.peakData.right))+'px');
        if (sm.flashVersion > 8 && self.config.useFavIcon && self.config.usePeakData) {
          self.setPageIcon(self.vuMeterData[parseInt(16*this.peakData.left,10)][parseInt(16*this.peakData.right,10)]);
        }
      };
      
      this.updateGraph = function() {
        if (pl.config.flashVersion < 9 || (!pl.config.useWaveformData && !pl.config.useEQData)) {
          return false;
        }
        var sbC = this._data.oGraph.getElementsByTagName('div'),
            scale, i, offset;
        if (pl.config.useWaveformData) {

    raw waveform

          scale = 8; // Y axis (+/- this distance from 0)
          for (i=255; i--;) {
            sbC[255-i].style.marginTop = (1+scale+Math.ceil(this.waveformData.left[i]*-scale))+'px';
          }
        } else {

    eq spectrum

          offset = 9;
          for (i=255; i--;) {
            sbC[255-i].style.marginTop = ((offset*2)-1+Math.ceil(this.eqData[i]*-offset))+'px';
          }
        }
      };
      
      this.resetGraph = function() {
        if (!pl.config.useEQData || pl.config.flashVersion<9) {
          return false;
        }
        var sbC = this._data.oGraph.getElementsByTagName('div'),
            scale = (!pl.config.useEQData?'9px':'17px'),
            nHeight = (!pl.config.fillGraph?'1px':'32px'),
            i;
        for (i=255; i--;) {
          sbC[255-i].style.marginTop = scale; // EQ scale
          sbC[255-i].style.height = nHeight;
        }
      };
      
      this.updateTime = function() {
        var str = self.strings.timing.replace('%s1',self.getTime(this.position,true));
        str = str.replace('%s2',self.getTime(self.getDurationEstimate(this),true));
        this._data.oTiming.innerHTML = str;
      };
    
      this.getTheDamnTarget = function(e) {
        return (e.target||(window.event?window.event.srcElement:null));
      };
      
      this.withinStatusBar = function(o) {
        return (self.isChildOfClass(o,'controls'));
      };
    
      this.handleClick = function(e) {

    a sound (or something) was clicked – determine what and handle appropriately

        if (e.button === 2) {
          if (!pl.config.allowRightClick) {
            pl.stopEvent(e);
          }
          return pl.config.allowRightClick; // ignore right-clicks
        }
        var o = self.getTheDamnTarget(e),
            sURL, soundURL, thisSound, oControls, oLI, str;
        if (!o) {
          return true;
        }
        if (self.dragActive) {
          self.stopDrag(); // to be safe
        }
        if (self.withinStatusBar(o)) {

    self.handleStatusClick(e);

          return false;
        }
        if (o.nodeName.toLowerCase() !== 'a') {
          o = self.getParentByNodeName(o,'a');
        }
        if (!o) {

    not a link

          return true;
        }

    OK, we’re dealing with a link

        sURL = o.getAttribute('href');
    
        if (!o.href || (!sm.canPlayLink(o) && !self.hasClass(o,'playable')) || self.hasClass(o,'exclude')) {

    do nothing, don’t return anything.

          return true;
    
        } else {

    we have something we’re interested in.

    find and init parent UL, if need be

          self.initUL(self.getParentByNodeName(o, 'ul'));

    and decorate the link too, if needed

          self.initItem(o);
    
          soundURL = o.href;
          thisSound = self.getSoundByObject(o);
    
          if (thisSound) {

    sound already exists

            self.setPageTitle(thisSound._data.originalTitle);
            if (thisSound === self.lastSound) {

    ..and was playing (or paused) and isn’t in an error state

              if (thisSound.readyState !== 2) {
                if (thisSound.playState !== 1) {

    not yet playing

                  thisSound.play();
                } else {
                  thisSound.togglePause();
                }
              } else {
                sm._writeDebug('Warning: sound failed to load (security restrictions, 404 or bad format)',2);
              }
            } else {

    ..different sound

              if (self.lastSound) {
                self.stopSound(self.lastSound);
              }
              if (spectrumContainer) {
                thisSound._data.oTimingBox.appendChild(spectrumContainer);
              }
              thisSound.togglePause(); // start playing current
            }
    
          } else {

    create sound

            thisSound = sm.createSound({
              id:o.id,
              url:decodeURI(soundURL),
              onplay:self.events.play,
              onstop:self.events.stop,
              onpause:self.events.pause,
              onresume:self.events.resume,
              onfinish:self.events.finish,
              whileloading:self.events.whileloading,
              whileplaying:self.events.whileplaying,
              onmetadata:self.events.metadata,
              onload:self.events.onload
            });

    append control template

            oControls = self.oControls.cloneNode(true);
            oLI = o.parentNode;
            oLI.appendChild(oControls);
            if (spectrumContainer) {
              oLI.appendChild(spectrumContainer);
            }
            self.soundsByObject[o.id] = thisSound;

    tack on some custom data

            thisSound._data = {
              oLink: o, // DOM reference within SM2 object event handlers
              oLI: oLI,
              oControls: self.select('controls',oLI),
              oStatus: self.select('statusbar',oLI),
              oLoading: self.select('loading',oLI),
              oPosition: self.select('position',oLI),
              oTimingBox: self.select('timing',oLI),
              oTiming: self.select('timing',oLI).getElementsByTagName('div')[0],
              oPeak: self.select('peak',oLI),
              oGraph: self.select('spectrum-box',oLI),
              className: self.css.sPlaying,
              originalTitle: o.innerHTML,
              metadata: null
            };
    
            if (spectrumContainer) {
              thisSound._data.oTimingBox.appendChild(spectrumContainer);
            }

    “Metadata”

            if (thisSound._data.oLI.getElementsByTagName('ul').length) {
              thisSound._data.metadata = new Metadata(thisSound);
            }

    set initial timer stuff (before loading)

            str = self.strings.timing.replace('%s1',self.config.emptyTime);
            str = str.replace('%s2',self.config.emptyTime);
            thisSound._data.oTiming.innerHTML = str;
            self.sounds.push(thisSound);
            if (self.lastSound) {
              self.stopSound(self.lastSound);
            }
            self.resetGraph.apply(thisSound);
            thisSound.play();
    
          }
    
          self.lastSound = thisSound; // reference for next call
          return self.stopEvent(e);
    
        }
    
      };
      
      this.handleMouseDown = function(e) {

    a sound link was clicked

        if (isTouchDevice && e.touches) {
          e = e.touches[0];
        }
        if (e.button === 2) {
          if (!pl.config.allowRightClick) {
            pl.stopEvent(e);
          }
          return pl.config.allowRightClick; // ignore right-clicks
        }
        var o = self.getTheDamnTarget(e);
        if (!o) {
          return true;
        }
        if (!self.withinStatusBar(o)) {
          return true;
        }
        self.dragActive = true;
        self.lastSound.pause();
        self.setPosition(e);
        if (!isTouchDevice) {
          _event.add(document,'mousemove',self.handleMouseMove);
        } else {
          _event.add(document,'touchmove',self.handleMouseMove);
        }
        self.addClass(self.lastSound._data.oControls,'dragging');
        return self.stopEvent(e);
      };
      
      this.handleMouseMove = function(e) {
        if (isTouchDevice && e.touches) {
          e = e.touches[0];
        }

    set position accordingly

        if (self.dragActive) {
          if (self.config.useThrottling) {

    be nice to CPU/externalInterface

            var d = new Date();
            if (d-self.dragExec>20) {
              self.setPosition(e);
            } else {
              window.clearTimeout(self.dragTimer);
              self.dragTimer = window.setTimeout(function(){self.setPosition(e);},20);
            }
            self.dragExec = d;
          } else {

    oh the hell with it

            self.setPosition(e);
          }
        } else {
          self.stopDrag();
        }
        e.stopPropagation = true;
        return false;
      };
      
      this.stopDrag = function(e) {
        if (self.dragActive) {
          self.removeClass(self.lastSound._data.oControls,'dragging');
          if (!isTouchDevice) {
            _event.remove(document,'mousemove',self.handleMouseMove);
          } else {
            _event.remove(document,'touchmove',self.handleMouseMove);
          }
          if (!pl.hasClass(self.lastSound._data.oLI,self.css.sPaused)) {
            self.lastSound.resume();
          }
          self.dragActive = false;
          return self.stopEvent(e);
        }
      };
      
      this.handleStatusClick = function(e) {
        self.setPosition(e);
        if (!pl.hasClass(self.lastSound._data.oLI,self.css.sPaused)) {
          self.resume();
        }
        return self.stopEvent(e);
      };
      
      this.stopEvent = function(e) {
        if (typeof e !== 'undefined') {
          if (typeof e.preventDefault !== 'undefined') {
            e.preventDefault();
          } else {
            e.stopPropagation = true;
            e.returnValue = false;
          }
        }
        return false;
      };
     
      this.setPosition = function(e) {

    called from slider control

        var oThis = self.getTheDamnTarget(e),
            x, oControl, oSound, nMsecOffset;
        if (!oThis) {
          return true;
        }
        oControl = oThis;
        while (!self.hasClass(oControl,'controls') && oControl.parentNode) {
          oControl = oControl.parentNode;
        }
        oSound = self.lastSound;
        x = parseInt(e.clientX,10);

    play sound at this position

        nMsecOffset = Math.floor((x-self.getOffX(oControl)-4)/(oControl.offsetWidth)*self.getDurationEstimate(oSound));
        if (!isNaN(nMsecOffset)) {
          nMsecOffset = Math.min(nMsecOffset,oSound.duration);
        }
        if (!isNaN(nMsecOffset)) {
          oSound.setPosition(nMsecOffset);
        }
      };
    
      this.stopSound = function(oSound) {
        sm._writeDebug('stopping sound: '+oSound.sID);
        sm.stop(oSound.sID);
        if (!isTouchDevice) { // iOS 4.2+ security blocks onfinish() -> playNext() if we set a .src in-between(?)
          sm.unload(oSound.sID);
        }
      };
    
      this.getDurationEstimate = function(oSound) {
        if (oSound.instanceOptions.isMovieStar) {
          return (oSound.duration);
        } else {
          return (!oSound._data.metadata || !oSound._data.metadata.data.givenDuration ? (oSound.durationEstimate||0) : oSound._data.metadata.data.givenDuration);
        }
      };
    
      this.createVUData = function() {
    
        var i=0, j=0,
          canvas = vuDataCanvas.getContext('2d'),
          vuGrad = canvas.createLinearGradient(0, 16, 0, 0),
          bgGrad, outline;
    
        vuGrad.addColorStop(0,'rgb(0,192,0)');
        vuGrad.addColorStop(0.30,'rgb(0,255,0)');
        vuGrad.addColorStop(0.625,'rgb(255,255,0)');
        vuGrad.addColorStop(0.85,'rgb(255,0,0)');
        bgGrad = canvas.createLinearGradient(0, 16, 0, 0);
        outline = 'rgba(0,0,0,0.2)';
        bgGrad.addColorStop(0,outline);
        bgGrad.addColorStop(1,'rgba(0,0,0,0.5)');
        for (i=0; i<16; i++) {
          self.vuMeterData[i] = [];
        }
        for (i=0; i<16; i++) {
          for (j=0; j<16; j++) {

    reset/erase canvas

            vuDataCanvas.setAttribute('width',16);
            vuDataCanvas.setAttribute('height',16);

    draw new stuffs

            canvas.fillStyle = bgGrad;
            canvas.fillRect(0,0,7,15);
            canvas.fillRect(8,0,7,15);
            /*

    shadow

            canvas.fillStyle = 'rgba(0,0,0,0.1)';
            canvas.fillRect(1,15-i,7,17-(17-i));
            canvas.fillRect(9,15-j,7,17-(17-j));
            */
            canvas.fillStyle = vuGrad;
            canvas.fillRect(0,15-i,7,16-(16-i));
            canvas.fillRect(8,15-j,7,16-(16-j));

    and now, clear out some bits.

            canvas.clearRect(0,3,16,1);
            canvas.clearRect(0,7,16,1);
            canvas.clearRect(0,11,16,1);
            self.vuMeterData[i][j] = vuDataCanvas.toDataURL('image/png');

    for debugging VU images

            /*
            var o = document.createElement('img');
            o.style.marginRight = '5px'; 
            o.src = self.vuMeterData[i][j];
            document.documentElement.appendChild(o);
            */
          }
        }
    
      };
    
      this.testCanvas = function() {

    canvas + toDataURL();

        var c = document.createElement('canvas'),
            ctx = null, ok;
        if (!c || typeof c.getContext === 'undefined') {
          return null;
        }
        ctx = c.getContext('2d');
        if (!ctx || typeof c.toDataURL !== 'function') {
            return null;
        }

    just in case..

        try {
            ok = c.toDataURL('image/png');
        } catch(e) {

    no canvas or no toDataURL()

          return null;
        }

    assume we’re all good.

        return c;
      };
    
      this.initItem = function(oNode) {
        if (!oNode.id) {
          oNode.id = 'pagePlayerMP3Sound'+(self.soundCount++);
        }
        self.addClass(oNode,self.css.sDefault); // add default CSS decoration
      };
    
      this.initUL = function(oULNode) {

    set up graph box stuffs

        if (sm.flashVersion >= 9) {
            self.addClass(oULNode,self.cssBase);
        }
      };
    
      this.init = function(oConfig) {
    
        if (oConfig) {

    allow overriding via arguments object

          sm._writeDebug('pagePlayer.init(): Using custom configuration');
          this.config = this._mergeObjects(oConfig,this.config);
        } else {
          sm._writeDebug('pagePlayer.init(): Using default configuration');
        }
    
        var i, spectrumBox, sbC, oF, oClone, oTiming;

    apply externally-defined override, if applicable

        this.cssBase = []; // optional features added to ul.playlist

    apply some items to SM2

        sm.useFlashBlock = true;
    
        if (sm.flashVersion >= 9) {
    
          sm.defaultOptions.usePeakData = this.config.usePeakData;
          sm.defaultOptions.useWaveformData = this.config.useWaveformData;
          sm.defaultOptions.useEQData = this.config.useEQData;
    
          if (this.config.usePeakData) {
            this.cssBase.push('use-peak');
          }
    
          if (this.config.useWaveformData || this.config.useEQData) {
            this.cssBase.push('use-spectrum');
          }
    
          this.cssBase = this.cssBase.join(' ');
    
          if (this.config.useFavIcon) {
            vuDataCanvas = self.testCanvas();
            if (vuDataCanvas && supportsFavicon) {

    these browsers support dynamically-updating the favicon

              self.createVUData();
            } else {

    browser doesn’t support doing this

              this.config.useFavIcon = false;
            }
          }
    
        } else if (this.config.usePeakData || this.config.useWaveformData || this.config.useEQData) {
    
          sm._writeDebug('Page player: Note: soundManager.flashVersion = 9 is required for peak/waveform/EQ features.');
    
        }
    
        controlTemplate = document.createElement('div');
    
         controlTemplate.innerHTML = [

    control markup inserted dynamically after each page player link if you want to change the UI layout, this is the place to do it.

          '  <div class="controls">',
          '   <div class="statusbar">',
          '    <div class="loading"></div>',
          '    <div class="position"></div>',
          '   </div>',
          '  </div>',
    
          '  <div class="timing">',
          '   <div id="sm2_timing" class="timing-data">',
          '    <span class="sm2_position">%s1</span> / <span class="sm2_total">%s2</span>',
          '   </div>',
          '  </div>',
    
          '  <div class="peak">',
          '   <div class="peak-box"><span class="l"></span><span class="r"></span></div>',
          '  </div>',
    
          ' <div class="spectrum-container">',
          '  <div class="spectrum-box">',
          '   <div class="spectrum"></div>',
          '  </div>',
          ' </div>'
    
        ].join('\n');
    
        if (sm.flashVersion >= 9) {

    create the spectrum box ish

          spectrumContainer = self.select('spectrum-container',controlTemplate);

    take out of template, too

          spectrumContainer = controlTemplate.removeChild(spectrumContainer);
    
          spectrumBox = self.select('spectrum-box',spectrumContainer);
    
          sbC = spectrumBox.getElementsByTagName('div')[0];
          oF = document.createDocumentFragment();
          oClone = null;
          for (i=256; i--;) {
            oClone = sbC.cloneNode(false);
            oClone.style.left = (i)+'px';
            oF.appendChild(oClone);
          }
          spectrumBox.removeChild(sbC);
          spectrumBox.appendChild(oF);
    
        } else {

    flash 8-only, take out the spectrum container and peak elements

          controlTemplate.removeChild(self.select('spectrum-container',controlTemplate));
          controlTemplate.removeChild(self.select('peak',controlTemplate));
    
        }
    
        self.oControls = controlTemplate.cloneNode(true);
    
        oTiming = self.select('timing-data',controlTemplate);
        self.strings.timing = oTiming.innerHTML;
        oTiming.innerHTML = '';
        oTiming.id = '';
    
        function doEvents(action) { // action: add / remove
    
          _event[action](document,'click',self.handleClick);
    
          if (!isTouchDevice) {
            _event[action](document,'mousedown',self.handleMouseDown);
            _event[action](document,'mouseup',self.stopDrag);
          } else {
            _event[action](document,'touchstart',self.handleMouseDown);
            _event[action](document,'touchend',self.stopDrag);
          }
    
          _event[action](window, 'unload', cleanup);
    
        }
    
        cleanup = function() {
          doEvents('remove');
        };
    
        doEvents('add');
    
        sm._writeDebug('pagePlayer.init(): Ready',1);
    
        if (self.config.autoStart) {

    grab the first ul.playlist link

          pl.handleClick({target:pl.getByClassName('playlist', 'ul')[0].getElementsByTagName('a')[0]});
        }
    
      };
    
    }
    
    soundManager.useFlashBlock = true;
    
    soundManager.onready(function() {
      pagePlayer = new PagePlayer();
      pagePlayer.init(typeof PP_CONFIG !== 'undefined' ? PP_CONFIG : null);
    });