|
|
var TimingCallbacks = function(target, params) { |
|
|
var self = this; |
|
|
if (!params) params = {}; |
|
|
self.qpm = params.qpm ? parseInt(params.qpm, 10) : null; |
|
|
if (!self.qpm) { |
|
|
var tempo = target.metaText ? target.metaText.tempo : null; |
|
|
self.qpm = target.getBpm(tempo); |
|
|
} |
|
|
self.extraMeasuresAtBeginning = params.extraMeasuresAtBeginning ? parseInt(params.extraMeasuresAtBeginning, 10) : 0; |
|
|
self.beatCallback = params.beatCallback; |
|
|
self.eventCallback = params.eventCallback; |
|
|
self.lineEndCallback = params.lineEndCallback; |
|
|
self.lineEndAnticipation = params.lineEndAnticipation ? parseInt(params.lineEndAnticipation, 10) : 0; |
|
|
self.beatSubdivisions = params.beatSubdivisions ? parseInt(params.beatSubdivisions, 10) : 1; |
|
|
self.joggerTimer = null; |
|
|
|
|
|
self.replaceTarget = function(newTarget) { |
|
|
self.noteTimings = newTarget.setTiming(self.qpm, self.extraMeasuresAtBeginning); |
|
|
if (newTarget.noteTimings.length === 0) |
|
|
self.noteTimings = newTarget.setTiming(0,0); |
|
|
if (self.lineEndCallback) { |
|
|
self.lineEndTimings = getLineEndTimings(newTarget.noteTimings, self.lineEndAnticipation); |
|
|
} |
|
|
self.startTime = null; |
|
|
self.currentBeat = 0; |
|
|
self.currentEvent = 0; |
|
|
self.currentLine = 0; |
|
|
self.currentTime = 0; |
|
|
self.isPaused = false; |
|
|
self.isRunning = false; |
|
|
self.pausedPercent = null; |
|
|
self.justUnpaused = false; |
|
|
self.newSeekPercent = 0; |
|
|
self.lastTimestamp = 0; |
|
|
|
|
|
if (self.noteTimings.length === 0) |
|
|
return; |
|
|
|
|
|
self.millisecondsPerBeat = 1000 / (self.qpm / 60) / self.beatSubdivisions; |
|
|
self.lastMoment = self.noteTimings[self.noteTimings.length-1].milliseconds; |
|
|
self.totalBeats = Math.round(self.lastMoment / self.millisecondsPerBeat); |
|
|
}; |
|
|
|
|
|
self.replaceTarget(target); |
|
|
|
|
|
self.doTiming = function (timestamp) { |
|
|
|
|
|
|
|
|
if (self.lastTimestamp === timestamp) |
|
|
return; |
|
|
self.lastTimestamp = timestamp; |
|
|
if (!self.startTime) { |
|
|
self.startTime = timestamp; |
|
|
} |
|
|
|
|
|
if (!self.isPaused && self.isRunning) { |
|
|
self.currentTime = timestamp - self.startTime; |
|
|
self.currentTime += 16; |
|
|
while (self.noteTimings.length > self.currentEvent && self.noteTimings[self.currentEvent].milliseconds < self.currentTime) { |
|
|
if (self.eventCallback && self.noteTimings[self.currentEvent].type === 'event') { |
|
|
var thisStartTime = self.startTime; |
|
|
self.eventCallback(self.noteTimings[self.currentEvent]); |
|
|
if (thisStartTime !== self.startTime) { |
|
|
self.currentTime = timestamp - self.startTime; |
|
|
} |
|
|
} |
|
|
self.currentEvent++; |
|
|
} |
|
|
if (self.lineEndCallback && self.lineEndTimings.length > self.currentLine && self.lineEndTimings[self.currentLine].milliseconds < self.currentTime && self.currentEvent < self.noteTimings.length) { |
|
|
var leftEvent = self.noteTimings[self.currentEvent].milliseconds === self.currentTime ? self.noteTimings[self.currentEvent] : self.noteTimings[self.currentEvent-1] |
|
|
self.lineEndCallback(self.lineEndTimings[self.currentLine], leftEvent, { line: self.currentLine, endTimings: self.lineEndTimings, currentTime: self.currentTime }); |
|
|
self.currentLine++; |
|
|
} |
|
|
if (self.currentTime < self.lastMoment) { |
|
|
requestAnimationFrame(self.doTiming); |
|
|
if (self.currentBeat * self.millisecondsPerBeat < self.currentTime) { |
|
|
var ret = self.doBeatCallback(timestamp); |
|
|
if (ret !== null) |
|
|
self.currentTime = ret; |
|
|
} |
|
|
} else if (self.currentBeat <= self.totalBeats) { |
|
|
|
|
|
if (self.beatCallback) { |
|
|
var ret2 = self.doBeatCallback(timestamp); |
|
|
if (ret2 !== null) |
|
|
self.currentTime = ret2; |
|
|
requestAnimationFrame(self.doTiming); |
|
|
} |
|
|
} |
|
|
|
|
|
if (self.currentTime >= self.lastMoment) { |
|
|
if (self.eventCallback) { |
|
|
|
|
|
|
|
|
var promise = self.eventCallback(null); |
|
|
self.shouldStop(promise).then(function(shouldStop) { |
|
|
if (shouldStop) |
|
|
self.stop(); |
|
|
}) |
|
|
} else |
|
|
self.stop(); |
|
|
} |
|
|
} |
|
|
}; |
|
|
|
|
|
self.shouldStop = function(promise) { |
|
|
|
|
|
|
|
|
return new Promise(function (resolve) { |
|
|
if (!promise) |
|
|
return resolve(true); |
|
|
if (promise === "continue") |
|
|
return resolve(false); |
|
|
if (promise.then) { |
|
|
promise.then(function (result) { |
|
|
resolve(result !== "continue"); |
|
|
}); |
|
|
} |
|
|
}); |
|
|
}; |
|
|
|
|
|
self.doBeatCallback = function(timestamp) { |
|
|
if (self.beatCallback) { |
|
|
var next = self.currentEvent; |
|
|
while (next < self.noteTimings.length && self.noteTimings[next].left === null) |
|
|
next++; |
|
|
var endMs; |
|
|
var ev; |
|
|
if (next < self.noteTimings.length) { |
|
|
endMs = self.noteTimings[next].milliseconds; |
|
|
next = Math.max(0, self.currentEvent - 1); |
|
|
while (next >= 0 && self.noteTimings[next].left === null) |
|
|
next--; |
|
|
|
|
|
ev = self.noteTimings[next]; |
|
|
} |
|
|
|
|
|
var position = {}; |
|
|
var debugInfo = {}; |
|
|
if (ev) { |
|
|
position.top = ev.top; |
|
|
position.height = ev.height; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var offMs = Math.max(0, timestamp-self.startTime-ev.milliseconds); |
|
|
var gapMs = endMs - ev.milliseconds; |
|
|
var gapPx = ev.endX - ev.left; |
|
|
var offPx = gapMs ? offMs * gapPx / gapMs : 0; |
|
|
position.left = ev.left + offPx; |
|
|
|
|
|
if (self.currentEvent === 0 && ev.milliseconds > timestamp-self.startTime) |
|
|
position.left = undefined |
|
|
|
|
|
debugInfo = { |
|
|
timestamp: timestamp, |
|
|
startTime: self.startTime, |
|
|
ev: ev, |
|
|
endMs: endMs, |
|
|
offMs: offMs, |
|
|
offPx: offPx, |
|
|
gapMs: gapMs, |
|
|
gapPx: gapPx |
|
|
}; |
|
|
} else { |
|
|
debugInfo = { |
|
|
timestamp: timestamp, |
|
|
startTime: self.startTime, |
|
|
}; |
|
|
} |
|
|
|
|
|
var thisStartTime = self.startTime; |
|
|
self.beatCallback( |
|
|
self.currentBeat / self.beatSubdivisions, |
|
|
self.totalBeats / self.beatSubdivisions, |
|
|
self.lastMoment, |
|
|
position, |
|
|
debugInfo); |
|
|
if (thisStartTime !== self.startTime) { |
|
|
return timestamp - self.startTime; |
|
|
} else |
|
|
self.currentBeat++; |
|
|
} |
|
|
return null; |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var JOGGING_INTERVAL = 60; |
|
|
|
|
|
self.animationJogger = function() { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (self.isRunning) { |
|
|
self.doTiming(performance.now()); |
|
|
self.joggerTimer = setTimeout(self.animationJogger, JOGGING_INTERVAL); |
|
|
} |
|
|
}; |
|
|
|
|
|
self.start = function(offsetPercent, units) { |
|
|
self.isRunning = true; |
|
|
if (self.isPaused) { |
|
|
self.isPaused = false; |
|
|
if (offsetPercent === undefined) |
|
|
self.justUnpaused = true; |
|
|
} |
|
|
if (offsetPercent) { |
|
|
self.setProgress(offsetPercent, units); |
|
|
} else if (offsetPercent === 0) { |
|
|
self.reset(); |
|
|
} else if (self.pausedPercent !== null) { |
|
|
var now = performance.now(); |
|
|
self.currentTime = self.lastMoment * self.pausedPercent; |
|
|
self.startTime = now - self.currentTime; |
|
|
self.pausedPercent = null; |
|
|
self.reportNext = true; |
|
|
} |
|
|
requestAnimationFrame(self.doTiming); |
|
|
self.joggerTimer = setTimeout(self.animationJogger, JOGGING_INTERVAL); |
|
|
}; |
|
|
self.pause = function() { |
|
|
self.isPaused = true; |
|
|
var now = performance.now(); |
|
|
self.pausedPercent = (now - self.startTime) / self.lastMoment; |
|
|
self.isRunning = false; |
|
|
if (self.joggerTimer) { |
|
|
clearTimeout(self.joggerTimer); |
|
|
self.joggerTimer = null; |
|
|
} |
|
|
}; |
|
|
self.currentMillisecond = function() { |
|
|
return self.currentTime; |
|
|
}; |
|
|
self.reset = function() { |
|
|
self.currentBeat = 0; |
|
|
self.currentEvent = 0; |
|
|
self.currentLine = 0; |
|
|
self.startTime = null; |
|
|
self.pausedPercent = null; |
|
|
}; |
|
|
self.stop = function() { |
|
|
self.pause(); |
|
|
self.reset(); |
|
|
}; |
|
|
self.setProgress = function(position, units) { |
|
|
|
|
|
var percent; |
|
|
switch (units) { |
|
|
case "seconds": |
|
|
self.currentTime = position * 1000; |
|
|
if (self.currentTime < 0) self.currentTime = 0; |
|
|
if (self.currentTime > self.lastMoment) self.currentTime = self.lastMoment; |
|
|
percent = self.currentTime / self.lastMoment; |
|
|
break; |
|
|
case "beats": |
|
|
self.currentTime = position * self.millisecondsPerBeat * self.beatSubdivisions; |
|
|
if (self.currentTime < 0) self.currentTime = 0; |
|
|
if (self.currentTime > self.lastMoment) self.currentTime = self.lastMoment; |
|
|
percent = self.currentTime / self.lastMoment; |
|
|
break; |
|
|
default: |
|
|
|
|
|
|
|
|
percent = position; |
|
|
if (percent < 0) percent = 0; |
|
|
if (percent > 1) percent = 1; |
|
|
self.currentTime = self.lastMoment * percent; |
|
|
break; |
|
|
} |
|
|
|
|
|
if (!self.isRunning) |
|
|
self.pausedPercent = percent; |
|
|
|
|
|
var now = performance.now(); |
|
|
self.startTime = now - self.currentTime; |
|
|
|
|
|
var oldEvent = self.currentEvent; |
|
|
self.currentEvent = 0; |
|
|
while (self.noteTimings.length > self.currentEvent && self.noteTimings[self.currentEvent].milliseconds < self.currentTime) { |
|
|
self.currentEvent++; |
|
|
} |
|
|
|
|
|
if (self.lineEndCallback) { |
|
|
self.currentLine = 0; |
|
|
while (self.lineEndTimings.length > self.currentLine && self.lineEndTimings[self.currentLine].milliseconds + self.lineEndAnticipation < self.currentTime) { |
|
|
self.currentLine++; |
|
|
} |
|
|
} |
|
|
|
|
|
var oldBeat = self.currentBeat; |
|
|
self.currentBeat = Math.floor(self.currentTime / self.millisecondsPerBeat); |
|
|
if (self.beatCallback && oldBeat !== self.currentBeat) |
|
|
self.doBeatCallback(self.startTime+self.currentTime); |
|
|
|
|
|
if (self.eventCallback && self.currentEvent >= 0 && self.noteTimings[self.currentEvent].type === 'event') |
|
|
self.eventCallback(self.noteTimings[self.currentEvent]); |
|
|
if (self.lineEndCallback) |
|
|
self.lineEndCallback(self.lineEndTimings[self.currentLine], self.noteTimings[self.currentEvent], { line: self.currentLine, endTimings: self.lineEndTimings }); |
|
|
|
|
|
self.joggerTimer = setTimeout(self.animationJogger, JOGGING_INTERVAL); |
|
|
}; |
|
|
}; |
|
|
|
|
|
function getLineEndTimings(timings, anticipation) { |
|
|
|
|
|
|
|
|
var callbackTimes = []; |
|
|
var lastTop = null; |
|
|
for (var i = 0; i < timings.length; i++) { |
|
|
var timing = timings[i]; |
|
|
if (timing.type !== 'end' && timing.top !== lastTop) { |
|
|
callbackTimes.push({ measureNumber: timing.measureNumber, milliseconds: timing.milliseconds-anticipation, top: timing.top, bottom: timing.top+timing.height }); |
|
|
lastTop = timing.top; |
|
|
} |
|
|
} |
|
|
return callbackTimes; |
|
|
} |
|
|
|
|
|
module.exports = TimingCallbacks; |
|
|
|
|
|
|