Skip to content

Commit

Permalink
timer based on webaudio api
Browse files Browse the repository at this point in the history
  • Loading branch information
Alexander Grahn committed Nov 10, 2023
1 parent 9931a0e commit 9f0b3c0
Show file tree
Hide file tree
Showing 4 changed files with 268 additions and 14 deletions.
3 changes: 3 additions & 0 deletions ABLoopPlayer.html
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@
<!-- hack for mobile devices -->
<script src="js/jquery.ui.touch-punch.min.js"></script>

<!-- main code that implements the video player -->
<script src="js/WAAClock-latest.js"></script>

<!-- styles used in this document -->
<link href="css/main.css" rel="stylesheet"/>

Expand Down
243 changes: 243 additions & 0 deletions js/WAAClock-latest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
;(function(e,t,n){function i(n,s){if(!t[n]){if(!e[n]){var o=typeof require=="function"&&require;if(!s&&o)return o(n,!0);if(r)return r(n,!0);throw new Error("Cannot find module '"+n+"'")}var u=t[n]={exports:{}};e[n][0].call(u.exports,function(t){var r=e[n][1][t];return i(r?r:t)},u,u.exports)}return t[n].exports}var r=typeof require=="function"&&require;for(var s=0;s<n.length;s++)i(n[s]);return i})({1:[function(require,module,exports){
var WAAClock = require('./lib/WAAClock')

module.exports = WAAClock
if (typeof window !== 'undefined') window.WAAClock = WAAClock

},{"./lib/WAAClock":2}],2:[function(require,module,exports){
var isBrowser = (typeof window !== 'undefined')

var CLOCK_DEFAULTS = {
toleranceLate: 0.1,
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
}
},{}]},{},[1])
;
36 changes: 22 additions & 14 deletions js/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ var searchStr; //url parameters
var timeA, timeB, dtAB; // s
var isTimeASet=false;
var isTimeBSet=false;
//var scrubTimer=[];
var scrubTimer;
var knownIDs=[];
var knownMedia=[];
Expand Down Expand Up @@ -207,7 +206,6 @@ $(document).ready(function(){
});
bmkAddButton.addEventListener("mouseup", function(e){bmkAdd();});
$("#mainDiv").show();
scrubTimer=new Timer(onScrubTimerUpdate, 5);
playSelectedFile("");
});

Expand Down Expand Up @@ -869,8 +867,7 @@ var resetUI=function(){
lstId=undefined;
$("#timeInputs").hide();
cancelABLoop();
//while(scrubTimer.length) clearInterval(scrubTimer.pop());
scrubTimer.stop();
try{scrubTimer.stop()}catch(e){};
$("#scrub").slider("option", "value", 0).hide();
loopButton.disabled=true;
$("#speed").slider("option", "disabled", true);
Expand Down Expand Up @@ -1219,12 +1216,18 @@ var onPlayerStateChange=function(e, id, ta, tb, s){ //event object, video id loo
}
vidId=id;
}
//if (!scrubTimer.length) scrubTimer.push(setInterval(onScrubTimerUpdate, 5));
if (!scrubTimer.started) scrubTimer.start();
try{
scrubTimer.stop();
scrubTimer.clear();
scrubTimer.start();
}catch(e){
scrubTimer = new WAAClock(new AudioContext());
scrubTimer.start();
scrubTimer.setTimeout(onScrubTimerUpdate, 0).repeat(0.005);
}
}
else if(e.data==YT.PlayerState.PAUSED||e.data==YT.PlayerState.ENDED){
//while(scrubTimer.length) clearInterval(scrubTimer.pop());
scrubTimer.stop();
try{scrubTimer.stop()}catch(e){};
loopMeas.splice(0);
}
else if (e.data==YT.PlayerState.UNSTARTED) {
Expand Down Expand Up @@ -1382,8 +1385,7 @@ var playSelectedFile=function(f){
myVideo.controls=true;
myVideo.width=$("#myResizable").width();
myVideo.addEventListener("durationchange", function(e){
//while(scrubTimer.length) clearInterval(scrubTimer.pop());
scrubTimer.stop();
try{scrubTimer.stop()}catch(e){};
loopMeas.splice(0);
if (isFinite(e.target.duration)){
$("#slider").slider("option", "max", getDuration());
Expand All @@ -1405,13 +1407,19 @@ var playSelectedFile=function(f){
loopMeas.splice(0);
setPlaybackRate(Number($("#speed").slider("value")));
this.removeEventListener("timeupdate", onTimeUpdateVT);
//if (!scrubTimer.length) scrubTimer.push(setInterval(onScrubTimerUpdate, 5));
if (!scrubTimer.started) scrubTimer.start();
try{
scrubTimer.stop();
scrubTimer.clear();
scrubTimer.start();
}catch(e){
scrubTimer = new WAAClock(new AudioContext());
scrubTimer.start();
scrubTimer.setTimeout(onScrubTimerUpdate, 0).repeat(0.005);
}
});
myVideo.addEventListener("pause", function(e){
this.addEventListener("timeupdate", onTimeUpdateVT);
//while(scrubTimer.length) clearInterval(scrubTimer.pop());
scrubTimer.stop();
try{scrubTimer.stop()}catch(e){};
loopMeas.splice(0);
});
myVideo.addEventListener("error", function(e){
Expand Down
Binary file modified zip/ABLoopPlayer.zip
Binary file not shown.

0 comments on commit 9f0b3c0

Please sign in to comment.