Jump To …

scheduler.js

/*global trurl*/


(function (global) {

var scheduler = function (conf) {
  var displayRegistry = {}, audioRegistry = {},
    audioActive = {}, displayActive = {},
    displayRemaining = {}, audioRemaining = {},
    that = {},
    id_seq = 0,
    event,
    tick = 0,
    frame = 0,
    timeOffset = 0,
    startTicks = {},
    startFrames = {},
    startTimes = {},
    pauseTimeOffset = false,
    i, iMax, j, 
    correctAudioLatency = false,
    frameTiming = 'time',
    startTime, time,
    totalTime, displayTotalTime, audioTotalTime,
    samplesPerKontrol,
    declickSamples,
    eventify;

kludgy...

  if (typeof module != 'undefined' && module.exports) {
    eventify = require('./util').eventify;  
  } else {
    eventify = trurl.util.eventify;
  }
  eventify(that);
  event = that.event;

  function audioStop (id) {
    delete audioActive[id];
    delete audioRemaining[id];
    event('audioStop', { id: id, time: time });
  }

  function register (opcode, start, dur) {
    var end, startTick, endTick, startFrame, endFrame, numFrames, maxFrame,
      startFunction, id = id_seq++;

    if (!dur) {
      dur = Infinity;
    }

    if (opcode.audioCallback) {
      startTick = Math.round(start * conf.sampleRate);
      end = start + dur;
      endTick = end * conf.sampleRate;
      if (end != Infinity && end > audioTotalTime) { audioTotalTime = end; }
      audioRegistry[id] = { opcode: opcode, start: start, dur: dur, end: end,
          startTick: startTick, endTick: endTick, currentSample: 0 };
      audioRegistry[id].numSamples = 
        Math.round(dur * conf.sampleRate); 
      audioRegistry[id].conf = conf;

    } else if (opcode.displayCallback) {
      if (correctAudioLatency) {
        start += conf.bufferSize / conf.sampleRate;
      }
      end = start + dur;
      startFrame = Math.round(start * conf.frameRate);
      endFrame = Math.round(end * conf.frameRate);
      numFrames = Math.round(dur * conf.frameRate);
      if (end != Infinity && end > displayTotalTime) { displayTotalTime = end; }
      displayRegistry[id] = { opcode: opcode, 
          start: start, end: end, dur: dur,
          startFrame: startFrame, endFrame: endFrame, numFrames: numFrames };
      displayRegistry[id].conf = conf;
    }

set empty p, de, ae if needed so we don't have to keep checking for them

    opcode.p = opcode.p || {};
    opcode.ae = opcode.ae || {};
    opcode.pe = opcode.pe || {};
    if (totalTime < audioTotalTime) { totalTime = audioTotalTime; }
    if (totalTime < displayTotalTime) { totalTime = displayTotalTime; }
    return id;
  }

  function loadAudioStarts () {
    var id, startTick, opcode;
    audioActive = {}; audioRemaining = {};
    startTicks = [];
    for (id in audioRegistry) {
      if (timeOffset < audioRegistry[id].end) {
        audioRemaining[id] = true;
      }
    }
    for (id in audioRemaining) {
      startTick = audioRegistry[id].startTick;
      opcode = audioRegistry[id].opcode;
      audioRegistry[id].currentSample = 0;
      if (startTick < tick) {
        audioRegistry[id].currentSample = tick - startTick;
        startTick = tick;
      }
      if (!startTicks[startTick]) {
        startTicks[startTick] = [];
      }
      startTicks[startTick].push(id);
      audioRemaining[id] = true;
    }
  }

  function loadDisplayStarts () {
    var id, startFrame, start;
    displayActive = {}; displayRemaining = {};
    startFrames = []; startTimes = [];
    for (id in displayRegistry) {
      if (timeOffset < displayRegistry[id].end) {
        displayRemaining[id] = true;
      }
    }
    for (id in displayRemaining) {
      start = displayRegistry[id].start;
      startFrame = displayRegistry[id].startFrame;
      if (startFrame < frame) {
        startFrame = frame;
      }
      if (!startFrames[startFrame]) {
        startFrames[startFrame] = [];
      }
      startFrames[startFrame].push(id);
      if (!startTimes[start]) {
        startTimes[start] = [];
      }
      startTimes[start].push(id);
    }
  }

  function displayStop (id) {
    delete displayActive[id];
    delete displayRemaining[id];
    event('displayStop', { id: id, time: time });
  }

  function getCurrentTime () {
    return new Date().getTime() / 1000;
  }

  function displayCallback (context) {
    var i, iMax, j, id, timeIt;

    if (pauseTimeOffset) { return context; }

    timeIt = new Date().getTime();

    if (!startTime) { startTime = getCurrentTime(); }

    if (frameTiming == 'frame') {
      time = frame * 1/conf.frameRate;
      if (startFrames[frame]) {
        for (i=0, iMax=startFrames[frame].length; i<iMax; i++) {
          id = startFrames[frame][i];
          event('displayStart', { id: id, time: time });
          displayActive[id] = displayRegistry[id];
        }
        delete startFrames[frame];
      }
      for (id in displayActive) {
        if (displayActive[id].turnoff || displayActive[id].endFrame <= frame) {
          delete displayActive[id].turnoff;
          displayStop(id);
        }
      }
    } else if (frameTiming == 'time') {
      time = getCurrentTime() - startTime + timeOffset;

TODO: use a sorted array here and pop off values.

      for (j in startTimes) {
        if (j <= time) {
          for (i=0, iMax=startTimes[j].length; i<iMax; i++) {
            id = startTimes[j][i];
            event('displayStart', { id: id, time: time });
            displayActive[id] = displayRegistry[id];
          }
          delete startTimes[j];
        }
      }
      for (id in displayActive) {
        if (displayActive[id].turnoff || displayActive[id].end <= time) {
          delete displayActive[id].turnoff;
          displayStop(id);
        }
      }
    }

    if (Object.keys(displayRemaining).length < 1) {
      event('displayNoEvents', { time: time });
      return;
    }

    context.fillStyle = conf.backgroundColor;
    context.fillRect(0, 0, context.canvas.width, context.canvas.height);

    if (frameTiming == 'frame') {
      for (i in displayActive) {
        displayActive[i].elapsedTime = time - displayActive[i].start;

this gives positions of 0..1 inclusive

        displayActive[i].position = (frame - displayActive[i].startFrame) /
            (displayActive[i].numFrames - 1);
        displayActive[i].opcode.displayCallback(context, displayActive[i]);
      }
    } else if (frameTiming == 'time') {
      for (i in displayActive) {
        displayActive[i].elapsedTime = time - displayActive[i].start;
        displayActive[i].position = displayActive[i].elapsedTime /
            displayActive[i].dur;                        
        displayActive[i].opcode.displayCallback(context, displayActive[i]);
      }
    }  

    frame++;
    timeIt = new Date().getTime() - timeIt;
    event('frameDone', { time: timeIt, context: context }); 

    return context;
  }


  function audioCallback (buf, buf2) {
    var samplesLength, numSamples, samplesLeft, samplesRequested,
      id, i, iMax, j, jMax, timeIt,
      tickOffset, turnoff, declickBorderCheck,
      reg, opcode, panl, panr, opcodeSamples, envSamples;

    if (pauseTimeOffset) { return; }

    timeIt = new Date().getTime();

    time = tick / conf.sampleRate;

    if (buf2) {
      samplesLength = buf.length;
    } else {
      samplesLength = buf.length / 2;
    }

    for (i in startTicks) {
      if (i >= tick && i < tick+samplesLength) {
        for (j=0, jMax=startTicks[i].length; j<jMax;
             j++) {
          id = startTicks[i][j];
          event('audioStart', { id: id, time: time });
          audioActive[id] = audioRegistry[id];
        }
        delete startTicks[i];
      }      
    }

    if (Object.keys(audioRemaining).length < 1) {
      event('audioNoEvents', { time: time });

samplesDone needed here if client is waiting to start display on first audioCallback()

      event('samplesDone', { buf: buf, buf2: buf2, time: timeIt }); 
      return;
    }

    for (id in audioActive) {
      turnoff = false;
      samplesRequested = samplesLength;
      numSamples = audioRegistry[id].numSamples;
      samplesLeft = 
        numSamples - audioRegistry[id].currentSample;
      tickOffset = 0;
      reg = audioActive[id];
      opcode = reg.opcode;
      if (typeof opcode.p.pan != 'undefined') {
        panr = opcode.p.pan;
      } else {
        panr = 0.5;
      }
      panl = 1 - panr;

      if (reg.currentSample === 0) {
        tickOffset = reg.startTick - tick;
      }
      samplesRequested = samplesRequested - tickOffset;

if event ends on this cycle request only the right number of samples from it and set it to turn off.

      if (samplesLength >= samplesLeft) {
        samplesRequested = samplesLeft;
        turnoff = true;
      }
      opcodeSamples = new Float32Array(samplesRequested);
      for (i in opcode.ae) {
        envSamples = new Float32Array(samplesRequested);
        opcode.aev[i] = opcode.ae[i].audioCallback(envSamples, reg);
      }
      opcode.audioCallback(opcodeSamples, reg);

delete envelope samples for garbage collection

      for (i in opcode.ae) {
        delete opcode.aev[i];
      }
      reg.currentSample += samplesRequested;

Don't declick at all if total number of samples is less than declickSamples.

      if (declickSamples && (numSamples > declickSamples)) {
        declickBorderCheck = samplesLeft - samplesRequested;
        if (declickBorderCheck && declickBorderCheck < declickSamples) {
          j = declickBorderCheck;
          iMax = samplesRequested - (declickSamples - declickBorderCheck);
          for (i=samplesRequested; i>iMax; i--) {
            opcodeSamples[i] *= j++/declickSamples;
          }
        } else if (turnoff) {
          j = 0;
          if (samplesRequested < declickSamples) {
            iMax = 0;
          } else {
            iMax = samplesRequested-declickSamples;
          }
          for (i=samplesRequested; i>iMax; i--) {
            opcodeSamples[i] *= j++/declickSamples;
          }
        }
      }

      if (buf2) {
        for (j=0; j<samplesRequested; j++) {
          buf[j+tickOffset] += panl*opcodeSamples[j];
          buf2[j+tickOffset] += panr*opcodeSamples[j];
        }
      } else {
        tickOffset *= 2;
        for (j=0; j<samplesRequested; j++) {
          buf[j*2+tickOffset] += panl*opcodeSamples[j];
          buf[j*2+tickOffset+1] += panr*opcodeSamples[j];
        }
      }

      if (turnoff) { 
        audioStop(id); 
      }
    }

    tick += samplesLength;
    timeIt = new Date().getTime() - timeIt;
    event('samplesDone', { buf: buf, buf2: buf2, time: timeIt }); 

    return buf;
  }

  function init () {
    conf.kRate = conf.kRate || conf.sampleRate / 100;
    samplesPerKontrol = conf.sampleRate / conf.kRate;
    tick = 0;
    frame = 0;    
    id_seq = 0;
    displayRegistry = {};
    displayRemaining = {};
    displayActive = {};
    audioRegistry = {};
    audioActive = {};
    startTime = null;
    startTicks = {};
    startFrames = {};
    startTimes = {};
    pauseTimeOffset = null;
    timeOffset = 0;
    audioTotalTime = 0;
    displayTotalTime = 0;
    totalTime = 0;
  }

  function run () {

calculate this here so scores can set own declickTime

    if (conf.declickTime) {
      declickSamples = Math.round(conf.declickTime * conf.sampleRate);
    } else {
      declickSamples = 0;
    }
    loadAudioStarts();
    loadDisplayStarts();
  }

  function setTimeOffset (t) {
    timeOffset = t;
    startTime = null;
    frame = Math.round(conf.frameRate * t);
    tick = Math.round(conf.sampleRate * t);
  }

  function pause () {
    if (pauseTimeOffset) {
      setTimeOffset(pauseTimeOffset);
      pauseTimeOffset = 0;
      run();
      return true;
    } else {
      pauseTimeOffset = time;
      return false;
    }
  }

  function setCorrectAudioLatency (b) {
    correctAudioLatency = b;
  }

  function setFrameTiming (f) {
    frameTiming = f;
  }

  function getTotalTime () {
    return totalTime;
  }
  function getAudioTotalTime () {
    return audioTotalTime;
  }
  function getDisplayTotalTime() {
    return displayTotalTime;
  }

  function getTime () {
    return time;
  }

  that.audioCallback = audioCallback;
  that.displayCallback = displayCallback;
  that.register = register;
  that.setCorrectAudioLatency = setCorrectAudioLatency;
  that.setFrameTiming = setFrameTiming;
  that.getTotalTime = getTotalTime;
  that.getAudioTotalTime = getAudioTotalTime;
  that.getDisplayTotalTime = getDisplayTotalTime;
  that.setTimeOffset = setTimeOffset;
  that.getTime = getTime;
  that.init = init;
  that.run = run;
  that.pause = pause;
  return that;
};

if (typeof module != 'undefined' && module.exports) {
  module.exports = scheduler;
} else {
  global.trurl = global.trurl || {};
  global.trurl.scheduler = scheduler;
}

}(this));