mirror of
https://github.com/Tonejs/Tone.js
synced 2024-11-16 08:38:00 +00:00
382 lines
No EOL
10 KiB
JavaScript
382 lines
No EOL
10 KiB
JavaScript
define(["Tone/core/Tone"], function (Tone) {
|
|
|
|
"use strict";
|
|
|
|
/**
|
|
* @class A Timeline class for scheduling and maintaining state
|
|
* along a timeline. All events must have a "time" property.
|
|
* Internally, events are stored in time order for fast
|
|
* retrieval.
|
|
* @extends {Tone}
|
|
* @param {Positive} [memory=Infinity] The number of previous events that are retained.
|
|
*/
|
|
Tone.Timeline = function(){
|
|
|
|
var options = Tone.defaults(arguments, ["memory"], Tone.Timeline);
|
|
Tone.call(this);
|
|
|
|
/**
|
|
* The array of scheduled timeline events
|
|
* @type {Array}
|
|
* @private
|
|
*/
|
|
this._timeline = [];
|
|
|
|
/**
|
|
* An array of items to remove from the list.
|
|
* @type {Array}
|
|
* @private
|
|
*/
|
|
this._toRemove = [];
|
|
|
|
/**
|
|
* Flag if the timeline is mid iteration
|
|
* @private
|
|
* @type {Boolean}
|
|
*/
|
|
this._iterating = false;
|
|
|
|
/**
|
|
* The memory of the timeline, i.e.
|
|
* how many events in the past it will retain
|
|
* @type {Positive}
|
|
*/
|
|
this.memory = options.memory;
|
|
};
|
|
|
|
Tone.extend(Tone.Timeline);
|
|
|
|
/**
|
|
* the default parameters
|
|
* @static
|
|
* @const
|
|
*/
|
|
Tone.Timeline.defaults = {
|
|
"memory" : Infinity
|
|
};
|
|
|
|
/**
|
|
* The number of items in the timeline.
|
|
* @type {Number}
|
|
* @memberOf Tone.Timeline#
|
|
* @name length
|
|
* @readOnly
|
|
*/
|
|
Object.defineProperty(Tone.Timeline.prototype, "length", {
|
|
get : function(){
|
|
return this._timeline.length;
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Insert an event object onto the timeline. Events must have a "time" attribute.
|
|
* @param {Object} event The event object to insert into the
|
|
* timeline.
|
|
* @returns {Tone.Timeline} this
|
|
*/
|
|
Tone.Timeline.prototype.add = function(event){
|
|
//the event needs to have a time attribute
|
|
if (this.isUndef(event.time)){
|
|
throw new Error("Tone.Timeline: events must have a time attribute");
|
|
}
|
|
if (this._timeline.length){
|
|
var index = this._search(event.time);
|
|
this._timeline.splice(index + 1, 0, event);
|
|
} else {
|
|
this._timeline.push(event);
|
|
}
|
|
//if the length is more than the memory, remove the previous ones
|
|
if (this.length > this.memory){
|
|
var diff = this.length - this.memory;
|
|
this._timeline.splice(0, diff);
|
|
}
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Remove an event from the timeline.
|
|
* @param {Object} event The event object to remove from the list.
|
|
* @returns {Tone.Timeline} this
|
|
*/
|
|
Tone.Timeline.prototype.remove = function(event){
|
|
if (this._iterating){
|
|
this._toRemove.push(event);
|
|
} else {
|
|
var index = this._timeline.indexOf(event);
|
|
if (index !== -1){
|
|
this._timeline.splice(index, 1);
|
|
}
|
|
}
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Get the nearest event whose time is less than or equal to the given time.
|
|
* @param {Number} time The time to query.
|
|
* @returns {Object} The event object set after that time.
|
|
*/
|
|
Tone.Timeline.prototype.get = function(time){
|
|
var index = this._search(time);
|
|
if (index !== -1){
|
|
return this._timeline[index];
|
|
} else {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Return the first event in the timeline without removing it
|
|
* @returns {Object} The first event object
|
|
*/
|
|
Tone.Timeline.prototype.peek = function(){
|
|
return this._timeline[0];
|
|
};
|
|
|
|
/**
|
|
* Return the first event in the timeline and remove it
|
|
* @returns {Object} The first event object
|
|
*/
|
|
Tone.Timeline.prototype.shift = function(){
|
|
return this._timeline.shift();
|
|
};
|
|
|
|
/**
|
|
* Get the event which is scheduled after the given time.
|
|
* @param {Number} time The time to query.
|
|
* @returns {Object} The event object after the given time
|
|
*/
|
|
Tone.Timeline.prototype.getAfter = function(time){
|
|
var index = this._search(time);
|
|
if (index + 1 < this._timeline.length){
|
|
return this._timeline[index + 1];
|
|
} else {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Get the event before the event at the given time.
|
|
* @param {Number} time The time to query.
|
|
* @returns {Object} The event object before the given time
|
|
*/
|
|
Tone.Timeline.prototype.getBefore = function(time){
|
|
var len = this._timeline.length;
|
|
//if it's after the last item, return the last item
|
|
if (len > 0 && this._timeline[len - 1].time < time){
|
|
return this._timeline[len - 1];
|
|
}
|
|
var index = this._search(time);
|
|
if (index - 1 >= 0){
|
|
return this._timeline[index - 1];
|
|
} else {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Cancel events after the given time
|
|
* @param {Number} time The time to query.
|
|
* @returns {Tone.Timeline} this
|
|
*/
|
|
Tone.Timeline.prototype.cancel = function(after){
|
|
if (this._timeline.length > 1){
|
|
var index = this._search(after);
|
|
if (index >= 0){
|
|
if (this._timeline[index].time === after){
|
|
//get the first item with that time
|
|
for (var i = index; i >= 0; i--){
|
|
if (this._timeline[i].time === after){
|
|
index = i;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
this._timeline = this._timeline.slice(0, index);
|
|
} else {
|
|
this._timeline = this._timeline.slice(0, index + 1);
|
|
}
|
|
} else {
|
|
this._timeline = [];
|
|
}
|
|
} else if (this._timeline.length === 1){
|
|
//the first item's time
|
|
if (this._timeline[0].time >= after){
|
|
this._timeline = [];
|
|
}
|
|
}
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Cancel events before or equal to the given time.
|
|
* @param {Number} time The time to cancel before.
|
|
* @returns {Tone.Timeline} this
|
|
*/
|
|
Tone.Timeline.prototype.cancelBefore = function(time){
|
|
if (this._timeline.length){
|
|
var index = this._search(time);
|
|
if (index >= 0){
|
|
this._timeline = this._timeline.slice(index + 1);
|
|
}
|
|
}
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Does a binary serach on the timeline array and returns the
|
|
* nearest event index whose time is after or equal to the given time.
|
|
* If a time is searched before the first index in the timeline, -1 is returned.
|
|
* If the time is after the end, the index of the last item is returned.
|
|
* @param {Number} time
|
|
* @return {Number} the index in the timeline array
|
|
* @private
|
|
*/
|
|
Tone.Timeline.prototype._search = function(time){
|
|
var beginning = 0;
|
|
var len = this._timeline.length;
|
|
var end = len;
|
|
if (len > 0 && this._timeline[len - 1].time <= time){
|
|
return len - 1;
|
|
}
|
|
while (beginning < end){
|
|
// calculate the midpoint for roughly equal partition
|
|
var midPoint = Math.floor(beginning + (end - beginning) / 2);
|
|
var event = this._timeline[midPoint];
|
|
var nextEvent = this._timeline[midPoint + 1];
|
|
if (event.time === time){
|
|
//choose the last one that has the same time
|
|
for (var i = midPoint; i < this._timeline.length; i++){
|
|
var testEvent = this._timeline[i];
|
|
if (testEvent.time === time){
|
|
midPoint = i;
|
|
}
|
|
}
|
|
return midPoint;
|
|
} else if (event.time < time && nextEvent.time > time){
|
|
return midPoint;
|
|
} else if (event.time > time){
|
|
//search lower
|
|
end = midPoint;
|
|
} else if (event.time < time){
|
|
//search upper
|
|
beginning = midPoint + 1;
|
|
}
|
|
}
|
|
return -1;
|
|
};
|
|
|
|
/**
|
|
* Internal iterator. Applies extra safety checks for
|
|
* removing items from the array.
|
|
* @param {Function} callback
|
|
* @param {Number=} lowerBound
|
|
* @param {Number=} upperBound
|
|
* @private
|
|
*/
|
|
Tone.Timeline.prototype._iterate = function(callback, lowerBound, upperBound){
|
|
this._iterating = true;
|
|
lowerBound = this.defaultArg(lowerBound, 0);
|
|
upperBound = this.defaultArg(upperBound, this._timeline.length - 1);
|
|
for (var i = lowerBound; i <= upperBound; i++){
|
|
callback.call(this, this._timeline[i]);
|
|
}
|
|
this._iterating = false;
|
|
if (this._toRemove.length > 0){
|
|
for (var j = 0; j < this._toRemove.length; j++){
|
|
var index = this._timeline.indexOf(this._toRemove[j]);
|
|
if (index !== -1){
|
|
this._timeline.splice(index, 1);
|
|
}
|
|
}
|
|
this._toRemove = [];
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Iterate over everything in the array
|
|
* @param {Function} callback The callback to invoke with every item
|
|
* @returns {Tone.Timeline} this
|
|
*/
|
|
Tone.Timeline.prototype.forEach = function(callback){
|
|
this._iterate(callback);
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Iterate over everything in the array at or before the given time.
|
|
* @param {Number} time The time to check if items are before
|
|
* @param {Function} callback The callback to invoke with every item
|
|
* @returns {Tone.Timeline} this
|
|
*/
|
|
Tone.Timeline.prototype.forEachBefore = function(time, callback){
|
|
//iterate over the items in reverse so that removing an item doesn't break things
|
|
var upperBound = this._search(time);
|
|
if (upperBound !== -1){
|
|
this._iterate(callback, 0, upperBound);
|
|
}
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Iterate over everything in the array after the given time.
|
|
* @param {Number} time The time to check if items are before
|
|
* @param {Function} callback The callback to invoke with every item
|
|
* @returns {Tone.Timeline} this
|
|
*/
|
|
Tone.Timeline.prototype.forEachAfter = function(time, callback){
|
|
//iterate over the items in reverse so that removing an item doesn't break things
|
|
var lowerBound = this._search(time);
|
|
this._iterate(callback, lowerBound + 1);
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Iterate over everything in the array at or after the given time. Similar to
|
|
* forEachAfter, but includes the item(s) at the given time.
|
|
* @param {Number} time The time to check if items are before
|
|
* @param {Function} callback The callback to invoke with every item
|
|
* @returns {Tone.Timeline} this
|
|
*/
|
|
Tone.Timeline.prototype.forEachFrom = function(time, callback){
|
|
//iterate over the items in reverse so that removing an item doesn't break things
|
|
var lowerBound = this._search(time);
|
|
//work backwards until the event time is less than time
|
|
while (lowerBound >= 0 && this._timeline[lowerBound].time >= time){
|
|
lowerBound--;
|
|
}
|
|
this._iterate(callback, lowerBound + 1);
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Iterate over everything in the array at the given time
|
|
* @param {Number} time The time to check if items are before
|
|
* @param {Function} callback The callback to invoke with every item
|
|
* @returns {Tone.Timeline} this
|
|
*/
|
|
Tone.Timeline.prototype.forEachAtTime = function(time, callback){
|
|
//iterate over the items in reverse so that removing an item doesn't break things
|
|
var upperBound = this._search(time);
|
|
if (upperBound !== -1){
|
|
this._iterate(function(event){
|
|
if (event.time === time){
|
|
callback.call(this, event);
|
|
}
|
|
}, 0, upperBound);
|
|
}
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Clean up.
|
|
* @return {Tone.Timeline} this
|
|
*/
|
|
Tone.Timeline.prototype.dispose = function(){
|
|
Tone.prototype.dispose.call(this);
|
|
this._timeline = null;
|
|
this._toRemove = null;
|
|
return this;
|
|
};
|
|
|
|
return Tone.Timeline;
|
|
}); |