Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

HTML5/jQuery metronome - performance problems

As mentioned in the title I'm trying to create a jQuery/JavaScript based metronome along with the HTML <audio /> tag to play the sound.

It works "okay", but it seems to me the setInterval method is not working accurately enough. I searched some threads here, but for I am new to both jQuery and JavaScript and I haven't found a working solution. Same for the "open new tab and setInterval stops or lags" - problem. I tried to prevent that with stop(true,true) but it didn't work as I expected.

I want the metronome to run "in background" without changing tempo when opening a new tab and doing something there. Also I want an exact metronome for sure ;)

Here's my testing environment located: http://nie-wieder.net/metronom/test.html

At the moment, JS-Code and HTML-markup are all in the test.html source, so you can look there.

Also, here's the concerned (as I think) js-code I use:

$(document).ready(function() {

    //vars
    var intervalReference   = 0;
    var currentCount        = 1;      
    var countIncrement      = .5;      
    var smin = 10;
    var smax =240;
    var svalue = 120;

    //soundchkbox
    $(".sndchck").attr("disabled", true);

    //preload sound
    $.ajax({
        url: "snd/tick.ogg",
        success: function() {
            $(".sndchck").removeAttr("disabled");
        }
    });

    // tick event
    var met = $("#bpm").slider({
            value: 120,
            min: smin,
            max: smax,
            step: 1,
            change: function( event, ui ) {
                var delay = (1000*60/ui.value)/2
                clearInterval(intervalReference);

                //seems to be the Problem for me
                intervalReference = setInterval(function(){
                    var $cur_sd = $('#sub_div_'+currentCount);
                    $cur_sd
                    .stop(true,true)
                    .animate({opacity: 1},15,
                                function() {
                                //Play HTML5 Sound
                                if($('#sound_check:checked').val()){
                                    $('#tick')
                                    .stop(true,true)
                                    .trigger("play");
                                }
                                    $(this).
                                    stop(true,true).
                                    animate({opacity:0});
                                }
                    );
                    currentCount += countIncrement;
                    if(currentCount > 4.5) currentCount = 1
                }, delay);
                createMusicTag(ui);
            }
        });
});

Any help would be great, I'm out of ideas for now.

like image 308
Dominik Avatar asked Apr 18 '12 14:04

Dominik


2 Answers

setInterval is not accurate. what you can try doing is something like:

var timestamp = (new Date()).getTime();
function run() {

     var now = (new Date()).getTime();

     if( now - timestamp >= 1000 ) {
         console.log( 'tick' );
         timestamp = now;
     }

     setTimeout(run, 10);
}
run();

This will (every hundredth of a second) compare the 'timestamp' with the current time to see if the diff is a second or more (deviation is 0.01 seconds) and if it is logs 'tick' and resets the current timestamp.

http://jsfiddle.net/rlemon/UqbwT/

This is the best approach to something that needs to be time accurate (imo).

Update: if you change the setTimeout time setting... you get less deviation. http://jsfiddle.net/rlemon/UqbwT/1/

Second update: After reviewing this post I thought there must be a more accurate way to use timers in javascript.. so with a bit of research I came acrossed this article. I do suggest you read it.

like image 171
rlemon Avatar answered Nov 07 '22 04:11

rlemon


After trying to coax requestAnimationFrame and setTimeout into accurate timing for a drum machine app and failing (a 3 year old could keep better tempo than my code), I gave up and switched to the Web Audio API and it instantly provided accurate audio timing for my purposes.

Here's a minimal example:

const scheduleBeep = time => {
  const osc = audioContext.createOscillator();
  osc.connect(audioContext.destination);
  osc.frequency.value = 300;
  osc.start(time);
  osc.stop(time + 0.1);
};

window.AudioContext = window.AudioContext || window.webkitAudioContext;
const audioContext = new AudioContext();
let timeBetweenSteps = 60 / 120;
let nextStepTime;
let interval;
const lookahead = 0.1;
const timeoutDelay = 30;

const schedule = () => {
  while (nextStepTime < audioContext.currentTime + lookahead) {
    nextStepTime += timeBetweenSteps;
    scheduleBeep(nextStepTime);
  }
};
document.querySelector("button")
  .addEventListener("click", evt => {
    clearInterval(interval);

    if (evt.target.textContent === "Run") {
      evt.target.textContent = "Stop";
      nextStepTime = audioContext.currentTime;
      interval = setInterval(schedule, timeoutDelay);
    }
    else {
      evt.target.textContent = "Run";
    }
  })
;
<button>Run</button>

And a slightly more involved example that adds a sound file and a BPM slider:

<body>
<form class="metronome">
  <button class="run">Run</button>
  <input class="bpm" type="range" min="60" max="500" value="120">
  <span class="bpm-readout">120</span>
</form>

<script>

const url = "https://upload.wikimedia.org/wikipedia/commons/e/e5/Abadie.jo-Marteau-1.ogg";

const $ = document.querySelector.bind(document);
const metroEls = {
  run: $(".metronome .run"),
  bpm: $(".metronome .bpm"),
  bpmReadout: $(".metronome .bpm-readout"),
};

window.AudioContext = 
  window.AudioContext || window.webkitAudioContext
;
const audioContext = new AudioContext();

(async () => {
  const response = await fetch(url);
  const arrayBuffer = await response.arrayBuffer();
  const audioBuffer = await audioContext
    .decodeAudioData(arrayBuffer)
  ;

  const scheduleSample = time => {
    const source = audioContext.createBufferSource();
    source.buffer = audioBuffer;
    source.connect(audioContext.destination);
    source.start(time);
  };

  let timeBetweenSteps = 60 / 120;
  let nextStepTime;
  let interval;
  const lookahead = 0.1;
  const timeoutDelay = 30;

  const schedule = () => {
    while (nextStepTime < 
             audioContext.currentTime + lookahead) {
      nextStepTime += timeBetweenSteps;
      scheduleSample(nextStepTime);
    }
  };
  metroEls.run.addEventListener("click", () => {
    if (metroEls.run.textContent === "Run") {
      metroEls.run.textContent = "Stop";
      nextStepTime = audioContext.currentTime;
      clearInterval(interval);
      interval = setInterval(schedule, timeoutDelay);
    }
    else {
      metroEls.run.textContent = "Run";
      clearInterval(interval);
    }
  });
  metroEls.bpm.addEventListener("change", e => {
    timeBetweenSteps = 60 / e.target.value;
    metroEls.bpmReadout.innerText = e.target.value;
  });
})();

</script>
</body>

Useful resources:

  • Paul Adenot: A robust metronome using the Web Audio API
  • Monica Dinculescu: Metronomes in JavaScript
  • Chris Wilson: Scheduling Web Audio with Precision
  • MDN web audio API examples
  • MDN Web audio API advanced techniques
  • Amila Welihinda's Vue drum machine
  • Aqilah Misuary: Understanding The Web Audio Clock
  • SitePoint CodePen: Playing an MP3 file with the Web Audio API
  • Tone.js is a library based on the Web Audio API that includes convenient looping abstactions.
like image 41
ggorlen Avatar answered Nov 07 '22 04:11

ggorlen