¿Cómo hacer que `setInterval` se comporte más sincronizado, o cómo usar` setTimeout` en su lugar?

91

Estoy trabajando en un programa de música que requiere que varios elementos de JavaScript estén sincronizados con otro. He estado usando setInterval, que funciona muy bien al principio. Sin embargo, con el tiempo, los elementos se desincronizan gradualmente, lo que es malo en un programa de música.

He leído en línea que setTimeoutes más preciso y, de setTimeoutalguna manera, puedes tener bucles. Sin embargo, no he encontrado una versión genérica que ilustre cómo esto es posible.

Básicamente tengo algunas funciones como estas:

//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 muy bien, inicialmente, pero en el transcurso de aproximadamente un minuto, los sonidos se desincronizan notablemente como he leído setInterval. He leído que setTimeoutpuede ser más exacto.

¿Alguien podría mostrarme un ejemplo básico de uso setTimeoutpara repetir algo indefinidamente? Alternativamente, si hay una manera de lograr resultados más sincronizados setIntervalo incluso con otra función, hágamelo saber.

usuario3084366
fuente
2
¿Por qué no publicas un código que nos muestre lo que quieres lograr y podemos darte mejores respuestas?
Andy
1
Leí en línea que setTimeout es más preciso : ¿Dónde lo leíste? Incluya un enlace. Supongo que es probablemente un caso en el setTimeoutque puede calcular cuánto tiempo fue realmente el retraso para ajustar el tiempo para el próximo tiempo de espera.
Matt Burland
2
¿Qué hay de requestAnimationFrame? Solo tendría que hacer referencia a la hora en que se encuentra el audio cada vez que se requestAnimationFrameejecuta su devolución de llamada.
Jasper
5
No se garantiza realmente que ninguno de los dos tipos de temporizador sea preciso. Los milisegundos dados son solo un tiempo de espera mínimo, pero la función aún se puede llamar más tarde. Si está intentando coordinar varios intervalos, intente consolidarlos en uno, controlando el intervalo.
Jonathan Lonowski
1
Si realmente desea sincronizar música con algo en pantalla, debe hacer referencia al progreso del tiempo a través del audio cuando actualice el DOM. De lo contrario, las cosas se desincronizarán la mayor parte del tiempo.
Jasper

Respuestas:

181

Puede crear un setTimeoutbucle usando recursividad:

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

El problema con setInterval()y setTimeout()es que no hay garantía de que su código se ejecute en el tiempo especificado. Al usarlo setTimeout()y llamarlo de forma recursiva, se asegura de que todas las operaciones anteriores dentro del tiempo de espera se completen antes de que comience la siguiente iteración del código.

War10ck
fuente
1
¿Cuál es la diferencia entre este método y el uso setInterval?
Jasper
4
El problema con este enfoque es que si la función tarda más de 1000 ms en completarse, todo se estropea. esto no está garantizado para ejecutarse cada 1000ms. setInterval es.
TJC
@TJC: Eso depende en gran medida de exactamente lo que esté tratando de lograr. Puede ser más importante que la función anterior se complete antes de la siguiente iteración, o tal vez no.
Matt Burland
4
@TJC Correcto, pero si sus operaciones anteriores no están completas antes de que se setInterval()vuelva a ejecutar, sus variables y / o datos podrían desincronizarse muy rápido. Si estoy buscando datos, por ejemplo, y el servidor tarda más de 1 segundo en responder, el uso de setInterval()mis datos anteriores no habría terminado de procesar antes de que continuara mi siguiente operación. Con este enfoque, no se garantiza que su función se inicie cada segundo. Sin embargo, se garantiza que sus datos anteriores habrán terminado de procesarse antes de que comience el siguiente intervalo.
War10ck
1
@ War10ck, dado un entorno basado en música, asumí que no se usaría para establecer variables o llamadas ajax donde el orden importaba.
TJC
30

Solo para complementar. Si necesita pasar una variable e iterarla, puede hacerlo así:

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

Salida:

1
2
3
...
9
10

Una línea por segundo.

Joao Paulo
fuente
12

Dado que ninguno de los tiempos va a ser muy preciso, una forma de utilizar setTimeoutpara ser un poco más preciso es calcular cuánto tiempo ha sido el retraso desde la última iteración y luego ajustar la siguiente iteración según corresponda. Por ejemplo:

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

Entonces, la primera vez que esperará (al menos) 1000 ms, cuando se ejecute su código, podría ser un poco tarde, digamos 1046 ms, por lo que restamos 46 ms de nuestro retraso para el siguiente ciclo y el siguiente retraso será solo 954 ms. Esto no evitará que el temporizador se dispare tarde (eso es de esperar), pero le ayudará a evitar que los retrasos se acumulen. (Nota: es posible que desee verificar thisDelay < 0qué significa que la demora fue más del doble de la demora objetivo y se perdió un ciclo, depende de usted cómo desea manejar ese caso).

Por supuesto, esto probablemente no le ayudará a mantener varios temporizadores sincronizados, en cuyo caso es posible que desee averiguar cómo controlarlos todos con el mismo temporizador.

Entonces, mirando su código, todos sus retrasos son múltiplos de 500, por lo que podría hacer algo como esto:

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
fuente
1
+1 Enfoque ordenado. Nunca pensé en compensar el tiempo de espera de la próxima ejecución. Eso probablemente lo acercará lo más posible a una medición exacta, considerando que JavaScript no es multiproceso y no se garantiza que se active en un intervalo de tiempo constante.
War10ck
¡Esto es genial! Voy a probar esto y les haré saber cómo va. Mi código es masivo, por lo que probablemente tomará un tiempo
user3084366
Considere: Hacer que este código se active en un ciclo de tiempo fijo (incluso si se corrige, como se indicó anteriormente) podría ser la manera incorrecta de pensar en el problema. Podría ser que debas establecer la duración de cada intervalo en "la próxima-vez-que-se-debe-jugar-algo- menos -el-tiempo-actual".
mwardm
Hiciste la mejor versión de la solución que acabo de escribir. Me gusta que los nombres de las variables usen el lenguaje de la música y que intentes gestionar los retrasos que se acumulan, que ni siquiera intento.
Miguel Valencia
8

La mejor manera de lidiar con la sincronización del audio es con Web Audio Api, tiene un reloj separado que es preciso independientemente de lo que esté sucediendo en el hilo principal. Hay una gran explicación, ejemplos, etc. de Chris Wilson aquí:

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

Eche un vistazo a este sitio para obtener más API de audio web, fue desarrollado para hacer exactamente lo que busca.

Bing
fuente
Realmente desearía que más personas votaran por esto. Las respuestas que se utilizan setTimeoutvan desde insuficientes hasta horriblemente demasiado complejas. Usar una función nativa parece una idea mucho mejor. Si la API no cumple su propósito, le recomendaría que intente encontrar una biblioteca de programación de terceros confiable.
tresve el
3

Utilizar setInterval()

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

Lo anterior se ejecutará alert("Hello");cada 3 segundos.

Pedro Lobito
fuente
3

Según su requisito

solo muéstrame un ejemplo básico del uso de setTimeout para hacer un bucle de algo

tenemos el siguiente ejemplo que puede ayudarlo

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

Utilizo esta forma en la vida laboral: "Olvídese de los bucles comunes" en este caso y use esta combinación de "setInterval" que incluye "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)
    }

PD: Entiende que el comportamiento real de (setTimeOut): todos comenzarán al mismo tiempo "los tres bla bla bla comenzarán la cuenta regresiva en el mismo momento" así que haz un timeout diferente para organizar la ejecución.

PS 2: el ejemplo de bucle de tiempo, pero para bucles de reacción puede usar eventos, prometer async await ..

Mohamed Abulnasr
fuente
1

problema de bucle setTimeout con solución

// 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
fuente
1
¿Alguna idea de por qué se comporta de manera diferente entre var y let?
Jay
1
@Jay ... La diferencia entre ellos es que var tiene un alcance de función y let tiene un alcance de bloque. para más aclaraciones puede ir a medium.com/@josephcardillo/…
Vahid Akhtar
1

Como señaló alguien más, la API de Web Audio tiene un mejor temporizador.

Pero en general, si estos eventos ocurren de manera constante, ¿qué tal si los pone a todos en el mismo temporizador? Estoy pensando en cómo funciona un secuenciador por pasos .

Prácticamente, ¿podría verse algo así?

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

La emoción menor
fuente
1
¡Una explicación de su solución sería muy apreciada!
ton
-2

Creo que es mejor esperar al final de la función.

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

Estas tres funciones son necesarias porque las vars de la segunda función se sobrescribirán con el valor predeterminado cada vez. Cuando el puntero del programa alcanza el setTimeout, ya se calcula un paso. Entonces solo la pantalla necesita un poco de tiempo.

BF
fuente
-2

Use let en lugar de var en el código:

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