setTimeout triggers too late in MIDI.js

96 views Asked by At

I'm using MIDI.js to play a MIDI file with several musical instruments.

The following things execute too late, how can I fix that?

  • First notes of the song. Like all notes, they are scheduled via start() of an AudioBufferSourceNode here.
  • MIDI program change events. They are scheduled via setTimeout here. Their "lateness" is even worse than that of the first notes.

When I stop the song and start it again, there are no problems anymore, but the delay values are very similar. So the delay values are probably not the cause of the problem.

(I use the latest official branch (named "abcjs") because the "master" branch is older and has more problems with such MIDI files.)

1

There are 1 answers

7
Vlad DX On

That is how JavaScript Event Loop works.

Calling setTimeout ... doesn't execute the callback function after the given interval.

The execution depends on the number of waiting tasks in the queue.

... the delay is the minimum time required for the runtime to process the request (not a guaranteed time).

https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop#zero_delays

Instead of setTimeout() you can use window.requestAnimationFrame() and calculate elapsed time for delay by yourself.

Window.requestAnimationFrame() - Web APIs | MDN

The window.requestAnimationFrame() method tells the browser that you wish to perform an animation and requests that the browser calls a specified function to update an animation before the next repaint. The method takes a callback as an argument to be invoked before the repaint.

... will request that your animation function be called before the browser performs the next repaint. The number of callbacks is usually 60 times per second, but will generally match the display refresh rate in most web browsers

https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame

performance.now() - Web APIs | MDN https://developer.mozilla.org/en-US/docs/Web/API/Performance/now

In our situation, we don't want to do any animation but want to just use it for a better-precision timeout.

const delayMs = 1000;
const startTime = performance.now();

function delay(func) {
    const delayStartTime = performance.now();

    function delayStep() {
    // Run again if still elapsed time is less than a delay
    if (performance.now() - delayStartTime <= delayMs) {
      window.requestAnimationFrame(delayStep);
    }
    else
    {
      // Run the delayed function
      func();
    }
  }
  
  // Run first time
  window.requestAnimationFrame(delayStep);
}

// Trying `setTimeout()`
setTimeout(() => doSomeJob('setTimeout()'), delayMs);
// Trying `delay()`
delay(() => doSomeJob('delay()'));

// Function that we'd like to run with a delay
function doSomeJob(marker)
{
  const elapsedTime = performance.now() - startTime;
  console.log(`${marker}: Ran after ${elapsedTime / 1000} seconds`);
}

If you run it many times, you'll see that delay() is pretty much all the time better than setTimeout(). The difference is very small because there is nothing else happens on the page. If there will be something intensive running, setTimeout() should demonstrate worse "precision".