Source: midi/scheduler.js

/**
 * Schedules functions to occur at a given time in the future, relative to the start time.
 * Can be used to play music in realtime.
 */
class Scheduler {

  constructor({ bpm=120 } = {}) {
    this.schedule = new Map();
    this.bpm = bpm;
  }

  /**
   * Delete all scheduled callbacks
   */
  clear() {
    this.schedule.clear();
  }

  /**
   * Schedule a callback to run.
   * Note, you may call this repeatedly with the same time parameter. It will append to any existing callbacks.
   * @param time {Number} - Time in milliseconds, relative to when {@link Scheduler#start scheduler.start()} is called.
   * @param callback {Function} - The code to execute at the scheduled time.
   */
  at(time, callback) {
    if (typeof time !== 'number') throw new TypeError('time must be a number');
    if (typeof callback !== 'function') throw new TypeError('callback must be a function');
    const timeInBeats = time * 60000/this.bpm;
    let callbacks = this.schedule.get(timeInBeats);
    if (!callbacks) {
      callbacks = [];
      this.schedule.set(timeInBeats, callbacks);
    }
    callbacks.push(callback);
  }

  /**
   * Run the scheduler and execute the callbacks as scheduled.
   */
  start() {
    this.times = [...this.schedule.keys()].sort((a,b) => a-b);
    this.startTime = new Date().getTime();
    this.tick();
  }

  /**
   * Stop the scheduler.
   */
  stop() {
    if (this.timeout) clearTimeout(this.timeout);
  }

  /**
   * @private
   */
  tick() {
    if (!this.nextTime) {
      this.nextSchedule = this.times.shift();
      if (this.nextSchedule == null) return;
      this.nextTime = this.startTime + this.nextSchedule;
    }
    if (this.nextTime <= new Date().getTime()) {
      const callbacks = this.schedule.get(this.nextSchedule) || [];
      this.nextSchedule = null;
      this.nextTime = null;
      callbacks.forEach(callback => callback.call(this.nextSchedule));
    }
    this.timeout = setTimeout(() => this.tick(), 1);
  }

}

module.exports = Scheduler;