Danny Bullo
Danny Bullo

Reputation: 75

Javascript: Alternative to setTimeOut for FAST Timer in MIDI Sequencer App

I'm working on a Javascript Music App that includes a Sequencer. For those who are not familiar, MIDI sequencers work pretty much like this: There is something called PPQ: pulses per quarter note. Each pulse is called "Tick". It depicts how may "subdivisions" there are per quarter note, like resolution. So Sequencers "play" the Events that are in the tracks one Tick at a time: Play Tick1, wait Tick Duration, Play tick2, Tick Duration, and so on.

Now, let's say we have a BPM (Beats per Min) of 120 with PPQ=96 (standard). That means that each Quarter Note Duration is 500ms, and each Tick Duration is 5.20833ms.

What Timer Alternatives we have in Javascript?

1) We have the old setTimeOut. It has several problems: the min. wait time is 4ms. (https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout#Minimum_delay_and_timeout_nesting) It is also subject to JITTER/time Variations. It is not precise and it is demanding, as call backs are stacked in the even loop.

2) There is an alternative to setTimeOut/setInterval which involves using requestAnimationFrame(). It is VERY precise and CPU efficient. However, the minimum time it can be set is around 16.7ms (the duration of a Frame in a typical 60FPS monitor)

Is there any other Alternative? To to precisely schedule an event every 2-5ms?

Note: the function done in side the loop, playEventsAtTick() is NOT demanding at all, so it would never take more time to execute than Tick Duration.

Thanks! Danny Bullo

Upvotes: 3

Views: 1450

Answers (4)

Danny Bullo
Danny Bullo

Reputation: 75

Get Off My Lawn: The approach you suggested does not completely work. Let's say I add a method to the web worker to STOP the Sequencer:

stop() {
    this.run = false;
}

The problem is that the method myWorker.onmessage = function (e) {...} never get's triggered. I suspect it is because the Web Worker Thread is "TOO BUSY" with the endless loop. any way to solve that?

Also, while playing, it works.....but the CPU goes up considerably..... The only possible Solution would be a Sleep() method, but Real SLEEP that does not exist in Javascript...

Thanks

Upvotes: 0

Get Off My Lawn
Get Off My Lawn

Reputation: 36311

In a separate thread, such as a web worker, you can create an endless loop. In this loop, all you need to do is calculate the time between beats. After the time is valid, you can then send a message to the main process, to do some visuals, play a sound or what ever you would like to do.

Here is a Working example

class MyWorker {

  constructor() {
    // Keeps the loop running
    this.run = true
    // Beats per minute
    this.bpm = 120
    // Time last beat was called
    this.lastLoopTime = this.milliseconds
  }

  get milliseconds() {
    return new Date().getTime()
  }

  start() {
    while (this.run) {
      // Get the current time
      let now = this.milliseconds
      // Get the elapsed time between now and the last beat
      let updateLength = now - this.lastLoopTime
      // If not enough time has passed restart from the beginning of the loop
      if (updateLength < (1000 * 60) / this.bpm) continue;
      // Enough time has passed update the last time
      this.lastLoopTime = now

      // Do any processing that you would like here

      // Send a message back to the main thread
      postMessage({ msg: 'beat', time: now })

    }
  }

}

new MyWorker().start()

Next we can create the index page, which will run the worker, and flash a square everytime a message comes back from the worker.

<!DOCTYPE html>
<html lang="en">
  <head>
    <script>
      // Start the worker
      var myWorker = new Worker('worker.js')
      // Listen for messages from the worker
      myWorker.onmessage = function (e) {
        var msg = e.data
        switch (msg.msg) {
          // If the message is a `beat` message, flash the square
          case 'beat':
            let div = document.querySelector('div')
            div.classList.add('red')
            setTimeout(() => div.classList.remove('red'), 100)
            break;
        }
      }
    </script>
    <style>
      div { width: 100px; height: 100px; border: solid 1px; }
      .red { background: red; }
    </style>
  </head>
  <body>
    <div></div>
  </body>
</html>

Upvotes: 0

Danny Bullo
Danny Bullo

Reputation: 75

Thanks nvioli. I'm aware of Web Audio API. However, I don't think that can help here. I'm not triggering AUDIO directly: I have MIDI events (or let's say just "EVENTS") stored in the TRACKS. And those events happen at any TICK. So the Sequencer needs to loop every Tick Duration to scan what to play at that particular tick.

Regards, Danny Bullo

Upvotes: 0

nvioli
nvioli

Reputation: 4209

To maintain any sanity in doing this kind of thing, you're going to want to do the audio processing on a devoted thread. Better yet, use the Web Audio API and let people who have been thinking about these problems for a long time do the hard work of sample-accuracy.

Also check out Web MIDI (chrome only).

Upvotes: 1

Related Questions