1

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.)

root
  • 1,812
  • 1
  • 12
  • 26

1 Answers1

0

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".

Vlad DX
  • 4,200
  • 19
  • 28
  • Is the `start()` of the `AudioBufferSourceNode` affected by that as well? How can I rewrite it with `window.requestAnimationFrame()`? – root Dec 29 '22 at 20:20
  • The weird thing is that the first notes are late only when the song starts playing *for the first time*. When I stop and start the song again, the notes play on time. What delays them during the first but not the second execution? – root Dec 29 '22 at 20:22
  • @root, I guess, the first time quite a lot of things happen in the browser. Please check out the docs about Event Loop. – Vlad DX Dec 29 '22 at 20:37
  • What things happen in my case with `MIDI.js`? I press the "play" button after the page and SoundFonts have loaded. I read about the event loop, but I can't find what exactly is different between the first and second playback of the song. – root Dec 29 '22 at 21:56
  • @root, I'm not sure exactly. I haven't seen the page. Do you have a URL? – Vlad DX Dec 29 '22 at 22:24
  • Try the official [MIDIPlayer-v2.html](https://github.com/mudcube/MIDI.js/blob/abcjs/examples/MIDIPlayer-v2.html) from the MIDI.js library. At its [line 200](https://github.com/mudcube/MIDI.js/blob/abcjs/examples/MIDIPlayer-v2.html#L200), use a MIDI file with more instruments, for example `load('https://bitmidi.com/uploads/107090.mid');` . You can comment out the extra `MIDI.noteOn()` at lines 157 and 198. Wait while the JavaScript console lists the SoundFonts that are being loaded, then click the "start" button. Later, click "stop" and "start" again. The first start sounds bad sometimes. – root Dec 29 '22 at 23:48
  • Could you please have a look or something? Your answer doesn't solve my question but for months now makes it look like there's already a valid answer or like I don't know about the event loop. – root Jun 10 '23 at 00:22