waaclock
Version:
A comprehensive event scheduling tool for Web Audio API.
235 lines (198 loc) • 7.28 kB
JavaScript
var isBrowser = (typeof window !== 'undefined')
var CLOCK_DEFAULTS = {
toleranceLate: 0.10,
toleranceEarly: 0.001
}
// ==================== Event ==================== //
var Event = function(clock, deadline, func) {
this.clock = clock
this.func = func
this._cleared = false // Flag used to clear an event inside callback
this.toleranceLate = clock.toleranceLate
this.toleranceEarly = clock.toleranceEarly
this._latestTime = null
this._earliestTime = null
this.deadline = null
this.repeatTime = null
this.schedule(deadline)
}
// Unschedules the event
Event.prototype.clear = function() {
this.clock._removeEvent(this)
this._cleared = true
return this
}
// Sets the event to repeat every `time` seconds.
Event.prototype.repeat = function(time) {
if (time === 0)
throw new Error('delay cannot be 0')
this.repeatTime = time
if (!this.clock._hasEvent(this))
this.schedule(this.deadline + this.repeatTime)
return this
}
// Sets the time tolerance of the event.
// The event will be executed in the interval `[deadline - early, deadline + late]`
// If the clock fails to execute the event in time, the event will be dropped.
Event.prototype.tolerance = function(values) {
if (typeof values.late === 'number')
this.toleranceLate = values.late
if (typeof values.early === 'number')
this.toleranceEarly = values.early
this._refreshEarlyLateDates()
if (this.clock._hasEvent(this)) {
this.clock._removeEvent(this)
this.clock._insertEvent(this)
}
return this
}
// Returns true if the event is repeated, false otherwise
Event.prototype.isRepeated = function() { return this.repeatTime !== null }
// Schedules the event to be ran before `deadline`.
// If the time is within the event tolerance, we handle the event immediately.
// If the event was already scheduled at a different time, it is rescheduled.
Event.prototype.schedule = function(deadline) {
this._cleared = false
this.deadline = deadline
this._refreshEarlyLateDates()
if (this.clock.context.currentTime >= this._earliestTime) {
this._execute()
} else if (this.clock._hasEvent(this)) {
this.clock._removeEvent(this)
this.clock._insertEvent(this)
} else this.clock._insertEvent(this)
}
Event.prototype.timeStretch = function(tRef, ratio) {
if (this.isRepeated())
this.repeatTime = this.repeatTime * ratio
var deadline = tRef + ratio * (this.deadline - tRef)
// If the deadline is too close or past, and the event has a repeat,
// we calculate the next repeat possible in the stretched space.
if (this.isRepeated()) {
while (this.clock.context.currentTime >= deadline - this.toleranceEarly)
deadline += this.repeatTime
}
this.schedule(deadline)
}
// Executes the event
Event.prototype._execute = function() {
if (this.clock._started === false) return
this.clock._removeEvent(this)
if (this.clock.context.currentTime < this._latestTime)
this.func(this)
else {
if (this.onexpired) this.onexpired(this)
console.warn('event expired')
}
// In the case `schedule` is called inside `func`, we need to avoid
// overrwriting with yet another `schedule`.
if (!this.clock._hasEvent(this) && this.isRepeated() && !this._cleared)
this.schedule(this.deadline + this.repeatTime)
}
// Updates cached times
Event.prototype._refreshEarlyLateDates = function() {
this._latestTime = this.deadline + this.toleranceLate
this._earliestTime = this.deadline - this.toleranceEarly
}
// ==================== WAAClock ==================== //
var WAAClock = module.exports = function(context, opts) {
var self = this
opts = opts || {}
this.tickMethod = opts.tickMethod || 'ScriptProcessorNode'
this.toleranceEarly = opts.toleranceEarly || CLOCK_DEFAULTS.toleranceEarly
this.toleranceLate = opts.toleranceLate || CLOCK_DEFAULTS.toleranceLate
this.context = context
this._events = []
this._started = false
}
// ---------- Public API ---------- //
// Schedules `func` to run after `delay` seconds.
WAAClock.prototype.setTimeout = function(func, delay) {
return this._createEvent(func, this._absTime(delay))
}
// Schedules `func` to run before `deadline`.
WAAClock.prototype.callbackAtTime = function(func, deadline) {
return this._createEvent(func, deadline)
}
// Stretches `deadline` and `repeat` of all scheduled `events` by `ratio`, keeping
// their relative distance to `tRef`. In fact this is equivalent to changing the tempo.
WAAClock.prototype.timeStretch = function(tRef, events, ratio) {
events.forEach(function(event) { event.timeStretch(tRef, ratio) })
return events
}
// Removes all scheduled events and starts the clock
WAAClock.prototype.start = function() {
if (this._started === false) {
var self = this
this._started = true
this._events = []
if (this.tickMethod === 'ScriptProcessorNode') {
var bufferSize = 256
// We have to keep a reference to the node to avoid garbage collection
this._clockNode = this.context.createScriptProcessor(bufferSize, 1, 1)
this._clockNode.connect(this.context.destination)
this._clockNode.onaudioprocess = function () {
setTimeout(function() { self.tick() }, 0)
}
} else if (this.tickMethod === 'manual') null // tick is called manually
else throw new Error('invalid tickMethod ' + this.tickMethod)
}
}
// Stops the clock
WAAClock.prototype.stop = function() {
if (this._started === true) {
this._started = false
this._clockNode.disconnect()
}
}
// ---------- Private ---------- //
// This function is ran periodically, and at each tick it executes
// events for which `currentTime` is included in their tolerance interval.
WAAClock.prototype.tick = function() {
var event = this._events.shift()
while(event && event._earliestTime <= this.context.currentTime) {
event._execute()
event = this._events.shift()
}
// Put back the last event
if(event) this._events.unshift(event)
}
// Creates an event and insert it to the list
WAAClock.prototype._createEvent = function(func, deadline) {
return new Event(this, deadline, func)
}
// Inserts an event to the list
WAAClock.prototype._insertEvent = function(event) {
this._events.splice(this._indexByTime(event._earliestTime), 0, event)
}
// Removes an event from the list
WAAClock.prototype._removeEvent = function(event) {
var ind = this._events.indexOf(event)
if (ind !== -1) this._events.splice(ind, 1)
}
// Returns true if `event` is in queue, false otherwise
WAAClock.prototype._hasEvent = function(event) {
return this._events.indexOf(event) !== -1
}
// Returns the index of the first event whose deadline is >= to `deadline`
WAAClock.prototype._indexByTime = function(deadline) {
// performs a binary search
var low = 0
, high = this._events.length
, mid
while (low < high) {
mid = Math.floor((low + high) / 2)
if (this._events[mid]._earliestTime < deadline)
low = mid + 1
else high = mid
}
return low
}
// Converts from relative time to absolute time
WAAClock.prototype._absTime = function(relTime) {
return relTime + this.context.currentTime
}
// Converts from absolute time to relative time
WAAClock.prototype._relTime = function(absTime) {
return absTime - this.context.currentTime
}