UNPKG

click-track

Version:

JavaScript utility for click track events.

349 lines (341 loc) 12.9 kB
'use strict'; var steEvents = require('ste-events'); function isTimer(obj) { return typeof obj === "object" && typeof obj.onUpdate === "function" && typeof obj.offUpdate === "function" && typeof obj.deconstruct === "function"; } var BasicTimer = /** @class */ (function () { function BasicTimer(autostart, length, loop) { if (autostart === void 0) { autostart = false; } if (length === void 0) { length = Infinity; } if (loop === void 0) { loop = true; } this.autostart = autostart; this.length = length; this.loop = loop; this.startTimeMarker = Date.now(); this.position = 0; this.animationFrameId = undefined; this.onTick = new steEvents.EventDispatcher(); if (autostart) { this.start(); } } BasicTimer.prototype.start = function () { this.startTimeMarker = Date.now(); if (this.animationFrameId) { cancelAnimationFrame(this.animationFrameId); } this.animationFrameId = requestAnimationFrame(this.updateTime.bind(this)); }; BasicTimer.prototype.stop = function () { if (this.animationFrameId) { cancelAnimationFrame(this.animationFrameId); } }; BasicTimer.prototype.updateTime = function () { this.animationFrameId = undefined; var now = Date.now(); var position = (now - this.startTimeMarker) / 1000; if (position !== this.position) { this.position = position; if (this.position > this.length) { if (!this.loop) { this.position = this.length; } else { this.position = this.position % this.length; } } try { this.onTick.dispatch(this, position); } finally { // Regardless of success, keep timer going this.animationFrameId = requestAnimationFrame(this.updateTime.bind(this)); } } }; BasicTimer.prototype.onUpdate = function (cb) { this.onTick.subscribe(cb); }; BasicTimer.prototype.offUpdate = function (cb) { this.onTick.unsubscribe(cb); }; BasicTimer.prototype.deconstruct = function () { this.onTick.clear(); if (this.animationFrameId) { cancelAnimationFrame(this.animationFrameId); } }; return BasicTimer; }()); // Processes cues and produces a tuple of two arrays: one that contains just the time indexes, and another that contains the original data, but with holes. function separateCueSequence(cues) { var leanMap = []; var dataMap = []; cues.forEach(function (cue, i) { if (typeof cue === "number") { leanMap[i] = cue; return; } leanMap[i] = cue[0]; dataMap[i] = cue[1]; }); return [leanMap, dataMap]; } var UniversalTimer = /** @class */ (function () { function UniversalTimer(getTimeFn) { this.getTimeFn = getTimeFn; this.position = 0; this.onTick = new steEvents.EventDispatcher(); this.animationFrameId = undefined; this.animationFrameId = requestAnimationFrame(this.updateTime.bind(this)); } UniversalTimer.prototype.updateTime = function () { this.animationFrameId = undefined; var position = this.getTimeFn(); try { if (position !== this.position) { this.position = position; this.onTick.dispatch(this, position); } } finally { // Regardless of success, keep timer going this.animationFrameId = requestAnimationFrame(this.updateTime.bind(this)); } }; UniversalTimer.prototype.onUpdate = function (cb) { this.onTick.subscribe(cb); }; UniversalTimer.prototype.offUpdate = function (cb) { this.onTick.unsubscribe(cb); }; UniversalTimer.prototype.deconstruct = function () { this.onTick.clear(); if (this.animationFrameId) { cancelAnimationFrame(this.animationFrameId); } }; return UniversalTimer; }()); // C represents the type of data to be used for cue events var ClickTrack = /** @class */ (function () { function ClickTrack(options) { this.tempo = 60; this.tempoBPS = 1; this.offset = 0; this.length = Infinity; this.hasOwnTimer = false; this.cues = []; this.cueData = []; this.currentBeat = -1; this.previousBeat = -1; this.currentCue = -1; this.previousCue = -1; this.events = new steEvents.NonUniformEventList(); this.dragSamples = new Array(); this.fixDrag = false; this.dragOffset = 0; if (options.tempo !== undefined) { // Validate tempo if (options.tempo === 0 || options.tempo < 0) { throw new Error("Invalid tempo (" + options.tempo + "), must be greater than 0."); } this.tempo = options.tempo; this.tempoBPS = this.tempo / 60; } if (options.offset !== undefined) { this.offset = options.offset; } if (options.fixDrag) { this.fixDrag = true; this.dragSamples = new Array(10).fill(0); } // Setup cues and cue data if (options.cues !== undefined) { var _a = separateCueSequence(options.cues), cueLean = _a[0], cueData = _a[1]; this.cues = cueLean; this.cueData = cueData; } // Setup timer if (isTimer(options.timerSource)) { // Custom timer this.timer = options.timerSource; this.length = Infinity; } else if (options.timerSource === undefined) { // Basic timer this.timer = new BasicTimer(options.autostart, options.length, options.loop); this.hasOwnTimer = true; if (options.length !== undefined) { this.length = options.length; } } else if (typeof options.timerSource === "function") { // universal timer using function this.timer = new UniversalTimer(options.timerSource); this.hasOwnTimer = true; } else { throw new Error('Constructing ClickTrack: Unknown value type for timerSource option.'); } // Bind the tick event this.timer.onUpdate(this.setTime.bind(this)); } // Adds event listener ClickTrack.prototype.on = function (event, fn) { this.events.get(event).subscribe(fn); }; // Adds event listener to only handle once ClickTrack.prototype.once = function (event, fn) { var _this = this; this.events.get(event).subscribe(function () { var args = []; for (var _i = 0; _i < arguments.length; _i++) { args[_i] = arguments[_i]; } _this.events.get(event).unsubscribe(fn); fn.apply(_this, args); }); }; // Removes event listener ClickTrack.prototype.off = function (event, fn) { this.events.get(event).unsubscribe(fn); }; ClickTrack.prototype.start = function () { if (this.timer instanceof BasicTimer) { this.timer.start(); } else { throw new Error('Click must be master to support start()'); } }; ClickTrack.prototype.deconstruct = function () { this.timer.offUpdate(this.setTime.bind(this)); if (this.hasOwnTimer) { this.timer.deconstruct(); } delete this.cues; delete this.cueData; }; // Sets the time in seconds ClickTrack.prototype.setTime = function (timer, time) { // Adjust for offset var offsetTime = time - this.offset; // Set previous click pointer to current click before we change current click this.previousBeat = this.currentBeat; // Calculate current beat this.currentBeat = offsetTime * this.tempoBPS; // Process beat events only if listeners are listening if (this.events.get("beat").count) { this.tickClickEvents(this.previousBeat, this.currentBeat); } // Process cue events only if cues exist and listeners are listening if (this.cues && (this.events.get("cue").count || this.events.get("firstCue").count || this.events.get("lastCue").count)) { this.tickCueEvents(this.previousBeat, this.currentBeat); } }; Object.defineProperty(ClickTrack.prototype, "beat", { get: function () { return this.currentBeat; }, enumerable: true, configurable: true }); ClickTrack.prototype.tickCueEvents = function (fromBeat, toBeat) { // Start scanning for current cue from previous cue, only if track moved forward. Otherwise scan from the beginning. var calcCueFrom = toBeat > fromBeat ? this.previousCue : 0; // Where the calculated cue index will be stored var calcCue; // This for loop will set calcCue to the index of the next cue for ( // Start iteration at current cue marker, but if less than 0 (-1) then start counter at 0 calcCue = Math.max(0, calcCueFrom); // Iterate until cue marker is greater than current time, or until no more cues this.cues[calcCue] < toBeat + this.dragOffset && calcCue < this.cues.length; // Increment by one calcCue++) ; // Subtract 1 to get the current cue index calcCue -= 1; // Set previous cue pointer before changing current cue this.previousCue = this.currentCue; // Set current cue this.currentCue = calcCue; // At the beginning if (this.currentCue === -1) { return; } // Nothing changed if (this.currentCue === this.previousCue) { return; } // Moved backwards if (this.currentCue < this.previousCue) { return; } // Traverse each cue from last 'til current for (var i = this.previousCue + 1; i <= this.currentCue; i++) { var cueData = this.cueData[i] || null; var time = this.cues[i] / this.tempoBPS; var drag = toBeat - this.cues[i]; if (this.fixDrag) { this.dragSamples.unshift(drag); this.dragSamples.pop(); } var event_1 = { time: time, beat: this.cues[i], data: cueData, cue: i, drag: drag, }; this.events.get("cue").dispatchAsync(this, event_1); if (i === 0) { this.events.get("firstCue").dispatchAsync(this, event_1); } if (i === this.cues.length - 1) { this.events.get("lastCue").dispatchAsync(this, event_1); } } if (this.fixDrag) { // Get average offset, but keep it between -1 and 1 this.dragOffset = Math.min(1, Math.max(-1, this.dragSamples.reduce(function (a, c) { return a + c; }, 0) / this.dragSamples.length / 2)); } }; ClickTrack.prototype.tickClickEvents = function (fromBeat, toBeat) { // We are assuming a backwards scrub happened, so we won't produce any events // In some cases, this might mean we looped back to the beginning. if (fromBeat > toBeat) { return; } // Beats are the same if (fromBeat === toBeat) { return; } var fromBeatInt = Math.max(0, Math.ceil(fromBeat)); var toBeatInt = Math.floor(toBeat); // Beat hasn't advanced a whole number yet if (fromBeatInt > toBeatInt) { return; } var clickEvents = []; for (var i = fromBeatInt; i <= toBeatInt; i++) { var time = i / this.tempoBPS; clickEvents.push({ time: time, beat: i, drag: toBeat - i, }); } // Loop through clicksBetween and dispatch for (var i = 0; i < clickEvents.length; i++) { this.events.get("beat").dispatchAsync(this, clickEvents[i]); } }; return ClickTrack; }()); module.exports = ClickTrack;