UNPKG

waaclock

Version:

A comprehensive event scheduling tool for Web Audio API.

235 lines (198 loc) 7.28 kB
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 }