Skip to content

Instantly share code, notes, and snippets.

@adamtal3
Created September 3, 2016 22:39
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save adamtal3/84e12816e5fd0795cc4514fb4c7f71c9 to your computer and use it in GitHub Desktop.
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
//Allow using this built library as an AMD module
//in another project. That other project will only
//see this AMD call, not the internal modules in
//the closure below.
define(factory);
} else {
//Browser globals case. Just assign the
//result to a property on the global.
window.peaks = factory();
}
}(this, function () {
//almond, and your modules will be inlined here
/**
* almond 0.2.5 Copyright (c) 2011-2012, The Dojo Foundation All Rights Reserved.
* Available via the MIT or new BSD license.
* see: http://github.com/jrburke/almond for details
*/
//Going sloppy to avoid 'use strict' string cost, but strict practices should
//be followed.
/*jslint sloppy: true */
/*global setTimeout: false */
var requirejs, require, define;
(function (undef) {
var main, req, makeMap, handlers,
defined = {},
waiting = {},
config = {},
defining = {},
hasOwn = Object.prototype.hasOwnProperty,
aps = [].slice;
function hasProp(obj, prop) {
return hasOwn.call(obj, prop);
}
/**
* Given a relative module name, like ./something, normalize it to
* a real name that can be mapped to a path.
* @param {String} name the relative name
* @param {String} baseName a real name that the name arg is relative
* to.
* @returns {String} normalized name
*/
function normalize(name, baseName) {
var nameParts, nameSegment, mapValue, foundMap,
foundI, foundStarMap, starI, i, j, part,
baseParts = baseName && baseName.split("/"),
map = config.map,
starMap = (map && map['*']) || {};
//Adjust any relative paths.
if (name && name.charAt(0) === ".") {
//If have a base name, try to normalize against it,
//otherwise, assume it is a top-level require that will
//be relative to baseUrl in the end.
if (baseName) {
//Convert baseName to array, and lop off the last part,
//so that . matches that "directory" and not name of the baseName's
//module. For instance, baseName of "one/two/three", maps to
//"one/two/three.js", but we want the directory, "one/two" for
//this normalization.
baseParts = baseParts.slice(0, baseParts.length - 1);
name = baseParts.concat(name.split("/"));
//start trimDots
for (i = 0; i < name.length; i += 1) {
part = name[i];
if (part === ".") {
name.splice(i, 1);
i -= 1;
} else if (part === "..") {
if (i === 1 && (name[2] === '..' || name[0] === '..')) {
//End of the line. Keep at least one non-dot
//path segment at the front so it can be mapped
//correctly to disk. Otherwise, there is likely
//no path mapping for a path starting with '..'.
//This can still fail, but catches the most reasonable
//uses of ..
break;
} else if (i > 0) {
name.splice(i - 1, 2);
i -= 2;
}
}
}
//end trimDots
name = name.join("/");
} else if (name.indexOf('./') === 0) {
// No baseName, so this is ID is resolved relative
// to baseUrl, pull off the leading dot.
name = name.substring(2);
}
}
//Apply map config if available.
if ((baseParts || starMap) && map) {
nameParts = name.split('/');
for (i = nameParts.length; i > 0; i -= 1) {
nameSegment = nameParts.slice(0, i).join("/");
if (baseParts) {
//Find the longest baseName segment match in the config.
//So, do joins on the biggest to smallest lengths of baseParts.
for (j = baseParts.length; j > 0; j -= 1) {
mapValue = map[baseParts.slice(0, j).join('/')];
//baseName segment has config, find if it has one for
//this name.
if (mapValue) {
mapValue = mapValue[nameSegment];
if (mapValue) {
//Match, update name to the new value.
foundMap = mapValue;
foundI = i;
break;
}
}
}
}
if (foundMap) {
break;
}
//Check for a star map match, but just hold on to it,
//if there is a shorter segment match later in a matching
//config, then favor over this star map.
if (!foundStarMap && starMap && starMap[nameSegment]) {
foundStarMap = starMap[nameSegment];
starI = i;
}
}
if (!foundMap && foundStarMap) {
foundMap = foundStarMap;
foundI = starI;
}
if (foundMap) {
nameParts.splice(0, foundI, foundMap);
name = nameParts.join('/');
}
}
return name;
}
function makeRequire(relName, forceSync) {
return function () {
//A version of a require function that passes a moduleName
//value for items that may need to
//look up paths relative to the moduleName
return req.apply(undef, aps.call(arguments, 0).concat([relName, forceSync]));
};
}
function makeNormalize(relName) {
return function (name) {
return normalize(name, relName);
};
}
function makeLoad(depName) {
return function (value) {
defined[depName] = value;
};
}
function callDep(name) {
if (hasProp(waiting, name)) {
var args = waiting[name];
delete waiting[name];
defining[name] = true;
main.apply(undef, args);
}
if (!hasProp(defined, name) && !hasProp(defining, name)) {
throw new Error('No ' + name);
}
return defined[name];
}
//Turns a plugin!resource to [plugin, resource]
//with the plugin being undefined if the name
//did not have a plugin prefix.
function splitPrefix(name) {
var prefix,
index = name ? name.indexOf('!') : -1;
if (index > -1) {
prefix = name.substring(0, index);
name = name.substring(index + 1, name.length);
}
return [prefix, name];
}
/**
* Makes a name map, normalizing the name, and using a plugin
* for normalization if necessary. Grabs a ref to plugin
* too, as an optimization.
*/
makeMap = function (name, relName) {
var plugin,
parts = splitPrefix(name),
prefix = parts[0];
name = parts[1];
if (prefix) {
prefix = normalize(prefix, relName);
plugin = callDep(prefix);
}
//Normalize according
if (prefix) {
if (plugin && plugin.normalize) {
name = plugin.normalize(name, makeNormalize(relName));
} else {
name = normalize(name, relName);
}
} else {
name = normalize(name, relName);
parts = splitPrefix(name);
prefix = parts[0];
name = parts[1];
if (prefix) {
plugin = callDep(prefix);
}
}
//Using ridiculous property names for space reasons
return {
f: prefix ? prefix + '!' + name : name, //fullName
n: name,
pr: prefix,
p: plugin
};
};
function makeConfig(name) {
return function () {
return (config && config.config && config.config[name]) || {};
};
}
handlers = {
require: function (name) {
return makeRequire(name);
},
exports: function (name) {
var e = defined[name];
if (typeof e !== 'undefined') {
return e;
} else {
return (defined[name] = {});
}
},
module: function (name) {
return {
id: name,
uri: '',
exports: defined[name],
config: makeConfig(name)
};
}
};
main = function (name, deps, callback, relName) {
var cjsModule, depName, ret, map, i,
args = [],
usingExports;
//Use name if no relName
relName = relName || name;
//Call the callback to define the module, if necessary.
if (typeof callback === 'function') {
//Pull out the defined dependencies and pass the ordered
//values to the callback.
//Default to [require, exports, module] if no deps
deps = !deps.length && callback.length ? ['require', 'exports', 'module'] : deps;
for (i = 0; i < deps.length; i += 1) {
map = makeMap(deps[i], relName);
depName = map.f;
//Fast path CommonJS standard dependencies.
if (depName === "require") {
args[i] = handlers.require(name);
} else if (depName === "exports") {
//CommonJS module spec 1.1
args[i] = handlers.exports(name);
usingExports = true;
} else if (depName === "module") {
//CommonJS module spec 1.1
cjsModule = args[i] = handlers.module(name);
} else if (hasProp(defined, depName) ||
hasProp(waiting, depName) ||
hasProp(defining, depName)) {
args[i] = callDep(depName);
} else if (map.p) {
map.p.load(map.n, makeRequire(relName, true), makeLoad(depName), {});
args[i] = defined[depName];
} else {
throw new Error(name + ' missing ' + depName);
}
}
ret = callback.apply(defined[name], args);
if (name) {
//If setting exports via "module" is in play,
//favor that over return value and exports. After that,
//favor a non-undefined return value over exports use.
if (cjsModule && cjsModule.exports !== undef &&
cjsModule.exports !== defined[name]) {
defined[name] = cjsModule.exports;
} else if (ret !== undef || !usingExports) {
//Use the return value from the function.
defined[name] = ret;
}
}
} else if (name) {
//May just be an object definition for the module. Only
//worry about defining if have a module name.
defined[name] = callback;
}
};
requirejs = require = req = function (deps, callback, relName, forceSync, alt) {
if (typeof deps === "string") {
if (handlers[deps]) {
//callback in this case is really relName
return handlers[deps](callback);
}
//Just return the module wanted. In this scenario, the
//deps arg is the module name, and second arg (if passed)
//is just the relName.
//Normalize module name, if it contains . or ..
return callDep(makeMap(deps, callback).f);
} else if (!deps.splice) {
//deps is a config object, not an array.
config = deps;
if (callback.splice) {
//callback is an array, which means it is a dependency list.
//Adjust args if there are dependencies
deps = callback;
callback = relName;
relName = null;
} else {
deps = undef;
}
}
//Support require(['a'])
callback = callback || function () {};
//If relName is a function, it is an errback handler,
//so remove it.
if (typeof relName === 'function') {
relName = forceSync;
forceSync = alt;
}
//Simulate async callback;
if (forceSync) {
main(undef, deps, callback, relName);
} else {
//Using a non-zero value because of concern for what old browsers
//do, and latest browsers "upgrade" to 4 if lower value is used:
//http://www.whatwg.org/specs/web-apps/current-work/multipage/timers.html#dom-windowtimers-settimeout:
//If want a value immediately, use require('id') instead -- something
//that works in almond on the global level, but not guaranteed and
//unlikely to work in other AMD implementations.
setTimeout(function () {
main(undef, deps, callback, relName);
}, 4);
}
return req;
};
/**
* Just drops the config on the floor, but returns req in case
* the config return value is used.
*/
req.config = function (cfg) {
config = cfg;
if (config.deps) {
req(config.deps, config.callback);
}
return req;
};
define = function (name, deps, callback) {
//This module may not have dependencies
if (!deps.splice) {
//deps is not an array, so probably means
//an object literal or factory function for
//the value. Adjust args.
callback = deps;
deps = [];
}
if (!hasProp(defined, name) && !hasProp(waiting, name)) {
waiting[name] = [name, deps, callback];
}
};
define.amd = {
jQuery: true
};
}());
define("almond", function(){});
/*!
* EventEmitter v4.2.4 - git.io/ee
* Oliver Caldwell
* MIT license
* @preserve
*/
(function () {
/**
* Class for managing events.
* Can be extended to provide event functionality in other classes.
*
* @class EventEmitter Manages event registering and emitting.
*/
function EventEmitter() {}
// Shortcuts to improve speed and size
// Easy access to the prototype
var proto = EventEmitter.prototype;
/**
* Finds the index of the listener for the event in it's storage array.
*
* @param {Function[]} listeners Array of listeners to search through.
* @param {Function} listener Method to look for.
* @return {Number} Index of the specified listener, -1 if not found
* @api private
*/
function indexOfListener(listeners, listener) {
var i = listeners.length;
while (i--) {
if (listeners[i].listener === listener) {
return i;
}
}
return -1;
}
/**
* Alias a method while keeping the context correct, to allow for overwriting of target method.
*
* @param {String} name The name of the target method.
* @return {Function} The aliased method
* @api private
*/
function alias(name) {
return function aliasClosure() {
return this[name].apply(this, arguments);
};
}
/**
* Returns the listener array for the specified event.
* Will initialise the event object and listener arrays if required.
* Will return an object if you use a regex search. The object contains keys for each matched event. So /ba[rz]/ might return an object containing bar and baz. But only if you have either defined them with defineEvent or added some listeners to them.
* Each property in the object response is an array of listener functions.
*
* @param {String|RegExp} evt Name of the event to return the listeners from.
* @return {Function[]|Object} All listener functions for the event.
*/
proto.getListeners = function getListeners(evt) {
var events = this._getEvents();
var response;
var key;
// Return a concatenated array of all matching events if
// the selector is a regular expression.
if (typeof evt === 'object') {
response = {};
for (key in events) {
if (events.hasOwnProperty(key) && evt.test(key)) {
response[key] = events[key];
}
}
}
else {
response = events[evt] || (events[evt] = []);
}
return response;
};
/**
* Takes a list of listener objects and flattens it into a list of listener functions.
*
* @param {Object[]} listeners Raw listener objects.
* @return {Function[]} Just the listener functions.
*/
proto.flattenListeners = function flattenListeners(listeners) {
var flatListeners = [];
var i;
for (i = 0; i < listeners.length; i += 1) {
flatListeners.push(listeners[i].listener);
}
return flatListeners;
};
/**
* Fetches the requested listeners via getListeners but will always return the results inside an object. This is mainly for internal use but others may find it useful.
*
* @param {String|RegExp} evt Name of the event to return the listeners from.
* @return {Object} All listener functions for an event in an object.
*/
proto.getListenersAsObject = function getListenersAsObject(evt) {
var listeners = this.getListeners(evt);
var response;
if (listeners instanceof Array) {
response = {};
response[evt] = listeners;
}
return response || listeners;
};
/**
* Adds a listener function to the specified event.
* The listener will not be added if it is a duplicate.
* If the listener returns true then it will be removed after it is called.
* If you pass a regular expression as the event name then the listener will be added to all events that match it.
*
* @param {String|RegExp} evt Name of the event to attach the listener to.
* @param {Function} listener Method to be called when the event is emitted. If the function returns true then it will be removed after calling.
* @return {Object} Current instance of EventEmitter for chaining.
*/
proto.addListener = function addListener(evt, listener) {
var listeners = this.getListenersAsObject(evt);
var listenerIsWrapped = typeof listener === 'object';
var key;
for (key in listeners) {
if (listeners.hasOwnProperty(key) && indexOfListener(listeners[key], listener) === -1) {
listeners[key].push(listenerIsWrapped ? listener : {
listener: listener,
once: false
});
}
}
return this;
};
/**
* Alias of addListener
*/
proto.on = alias('addListener');
/**
* Semi-alias of addListener. It will add a listener that will be
* automatically removed after it's first execution.
*
* @param {String|RegExp} evt Name of the event to attach the listener to.
* @param {Function} listener Method to be called when the event is emitted. If the function returns true then it will be removed after calling.
* @return {Object} Current instance of EventEmitter for chaining.
*/
proto.addOnceListener = function addOnceListener(evt, listener) {
return this.addListener(evt, {
listener: listener,
once: true
});
};
/**
* Alias of addOnceListener.
*/
proto.once = alias('addOnceListener');
/**
* Defines an event name. This is required if you want to use a regex to add a listener to multiple events at once. If you don't do this then how do you expect it to know what event to add to? Should it just add to every possible match for a regex? No. That is scary and bad.
* You need to tell it what event names should be matched by a regex.
*
* @param {String} evt Name of the event to create.
* @return {Object} Current instance of EventEmitter for chaining.
*/
proto.defineEvent = function defineEvent(evt) {
this.getListeners(evt);
return this;
};
/**
* Uses defineEvent to define multiple events.
*
* @param {String[]} evts An array of event names to define.
* @return {Object} Current instance of EventEmitter for chaining.
*/
proto.defineEvents = function defineEvents(evts) {
for (var i = 0; i < evts.length; i += 1) {
this.defineEvent(evts[i]);
}
return this;
};
/**
* Removes a listener function from the specified event.
* When passed a regular expression as the event name, it will remove the listener from all events that match it.
*
* @param {String|RegExp} evt Name of the event to remove the listener from.
* @param {Function} listener Method to remove from the event.
* @return {Object} Current instance of EventEmitter for chaining.
*/
proto.removeListener = function removeListener(evt, listener) {
var listeners = this.getListenersAsObject(evt);
var index;
var key;
for (key in listeners) {
if (listeners.hasOwnProperty(key)) {
index = indexOfListener(listeners[key], listener);
if (index !== -1) {
listeners[key].splice(index, 1);
}
}
}
return this;
};
/**
* Alias of removeListener
*/
proto.off = alias('removeListener');
/**
* Adds listeners in bulk using the manipulateListeners method.
* If you pass an object as the second argument you can add to multiple events at once. The object should contain key value pairs of events and listeners or listener arrays. You can also pass it an event name and an array of listeners to be added.
* You can also pass it a regular expression to add the array of listeners to all events that match it.
* Yeah, this function does quite a bit. That's probably a bad thing.
*
* @param {String|Object|RegExp} evt An event name if you will pass an array of listeners next. An object if you wish to add to multiple events at once.
* @param {Function[]} [listeners] An optional array of listener functions to add.
* @return {Object} Current instance of EventEmitter for chaining.
*/
proto.addListeners = function addListeners(evt, listeners) {
// Pass through to manipulateListeners
return this.manipulateListeners(false, evt, listeners);
};
/**
* Removes listeners in bulk using the manipulateListeners method.
* If you pass an object as the second argument you can remove from multiple events at once. The object should contain key value pairs of events and listeners or listener arrays.
* You can also pass it an event name and an array of listeners to be removed.
* You can also pass it a regular expression to remove the listeners from all events that match it.
*
* @param {String|Object|RegExp} evt An event name if you will pass an array of listeners next. An object if you wish to remove from multiple events at once.
* @param {Function[]} [listeners] An optional array of listener functions to remove.
* @return {Object} Current instance of EventEmitter for chaining.
*/
proto.removeListeners = function removeListeners(evt, listeners) {
// Pass through to manipulateListeners
return this.manipulateListeners(true, evt, listeners);
};
/**
* Edits listeners in bulk. The addListeners and removeListeners methods both use this to do their job. You should really use those instead, this is a little lower level.
* The first argument will determine if the listeners are removed (true) or added (false).
* If you pass an object as the second argument you can add/remove from multiple events at once. The object should contain key value pairs of events and listeners or listener arrays.
* You can also pass it an event name and an array of listeners to be added/removed.
* You can also pass it a regular expression to manipulate the listeners of all events that match it.
*
* @param {Boolean} remove True if you want to remove listeners, false if you want to add.
* @param {String|Object|RegExp} evt An event name if you will pass an array of listeners next. An object if you wish to add/remove from multiple events at once.
* @param {Function[]} [listeners] An optional array of listener functions to add/remove.
* @return {Object} Current instance of EventEmitter for chaining.
*/
proto.manipulateListeners = function manipulateListeners(remove, evt, listeners) {
var i;
var value;
var single = remove ? this.removeListener : this.addListener;
var multiple = remove ? this.removeListeners : this.addListeners;
// If evt is an object then pass each of it's properties to this method
if (typeof evt === 'object' && !(evt instanceof RegExp)) {
for (i in evt) {
if (evt.hasOwnProperty(i) && (value = evt[i])) {
// Pass the single listener straight through to the singular method
if (typeof value === 'function') {
single.call(this, i, value);
}
else {
// Otherwise pass back to the multiple function
multiple.call(this, i, value);
}
}
}
}
else {
// So evt must be a string
// And listeners must be an array of listeners
// Loop over it and pass each one to the multiple method
i = listeners.length;
while (i--) {
single.call(this, evt, listeners[i]);
}
}
return this;
};
/**
* Removes all listeners from a specified event.
* If you do not specify an event then all listeners will be removed.
* That means every event will be emptied.
* You can also pass a regex to remove all events that match it.
*
* @param {String|RegExp} [evt] Optional name of the event to remove all listeners for. Will remove from every event if not passed.
* @return {Object} Current instance of EventEmitter for chaining.
*/
proto.removeEvent = function removeEvent(evt) {
var type = typeof evt;
var events = this._getEvents();
var key;
// Remove different things depending on the state of evt
if (type === 'string') {
// Remove all listeners for the specified event
delete events[evt];
}
else if (type === 'object') {
// Remove all events matching the regex.
for (key in events) {
if (events.hasOwnProperty(key) && evt.test(key)) {
delete events[key];
}
}
}
else {
// Remove all listeners in all events
delete this._events;
}
return this;
};
/**
* Alias of removeEvent.
*
* Added to mirror the node API.
*/
proto.removeAllListeners = alias('removeEvent');
/**
* Emits an event of your choice.
* When emitted, every listener attached to that event will be executed.
* If you pass the optional argument array then those arguments will be passed to every listener upon execution.
* Because it uses `apply`, your array of arguments will be passed as if you wrote them out separately.
* So they will not arrive within the array on the other side, they will be separate.
* You can also pass a regular expression to emit to all events that match it.
*
* @param {String|RegExp} evt Name of the event to emit and execute listeners for.
* @param {Array} [args] Optional array of arguments to be passed to each listener.
* @return {Object} Current instance of EventEmitter for chaining.
*/
proto.emitEvent = function emitEvent(evt, args) {
var listeners = this.getListenersAsObject(evt);
var listener;
var i;
var key;
var response;
for (key in listeners) {
if (listeners.hasOwnProperty(key)) {
i = listeners[key].length;
while (i--) {
// If the listener returns true then it shall be removed from the event
// The function is executed either with a basic call or an apply if there is an args array
listener = listeners[key][i];
if (listener.once === true) {
this.removeListener(evt, listener.listener);
}
response = listener.listener.apply(this, args || []);
if (response === this._getOnceReturnValue()) {
this.removeListener(evt, listener.listener);
}
}
}
}
return this;
};
/**
* Alias of emitEvent
*/
proto.trigger = alias('emitEvent');
/**
* Subtly different from emitEvent in that it will pass its arguments on to the listeners, as opposed to taking a single array of arguments to pass on.
* As with emitEvent, you can pass a regex in place of the event name to emit to all events that match it.
*
* @param {String|RegExp} evt Name of the event to emit and execute listeners for.
* @param {...*} Optional additional arguments to be passed to each listener.
* @return {Object} Current instance of EventEmitter for chaining.
*/
proto.emit = function emit(evt) {
var args = Array.prototype.slice.call(arguments, 1);
return this.emitEvent(evt, args);
};
/**
* Sets the current value to check against when executing listeners. If a
* listeners return value matches the one set here then it will be removed
* after execution. This value defaults to true.
*
* @param {*} value The new value to check for when executing listeners.
* @return {Object} Current instance of EventEmitter for chaining.
*/
proto.setOnceReturnValue = function setOnceReturnValue(value) {
this._onceReturnValue = value;
return this;
};
/**
* Fetches the current value to check against when executing listeners. If
* the listeners return value matches this one then it should be removed
* automatically. It will return true by default.
*
* @return {*|Boolean} The current value to check for or the default, true.
* @api private
*/
proto._getOnceReturnValue = function _getOnceReturnValue() {
if (this.hasOwnProperty('_onceReturnValue')) {
return this._onceReturnValue;
}
else {
return true;
}
};
/**
* Fetches the events object and creates one if required.
*
* @return {Object} The events storage object.
* @api private
*/
proto._getEvents = function _getEvents() {
return this._events || (this._events = {});
};
// Expose the class either via AMD, CommonJS or the global object
if (typeof define === 'function' && define.amd) {
define('EventEmitter',[],function () {
return EventEmitter;
});
}
else if (typeof module === 'object' && module.exports){
module.exports = EventEmitter;
}
else {
this.EventEmitter = EventEmitter;
}
}.call(this));
define('m/bootstrap',['EventEmitter'], function (EventEmitter) {
var pubsub = new EventEmitter();
var bootstrap = {
pubsub: pubsub
};
return bootstrap;
});
/**
* WAVEFORM.MIXINS.JS
*
* Common functions used in multiple modules are
* collected here for DRY purposes.
*/
define('m/player/waveform/waveform.mixins',[],function () {
// Private methods
/**
* Create a Left or Right side handle group in Kinetic based on given options.
* @param {int} height Height of handle group container (canvas)
* @param {string} color Colour hex value for handle and line marker
* @param {Boolean} inMarker Is this marker the inMarker (LHS) or outMarker (RHS)
* @return {Function}
*/
var createHandle = function (height, color, inMarker) {
/**
* @param {Boolean} draggable If true, marker is draggable
* @param {Object} segment Parent segment object with in and out times
* @param {Object} parent Parent context
* @param {Function} onDrag Callback after drag completed
* @return {Kinetic Object} Kinetic group object of handle marker elements
*/
return function (draggable, segment, parent, onDrag) {
var markerTop = 0,
markerX = 0.5,
handleTop = (height / 2) - 10.5,
handleBottom = (height / 2) + 9.5,
markerBottom = height,
handleX = inMarker ? -19.5 : 19.5;
var handlePoints = [
[markerX, markerTop],
[markerX, handleTop],
[handleX, handleTop],
[handleX, handleBottom],
[markerX, handleBottom],
[markerX, markerBottom]
];
var group = new Kinetic.Group({
draggable: draggable,
dragBoundFunc: function(pos) {
var limit;
if (inMarker) {
limit = segment.outMarker.getX() - segment.outMarker.getWidth();
if (pos.x > limit) pos.x = limit;
}
else {
limit = segment.inMarker.getX() + segment.inMarker.getWidth();
if (pos.x < limit) pos.x = limit;
}
return {
x: pos.x,
y: this.getAbsolutePosition().y
};
}
}).on("dragmove", function (event) {
onDrag(segment, parent);
});
var xPosition = inMarker ? -24 : 24;
var text = new Kinetic.Text({
x: xPosition,
y: (height / 2) - 5,
text: "",
fontSize: 10,
fontFamily: 'sans-serif',
fill: "#000",
textAlign: "center"
});
text.hide();
group.label = text;
group.add(text);
var handle = new Kinetic.Polygon({
points: handlePoints,
fill: color,
stroke: color,
strokeWidth: 1
}).on("mouseover", function (event) {
text.show();
if (inMarker) text.setX(xPosition - text.getWidth());
segment.view.segmentLayer.draw();
}).on("mouseout", function (event) {
text.hide();
segment.view.segmentLayer.draw();
});
group.add(handle);
return group;
};
};
/**
* Draw a waveform on a canvas context
* @param {Object} ctx Canvas Context to draw on
* @param {Array} min Min values for waveform
* @param {Array} max Max values for waveform
* @param {Int} offset_start Where to start drawing
* @param {Int} offset_length How much to draw
* @param {Function} y Calculate height (see fn interpolateHeight)
*/
var drawWaveform = function (ctx, min, max, offset_start, offset_length, y) {
ctx.beginPath();
min.forEach(function(val, x){
ctx.lineTo(offset_start + x + 0.5, y(val) + 0.5);
});
max.reverse().forEach(function(val, x){
ctx.lineTo(offset_start + (offset_length - x) + 0.5, y(val) + 0.5);
});
ctx.closePath();
};
// Public API
return {
interpolateHeight: function interpolateHeightGenerator (total_height){
var amplitude = 256;
return function interpolateHeight (size){
return total_height - ((size + 128) * total_height) / amplitude;
};
},
/**
* Draws a whole waveform
*
* @param {WaveformData} waveform
* @param {Canvas} canvas
* @param {Function} y interpolateHeightGenerator instance
*/
waveformDrawFunction: function (waveform, canvas, y) {
var offset_length = waveform.offset_length,
ctx = canvas.getContext();
drawWaveform(ctx, waveform.min, waveform.max, 0, offset_length, y);
canvas.fillStroke(this);
},
/**
*
* @param {WaveformData} waveform
* @param {Canvas} canvas
* @param {interpolateHeight} y
*/
waveformSegmentDrawFunction: function(waveform, id, canvas, y){
if (waveform.segments[id] === undefined){
return;
}
var segment = waveform.segments[id],
offset_length = segment.offset_length,
offset_start = segment.offset_start - waveform.offset_start,
ctx = canvas.getContext();
drawWaveform(ctx, segment.min, segment.max, offset_start, offset_length, y);
canvas.fillStroke(this);
},
waveformOffsetDrawFunction: function(waveform, canvas, y){
if (waveform.segments.zoom === undefined){
return;
}
var offset_length = waveform.segments.zoom.offset_length,
offset_start = waveform.segments.zoom.offset_start - waveform.offset_start,
ctx = canvas.getContext();
drawWaveform(ctx, waveform.segments.zoom.min, waveform.segments.zoom.max, offset_start, offset_length, y);
canvas.fillStroke(this);
},
/**
* Format a time nicely
* @param {int} time Time in seconds to be formatted
* @param {Boolean} dropHundredths Don't display hundredths of a second if true
* @return {String} Formatted time string
*/
niceTime: function (time, dropHundredths) {
var hundredths, seconds, minutes, hours, result = [];
hundredths = Math.floor((time % 1) * 100);
seconds = Math.floor(time);
minutes = Math.floor(seconds / 60);
hours = Math.floor(minutes / 60);
if (hours>0) result.push(hours); // Hours
result.push(minutes % 60); // Mins
result.push(seconds % 60); // Seconds
for (var i = 0; i < result.length; i++) {
var x = result[i];
if (x < 10) {
result[i] = "0" + x;
} else {
result[i] = x;
}
}
result = result.join(":");
if (!dropHundredths) {
if (hundredths < 10) {
hundredths = "0" + hundredths;
}
result += "." + hundredths; // Hundredths of a second
}
return result;
},
/**
* Return a function that on execution creates and returns a new
* IN handle object
* @param {Object} options Root Peaks.js options containing config info for handle
* @return {Function} Provides Kinetic handle group on execution
*/
defaultInMarker: function (options) {
return createHandle(options.height, options.outMarkerColor, true);
},
/**
* Return a function that on execution creates and returns a new
* OUT handle object
* @param {Object} options Root Peaks.js options containing config info for handle
* @return {Function} Provides Kinetic handle group on execution
*/
defaultOutMarker: function (options) {
return createHandle(options.height, options.outMarkerColor, false);
},
defaultSegmentLabelDraw: function (options) {
return function (segment, parent) {
return new Kinetic.Text({
x: 12,
y: 12,
text: parent.labelText,
fontSize: 12,
fontFamily: 'Arial, sans-serif',
fill: "#000",
textAlign: "center"
});
};
}
};
});
/**
* Player API
*
* Functionality layer for interfacing with the html5 audio API.
*
* player.init - takes a player object and sets up the player
*
* player.play - starts the audio playback and updates internal variables
*
* player.stop - stops playback
*
* player.seek - seek to a certain percentage
*
* player.timeUpdate - assignable function that is called on player update during playback (normalised)
*
* player.getPercentage - get the percentage playthrough
*
* player.getTime - get a nicely formatted string representing the current timecode
*
* player.getDuration - get a nice formatted time representing the clip duration
*
* player.getTimeFromPercentage - get the time in track of a percentage playthrough without setting
*
* player.setVolume
*/
define('m/player/player',["m/bootstrap", "m/player/waveform/waveform.mixins"], function (bootstrap, mixins) {
var radio = function () {
var _helpers = {
timeFromPercentage: function (time, percentage) {
return time*(percentage/100);
}
};
return {
init: function (audioElement) {
var that = this;
this.player = audioElement;
this.duration = this.player.duration;
if (this.player.readyState === 4) {
bootstrap.pubsub.emit("player_load", that);
}
this.player.addEventListener("timeupdate", function () {
bootstrap.pubsub.emit("player_time_update", that.getTime());
});
this.player.addEventListener("play", function () {
bootstrap.pubsub.emit("player_play", that.getTime());
});
this.player.addEventListener("pause", function () {
bootstrap.pubsub.emit("player_pause", that.getTime());
});
bootstrap.pubsub.on("waveform_seek", function (seconds) {
that.seekBySeconds(seconds);
});
},
setSource: function(source) {
this.player.setAttribute('src', source);
},
getSource: function() {
return this.player.src;
},
play: function () {
this.player.play();
bootstrap.pubsub.emit("radio_play", this.getTime());
},
pause: function () {
this.player.pause();
bootstrap.pubsub.emit("radio_pause", this.getTime());
},
getTime: function () {
return this.player.currentTime;
},
getTimeFromPercentage: function (p) {
return mixins.niceTime(this.duration * p / 100, false);
},
getSecsFromPercentage: function (p) {
return Math.floor(this.duration * p / 100);
},
getDuration: function () {
return this.duration;
},
getPercentage: function () {
return this.getPercentageFromSeconds(this.player.currentTime);
},
getPercentageFromSeconds: function (s) {
var percentage = (s / this.duration) * 100;
return Math.round(percentage * 100) / 100; // 2DP
},
seek: function (percentage) {
this.player.currentTime = _helpers.timeFromPercentage(this.duration, percentage);
},
seekBySeconds: function (seconds) {
this.player.currentTime = seconds;
}
};
};
return radio;
});
/*! waveform-data.js - v1.1.0 - 2013-10-23
* Copyright (c) 2013 ; Licensed LGPL-3.0 */
!function(a){"object"==typeof exports?module.exports=a():"function"==typeof define&&define.amd?define('WaveformData',a):"undefined"!=typeof window?window.WaveformData=a():"undefined"!=typeof global?global.WaveformData=a():"undefined"!=typeof self&&(self.WaveformData=a())}(function(){return function a(b,c,d){function e(g,h){if(!c[g]){if(!b[g]){var i="function"==typeof require&&require;if(!h&&i)return i(g,!0);if(f)return f(g,!0);throw new Error("Cannot find module '"+g+"'")}var j=c[g]={exports:{}};b[g][0].call(j.exports,function(a){var c=b[g][1][a];return e(c?c:a)},j,j.exports,a,b,c,d)}return c[g].exports}for(var f="function"==typeof require&&require,g=0;g<d.length;g++)e(d[g]);return e}({1:[function(a,b){var c=b.exports=function(a){this.data=a};c.isCompatible=function(a){return a&&"object"==typeof a&&"byteLength"in a},c.fromResponseData=function(a){return new c(new DataView(a))},c.prototype={get version(){return this.data.getInt32(0,!0)},get is_8_bit(){return!!this.data.getUint32(4,!0)},get is_16_bit(){return!this.is_8_bit},get sample_rate(){return this.data.getInt32(8,!0)},get scale(){return this.data.getInt32(12,!0)},get length(){return this.data.getUint32(16,!0)},at:function(a){return Math.round(this.data.getInt8(20+a))}}},{}],2:[function(a,b){b.exports={arraybuffer:a("./arraybuffer.js"),object:a("./object.js")}},{"./arraybuffer.js":1,"./object.js":3}],3:[function(a,b){var c=b.exports=function(a){this.data=a};c.isCompatible=function(a){return a&&"object"==typeof a&&"sample_rate"in a||"string"==typeof a&&"sample_rate"in JSON.parse(a)},c.fromResponseData=function(a){return"string"==typeof a?new c(JSON.parse(a)):new c(a)},c.prototype={get version(){return this.data.version||1},get is_8_bit(){return 8===this.data.bits},get is_16_bit(){return!this.is_8_bit},get sample_rate(){return this.data.sample_rate},get scale(){return this.data.samples_per_pixel},get length(){return this.data.length},at:function(a){return Math.round(this.data.data[a])}}},{}],4:[function(a,b){var c=a("./segment.js"),d=b.exports=function(a,b){this.adapter=b.fromResponseData(a),this.segments={},this.offset(0,this.adapter.length)};d.create=function(a){var b=null,c=null;if(a&&"object"==typeof a&&(c="responseType"in a?a.response:a.responseText||a.response),Object.keys(d.adapters).some(function(e){return d.adapters[e].isCompatible(c||a)?(b=d.adapters[e],!0):void 0}),null===b)throw new TypeError("Could not detect a WaveformData adapter from the input.");return new d(c||a,b)},d.prototype={offset:function(a,b){var c=this.adapter.length;if(0>b)throw new RangeError("End point must be non-negative.");if(a>=b)throw new RangeError("We can't end prior to the starting point.");if(0>a)throw new RangeError("Start point must be non-negative.");if(a>=c)throw new RangeError("Start point must be within range.");b>c&&(b=c),this.offset_start=a,this.offset_end=b,this.offset_length=b-a},set_segment:function(a,b,d){return d=d||"default",this.segments[d]=new c(this,a,b),this.segments[d]},resample:function(a){"number"==typeof a&&(a={width:a});var b=[],c=a.scale||Math.floor(this.duration*this.adapter.sample_rate/a.width),e=this.adapter.scale,f=this.adapter.length,g=f?this.min_sample(0):0,h=f?this.max_sample(0):0,i=0,j=0,k=-128,l=127;if(e>c)throw new Error("Zoom level "+c+" too low, minimum: "+e);for(var m,n,o,p,q,r=function(a){return Math.floor(a*c)},s=function(a,c){b.push(a,c)};f>i;){for(;Math.floor(r(j)/e)===i;)j&&s(g,h),q=i,j++,m=r(j),n=r(j-1),m!==n&&(g=l,h=k);for(m=r(j),o=Math.floor(m/e),o>f&&(o=f);o>i;)p=this.min_sample(i),g>p&&(g=p),p=this.max_sample(i),p>h&&(h=p),i++}return i!==q&&s(g,h),new d({version:this.adapter.version,samples_per_pixel:c,length:b.length/2,data:b,sample_rate:this.adapter.sample_rate},d.adapters.object)},get min(){return this.offsetValues(this.offset_start,this.offset_length,0)},get max(){return this.offsetValues(this.offset_start,this.offset_length,1)},offsetValues:function(a,b,c){var d=this.adapter,e=Array.apply(null,new Array(b));return c+=2*a,e.map(function(a,b){return d.at(2*b+c)})},get duration(){return this.adapter.length*this.adapter.scale/this.adapter.sample_rate},get offset_duration(){return this.offset_length*this.adapter.scale/this.adapter.sample_rate},get pixels_per_second(){return this.adapter.sample_rate/this.adapter.scale},get seconds_per_pixel(){return this.adapter.scale/this.adapter.sample_rate},at:function(a){return this.adapter.at(a)},at_time:function(a){return Math.floor(a*this.adapter.sample_rate/this.adapter.scale)},time:function(a){return a*this.seconds_per_pixel},in_offset:function(a){return a>=this.offset_start&&a<this.offset_end},min_sample:function(a){return this.adapter.at(2*a)},max_sample:function(a){return this.adapter.at(2*a+1)}},d.adapters={},d.adapter=function(a){this.data=a}},{"./segment.js":5}],5:[function(a,b){var c=b.exports=function(a,b,c){this.context=a,this.start=b,this.end=c};c.prototype={get offset_start(){return this.start<this.context.offset_start&&this.end>this.context.offset_start?this.context.offset_start:this.start>=this.context.offset_start&&this.start<this.context.offset_end?this.start:null},get offset_end(){return this.end>this.context.offset_start&&this.end<=this.context.offset_end?this.end:this.end>this.context.offset_end&&this.start<this.context.offset_end?this.context.offset_end:null},get offset_length(){return this.offset_end-this.offset_start},get length(){return this.end-this.start},get visible(){return this.context.in_offset(this.start)||this.context.in_offset(this.end)||this.context.offset_start>this.start&&this.context.offset_start<this.end},get min(){return this.visible?this.context.offsetValues(this.offset_start,this.offset_length,0):[]},get max(){return this.visible?this.context.offsetValues(this.offset_start,this.offset_length,1):[]}}},{}],6:[function(a,b){var c=a("./lib/core");c.adapters=a("./lib/adapters"),b.exports=c},{"./lib/adapters":2,"./lib/core":4}]},{},[6])(6)});
define('m/player/waveform/waveform.axis',["m/player/waveform/waveform.mixins"], function (mixins) {
/*
* Rounds the given value up to the nearest given multiple.
* e.g: roundUpToNearest(5.5, 3) returns 6
* roundUpToNearest(141.0, 10) returns 150
* roundUpToNearest(-5.5, 3) returns -6
*/
function roundUpToNearest(value, multiple) {
var remainder = value % multiple;
if (remainder === 0) {
return value;
}
else {
return value + multiple - remainder;
}
}
function WaveformAxis(view) {
this.view = view; // store reference to waveform view object
this.axisShape = null;
}
WaveformAxis.prototype.drawAxis = function (currentFrameStartTime) {
var that = this;
if (!this.axisShape) { // create
this.axisShape = new Kinetic.Shape({
drawFunc: function(canvas) {
that.axisDrawFunction(canvas, currentFrameStartTime);
},
fill: 'rgba(38, 255, 161, 1)',
strokeWidth: 0
});
this.view.uiLayer.add(this.axisShape);
this.view.uiLayer.draw();
}
else { // update
this.axisShape.setDrawFunc(function (canvas) {
that.axisDrawFunction(canvas, currentFrameStartTime);
});
this.view.uiLayer.draw();
}
};
/*
* Returns number of seconds for each x-axis marker, appropriate for the
* current zoom level, ensuring that markers are not too close together
* and that markers are placed at intuitive time intervals (i.e., every 1,
* 2, 5, 10, 20, 30 seconds, then every 1, 2, 5, 10, 20, 30 minutes, then
* every 1, 2, 5, 10, 20, 30 hours).
*/
WaveformAxis.prototype.getAxisLabelScale = function() {
var baseSecs = 1; // seconds
var steps = [1, 2, 5, 10, 20, 30];
var minSpacing = 60;
var index = 0;
var secs;
for (;;) {
secs = baseSecs * steps[index];
var pixels = this.view.data.at_time(secs);
if (pixels < minSpacing) {
if (++index == steps.length) {
baseSecs *= 60; // seconds -> minutes -> hours
index = 0;
}
}
else {
break;
}
}
return secs;
};
WaveformAxis.prototype.axisDrawFunction = function (canvas, currentFrameStartTime) {
// Draw axis markers
var markerHeight = 10;
// Time interval between axis markers (seconds)
var axisLabelIntervalSecs = this.getAxisLabelScale();
// Time of first axis marker (seconds)
var firstAxisLabelSecs = roundUpToNearest(currentFrameStartTime, axisLabelIntervalSecs);
// Distance between waveform start time and first axis marker (seconds)
var axisLabelOffsetSecs = firstAxisLabelSecs - currentFrameStartTime;
// Distance between waveform start time and first axis marker (pixels)
var axisLabelOffsetPixels = this.view.data.at_time(axisLabelOffsetSecs);
var ctx = canvas.getContext();
ctx.strokeStyle = "#ccc";
ctx.lineWidth = 1;
// Set text style
ctx.font = "11px sans-serif";
ctx.fillStyle = "#aaa";
ctx.textAlign = "left";
ctx.textBaseline = "bottom";
var secs = firstAxisLabelSecs;
for (;;) {
// Position of axis marker (pixels)
x = axisLabelOffsetPixels + this.view.data.at_time(secs - firstAxisLabelSecs);
if (x >= this.view.width) {
break;
}
// Draw the axis out old-skool canvas style
ctx.beginPath();
ctx.moveTo(x + 0.5, 0);
ctx.lineTo(x + 0.5, 0 + markerHeight);
ctx.moveTo(x + 0.5, this.view.height);
ctx.lineTo(x + 0.5, this.view.height - markerHeight);
ctx.stroke();
var label = mixins.niceTime(secs, true);
var labelWidth = ctx.measureText(label).width;
var labelX = x - labelWidth / 2;
var labelY = this.view.height - 1 - markerHeight;
if (labelX >= 0) {
ctx.fillText(label, labelX, labelY);
}
secs += axisLabelIntervalSecs;
}
};
return WaveformAxis;
});
define('m/player/waveform/waveform.overview',[
"m/bootstrap",
"m/player/waveform/waveform.axis",
"m/player/waveform/waveform.mixins"
], function (bootstrap, WaveformAxis, mixins) {
function WaveformOverview(waveformData, $container, options) {
var that = this;
that.options = options;
that.data = waveformData;
that.$container = $container;
that.width = that.$container.width();
that.height = options.height;
that.frameOffset = 0;
that.seeking = false;
that.stage = new Kinetic.Stage({
container: $container[0],
width: that.width,
height: that.height
});
that.waveformLayer = new Kinetic.Layer();
that.background = new Kinetic.Rect({
x: 0,
y: 0,
width: that.width,
height: that.height
});
that.waveformLayer.add(that.background);
that.uiLayer = new Kinetic.Layer();
that.refLayer = new Kinetic.Layer();
that.axis = new WaveformAxis(that);
that.createWaveform();
that.createRefWaveform();
that.axis.drawAxis(0);
that.createUi();
// INTERACTION ===============================================
that.stage.on("mousedown mouseup", function (event) {
if (event.targetNode &&
!event.targetNode.attrs.draggable &&
!event.targetNode.parent.attrs.draggable) {
if (event.type == "mousedown") {
that.seeking = true;
var width = that.refWaveformShape.getWidth();
that.updateRefWaveform(
that.data.time(event.layerX),
that.data.time(event.layerX + width)
);
that.playheadPixel = event.layerX;
that.updateUi(that.playheadPixel);
bootstrap.pubsub.emit("overview_user_seek", that.data.time(event.layerX));
that.stage.on("mousemove", function (event) {
that.updateRefWaveform(
that.data.time(event.layerX),
that.data.time(event.layerX + width)
);
that.playheadPixel = event.layerX;
that.updateUi(that.playheadPixel);
bootstrap.pubsub.emit("overview_user_seek", that.data.time(event.layerX));
});
$(document).on("mouseup", function () {
that.stage.off("mousemove");
that.seeking = false;
});
} else {
that.stage.off("mousemove");
that.seeking = false;
}
}
});
// EVENTS ====================================================
bootstrap.pubsub.on("player_time_update", function (time) {
if (!that.seeking) {
that.currentTime = time;
that.playheadPixel = that.data.at_time(that.currentTime);
that.updateUi(that.playheadPixel);
}
});
bootstrap.pubsub.on("waveform_zoom_displaying", function (start, end) {
that.updateRefWaveform(start, end);
});
bootstrap.pubsub.on("resizeEndOverview", function (width, newWaveformData) {
that.width = width;
that.data = newWaveformData;
that.stage.setWidth(that.width);
that.updateWaveform();
bootstrap.pubsub.emit("overview_resized");
});
}
WaveformOverview.prototype.createWaveform = function() {
var that = this;
this.waveformShape = new Kinetic.Shape({
drawFunc: function(canvas) {
mixins.waveformDrawFunction.call(this, that.data, canvas, mixins.interpolateHeight(that.height));
},
fill: that.options.overviewWaveformColor,
strokeWidth: 0
});
this.waveformLayer.add(this.waveformShape);
this.stage.add(this.waveformLayer);
};
WaveformOverview.prototype.createRefWaveform = function () {
var that = this;
this.refWaveformShape = new Kinetic.Shape({
drawFunc: function(canvas) {
mixins.waveformOffsetDrawFunction.call(this, that.data, canvas, mixins.interpolateHeight(that.height));
},
fill: that.options.zoomWaveformColor,
strokeWidth: 0
});
this.refLayer.add(this.refWaveformShape);
this.stage.add(this.refLayer);
};
WaveformOverview.prototype.createUi = function() {
var that = this;
this.playheadLine = new Kinetic.Line({
points: that._getPlayheadPoints(0),
stroke: 'rgba(0,0,0,1)',
strokeWidth: 1
});
this.uiLayer.add(this.playheadLine);
this.stage.add(this.uiLayer);
};
WaveformOverview.prototype.updateWaveform = function () {
var that = this;
that.waveformShape.setDrawFunc(function(canvas) {
mixins.waveformDrawFunction.call(this, that.data, canvas, mixins.interpolateHeight(that.height));
});
that.waveformLayer.draw();
};
WaveformOverview.prototype.updateWaveform = function () {
var that = this;
that.waveformShape.setDrawFunc(function(canvas) {
mixins.waveformDrawFunction.call(this, that.data, canvas, mixins.interpolateHeight(that.height));
});
that.waveformLayer.draw();
};
WaveformOverview.prototype.updateRefWaveform = function (time_in, time_out) {
var that = this;
var offset_in = that.data.at_time(time_in);
var offset_out = that.data.at_time(time_out);
that.refWaveformShape.setDrawFunc(function(canvas) {
that.data.set_segment(offset_in, offset_out, "zoom");
mixins.waveformOffsetDrawFunction.call(this, that.data, canvas, mixins.interpolateHeight(that.height));
});
that.refWaveformShape.setWidth(that.data.at_time(time_out) - that.data.at_time(time_in));
that.refLayer.draw();
};
WaveformOverview.prototype.updateUi = function (pixel) {
var that = this;
that.playheadLine.setAttr("points", that._getPlayheadPoints(pixel));
that.uiLayer.draw();
};
WaveformOverview.prototype._getPlayheadPoints = function (pixelOffset) {
var that = this;
return [{x:pixelOffset+0.5, y:0},{x:pixelOffset+0.5, y:that.height}];
};
return WaveformOverview;
});
/**
* WAVEFORM.ZOOMVIEW.JS
*
* This module handles all functionality related to the zoomed in
* waveform view canvas and initialises its own instance of the axis
* object.
*
*/
define('m/player/waveform/waveform.zoomview',[
"m/bootstrap",
"m/player/waveform/waveform.axis",
"m/player/waveform/waveform.mixins",
], function (bootstrap, WaveformAxis, mixins) {
function WaveformZoomView(waveformData, $container, options) {
var that = this;
that.options = options;
that.rootData = waveformData;
that.playing = false;
that.seeking = false;
that.current_zoom_level = 0;
that.currentTime = 0;
that.data = that.rootData.resample({
scale: options.zoomLevels[that.current_zoom_level]
});
that.pixelLength = that.data.adapter.length;
that.pixelsPerSecond = that.data.pixels_per_second;
that.frameOffset = 0; // the pixel offset of the current frame being displayed
that.$container = $container;
that.width = that.$container.width();
that.height = options.height;
that.stage = new Kinetic.Stage({
container: $container[0],
width: that.width,
height: that.height
});
that.zoomWaveformLayer = new Kinetic.Layer();
that.uiLayer = new Kinetic.Layer();
that.background = new Kinetic.Rect({
x: 0,
y: 0,
width: that.width,
height: that.height
});
that.zoomWaveformLayer.add(that.background);
that.axis = new WaveformAxis(that);
that.createZoomWaveform();
that.axis.drawAxis(0);
that.createUi();
// INTERACTION ===============================================
that.stage.on("mousedown mouseup", function (event) {
if (event.targetNode &&
!event.targetNode.attrs.draggable &&
!event.targetNode.parent.attrs.draggable) {
if (event.type == "mousedown") {
that.seeking = true;
var x = event.layerX, dX, p;
// Set playhead position
that.currentTime = that.data.time(that.frameOffset + x);
that.syncPlayhead(that.frameOffset + x);
// enable drag if necessary
that.stage.on("mousemove", function (event) {
dX = event.layerX > x ? x - event.layerX : (x - event.layerX)*1;
x = event.layerX;
p = that.frameOffset+dX;
p = p < 0 ? 0 : p > (that.pixelLength - that.width) ? (that.pixelLength - that.width) : p;
that.updateZoomWaveform(p);
});
$(document).on("mouseup", function () {
that.stage.off("mousemove");
that.seeking = false;
});
} else {
that.stage.off("mousemove");
that.seeking = false;
}
}
});
// EVENTS ====================================================
bootstrap.pubsub.on("player_time_update", function (time) {
if (!that.seeking && !that.playing) {
that.currentTime = time;
that.seekFrame(that.data.at_time(that.currentTime));
}
});
bootstrap.pubsub.on("overview_user_seek", function (time) {
that.currentTime = time;
that.seekFrame(that.data.at_time(that.currentTime));
});
bootstrap.pubsub.on("player_play", function (time) {
that.playing = true;
that.currentTime = time;
that.playFrom(time, that.data.at_time(time));
});
bootstrap.pubsub.on("player_pause", function (time) {
that.playing = false;
that.currentTime = time;
if (that.playheadLineAnimation) {
that.playheadLineAnimation.stop();
}
that.syncPlayhead(that.data.at_time(that.currentTime));
});
bootstrap.pubsub.on("waveform_zoom_level_changed", function (zoom_level) {
if (that.playing) {
return;
}
if (zoom_level != that.current_zoom_level) {
that.current_zoom_level = zoom_level;
that.data = that.rootData.resample({
scale: zoom_level
});
that.pixelsPerSecond = that.data.pixels_per_second;
that.seekFrame(that.data.at_time(that.currentTime));
}
});
bootstrap.pubsub.on("window_resized", function (width, newWaveformData) {
that.width = width;
that.data = newWaveformData;
that.stage.setWidth(that.width);
that.updateZoomWaveform(that.frameOffset);
bootstrap.pubsub.emit("zoomview_resized");
});
// KEYBOARD EVENTS =========================================
bootstrap.pubsub.on("kybrd_left", function () {
that.currentTime -= that.options.nudgeIncrement;
that.seekFrame(that.data.at_time(that.currentTime));
});
bootstrap.pubsub.on("kybrd_right", function () {
that.currentTime += that.options.nudgeIncrement;
that.seekFrame(that.data.at_time(that.currentTime));
});
bootstrap.pubsub.on("kybrd_shift_left", function () {
that.currentTime -= (that.options.nudgeIncrement*10);
that.seekFrame(that.data.at_time(that.currentTime));
});
bootstrap.pubsub.on("kybrd_shift_right", function () {
that.currentTime += (that.options.nudgeIncrement*10);
that.seekFrame(that.data.at_time(that.currentTime));
});
}
WaveformZoomView.prototype.createZoomWaveform = function() {
var that = this;
that.zoomWaveformShape = new Kinetic.Shape({
drawFunc: function(canvas) {
that.data.offset(0, that.width);
mixins.waveformDrawFunction.call(this, that.data, canvas, mixins.interpolateHeight(that.height));
},
fill: that.options.zoomWaveformColor,
strokeWidth: 0
});
that.zoomWaveformLayer.add(that.zoomWaveformShape);
that.stage.add(that.zoomWaveformLayer);
bootstrap.pubsub.emit("waveform_zoom_displaying", 0 * that.data.seconds_per_pixel, that.width * that.data.seconds_per_pixel);
};
WaveformZoomView.prototype.createUi = function() {
var that = this;
that.zoomPlayheadLine = new Kinetic.Line({
points: [{x: 0, y: 0},{x: 0, y: that.height}],
stroke: 'rgba(0,0,0,1)',
strokeWidth: 1
});
that.zoomPlayheadText = new Kinetic.Text({
x:2,
y: 12,
text: "00:00:00",
fontSize: 11,
fontFamily: 'sans-serif',
fill: '#aaa',
align: 'right'
});
that.zoomPlayheadGroup = new Kinetic.Group({
x: 0,
y: 0
}).add(that.zoomPlayheadLine).add(that.zoomPlayheadText);
that.uiLayer.add(that.zoomPlayheadGroup);
that.stage.add(that.uiLayer);
};
WaveformZoomView.prototype.updateZoomWaveform = function (pixelOffset) {
var that = this;
that.frameOffset = pixelOffset;
var display = (that.playheadPixel >= pixelOffset) && (that.playheadPixel <= pixelOffset + that.width);
if (display) {
var remPixels = that.playheadPixel - pixelOffset;
that.zoomPlayheadGroup.show().setAttr("x", remPixels + 0.5);
that.zoomPlayheadText.setText(mixins.niceTime(that.data.time(that.playheadPixel), false));
} else {
that.zoomPlayheadGroup.hide();
}
that.uiLayer.draw();
that.zoomWaveformShape.setDrawFunc(function(canvas) {
that.data.offset(pixelOffset, pixelOffset + that.width);
mixins.waveformDrawFunction.call(this, that.data, canvas, mixins.interpolateHeight(that.height));
});
that.zoomWaveformLayer.draw();
that.axis.drawAxis(that.data.time(pixelOffset));
// if (that.snipWaveformShape) that.updateSnipWaveform(that.currentSnipStartTime, that.currentSnipEndTime);
bootstrap.pubsub.emit("waveform_zoom_displaying", pixelOffset * that.data.seconds_per_pixel, (pixelOffset+that.width) * that.data.seconds_per_pixel);
};
// UI functions ==============================
WaveformZoomView.prototype.playFrom = function (time, startPosition) {
var that = this;
if (that.playheadLineAnimation) {
that.playheadLineAnimation.stop();
}
startPosition = startPosition - that.frameOffset;
var startSeconds = time;
var frameSeconds = 0;
that.playheadLineAnimation = new Kinetic.Animation(function (frame) {
var time = frame.time,
timeDiff = frame.timeDiff,
frameRate = frame.frameRate;
var seconds = time / 1000;
var positionInFrame = Math.round(startPosition + (that.pixelsPerSecond * (seconds-frameSeconds)));
that.playheadPixel = that.frameOffset + positionInFrame;
if (positionInFrame > that.width) {
that.newFrame();
that.zoomPlayheadGroup.setAttr("x", 0);
that.zoomPlayheadText.setText(mixins.niceTime(that.data.time(0), false));
startPosition = 0;
var s = seconds - frameSeconds;
frameSeconds += s; // TODO: ??
} else {
that.zoomPlayheadGroup.setAttr("x", positionInFrame + 0.5);
that.zoomPlayheadText.setText(mixins.niceTime(that.data.time(that.frameOffset + positionInFrame), false));
}
}, that.uiLayer);
that.playheadLineAnimation.start();
};
WaveformZoomView.prototype.newFrame = function () {
var that = this;
that.frameOffset += that.width;
that.updateZoomWaveform(that.frameOffset);
};
WaveformZoomView.prototype.syncPlayhead = function (pixelIndex) {
var that = this;
that.playheadPixel = pixelIndex;
var display = (that.playheadPixel >= that.frameOffset) && (that.playheadPixel <= that.frameOffset + that.width);
if (display) {
var remPixels = that.playheadPixel - that.frameOffset;
that.zoomPlayheadGroup.show().setAttr("x", remPixels + 0.5);
that.zoomPlayheadText.setText(mixins.niceTime(that.data.time(that.playheadPixel), false));
} else {
that.zoomPlayheadGroup.hide();
}
that.uiLayer.draw();
bootstrap.pubsub.emit("waveform_seek", that.data.time(pixelIndex));
};
WaveformZoomView.prototype.seekFrame = function (pixelIndex) {
var that = this;
var upperLimit = that.data.adapter.length - that.width;
if (pixelIndex > that.width && pixelIndex < upperLimit) {
that.frameOffset = pixelIndex - Math.round(that.width / 2);
} else if (pixelIndex >= upperLimit) {
that.frameOffset = upperLimit;
} else {
that.frameOffset = 0;
}
that.syncPlayhead(pixelIndex);
that.updateZoomWaveform(that.frameOffset);
};
return WaveformZoomView;
});
/**
* WAVEFORM.SEGMENTS.JS
*
* This module handles all functionality related to the adding,
* removing and manipulation of segments
*/
define('m/player/waveform/waveform.segments',[
"m/bootstrap",
"m/player/waveform/waveform.mixins",
], function (bootstrap, mixins) {
return function (waveformView, options) {
var that = this;
that.segments = [];
var views = [waveformView.waveformZoomView, waveformView.waveformOverview];
var createSegmentWaveform = function (segmentId, startTime, endTime, editable, color, labelText) {
var that = this;
var segment = {
id: segmentId,
startTime: startTime,
endTime: endTime,
labelText: labelText || ""
};
var segmentZoomGroup = new Kinetic.Group();
var segmentOverviewGroup = new Kinetic.Group();
var segmentGroups = [segmentZoomGroup, segmentOverviewGroup];
color = color || getSegmentColor();
var menter = function (event) {
this.parent.label.show();
this.parent.view.segmentLayer.draw();
};
var mleave = function (event) {
this.parent.label.hide();
this.parent.view.segmentLayer.draw();
};
for (var i = 0; i < segmentGroups.length; i++) {
var view = views[i];
var segmentGroup = segmentGroups[i];
if (!view.segmentLayer) {
view.segmentLayer = new Kinetic.Layer();
view.stage.add(view.segmentLayer);
view.segmentLayer.moveToTop();
}
segmentGroup.waveformShape = new Kinetic.Shape({
fill: color,
strokeWidth: 0
});
segmentGroup.waveformShape.on("mouseenter", menter);
segmentGroup.waveformShape.on("mouseleave", mleave);
segmentGroup.add(segmentGroup.waveformShape);
segmentGroup.label = new options.segmentLabelDraw(segmentGroup, segment);
segmentGroup.add(segmentGroup.label.hide());
if (editable) {
segmentGroup.inMarker = new options.segmentInMarker(true, segmentGroup, segment, segmentHandleDrag);
segmentGroup.add(segmentGroup.inMarker);
segmentGroup.outMarker = new options.segmentOutMarker(true, segmentGroup, segment, segmentHandleDrag);
segmentGroup.add(segmentGroup.outMarker);
}
view.segmentLayer.add(segmentGroup);
view.segmentLayer.draw();
}
segment.zoom = segmentZoomGroup;
segment.zoom.view = waveformView.waveformZoomView;
segment.overview = segmentOverviewGroup;
segment.overview.view = waveformView.waveformOverview;
segment.color = color;
segment.editable = editable;
return segment;
};
var updateSegmentWaveform = function (segment) {
// Binding with data
waveformView.waveformOverview.data.set_segment(waveformView.waveformOverview.data.at_time(segment.startTime), waveformView.waveformOverview.data.at_time(segment.endTime), segment.id);
waveformView.waveformZoomView.data.set_segment(waveformView.waveformZoomView.data.at_time(segment.startTime), waveformView.waveformZoomView.data.at_time(segment.endTime), segment.id);
// Overview
var overviewStartOffset = waveformView.waveformOverview.data.at_time(segment.startTime);
var overviewEndOffset = waveformView.waveformOverview.data.at_time(segment.endTime);
segment.overview.waveformShape.setDrawFunc(function(canvas) {
mixins.waveformSegmentDrawFunction.call(this, waveformView.waveformOverview.data, segment.id, canvas, mixins.interpolateHeight(waveformView.waveformOverview.height));
});
segment.overview.setWidth(overviewEndOffset - overviewStartOffset);
if (segment.editable) {
if (segment.overview.inMarker) segment.overview.inMarker.show().setX(overviewStartOffset - segment.overview.inMarker.getWidth());
if (segment.overview.outMarker) segment.overview.outMarker.show().setX(overviewEndOffset);
// Change Text
segment.overview.inMarker.label.setText(mixins.niceTime(segment.startTime, false));
segment.overview.outMarker.label.setText(mixins.niceTime(segment.endTime, false));
}
// Label
// segment.overview.label.setX(overviewStartOffset);
segment.overview.view.segmentLayer.draw();
// Zoom
var zoomStartOffset = waveformView.waveformZoomView.data.at_time(segment.startTime);
var zoomEndOffset = waveformView.waveformZoomView.data.at_time(segment.endTime);
var frameStartOffset = waveformView.waveformZoomView.frameOffset;
var frameEndOffset = waveformView.waveformZoomView.frameOffset + waveformView.waveformZoomView.width;
if (zoomStartOffset < frameStartOffset) zoomStartOffset = frameStartOffset;
if (zoomEndOffset > frameEndOffset) zoomEndOffset = frameEndOffset;
if (waveformView.waveformZoomView.data.segments[segment.id].visible) {
var startPixel = zoomStartOffset - frameStartOffset;
var endPixel = zoomEndOffset - frameStartOffset;
segment.zoom.show();
segment.zoom.waveformShape.setDrawFunc(function(canvas) {
mixins.waveformSegmentDrawFunction.call(this, waveformView.waveformZoomView.data, segment.id, canvas, mixins.interpolateHeight(waveformView.waveformZoomView.height));
});
if (segment.editable) {
if (segment.zoom.inMarker) segment.zoom.inMarker.show().setX(startPixel - segment.zoom.inMarker.getWidth());
if (segment.zoom.outMarker) segment.zoom.outMarker.show().setX(endPixel);
// Change Text
segment.zoom.inMarker.label.setText(mixins.niceTime(segment.startTime, false));
segment.zoom.outMarker.label.setText(mixins.niceTime(segment.endTime, false));
}
} else {
segment.zoom.hide();
}
// Label
// segment.zoom.label.setX(0);
// segment.zoom.label.setY(12);
segment.zoom.view.segmentLayer.draw();
};
var segmentHandleDrag = function (thisSeg, segment) {
if (thisSeg.inMarker.getX() > 0) {
var inOffset = thisSeg.view.frameOffset + thisSeg.inMarker.getX() + thisSeg.inMarker.getWidth();
segment.startTime = thisSeg.view.data.time(inOffset);
}
if (thisSeg.outMarker.getX() < thisSeg.view.width) {
var outOffset = thisSeg.view.frameOffset + thisSeg.outMarker.getX();
segment.endTime = thisSeg.view.data.time(outOffset);
}
updateSegmentWaveform(segment);
};
var getSegmentColor = function () {
var c;
if (options.randomizeSegmentColor) {
var g = function () { return Math.floor(Math.random()*255); };
c = 'rgba('+g()+', '+g()+', '+g()+', 1)';
} else {
c = options.segmentColor;
}
return c;
};
that.init = function () {
bootstrap.pubsub.on("waveform_zoom_displaying", this.updateSegments);
};
this.updateSegments = function () {
that.segments.forEach(function(segment){
updateSegmentWaveform(segment);
});
};
this.createSegment = function (startTime, endTime, editable, color, labelText) {
var segmentId = "segment" + that.segments.length;
var segment = createSegmentWaveform(segmentId, startTime, endTime, editable, color, labelText);
updateSegmentWaveform(segment);
that.segments.push(segment);
};
};
});
/**
* WAVEFORM.CORE.JS
*
* This module bootstraps all our waveform components and manages
* initialisation as well as some component-wide events such as
* viewport resizing.
*/
define('m/player/waveform/waveform.core',[
"m/bootstrap",
"WaveformData",
"m/player/waveform/waveform.overview",
"m/player/waveform/waveform.zoomview",
"m/player/waveform/waveform.segments"
], function (bootstrap, WaveformData, WaveformOverview, WaveformZoomView, WaveformSegments) {
return function () {
return {
init: function (options, ui) {
this.ui = ui; // See buildUi in main.js
this.options = options;
var xhr, that = this;
// Detect if we can support standard XHR of fall back to IE XDR
if ('withCredentials' in new XMLHttpRequest()) {
/* supports cross-domain requests */
xhr = new XMLHttpRequest();
} else if(typeof XDomainRequest !== "undefined"){
// Use IE-specific "CORS" code with XDR
xhr = new XDomainRequest();
}
var fileEx = new RegExp(/\.[^.]*$/);
var extension = that.options.dataUri.match(fileEx);
// open an XHR request to the data soure file
xhr.open('GET', that.options.dataUri, true);
if (extension && (extension[0] === ".dat" || extension[0] === ".DAT" ) ) {
// Detect if we can support ArrayBuffer for byte data or fall back to JSON
if (typeof Uint8Array !== "undefined") {
xhr.responseType = 'arraybuffer';
} else {
that.options.dataUri.replace(fileEx, ".json");
if (console && console.info) console.info("Changing request type to .json as browser does not support ArrayBuffer");
}
}
xhr.onload = function(response) {
//xhr object, supposedly ArrayBuffer
//XDomainRequest object (always in JSON)
if ('XDomainRequest' in window || ('readyState' in this && (this.readyState === 4 && this.status === 200))){
handleData(WaveformData.create(response.target));
}
};
xhr.send(); // Look at it go!
/**
* Handle data provided by our waveform data module after parsing the XHR request
* @param {Object} origWaveformData Parsed ArrayBuffer or JSON response
*/
var handleData = function (origWaveformData) {
that.origWaveformData = origWaveformData;
var overviewWaveformData = that.origWaveformData.resample(that.ui.$player.width());
that.waveformOverview = new WaveformOverview(overviewWaveformData, that.ui.$overview, that.options);
bootstrap.pubsub.emit("waveformOverviewReady");
that.bindResize();
};
},
openZoomView: function () {
var that = this;
$("#waveformZoomContainer").show();
that.waveformZoomView = new WaveformZoomView(that.origWaveformData, that.ui.$zoom, that.options);
bootstrap.pubsub.emit("waveform_zoom_start");
that.segments = new WaveformSegments(that, that.options);
that.segments.init();
},
/**
* Deal with window resize event over both waveform views.
*/
bindResize: function () {
var that = this;
$(window).on("resize", function () {
that.ui.$overview.hide();
that.ui.$zoom.hide();
if (this.resizeTimeoutId) clearTimeout(this.resizeTimeoutId);
this.resizeTimeoutId = setTimeout(function(){
var w = that.ui.$player.width();
var overviewWaveformData = that.origWaveformData.resample(w);
bootstrap.pubsub.emit("resizeEndOverview", w, overviewWaveformData);
bootstrap.pubsub.emit("window_resized", w, that.origWaveformData);
}, 500);
});
bootstrap.pubsub.on("overview_resized", function () {
that.ui.$overview.fadeIn(200);
});
bootstrap.pubsub.on("zoomview_resized", function () {
that.ui.$zoom.fadeIn(200);
});
}
};
};
});
define('templates/main',[],function(){
this["JST"] = this["JST"] || {};
this["JST"]["main"] = function(obj) {
obj || (obj = {});
var __t, __p = '', __e = _.escape;
with (obj) {
__p += '<div id="waveform">\n <div id="zoom-container"></div>\n <div id="overview-container"></div>\n</div>\n';
}
return __p
};
return this["JST"];
});
define('m/player/player.keyboard',["m/bootstrap"], function (bootstrap) {
return {
init: function () {
var pubsub = bootstrap.pubsub;
var that = this;
var SPACE = 32,
TAB = 9,
LEFT_ARROW = 37,
RIGHT_ARROW = 39;
$(document).on("keydown keypress keyup", function (event) { // Arrow keys only triggered on keydown, not keypress
var c = event.keyCode,
t = event.type,
$t = $(event.target);
if (!$t.is('input') && !$t.is('object')) {
if ([SPACE, TAB, LEFT_ARROW, RIGHT_ARROW].indexOf(c) > -1) {
event.preventDefault();
}
if (t === "keydown" || t === "keypress") {
switch (c) {
case SPACE:
pubsub.emit("kybrd_space");
break;
case TAB:
pubsub.emit("kybrd_tab");
break;
}
} else if (t === "keyup") {
switch (c) {
case LEFT_ARROW:
if (event.shiftKey) pubsub.emit("kybrd_shift_left");
else pubsub.emit("kybrd_left");
break;
case RIGHT_ARROW:
if (event.shiftKey) pubsub.emit("kybrd_shift_right");
else pubsub.emit("kybrd_right");
break;
}
}
}
});
}
};
});
require.config({
paths: {
'm': "waveform_viewer"
}
});
// Load jquery from global if present
if (window.$) {
define('jquery', [], function() {
return jQuery;
});
} else {
throw new Error("Peaks.js requires jQuery");
}
define('main',[
'm/bootstrap',
'm/player/player',
'm/player/waveform/waveform.core',
'm/player/waveform/waveform.mixins',
'templates/main',
'm/player/player.keyboard',
'jquery'
], function(bootstrap, AudioPlayer, Waveform, mixins, JST, keyboard, $){
var buildUi = function () {
return {
$player: $("#waveform"),
$zoom: $("#zoom-container"),
$overview: $("#overview-container")
};
};
var api = { // PUBLIC API
init: function (opts) {
if (!opts.audioElement) {
throw new Error("Please provide an audio element.");
} else if (!opts.container) {
throw new Error("Please provide a container object.");
} else if (opts.container.width < 1 || opts.container.height < 1) {
throw new Error("Please ensure that the container has a defined width and height.");
} else {
api.options = $.extend({
zoomLevels: [512, 1024, 2048, 4096], // Array of scale factors (samples per pixel) for the zoom levels (big >> small)
keyboard: false, // Bind keyboard controls
nudgeIncrement: 0.01, // Keyboard nudge increment in seconds (left arrow/right arrow)
inMarkerColor: '#a0a0a0', // Colour for the in marker of segments
outMarkerColor: '#a0a0a0', // Colour for the out marker of segments
zoomWaveformColor: 'rgba(0, 225, 128, 1)', // Colour for the zoomed in waveform
overviewWaveformColor: 'rgba(0,0,0,0.2)', // Colour for the overview waveform
randomizeSegmentColor: true, // Random colour per segment (overrides segmentColor)
height: 200, // height of the waveform canvases in pixels
segmentColor: 'rgba(255, 161, 39, 1)' // Colour for segments on the waveform,
}, opts);
api.options = $.extend({
segmentInMarker: mixins.defaultInMarker(api.options),
segmentOutMarker: mixins.defaultOutMarker(api.options),
segmentLabelDraw: mixins.defaultSegmentLabelDraw(api.options)
}, api.options);
api.currentZoomLevel = 0;
$(api.options.container).html(JST.main).promise().done(function () {
if (api.options.keyboard) keyboard.init();
api.player = new AudioPlayer();
api.player.init(api.options.audioElement);
api.waveform = new Waveform();
api.waveform.init(api.options, buildUi());
window.peaks = api; // Attach to window object for simple external calls
bootstrap.pubsub.on("waveformOverviewReady", function () {
api.waveform.openZoomView();
if (api.options.segments) { // Any initial segments to be displayed?
api.segments.addSegment(api.options.segments);
}
});
});
}
},
segments: {
addSegment: function (startTime, endTime, segmentEditable, color, labelText) {
if (typeof startTime == "number") {
api.waveform.segments.createSegment(startTime, endTime, segmentEditable, color, labelText);
} else if (typeof startTime == "object" && startTime.length){
for (var i = 0; i < startTime.length; i++) {
var segment = startTime[i];
api.waveform.segments.createSegment(segment.startTime, segment.endTime, segment.editable, segment.color, segment.labelText);
}
}
},
// removeSegment: function (segment) {
// },
// clearSegments : function () { // Remove all segments
// },
getSegments: function () {
return api.waveform.segments.segments;
}
},
time: {
getCurrentTime: function () {
return api.player.getTime();
}
},
zoom: { // namepsace for zooming related methods
/**
* Zoom in one level
*/
zoomIn: function () {
api.zoom.setZoom(api.currentZoomLevel - 1);
},
/**
* Zoom out one level
*/
zoomOut: function () {
api.zoom.setZoom(api.currentZoomLevel + 1);
},
/**
* Given a particular zoom level, triggers a resampling of the data in the zoomed view
*
* @param {number} zoomLevelIndex
*/
setZoom: function (zoomLevelIndex) { // Set zoom level to index of current zoom levels
if (zoomLevelIndex >= api.options.zoomLevels.length){
zoomLevelIndex = api.options.zoomLevels.length - 1;
}
if (zoomLevelIndex < 0){
zoomLevelIndex = 0;
}
api.currentZoomLevel = zoomLevelIndex;
bootstrap.pubsub.emit("waveform_zoom_level_changed", api.options.zoomLevels[zoomLevelIndex]);
},
/**
* Returns the current zoom level
*
* @returns {number}
*/
getZoom: function () {
return api.currentZoomLevel;
}
}
};
return api;
});
//The modules for your project will be inlined above
//this snippet. Ask almond to synchronously require the
//module value for 'main' here and return it as the
//value to use for the public API for the built file.
return require('main');
}));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment