text: (function(window) {
  
      var weburl = 'http://localhost/music/'
      /**
       * SoundManager 2: "Bar UI" player
       * Copyright (c) 2014, Scott Schiller. All rights reserved.
       * http://www.schillmania.com/projects/soundmanager2/
       * Code provided under BSD license.
       * http://schillmania.com/projects/soundmanager2/license.txt
       */
  
      /* global console, document, navigator, soundManager, window */
  
      'use strict';
  
      var Player,
          players = [],
          // CSS selector that will get us the top-level DOM node for the player UI.
          playerSelector = '.sm2-bar-ui',
          playerOptions,
          utils;
  
      /**
       * The following are player object event callback examples.
       * Override globally by setting window.sm2BarPlayers.on = {}, or individually by window.sm2BarPlayers[0].on = {} etc.
       * soundObject is provided for whileplaying() etc., but playback control should be done via the player object.
       */
      players.on = {
          /*
          play: function(player, soundObject) {
            console.log('playing', player);
          },
          whileplaying: function(player, soundObject) {
            console.log('whileplaying', player, soundObject);
          },
          finish: function(player, soundObject) {
            // each sound
            console.log('finish', player);
          },
          pause: function(player, soundObject) {
            console.log('pause', player);
          },
          error: function(player, soundObject) {
            console.log('error', player);
          },
          end: function(player, soundObject) {
            // end of playlist
            console.log('end', player);
          }
          */
      };
  
      playerOptions = {
          // useful when multiple players are in use, or other SM2 sounds are active etc.
          stopOtherSounds: true,
          // CSS class to let the browser load the URL directly e.g., <a href="foo.mp3" class="sm2-exclude">download foo.mp3</a>
          excludeClass: 'sm2-exclude'
      };
  
      soundManager.setup({
          // trade-off: higher UI responsiveness (play/progress bar), but may use more CPU.
          html5PollingInterval: 50,
          flashVersion: 9
      });
  
      soundManager.onready(function() {
  
          var nodes, i, j;
  
          nodes = utils.dom.getAll(playerSelector);
  
          if (nodes && nodes.length) {
              for (i = 0, j = nodes.length; i < j; i++) {
                  players.push(new Player(nodes[i]));
              }
          }
  
      });
  
      /**
       * player bits
       */
  
      Player = function(playerNode) {
  
          var css, dom, extras, playlistController, soundObject, actions, actionData, defaultItem, defaultVolume, firstOpen, exports;
  
          css = {
              disabled: 'disabled',
              selected: 'selected',
              active: 'active',
              legacy: 'legacy',
              noVolume: 'no-volume',
              playlistOpen: 'playlist-open'
          };
  
          dom = {
              o: null,
              playlist: null,
              playlistTarget: null,
              playlistContainer: null,
              time: null,
              player: null,
              progress: null,
              progressTrack: null,
              progressBar: null,
              duration: null,
              volume: null
          };
  
          // prepended to tracks when a sound fails to load/play
          extras = {
              loadFailedCharacter: '<span title="Failed to load/play." class="load-error">✖</span>'
          };
  
          function stopOtherSounds() {
  
              if (playerOptions.stopOtherSounds) {
                  soundManager.stopAll();
              }
  
          }
  
          function callback(method, oSound) {
              if (method) {
                  // fire callback, passing current player and sound objects
                  if (exports.on && exports.on[method]) {
                      exports.on[method](exports, oSound);
                  } else if (players.on[method]) {
                      players.on[method](exports, oSound);
                  }
              }
          }
  
          function getTime(msec, useString) {
  
              // convert milliseconds to hh:mm:ss, return as object literal or string
  
              var nSec = Math.floor(msec / 1000),
                  hh = Math.floor(nSec / 3600),
                  min = Math.floor(nSec / 60) - Math.floor(hh * 60),
                  sec = Math.floor(nSec - (hh * 3600) - (min * 60));
  
              // if (min === 0 && sec === 0) return null; // return 0:00 as null
  
              return (useString ? ((hh ? hh + ':' : '') + (hh && min < 10 ? '0' + min : min) + ':' + (sec < 10 ? '0' + sec : sec)) : { min: min, sec: sec });
  
          }
  
          function setTitle(item) {
  
              // given a link, update the "now playing" UI.
  
              // if this is an <li> with an inner link, grab and use the text from that.
              var links = item.getElementsByTagName('a');
  
              if (links.length) {
                  item = links[0];
              }
  
              // remove any failed character sequence, also
              dom.playlistTarget.innerHTML = '<ul class="sm2-playlist-bd"><li>' + item.innerHTML.replace(extras.loadFailedCharacter, '') + '</li></ul>';
  
              if (dom.playlistTarget.getElementsByTagName('li')[0].scrollWidth > dom.playlistTarget.offsetWidth) {
                  // this item can use <marquee>, in fact.
                  dom.playlistTarget.innerHTML = '<ul class="sm2-playlist-bd"><li><marquee>' + item.innerHTML + '</marquee></li></ul>';
              }
  
          }
  
          function makeSound(url) {
  
              var sound = soundManager.createSound({
                  //id: Math.floor(Math.random() * 20),
  
                  url: url,
  
                  volume: defaultVolume,
  
                  whileplaying: function() {
  
                      var progressMaxLeft = 100,
                          left,
                          width;
  
                      left = Math.min(progressMaxLeft, Math.max(0, (progressMaxLeft * (this.position / this.durationEstimate)))) + '%';
                      width = Math.min(100, Math.max(0, (100 * (this.position / this.durationEstimate)))) + '%';
  
                      if (this.duration) {
  
                          dom.progress.style.left = left;
                          dom.progressBar.style.width = width;
  
                          // TODO: only write changes
                          dom.time.innerHTML = getTime(this.position, true);
  
                      }
  
                      callback('whileplaying', this);
  
                  },
  
                  onbufferchange: function(isBuffering) {
  
                      if (isBuffering) {
                          utils.css.add(dom.o, 'buffering');
                      } else {
                          utils.css.remove(dom.o, 'buffering');
                      }
  
                  },
  
                  onplay: function() {
                      utils.css.swap(dom.o, 'paused', 'playing');
                      callback('play', this);
                  },
  
                  onpause: function() {
                      utils.css.swap(dom.o, 'playing', 'paused');
                      callback('pause', this);
                  },
  
                  onresume: function() {
                      utils.css.swap(dom.o, 'paused', 'playing');
                  },
  
                  whileloading: function() {
  
                      if (!this.isHTML5) {
                          dom.duration.innerHTML = getTime(this.durationEstimate, true);
                      }
  
                  },
  
                  onload: function(ok) {
  
                      if (ok) {
  
                          dom.duration.innerHTML = getTime(this.duration, true);
  
                      } else if (this._iO && this._iO.onerror) {
  
                          this._iO.onerror();
  
                      }
  
                  },
  
                  onerror: function() {
  
                      // sound failed to load.
                      var item, element, html;
  
                      item = playlistController.getItem();
  
                      if (item) {
  
                          // note error, delay 2 seconds and advance?
                          // playlistTarget.innerHTML = '<ul class="sm2-playlist-bd"><li>' + item.innerHTML + '</li></ul>';
  
                          if (extras.loadFailedCharacter) {
                              dom.playlistTarget.innerHTML = dom.playlistTarget.innerHTML.replace('<li>', '<li>' + extras.loadFailedCharacter + ' ');
                              if (playlistController.data.playlist && playlistController.data.playlist[playlistController.data.selectedIndex]) {
                                  element = playlistController.data.playlist[playlistController.data.selectedIndex].getElementsByTagName('a')[0];
                                  html = element.innerHTML;
                                  if (html.indexOf(extras.loadFailedCharacter) === -1) {
                                      element.innerHTML = extras.loadFailedCharacter + ' ' + html;
                                  }
                              }
                          }
  
                      }
  
                      callback('error', this);
  
                      // load next, possibly with delay.
  
                      if (navigator.userAgent.match(/mobile/i)) {
                          // mobile will likely block the next play() call if there is a setTimeout() - so don't use one here.
                          actions.next();
                      } else {
                          if (playlistController.data.timer) {
                              window.clearTimeout(playlistController.data.timer);
                          }
                          playlistController.data.timer = window.setTimeout(actions.next, 2000);
                      }
  
                  },
  
                  onstop: function() {
  
                      utils.css.remove(dom.o, 'playing');
  
                  },
  
                  onfinish: function() {
  
                      var lastIndex, item;
  
                      utils.css.remove(dom.o, 'playing');
  
                      dom.progress.style.left = '0%';
  
                      lastIndex = playlistController.data.selectedIndex;
  
                      callback('finish', this);
  
                      // next track?
                      item = playlistController.getNext();
  
                      // don't play the same item over and over again, if at end of playlist (excluding single item case.)
                      if (item && (playlistController.data.selectedIndex !== lastIndex || (playlistController.data.playlist.length === 1 && playlistController.data.loopMode))) {
  
                          playlistController.select(item);
  
                          setTitle(item);
  
                          stopOtherSounds();
  
                          // play next
                          this.play({
                              url: playlistController.getURL()
                          });
  
                      } else {
  
                          // end of playlist case
  
                          // explicitly stop?
                          // this.stop();
  
                          callback('end', this);
  
                      }
  
                  }
  
              });
  
              return sound;
  
          }
  
          function playLink(link, fromClick) {
  
              var canplay = false;
  
              if (fromClick) {
  
                  soundlink = link.href;
                  canplay = true;
  
              } else {
  
                  var track = { url: link.getAttribute("href") };
                  findUrl(track, function(item) {
                      soundlink = item[0].url;
                      canplay = soundManager.canPlayURL(soundlink);
                  });
  
              }
  
              // if a link is OK, play it.
  
              if (canplay) {
  
                  // if there's a timer due to failure to play one track, cancel it.
                  // catches case when user may use previous/next after an error.
                  if (playlistController.data.timer) {
                      window.clearTimeout(playlistController.data.timer);
                      playlistController.data.timer = null;
                  }
  
                  if (!soundObject) {
                      soundObject = makeSound(soundlink);
                  }
  
                  // required to reset pause/play state on iOS so whileplaying() works? odd.
                  soundObject.stop();
  
                  playlistController.select(link.parentNode);
  
                  setTitle(link.parentNode);
  
                  // reset the UI
                  // TODO: function that also resets/hides timing info.
                  dom.progress.style.left = '0px';
                  dom.progressBar.style.width = '0px';
  
                  stopOtherSounds();
  
                  soundObject.play({
                      url: soundlink,
                      position: 0
                  });
              }
  
          }
  
          function findUrl(track, callbacks, ignore) {
              if (ignore) { return; }
              $.ajax({
                  url: weburl + 'tracks/get-track-url',
                  type: 'POST',
                  data: track,
                  dataType: 'json',
                  async: false,
                  success: function(data) {
                      var item = $.parseJSON(data);
                      if (item[0].result > 0) {
                          //console.log(item);
                          if (callbacks) callbacks(item);
  
                      } else {
                          console.log("something wrong", item);
                      }
                  },
                  error: function(e) {
                      console.log("Failed request data from AJAX request", e);
                  },
              });
  
          }
  
          function PlaylistController() {
  
              var data;
  
              data = {
  
                  // list of nodes?
                  playlist: [],
  
                  // NOTE: not implemented yet.
                  // shuffledIndex: [],
                  // shuffleMode: false,
  
                  // selection
                  selectedIndex: 0,
  
                  loopMode: false,
  
                  timer: null
  
              };
  
              function getPlaylist() {
  
                  return data.playlist;
  
              }
  
              function getItem(offset) {
  
                  var list,
                      item;
  
                  // given the current selection (or an offset), return the current item.
  
                  // if currently null, may be end of list case. bail.
                  if (data.selectedIndex === null) {
                      return offset;
                  }
  
                  list = getPlaylist();
  
                  // use offset if provided, otherwise take default selected.
                  offset = (offset !== undefined ? offset : data.selectedIndex);
  
                  // safety check - limit to between 0 and list length
                  offset = Math.max(0, Math.min(offset, list.length));
  
                  item = list[offset];
  
                  return item;
  
              }
  
              function findOffsetFromItem(item) {
  
                  // given an <li> item, find it in the playlist array and return the index.
                  var list,
                      i,
                      j,
                      offset;
  
                  offset = -1;
  
                  list = getPlaylist();
  
                  if (list) {
  
                      for (i = 0, j = list.length; i < j; i++) {
                          if (list[i] === item) {
                              offset = i;
                              break;
                          }
                      }
  
                  }
  
                  return offset;
  
              }
  
              function getNext() {
  
                  // don't increment if null.
                  if (data.selectedIndex !== null) {
                      data.selectedIndex++;
                  }
  
                  if (data.playlist.length > 1) {
  
                      if (data.selectedIndex >= data.playlist.length) {
  
                          if (data.loopMode) {
  
                              // loop to beginning
                              data.selectedIndex = 0;
  
                          } else {
  
                              // no change
                              data.selectedIndex--;
  
                              // end playback
                              // data.selectedIndex = null;
  
                          }
  
                      }
  
                  } else {
  
                      data.selectedIndex = null;
  
                  }
  
                  return getItem();
  
              }
  
              function getPrevious() {
  
                  data.selectedIndex--;
  
                  if (data.selectedIndex < 0) {
                      // wrapping around beginning of list? loop or exit.
                      if (data.loopMode) {
                          data.selectedIndex = data.playlist.length - 1;
                      } else {
                          // undo
                          data.selectedIndex++;
                      }
                  }
  
                  return getItem();
  
              }
  
              function resetLastSelected() {
  
                  // remove UI highlight(s) on selected items.
                  var items,
                      i, j;
  
                  items = utils.dom.getAll(dom.playlist, '.' + css.selected);
  
                  for (i = 0, j = items.length; i < j; i++) {
                      utils.css.remove(items[i], css.selected);
                  }
  
              }
  
              function select(item) {
  
                  var offset,
                      itemTop,
                      itemBottom,
                      containerHeight,
                      scrollTop,
                      itemPadding,
                      liElement;
  
                  // remove last selected, if any
                  resetLastSelected();
  
                  if (item) {
  
                      liElement = utils.dom.ancestor('li', item);
  
                      utils.css.add(liElement, css.selected);
  
                      itemTop = item.offsetTop;
                      itemBottom = itemTop + item.offsetHeight;
                      containerHeight = dom.playlistContainer.offsetHeight;
                      scrollTop = dom.playlist.scrollTop;
                      itemPadding = 8;
  
                      if (itemBottom > containerHeight + scrollTop) {
                          // bottom-align
                          dom.playlist.scrollTop = (itemBottom - containerHeight) + itemPadding;
                      } else if (itemTop < scrollTop) {
                          // top-align
                          dom.playlist.scrollTop = item.offsetTop - itemPadding;
                      }
  
                  }
  
                  // update selected offset, too.
                  offset = findOffsetFromItem(liElement);
  
                  data.selectedIndex = offset;
  
              }
  
              function playItemByOffset(offset) {
  
                  var item;
  
                  offset = (offset || 0);
  
                  item = getItem(offset);
  
                  if (item) {
                      playLink(item.getElementsByTagName('a')[0]);
                  }
  
              }
  
              function getURL() {
  
                  // return URL of currently-selected item
                  var item, url;
  
                  item = getItem();
  
                  //var track = { url: item.getElementsByTagName('a')[0].href };
                  var track = { url: item.getElementsByTagName('a')[0].getAttribute("href") };
                  findUrl(track, function(item) {
                      soundlink = item[0].url;
                  });
  
                  if (item) {
                      url = soundlink;
                  }
  
                  return url;
  
              }
  
              function refreshDOM() {
  
                  // get / update playlist from DOM
  
                  if (!dom.playlist) {
                      if (window.console && console.warn) {
                          console.warn('refreshDOM(): playlist node not found?');
                      }
                      return;
                  }
  
                  data.playlist = dom.playlist.getElementsByTagName('li');
  
              }
  
              function initDOM() {
  
                  dom.playlistTarget = utils.dom.get(dom.o, '.sm2-playlist-target');
                  dom.playlistContainer = utils.dom.get(dom.o, '.sm2-playlist-drawer');
                  dom.playlist = utils.dom.get(dom.o, '.sm2-playlist-bd');
  
              }
  
              function initPlaylistController() {
  
                  // inherit the default SM2 volume
                  defaultVolume = soundManager.defaultOptions.volume;
  
                  initDOM();
                  refreshDOM();
  
                  // animate playlist open, if HTML classname indicates so.
                  if (utils.css.has(dom.o, css.playlistOpen)) {
                      // hackish: run this after API has returned
                      window.setTimeout(function() {
                          actions.menu(true);
                      }, 1);
                  }
  
              }
  
              initPlaylistController();
  
              return {
                  data: data,
                  refresh: refreshDOM,
                  getNext: getNext,
                  getPrevious: getPrevious,
                  getItem: getItem,
                  getURL: getURL,
                  playItemByOffset: playItemByOffset,
                  select: select
              };
  
          }
  
          function isRightClick(e) {
  
              // only pay attention to left clicks. old IE differs where there's no e.which, but e.button is 1 on left click.
              if (e && ((e.which && e.which === 2) || (e.which === undefined && e.button !== 1))) {
                  // http://www.quirksmode.org/js/events_properties.html#button
                  return true;
              }
  
              return false;
  
          }
  
          function getActionData(target) {
  
              // DOM measurements for volume slider
  
              if (!target) {
                  return;
              }
  
              actionData.volume.x = utils.position.getOffX(target);
              actionData.volume.y = utils.position.getOffY(target);
  
              actionData.volume.width = target.offsetWidth;
              actionData.volume.height = target.offsetHeight;
  
              // potentially dangerous: this should, but may not be a percentage-based value.
              actionData.volume.backgroundSize = parseInt(utils.style.get(target, 'background-size'), 10);
  
              // IE gives pixels even if background-size specified as % in CSS. Boourns.
              if (window.navigator.userAgent.match(/msie|trident/i)) {
                  actionData.volume.backgroundSize = (actionData.volume.backgroundSize / actionData.volume.width) * 100;
              }
  
          }
  
          function handleMouseDown(e) {
  
              var links,
                  target;
  
              target = e.target || e.srcElement;
  
              if (isRightClick(e)) {
                  return;
              }
  
              // normalize to <a>, if applicable.
              if (target.nodeName.toLowerCase() !== 'a') {
  
                  links = target.getElementsByTagName('a');
                  if (links && links.length) {
                      target = target.getElementsByTagName('a')[0];
                  }
  
              }
  
              if (utils.css.has(target, 'sm2-volume-control')) {
  
                  // drag case for volume
  
                  getActionData(target);
  
                  utils.events.add(document, 'mousemove', actions.adjustVolume);
                  utils.events.add(document, 'touchmove', actions.adjustVolume);
                  utils.events.add(document, 'mouseup', actions.releaseVolume);
                  utils.events.add(document, 'touchend', actions.releaseVolume);
  
                  // and apply right away
                  actions.adjustVolume(e);
  
              }
  
          }
  
          function handleMouse(e) {
  
              var target, barX, barWidth, x, clientX, newPosition, sound;
  
              target = dom.progressTrack;
  
              barX = utils.position.getOffX(target);
              barWidth = target.offsetWidth;
              clientX = utils.events.getClientX(e);
  
              x = (clientX - barX);
  
              newPosition = (x / barWidth);
  
              sound = soundObject;
  
              if (sound && sound.duration) {
  
                  sound.setPosition(sound.duration * newPosition);
  
                  // a little hackish: ensure UI updates immediately with current position, even if audio is buffering and hasn't moved there yet.
                  if (sound._iO && sound._iO.whileplaying) {
                      sound._iO.whileplaying.apply(sound);
                  }
  
              }
  
              if (e.preventDefault) {
                  e.preventDefault();
              }
  
              return false;
  
          }
  
          function releaseMouse(e) {
  
              utils.events.remove(document, 'mousemove', handleMouse);
              utils.events.remove(document, 'touchmove', handleMouse);
  
              utils.css.remove(dom.o, 'grabbing');
  
              utils.events.remove(document, 'mouseup', releaseMouse);
              utils.events.remove(document, 'touchend', releaseMouse);
  
              utils.events.preventDefault(e);
  
              return false;
  
          }
  
          function handleProgressMouseDown(e) {
  
              if (isRightClick(e)) {
                  return;
              }
  
              utils.css.add(dom.o, 'grabbing');
  
              utils.events.add(document, 'mousemove', handleMouse);
              utils.events.add(document, 'touchmove', handleMouse);
              utils.events.add(document, 'mouseup', releaseMouse);
              utils.events.add(document, 'touchend', releaseMouse);
  
              handleMouse(e);
  
          }
  
          function handleClick(e) {
  
              var evt,
                  target,
                  offset,
                  targetNodeName,
                  methodName,
                  href,
                  handled,
                  soundlink,
                  canplay;
  
              evt = (e || window.event);
  
              target = evt.target || evt.srcElement;
  
              if (target && target.nodeName) {
  
                  targetNodeName = target.nodeName.toLowerCase();
  
                  if (targetNodeName !== 'a') {
  
                      // old IE (IE 8) might return nested elements inside the <a>, eg., <b> etc. Try to find the parent <a>.
  
                      if (target.parentNode) {
  
                          do {
                              target = target.parentNode;
                              targetNodeName = target.nodeName.toLowerCase();
                          } while (targetNodeName !== 'a' && target.parentNode);
  
                          if (!target) {
                              // something went wrong. bail.
                              return false;
                          }
  
                      }
  
                  }
  
                  if (targetNodeName === 'a') {
  
                      // yep, it's a link.
  
                      href = target.href;
                      var basedcode = target.getAttribute("href");
  
                      var track = { url: basedcode };
  
                      findUrl(track, function(item) {
                          if (item) {
                              soundlink = item[0].url;
                              canplay = soundManager.canPlayURL(soundlink);
                          }
                      });
  
                      if (canplay) {
  
                          // not excluded
                          if (!utils.css.has(target, playerOptions.excludeClass)) {
  
                              // find this in the playlist
                              target.href = soundlink;
                              playLink(target, true);
  
                              handled = true;
                              target.href = basedcode;
                          }
  
                      } else {
  
                          // is this one of the action buttons, eg., play/pause, volume, etc.?
                          offset = target.href.lastIndexOf('#');
                          ignore = true;
  
                          if (offset !== -1) {
  
                              methodName = target.href.substr(offset + 1);
  
                              if (methodName && actions[methodName]) {
                                  handled = true;
                                  actions[methodName](e);
                              }
  
                          }
  
                      }
  
                      // fall-through case
  
                      if (handled) {
                          // prevent browser fall-through
                          return utils.events.preventDefault(evt);
                      }
  
                  }
  
              }
  
              return true;
  
          }
  
          function init() {
  
              // init DOM?
  
              if (!playerNode && window.console && console.warn) {
                  console.warn('init(): No playerNode element?');
              }
  
              dom.o = playerNode;
  
              // are we dealing with a crap browser? apply legacy CSS if so.
              if (window.navigator.userAgent.match(/msie [678]/i)) {
                  utils.css.add(dom.o, css.legacy);
              }
  
              if (window.navigator.userAgent.match(/mobile/i)) {
                  // majority of mobile devices don't let HTML5 audio set volume.
                  utils.css.add(dom.o, css.noVolume);
              }
  
              dom.progress = utils.dom.get(dom.o, '.sm2-progress-ball');
  
              dom.progressTrack = utils.dom.get(dom.o, '.sm2-progress-track');
  
              dom.progressBar = utils.dom.get(dom.o, '.sm2-progress-bar');
  
              dom.volume = utils.dom.get(dom.o, 'a.sm2-volume-control');
  
              // measure volume control dimensions
              if (dom.volume) {
                  getActionData(dom.volume);
              }
  
              dom.duration = utils.dom.get(dom.o, '.sm2-inline-duration');
  
              dom.time = utils.dom.get(dom.o, '.sm2-inline-time');
  
              playlistController = new PlaylistController();
  
              defaultItem = playlistController.getItem(0);
  
              playlistController.select(defaultItem);
  
              if (defaultItem) {
                  setTitle(defaultItem);
              }
  
              utils.events.add(dom.o, 'mousedown', handleMouseDown);
              utils.events.add(dom.o, 'touchstart', handleMouseDown);
              utils.events.add(dom.o, 'click', handleClick);
              utils.events.add(dom.progressTrack, 'mousedown', handleProgressMouseDown);
              utils.events.add(dom.progressTrack, 'touchstart', handleProgressMouseDown);
  
          }
  
          // ---
  
          actionData = {
  
              volume: {
                  x: 0,
                  y: 0,
                  width: 0,
                  height: 0,
                  backgroundSize: 0
              }
  
          };
  
          actions = {
  
              play: function(offsetOrEvent) {
  
                  /**
                   * This is an overloaded function that takes mouse/touch events or offset-based item indices.
                   * Remember, "auto-play" will not work on mobile devices unless this function is called immediately from a touch or click event.
                   * If you have the link but not the offset, you can also pass a fake event object with a target of an <a> inside the playlist - e.g. { target: someMP3Link }
                   */
  
                  var target,
                      href,
                      e;
  
                  if (offsetOrEvent !== undefined && !isNaN(offsetOrEvent)) {
                      // smells like a number.
                      playlistController.playItemByOffset(offsetOrEvent);
                      return;
                  }
  
                  // DRY things a bit
                  e = offsetOrEvent;
  
                  if (e && e.target) {
  
                      target = e.target || e.srcElement;
  
                      href = target.href;
  
                  }
  
                  // haaaack - if null due to no event, OR '#' due to play/pause link, get first link from playlist
                  if (!href || href.indexOf('#') !== -1) {
                      // href = dom.playlist.getElementsByTagName('a')[0].href;
                      var code = dom.playlist.getElementsByTagName('a')[0].getAttribute("href")
                      var track = { url: code };
  
                      findUrl(track, function(item) {
                          href = item[0].url;
                      });
                  }
  
                  if (!soundObject) {
                      soundObject = makeSound(href);
                  }
  
                  // edge case: if the current sound is not playing, stop all others.
                  if (!soundObject.playState) {
                      stopOtherSounds();
                  }
  
                  // TODO: if user pauses + unpauses a sound that had an error, try to play next?
                  soundObject.togglePause();
  
                  // special case: clear "play next" timeout, if one exists.
                  // edge case: user pauses after a song failed to load.
                  if (soundObject.paused && playlistController.data.timer) {
                      window.clearTimeout(playlistController.data.timer);
                      playlistController.data.timer = null;
                  }
  
              },
  
              pause: function() {
  
                  if (soundObject && soundObject.readyState) {
                      soundObject.pause();
                  }
  
              },
  
              resume: function() {
  
                  if (soundObject && soundObject.readyState) {
                      soundObject.resume();
                  }
  
              },
  
              stop: function() {
  
                  // just an alias for pause, really.
                  // don't actually stop because that will mess up some UI state, i.e., dragging the slider.
                  return actions.pause();
  
              },
  
              next: function( /* e */ ) {
  
                  var item, lastIndex;
  
                  // special case: clear "play next" timeout, if one exists.
                  if (playlistController.data.timer) {
                      window.clearTimeout(playlistController.data.timer);
                      playlistController.data.timer = null;
                  }
  
                  lastIndex = playlistController.data.selectedIndex;
  
                  item = playlistController.getNext(true);
  
                  // don't play the same item again
                  if (item && playlistController.data.selectedIndex !== lastIndex) {
                      playLink(item.getElementsByTagName('a')[0]);
                  }
  
              },
  
              prev: function( /* e */ ) {
  
                  var item, lastIndex;
  
                  lastIndex = playlistController.data.selectedIndex;
  
                  item = playlistController.getPrevious();
  
                  // don't play the same item again
                  if (item && playlistController.data.selectedIndex !== lastIndex) {
                      playLink(item.getElementsByTagName('a')[0]);
                  }
  
              },
  
              shuffle: function(e) {
  
                  // NOTE: not implemented yet.
  
                  var target = (e ? e.target || e.srcElement : utils.dom.get(dom.o, '.shuffle'));
  
                  if (target && !utils.css.has(target, css.disabled)) {
                      utils.css.toggle(target.parentNode, css.active);
                      playlistController.data.shuffleMode = !playlistController.data.shuffleMode;
                  }
  
              },
  
              repeat: function(e) {
  
                  var target = (e ? e.target || e.srcElement : utils.dom.get(dom.o, '.repeat'));
  
                  if (target && !utils.css.has(target, css.disabled)) {
                      utils.css.toggle(target.parentNode, css.active);
                      playlistController.data.loopMode = !playlistController.data.loopMode;
                  }
  
              },
  
              menu: function(ignoreToggle) {
  
                  var isOpen;
  
                  isOpen = utils.css.has(dom.o, css.playlistOpen);
  
                  // hackish: reset scrollTop in default first open case. odd, but some browsers have a non-zero scroll offset the first time the playlist opens.
                  if (playlistController && !playlistController.data.selectedIndex && !firstOpen) {
                      dom.playlist.scrollTop = 0;
                      firstOpen = true;
                  }
  
                  // sniff out booleans from mouse events, as this is referenced directly by event handlers.
                  if (typeof ignoreToggle !== 'boolean' || !ignoreToggle) {
  
                      if (!isOpen) {
                          // explicitly set height:0, so the first closed -> open animation runs properly
                          dom.playlistContainer.style.height = '0px';
                      }
  
                      isOpen = utils.css.toggle(dom.o, css.playlistOpen);
  
                  }
  
                  // playlist
                  //dom.playlistContainer.style.height = (isOpen ? dom.playlistContainer.scrollHeight : 0) + 'px';
                  dom.playlistContainer.style.height = (isOpen ? dom.playlistContainer.scrollHeight : 0) + 'px';
                  document.getElementsByTagName('footer')[0].style.marginBottom = (isOpen ? dom.playlistContainer.scrollHeight + 45 : 0) + 'px';
                  loadPlaylist(); // load user's playlist
                  // if (isOpen) {
                  //   loadPlaylist();
                  //   dom.playlistContainer.style.height = (isOpen ? dom.playlistContainer.scrollHeight : 0) + 'px';
                  //   document.getElementsByTagName('footer')[0].style.marginBottom = (isOpen ? dom.playlistContainer.scrollHeight+45 : 0) + 'px';
                  // }
  
              },
              menuOpen: function() {
                  var isOpen = dom.playlistContainer.style.height;
                  if (isOpen !== 0 && isOpen !== null && isOpen !== '') {
                      return true;
                  } else {
                      return false;
                  }
  
              },
  
              adjustVolume: function(e) {
  
                  /**
                   * NOTE: this is the mousemove() event handler version.
                   * Use setVolume(50), etc., to assign volume directly.
                   */
  
                  var backgroundMargin,
                      pixelMargin,
                      target,
                      value,
                      volume;
  
                  value = 0;
  
                  target = dom.volume;
  
                  // safety net
                  if (e === undefined) {
                      return false;
                  }
  
                  // normalize between mouse and touch events
                  var clientX = utils.events.getClientX(e);
  
                  if (!e || clientX === undefined) {
                      // called directly or with a non-mouseEvent object, etc.
                      // proxy to the proper method.
                      if (arguments.length && window.console && window.console.warn) {
                          console.warn('Bar UI: call setVolume(' + e + ') instead of adjustVolume(' + e + ').');
                      }
                      return actions.setVolume.apply(this, arguments);
                  }
  
                  // based on getStyle() result
                  // figure out spacing around background image based on background size, eg. 60% background size.
                  // 60% wide means 20% margin on each side.
                  backgroundMargin = (100 - actionData.volume.backgroundSize) / 2;
  
                  // relative position of mouse over element
                  value = Math.max(0, Math.min(1, (clientX - actionData.volume.x) / actionData.volume.width));
  
                  target.style.clip = 'rect(0px, ' + (actionData.volume.width * value) + 'px, ' + actionData.volume.height + 'px, ' + (actionData.volume.width * (backgroundMargin / 100)) + 'px)';
  
                  // determine logical volume, including background margin
                  pixelMargin = ((backgroundMargin / 100) * actionData.volume.width);
  
                  volume = Math.max(0, Math.min(1, ((clientX - actionData.volume.x) - pixelMargin) / (actionData.volume.width - (pixelMargin * 2)))) * 100;
  
                  // set volume
                  if (soundObject) {
                      soundObject.setVolume(volume);
                  }
  
                  defaultVolume = volume;
  
                  return utils.events.preventDefault(e);
  
              },
  
              releaseVolume: function( /* e */ ) {
  
                  utils.events.remove(document, 'mousemove', actions.adjustVolume);
                  utils.events.remove(document, 'touchmove', actions.adjustVolume);
                  utils.events.remove(document, 'mouseup', actions.releaseVolume);
                  utils.events.remove(document, 'touchend', actions.releaseVolume);
  
              },
  
              setVolume: function(volume) {
  
                  // set volume (0-100) and update volume slider UI.
  
                  var backgroundSize,
                      backgroundMargin,
                      backgroundOffset,
                      target,
                      from,
                      to;
  
                  if (volume === undefined || isNaN(volume)) {
                      return;
                  }
  
                  if (dom.volume) {
  
                      target = dom.volume;
  
                      // based on getStyle() result
                      backgroundSize = actionData.volume.backgroundSize;
  
                      // figure out spacing around background image based on background size, eg. 60% background size.
                      // 60% wide means 20% margin on each side.
                      backgroundMargin = (100 - backgroundSize) / 2;
  
                      // margin as pixel value relative to width
                      backgroundOffset = actionData.volume.width * (backgroundMargin / 100);
  
                      from = backgroundOffset;
                      to = from + ((actionData.volume.width - (backgroundOffset * 2)) * (volume / 100));
  
                      target.style.clip = 'rect(0px, ' + to + 'px, ' + actionData.volume.height + 'px, ' + from + 'px)';
  
                  }
  
                  // apply volume to sound, as applicable
                  if (soundObject) {
                      soundObject.setVolume(volume);
                  }
  
                  defaultVolume = volume;
  
              }
  
          };
  
          init();
  
          // TODO: mixin actions -> exports
  
          exports = {
              // Per-instance events: window.sm2BarPlayers[0].on = { ... } etc. See global players.on example above for reference.
              on: null,
              actions: actions,
              dom: dom,
              playlistController: playlistController
          };
  
          return exports;
  
      };
  
      // barebones utilities for logic, CSS, DOM, events etc.
  
      utils = {
  
          array: (function() {
  
              function compare(property) {
  
                  var result;
  
                  return function(a, b) {
  
                      if (a[property] < b[property]) {
                          result = -1;
                      } else if (a[property] > b[property]) {
                          result = 1;
                      } else {
                          result = 0;
                      }
                      return result;
                  };
  
              }
  
              function shuffle(array) {
  
                  // Fisher-Yates shuffle algo
  
                  var i, j, temp;
  
                  for (i = array.length - 1; i > 0; i--) {
                      j = Math.floor(Math.random() * (i + 1));
                      temp = array[i];
                      array[i] = array[j];
                      array[j] = temp;
                  }
  
                  return array;
  
              }
  
              return {
                  compare: compare,
                  shuffle: shuffle
              };
  
          }()),
  
          css: (function() {
  
              function hasClass(o, cStr) {
  
                  return (o.className !== undefined ? new RegExp('(^|\\s)' + cStr + '(\\s|$)').test(o.className) : false);
  
              }
  
              function addClass(o, cStr) {
  
                  if (!o || !cStr || hasClass(o, cStr)) {
                      return; // safety net
                  }
                  o.className = (o.className ? o.className + ' ' : '') + cStr;
  
              }
  
              function removeClass(o, cStr) {
  
                  if (!o || !cStr || !hasClass(o, cStr)) {
                      return;
                  }
                  o.className = o.className.replace(new RegExp('( ' + cStr + ')|(' + cStr + ')', 'g'), '');
  
              }
  
              function swapClass(o, cStr1, cStr2) {
  
                  var tmpClass = {
                      className: o.className
                  };
  
                  removeClass(tmpClass, cStr1);
                  addClass(tmpClass, cStr2);
  
                  o.className = tmpClass.className;
  
              }
  
              function toggleClass(o, cStr) {
  
                  var found,
                      method;
  
                  found = hasClass(o, cStr);
  
                  method = (found ? removeClass : addClass);
  
                  method(o, cStr);
  
                  // indicate the new state...
                  return !found;
  
              }
  
              return {
                  has: hasClass,
                  add: addClass,
                  remove: removeClass,
                  swap: swapClass,
                  toggle: toggleClass
              };
  
          }()),
  
          dom: (function() {
  
              function getAll(param1, param2) {
  
                  var node,
                      selector,
                      results;
  
                  if (arguments.length === 1) {
  
                      // .selector case
                      node = document.documentElement;
                      // first param is actually the selector
                      selector = param1;
  
                  } else {
  
                      // node, .selector
                      node = param1;
                      selector = param2;
  
                  }
  
                  // sorry, IE 7 users; IE 8+ required.
                  if (node && node.querySelectorAll) {
  
                      results = node.querySelectorAll(selector);
  
                  }
  
                  return results;
  
              }
  
              function get( /* parentNode, selector */ ) {
  
                  var results = getAll.apply(this, arguments);
  
                  // hackish: if an array, return the last item.
                  if (results && results.length) {
                      return results[results.length - 1];
                  }
  
                  // handle "not found" case
                  return results && results.length === 0 ? null : results;
  
              }
  
              function ancestor(nodeName, element, checkCurrent) {
  
                  if (!element || !nodeName) {
                      return element;
                  }
  
                  nodeName = nodeName.toUpperCase();
  
                  // return if current node matches.
                  if (checkCurrent && element && element.nodeName === nodeName) {
                      return element;
                  }
  
                  while (element && element.nodeName !== nodeName && element.parentNode) {
                      element = element.parentNode;
                  }
  
                  return (element && element.nodeName === nodeName ? element : null);
  
              }
  
              return {
                  ancestor: ancestor,
                  get: get,
                  getAll: getAll
              };
  
          }()),
  
          position: (function() {
  
              function getOffX(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;
  
              }
  
              function getOffY(o) {
  
                  // http://www.xs4all.nl/~ppk/js/findpos.html
                  var curtop = 0;
  
                  if (o.offsetParent) {
  
                      while (o.offsetParent) {
  
                          curtop += o.offsetTop;
  
                          o = o.offsetParent;
  
                      }
  
                  } else if (o.y) {
  
                      curtop += o.y;
  
                  }
  
                  return curtop;
  
              }
  
              return {
                  getOffX: getOffX,
                  getOffY: getOffY
              };
  
          }()),
  
          style: (function() {
  
              function get(node, styleProp) {
  
                  // http://www.quirksmode.org/dom/getstyles.html
                  var value;
  
                  if (node.currentStyle) {
  
                      value = node.currentStyle[styleProp];
  
                  } else if (window.getComputedStyle) {
  
                      value = document.defaultView.getComputedStyle(node, null).getPropertyValue(styleProp);
  
                  }
  
                  return value;
  
              }
  
              return {
                  get: get
              };
  
          }()),
  
          events: (function() {
  
              var add, remove, preventDefault, getClientX;
  
              add = function(o, evtName, evtHandler) {
                  // return an object with a convenient detach method.
                  var eventObject = {
                      detach: function() {
                          return remove(o, evtName, evtHandler);
                      }
                  };
                  if (window.addEventListener) {
                      o.addEventListener(evtName, evtHandler, false);
                  } else {
                      o.attachEvent('on' + evtName, evtHandler);
                  }
                  return eventObject;
              };
  
              remove = (window.removeEventListener !== undefined ? function(o, evtName, evtHandler) {
                  return o.removeEventListener(evtName, evtHandler, false);
              } : function(o, evtName, evtHandler) {
                  return o.detachEvent('on' + evtName, evtHandler);
              });
  
              preventDefault = function(e) {
                  if (e.preventDefault) {
                      e.preventDefault();
                  } else {
                      e.returnValue = false;
                      e.cancelBubble = true;
                  }
                  return false;
              };
  
              getClientX = function(e) {
                  // normalize between desktop (mouse) and touch (mobile/tablet/?) events.
                  // note pageX for touch, which normalizes zoom/scroll/pan vs. clientX.
                  return (e && (e.clientX || (e.touches && e.touches[0] && e.touches[0].pageX)));
              };
  
              return {
                  add: add,
                  preventDefault: preventDefault,
                  remove: remove,
                  getClientX: getClientX
              };
  
          }()),
  
          features: (function() {
  
              var getAnimationFrame,
                  localAnimationFrame,
                  localFeatures,
                  prop,
                  styles,
                  testDiv,
                  transform;
  
              testDiv = document.createElement('div');
  
              /**
               * hat tip: paul irish
               * http://paulirish.com/2011/requestanimationframe-for-smart-animating/
               * https://gist.github.com/838785
               */
  
              localAnimationFrame = (window.requestAnimationFrame ||
                  window.webkitRequestAnimationFrame ||
                  window.mozRequestAnimationFrame ||
                  window.oRequestAnimationFrame ||
                  window.msRequestAnimationFrame ||
                  null);
  
              // apply to window, avoid "illegal invocation" errors in Chrome
              getAnimationFrame = localAnimationFrame ? function() {
                  return localAnimationFrame.apply(window, arguments);
              } : null;
  
              function has(propName) {
  
                  // test for feature support
                  return (testDiv.style[propName] !== undefined ? propName : null);
  
              }
  
              // note local scope.
              localFeatures = {
  
                  transform: {
                      ie: has('-ms-transform'),
                      moz: has('MozTransform'),
                      opera: has('OTransform'),
                      webkit: has('webkitTransform'),
                      w3: has('transform'),
                      prop: null // the normalized property value
                  },
  
                  rotate: {
                      has3D: false,
                      prop: null
                  },
  
                  getAnimationFrame: getAnimationFrame
  
              };
  
              localFeatures.transform.prop = (
                  localFeatures.transform.w3 ||
                  localFeatures.transform.moz ||
                  localFeatures.transform.webkit ||
                  localFeatures.transform.ie ||
                  localFeatures.transform.opera
              );
  
              function attempt(style) {
  
                  try {
                      testDiv.style[transform] = style;
                  } catch (e) {
                      // that *definitely* didn't work.
                      return false;
                  }
                  // if we can read back the style, it should be cool.
                  return !!testDiv.style[transform];
  
              }
  
              if (localFeatures.transform.prop) {
  
                  // try to derive the rotate/3D support.
                  transform = localFeatures.transform.prop;
                  styles = {
                      css_2d: 'rotate(0deg)',
                      css_3d: 'rotate3d(0,0,0,0deg)'
                  };
  
                  if (attempt(styles.css_3d)) {
                      localFeatures.rotate.has3D = true;
                      prop = 'rotate3d';
                  } else if (attempt(styles.css_2d)) {
                      prop = 'rotate';
                  }
  
                  localFeatures.rotate.prop = prop;
  
              }
  
              testDiv = null;
  
              return localFeatures;
  
          }())
  
      };
  
      // ---
  
      // expose to global
      window.sm2BarPlayers = players;
      window.sm2BarPlayerOptions = playerOptions;
      window.SM2BarPlayer = Player;
  
  }(window));