101

I am working on a music program that requires multiple JavaScript elements to be in sync with another. I’ve been using setInterval, which works really well initially. However, over time the elements gradually become out of sync which is bad in a music program.

I’ve read online that setTimeout is more accurate, and you can have setTimeout loops somehow. However, I have not found a generic version that illustrates how this is possible.

Basically I have some functions like such:

//drums
setInterval(function {
  //code for the drums playing goes here
}, 8000);

//chords
setInterval(function {
  //code for the chords playing goes here
}, 1000);

//bass
setInterval(function {
  //code for the bass playing goes here
}, 500);

It works super well, initially, but over the course of about a minute, the sounds become noticeably out of sync as I’ve read happens with setInterval. I’ve read that setTimeout can be more consistently accurate.

Could someone just show me a basic example of using setTimeout to loop something indefinitely? Alternatively, if there is a way to achieve more synchronous results with setInterval or even another function, please let me know.

Sebastian Simon
  • 18,263
  • 7
  • 55
  • 75
user3084366
  • 1,103
  • 2
  • 9
  • 12
  • 2
    Why don't you post some code showing us what you want to achieve and we can give you better answers. – Andy Mar 03 '14 at 18:13
  • 1
    *I've read online that setTimeout is more accurate*: Where did you read that? Include a link. I'm assuming it's probably a case with `setTimeout` you can calculate how long the delay really was a adjust the time for the next timeout. – Matt Burland Mar 03 '14 at 18:14
  • 2
    What about `requestAnimationFrame`? You'd just have to reference the time that the audio is at each time your `requestAnimationFrame` callback runs. – Jasper Mar 03 '14 at 18:16
  • http://warp.byu.edu/site/content/1117 – user3084366 Mar 03 '14 at 18:18
  • 5
    Neither type of timer is really guaranteed to be precise. The milliseconds given is just a minimum wait time, but the function can still be called later. If you're trying to coordinate multiple intervals, try instead consolidating to one, controlling interval. – Jonathan Lonowski Mar 03 '14 at 18:19
  • 1
    If you really want to sync music to something on-screen, you need to reference the time progress through the audio when you update the DOM. Otherwise things will get out of sync most of the time. – Jasper Mar 03 '14 at 18:21
  • Yea, putting it one timer was an option I thought of. I suppose I will try it, thanks. – user3084366 Mar 03 '14 at 18:22
  • 1
    I think the web audio api would provide the moat accuracy of what you are trying to do – powerc9000 Mar 03 '14 at 18:33
  • Does this answer your question? [How to use setInterval function within for loop](https://stackoverflow.com/questions/7749090/how-to-use-setinterval-function-within-for-loop) – Dupinder Singh May 15 '20 at 17:08

13 Answers13

197

You can create a setTimeout loop using recursion:

function timeout() {
    setTimeout(function () {
        // Do Something Here
        // Then recall the parent function to
        // create a recursive loop.
        timeout();
    }, 1000);
}

The problem with setInterval() and setTimeout() is that there is no guarantee your code will run in the specified time. By using setTimeout() and calling it recursively, you're ensuring that all previous operations inside the timeout are complete before the next iteration of the code begins.

War10ck
  • 12,387
  • 7
  • 41
  • 54
  • 1
    What's the difference between this method and using `setInterval`? – Jasper Mar 03 '14 at 18:14
  • 4
    the issue with this approach is that if the function takes longer than 1000ms to complete, it all goes belly up. this isnt guaranteed to execute every 1000ms. setInterval is. – TJC Mar 03 '14 at 18:15
  • 1
    @TJC: That very much depends on exactly what you are trying to achieve. It might be more important that the previous function complete before the next iteration, or maybe not. – Matt Burland Mar 03 '14 at 18:16
  • 7
    @TJC Correct, but if your previous operations are not complete before `setInterval()` re-executes, your variables and/or data could get out of sync really fast. If I'm ajaxing for data for example, and the server takes longer than 1 sec to respond, using `setInterval()` my previous data would not have finished processing before my next operation continued. With this approach, it's not guaranteed that your function will kick off every second. However, it is guaranteed that your previous data will have finished processing before the next interval kicks off. – War10ck Mar 03 '14 at 18:16
  • 1
    @War10ck, given a music based environment, I assumed that it was not going to be used to set variables or ajax calls where order mattered. – TJC Mar 03 '14 at 18:20
  • What if I want to return data from the timeout ? @War10ck – NerdioN Sep 25 '20 at 08:00
35

Only to supplement. If you need to pass a variable and iterate it, you can do just like so:

function start(counter){
  if(counter < 10){
    setTimeout(function(){
      counter++;
      console.log(counter);
      start(counter);
    }, 1000);
  }
}
start(0);

Output:

1
2
3
...
9
10

One line per second.

João Paulo
  • 6,300
  • 4
  • 51
  • 80
13

Given that neither time is going to be very accurate, one way to use setTimeout to be a little more accurate is to calculate how long the delay was since the last iteration, and then adjust the next iteration as appropriate. For example:

var myDelay = 1000;
var thisDelay = 1000;
var start = Date.now();

function startTimer() {    
    setTimeout(function() {
        // your code here...
        // calculate the actual number of ms since last time
        var actual = Date.now() - start;
        // subtract any extra ms from the delay for the next cycle
        thisDelay = myDelay - (actual - myDelay);
        start = Date.now();
        // start the timer again
        startTimer();
    }, thisDelay);
}

So the first time it'll wait (at least) 1000 ms, when your code gets executed, it might be a little late, say 1046 ms, so we subtract 46 ms from our delay for the next cycle and the next delay will be only 954 ms. This won't stop the timer from firing late (that's to be expected), but helps you to stop the delays from pilling up. (Note: you might want to check for thisDelay < 0 which means the delay was more than double your target delay and you missed a cycle - up to you how you want to handle that case).

Of course, this probably won't help you keep several timers in sync, in which case you might want to figure out how to control them all with the same timer.

So looking at your code, all your delays are a multiple of 500, so you could do something like this:

var myDelay = 500;
var thisDelay = 500;
var start = Date.now();
var beatCount = 0;

function startTimer() {    
    setTimeout(function() {
        beatCount++;
        // your code here...
        //code for the bass playing goes here  

        if (count%2 === 0) {
            //code for the chords playing goes here (every 1000 ms)
        }

        if (count%16) {
            //code for the drums playing goes here (every 8000 ms)
        }

        // calculate the actual number of ms since last time
        var actual = Date.now() - start;
        // subtract any extra ms from the delay for the next cycle
        thisDelay = myDelay - (actual - myDelay);
        start = Date.now();
        // start the timer again
        startTimer();
    }, thisDelay);
}
Matt Burland
  • 44,552
  • 18
  • 99
  • 171
  • 1
    +1 Neat approach. I never thought about offsetting the timeout period of the next run. That would probably get you as close to an exact measurement as you can get considering JavaScript is not multi-threaded and not guaranteed to fire at a consistent time interval. – War10ck Mar 03 '14 at 19:05
  • This is great! I'm going to try this and let you know how it goes. My code is massive so it'll probably take a while – user3084366 Mar 03 '14 at 21:32
  • Consider: Having this code trigger on a fixed (even if corrected, as above) time-loop might actually be the wrong way to think about the problem. It could be that you should actually set each interval length to "next-time-something-is-due-to-be-played _minus_ current-time". – mwardm Jun 06 '18 at 14:50
  • You made the better version of the solution I just wrote down. I like that your variable names use language from music and that you try and manage the delays that build up, which I don't even try. – Miguel Valencia May 15 '20 at 16:52
12

The best way to deal with audio timing is with the Web Audio Api, it has a separate clock that is accurate regardless of what is happening in the main thread. There is a great explanation, examples, etc from Chris Wilson here:

http://www.html5rocks.com/en/tutorials/audio/scheduling/

Have a look around this site for more Web Audio API, it was developed to do exactly what you are after.

Bing
  • 149
  • 1
  • 5
  • I really wish more people would upvote this. Answers using `setTimeout` range from insufficient to horribly overcomplex. Using a native function seems like a much better idea. If the API doesn't serve your purpose, then I would recommend trying to find a reliable third party scheduling library. – trey-jones Sep 13 '19 at 15:14
6

Use setInterval()

setInterval(function(){
 alert("Hello"); 
}, 3000);

The above code will execute alert("Hello"); every 3 seconds.

Pedro Lobito
  • 94,083
  • 31
  • 258
  • 268
6

According to your requirement

just show me a basic example of using setTimeout to loop something

we have following example which can help you

var itr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
var  interval = 1000; //one second
itr.forEach((itr, index) => {

  setTimeout(() => {
    console.log(itr)
  }, index * interval)
})
Dupinder Singh
  • 7,175
  • 6
  • 37
  • 61
1

I use this way in work life: "Forget common loops" in this case and use this combination of "setInterval" includes "setTimeOut"s:

    function iAsk(lvl){
        var i=0;
        var intr =setInterval(function(){ // start the loop 
            i++; // increment it
            if(i>lvl){ // check if the end round reached.
                clearInterval(intr);
                return;
            }
            setTimeout(function(){
                $(".imag").prop("src",pPng); // do first bla bla bla after 50 millisecond
            },50);
            setTimeout(function(){
                 // do another bla bla bla after 100 millisecond.
                seq[i-1]=(Math.ceil(Math.random()*4)).toString();
                $("#hh").after('<br>'+i + ' : rand= '+(Math.ceil(Math.random()*4)).toString()+' > '+seq[i-1]);
                $("#d"+seq[i-1]).prop("src",pGif);
                var d =document.getElementById('aud');
                d.play();                   
            },100);
            setTimeout(function(){
                // keep adding bla bla bla till you done :)
                $("#d"+seq[i-1]).prop("src",pPng);
            },900);
        },1000); // loop waiting time must be >= 900 (biggest timeOut for inside actions)
    }

PS: Understand that the real behavior of (setTimeOut): they all will start in same time "the three bla bla bla will start counting down in the same moment" so make a different timeout to arrange the execution.

PS 2: the example for timing loop, but for a reaction loops you can use events, promise async await ..

Mohamed Abulnasr
  • 589
  • 7
  • 18
1

setTimeout loop problem with solution

// it will print 5 times 5.
for(var i=0;i<5;i++){
setTimeout(()=> 
console.log(i), 
2000)
}               // 5 5 5 5 5

// improved using let
for(let i=0;i<5;i++){
setTimeout(()=> 
console.log('improved using let: '+i), 
2000)
}

// improved using closure
for(var i=0;i<5;i++){
((x)=>{
setTimeout(()=> 
console.log('improved using closure: '+x), 
2000)
})(i);
} 
akhtarvahid
  • 9,445
  • 2
  • 26
  • 29
  • 1
    Any idea why it behaves differently between var and let? – Jay Aug 20 '19 at 11:56
  • 1
    @Jay ...Difference between them is that var is function scoped and let is block scoped. for more clarification you can go through https://medium.com/@josephcardillo/the-difference-between-var-let-and-const-in-javascript-part-2-60fa568d0a0 – akhtarvahid Aug 20 '19 at 13:50
1

As someone else pointed out, the Web Audio API has a better timer.

But in general, if these events happen consistently, how about you put them all on the same timer? I'm thinking about how a step sequencer works.

Practically, could it looks something like this?

var timer = 0;
var limit = 8000; // 8000 will be the point at which the loop repeats

var drumInterval = 8000;
var chordInterval = 1000;
var bassInterval = 500;

setInterval(function {
    timer += 500;

    if (timer == drumInterval) {
        // Do drum stuff
    }

    if (timer == chordInterval) {
        // Do chord stuff
    }

    if (timer == bassInterval) {
        // Do bass stuff
    }

    // Reset timer once it reaches limit
    if (timer == limit) {
        timer = 0;
    }

}, 500); // Set the timer to the smallest common denominator
Miguel Valencia
  • 221
  • 3
  • 14
0

function appendTaskOnStack(task, ms, loop) {
    window.nextTaskAfter = (window.nextTaskAfter || 0) + ms;

    if (!loop) {
        setTimeout(function() {
            appendTaskOnStack(task, ms, true);
        }, window.nextTaskAfter);
    } 
    else {
        if (task) 
            task.apply(Array(arguments).slice(3,));
        window.nextTaskAfter = 0;
    }
}

for (var n=0; n < 10; n++) {
    appendTaskOnStack(function(){
        console.log(n)
    }, 100);
}

Meloman
  • 3,558
  • 3
  • 41
  • 51
0
function timerCycle() {
  if (stoptime == false) {
    sec = parseInt(sec);
    min = parseInt(min);
    hr = parseInt(hr);
    sec = sec + 1;
    if (sec == 60) {
      min = min + 1;
      sec = 0;
    }

    if (min == 60) {
      hr = hr + 1;
      min = 0;
      sec = 0;
    }

    if (sec < 10 || sec == 0) {
      sec = "0" + sec;
    }
    if (min < 10 || min == 0) {
      min = "0" + min;
    }
    if (hr < 10 || hr == 0) {
      hr = "0" + hr;
    }

    timer.innerHTML = hr + " : " + min + " : " + sec;

    setTimeout(timerCycle, 1000);
  }
}

function startTimer() {
  if (stoptime == true) {
    stoptime = false;
    timerCycle();
  }
}

function stopTimer() {
  if (stoptime == false) {
    stoptime = true;
  }
}
function resetTimer() {
  hr = 0;
  min = 0;
  sec = 0;
  stopTimer();
  timer.innerHTML = "00:00:00";
}
doudn
  • 1
-4

I think it's better to timeout at the end of the function.

function main(){
    var something; 
    make=function(walkNr){
         if(walkNr===0){
           // var something for this step      
           // do something
         }
         else if(walkNr===1){
           // var something for that step 
           // do something different
         }

         // ***
         // finally
         else if(walkNr===10){
           return something;
         }
         // show progress if you like
         setTimeout(funkion(){make(walkNr)},15,walkNr++);  
   }
return make(0);
}   

This three functions are necessary because vars in the second function will be overwritten with default value each time. When the program pointer reach the setTimeout one step is already calculated. Then just the screen needs a little time.

B.F.
  • 477
  • 6
  • 9
-4

Use let instead of var in code :

for(let i=1;i<=5;i++){setTimeout(()=>{console.log(i)},1000);}
alexander.polomodov
  • 5,396
  • 14
  • 39
  • 46
ALoK VeRMa
  • 91
  • 6