Como fazer `setInterval` se comportar mais em sincronia, ou como usar` setTimeout` em vez disso?

91

Estou trabalhando em um programa de música que requer vários elementos JavaScript para estar em sincronia com outro. Tenho usado setInterval, o que funciona muito bem inicialmente. No entanto, com o tempo, os elementos ficam gradualmente fora de sincronia, o que é ruim em um programa de música.

Eu li online que setTimeouté mais preciso e você pode ter setTimeoutloops de alguma forma. No entanto, não encontrei uma versão genérica que ilustre como isso é possível.

Basicamente, tenho algumas funções como:

//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);

Funciona muito bem, inicialmente, mas ao longo de cerca de um minuto, os sons tornam-se visivelmente fora de sincronia conforme li acontece com setInterval. Eu li que setTimeoutpode ser mais preciso.

Alguém poderia me mostrar um exemplo básico de uso setTimeoutpara fazer um loop indefinido de algo? Como alternativa, se houver uma maneira de obter resultados mais sincronizados com setIntervalou mesmo outra função, entre em contato.

user3084366
fonte
2
Por que você não posta algum código que mostra o que você deseja alcançar e podemos dar-lhe as melhores respostas.
Andy
1
Eu li online que setTimeout é mais preciso : Onde você leu isso? Inclua um link. Estou assumindo que é provavelmente um caso em que setTimeoutvocê pode calcular quanto tempo o atraso realmente foi para ajustar o tempo para o próximo tempo limite.
Matt Burland,
2
Sobre o quê requestAnimationFrame? Você só precisa fazer referência ao tempo em que o áudio está a cada vez que seu requestAnimationFrameretorno de chamada é executado.
Jasper
5
Nenhum tipo de cronômetro é realmente preciso. Os milissegundos dados são apenas um tempo de espera mínimo, mas a função ainda pode ser chamada mais tarde. Se você estiver tentando coordenar vários intervalos, tente consolidar em um intervalo de controle.
Jonathan Lonowski
1
Se você realmente deseja sincronizar a música com algo na tela, você precisa consultar o tempo de progresso através do áudio ao atualizar o DOM. Caso contrário, as coisas ficarão fora de sincronia na maioria das vezes.
Jasper

Respostas:

183

Você pode criar um setTimeoutloop usando recursão:

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

O problema com setInterval()e setTimeout()é que não há garantia de que seu código será executado no tempo especificado. Ao usá setTimeout()-lo e chamá-lo recursivamente, você está garantindo que todas as operações anteriores dentro do tempo limite sejam concluídas antes que a próxima iteração do código comece.

War10ck
fonte
1
Qual é a diferença entre este método e o uso setInterval?
Jasper
4
o problema com essa abordagem é que, se a função demorar mais de 1000 ms para ser concluída, tudo vai abaixo. não há garantia de execução a cada 1000 ms. setInterval é.
TJC
@TJC: Isso depende exatamente do que você está tentando alcançar. Pode ser mais importante que a função anterior seja concluída antes da próxima iteração, ou talvez não.
Matt Burland,
5
@TJC Correto, mas se suas operações anteriores não forem concluídas antes setInterval()de serem executadas novamente, suas variáveis ​​e / ou dados podem ficar fora de sincronia muito rápido. Se eu estiver buscando dados, por exemplo, e o servidor demorar mais de 1 segundo para responder, usar setInterval()meus dados anteriores não teria concluído o processamento antes de minha próxima operação continuar. Com essa abordagem, não há garantia de que sua função será iniciada a cada segundo. No entanto, é garantido que seus dados anteriores terão concluído o processamento antes do início do próximo intervalo.
War10ck
1
@ War10ck, dado um ambiente baseado em música, presumi que ele não seria usado para definir variáveis ​​ou chamadas ajax onde a ordem importava.
TJC
31

Apenas para complementar. Se você precisar passar uma variável e iterá-la, pode fazer da seguinte forma:

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

Resultado:

1
2
3
...
9
10

Uma linha por segundo.

João paulo
fonte
12

Dado que nenhum dos tempos será muito preciso, uma maneira de usar setTimeoutpara ser um pouco mais preciso é calcular quanto tempo o atraso ocorreu desde a última iteração e, em seguida, ajustar a próxima iteração conforme apropriado. Por exemplo:

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);
}

Então, na primeira vez que ele vai esperar (pelo menos) 1000 ms, quando seu código for executado, pode ser um pouco tarde, digamos 1046 ms, então subtraímos 46 ms do nosso atraso para o próximo ciclo e o próximo atraso será apenas 954 ms. Isso não impedirá o cronômetro de disparar atrasado (isso é esperado), mas ajuda a impedir que os atrasos se acumulem. (Nota: você pode querer verificarthisDelay < 0 que significa que o atraso foi mais do que o dobro do seu atraso alvo e você perdeu um ciclo - depende de como você deseja lidar com esse caso).

Claro, isso provavelmente não ajudará você a manter vários cronômetros em sincronia, caso em que você pode querer descobrir como controlá-los todos com o mesmo cronômetro.

Então, olhando para seu código, todos os seus atrasos são múltiplos de 500, então você poderia fazer algo assim:

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
fonte
1
+1 abordagem limpa. Nunca pensei em compensar o período de tempo limite da próxima execução. Isso provavelmente o deixaria o mais próximo possível de uma medida exata, considerando que o JavaScript não é multithread e não tem garantia de disparar em um intervalo de tempo consistente.
War10ck
Isso é ótimo! Vou tentar fazer isso e deixar você saber como foi. Meu código é enorme, então provavelmente vai demorar um pouco
user3084366
Considere: Ter este código disparado em um loop de tempo fixo (mesmo se corrigido, como acima) pode, na verdade, ser a maneira errada de pensar sobre o problema. Pode ser que você deva definir cada duração de intervalo para "próxima vez que algo está para ser tocado menos a hora atual".
mwardm
Você fez a versão melhor da solução que acabei de escrever. Gosto que seus nomes de variáveis ​​usem linguagem musical e que você tente gerenciar os atrasos que se acumulam, o que eu nem mesmo tento.
Miguel Valencia
9

A melhor maneira de lidar com o tempo de áudio é com a API de áudio da Web, ela tem um relógio separado que é preciso, independentemente do que está acontecendo no thread principal. Há uma ótima explicação, exemplos, etc. de Chris Wilson aqui:

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

Dê uma olhada neste site para mais API de áudio da web, ela foi desenvolvida para fazer exatamente o que você procura.

Bing
fonte
Eu realmente gostaria que mais pessoas votassem a favor. As respostas usando setTimeoutvariam de insuficiente a terrivelmente complexo. Usar uma função nativa parece uma ideia muito melhor. Se a API não servir ao seu propósito, eu recomendo tentar encontrar uma biblioteca de programação terceirizada confiável.
quinta
3

Usar setInterval()

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

O comando acima será executado a alert("Hello");cada 3 segundos.

Pedro Lobito
fonte
3

De acordo com sua exigência

apenas me mostre um exemplo básico de uso de setTimeout para fazer um loop de algo

temos o seguinte exemplo que pode ajudá-lo

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
fonte
1

Eu uso esta forma na vida profissional: "Esqueça os loops comuns" neste caso e uso esta combinação de "setInterval" inclui "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: Entenda que o comportamento real de (setTimeOut): todos eles começarão no mesmo tempo "os três blá blá blá começarão a contagem regressiva no mesmo momento", então faça um timeout diferente para organizar a execução.

PS 2: o exemplo para loop de tempo, mas para loops de reação você pode usar eventos, promessa assíncrona, espera ..

Mohamed Abulnasr
fonte
1

Problema de loop setTimeout com solução

// 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);
} 

Vahid Akhtar
fonte
1
Alguma ideia de por que ele se comporta de maneira diferente entre var e let?
Jay
1
@Jay ... A diferença entre eles é que var tem escopo de função e let tem escopo de bloco. para mais esclarecimentos, você pode acessar medium.com/@josephcardillo/…
Vahid Akhtar
1

Como outra pessoa observou, a API de áudio da web tem um cronômetro melhor.

Mas, em geral, se esses eventos acontecerem de forma consistente, que tal colocá-los todos no mesmo cronômetro? Estou pensando em como um sequenciador de etapas .

Praticamente, poderia ser algo assim?

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
fonte
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);
}

A emoção secundária
fonte
1
Uma explicação de sua solução seria muito apreciada!
tonelada
-2

Eu acho que é melhor fazer o tempo limite no final da função.

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);
}   

Essas três funções são necessárias porque os vars na segunda função serão sobrescritos com o valor padrão a cada vez. Quando o ponteiro do programa atinge setTimeout, um passo já está calculado. Então, só a tela precisa de um pouco de tempo.

BF
fonte
-2

Use let em vez de var no código:

for(let i=1;i<=5;i++){setTimeout(()=>{console.log(i)},1000);}
ALoK VeRMa
fonte