Comment faire en sorte que `setInterval` se comporte plus en synchronisation, ou comment utiliser` setTimeout` à la place?

91

Je travaille sur un programme musical qui nécessite plusieurs éléments JavaScript pour être synchronisés avec un autre. J'utilise setInterval, qui fonctionne très bien au début. Cependant, avec le temps, les éléments deviennent progressivement désynchronisés, ce qui est mauvais dans un programme musical.

J'ai lu en ligne qui setTimeoutest plus précis, et vous pouvez avoir des setTimeoutboucles d'une manière ou d'une autre. Cependant, je n'ai pas trouvé de version générique qui illustre comment cela est possible.

En gros, j'ai quelques fonctions comme celles-ci:

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

Cela fonctionne très bien, au début, mais en l'espace d'une minute environ, les sons deviennent sensiblement désynchronisés au fur et à mesure que je l'ai lu setInterval. J'ai lu que cela setTimeoutpeut être plus précis.

Quelqu'un pourrait-il simplement me montrer un exemple de base d'utilisation setTimeoutpour boucler quelque chose indéfiniment? Sinon, s'il existe un moyen d'obtenir des résultats plus synchrones avec setIntervalou même une autre fonction, veuillez me le faire savoir.

utilisateur3084366
la source
2
Pourquoi ne publiez-vous pas un code indiquant ce que vous voulez réaliser et nous pouvons vous donner de meilleures réponses.
Andy
1
J'ai lu en ligne que setTimeout est plus précis : où avez-vous lu cela? Incluez un lien. Je suppose que c'est probablement un cas où setTimeoutvous pouvez calculer combien de temps le délai a vraiment été un ajustement du temps pour le prochain délai.
Matt Burland
2
Et quoi requestAnimationFrame? Vous auriez juste à référencer l'heure à laquelle l'audio est à chaque fois que votre requestAnimationFramerappel s'exécute.
Jasper
5
Aucun des deux types de minuterie n'est vraiment garanti pour être précis. Les millisecondes données ne représentent qu'un temps d'attente minimum, mais la fonction peut toujours être appelée ultérieurement. Si vous essayez de coordonner plusieurs intervalles, essayez plutôt de consolider en un seul, en contrôlant l'intervalle.
Jonathan Lonowski
1
Si vous voulez vraiment synchroniser la musique avec quelque chose à l'écran, vous devez référencer la progression du temps dans l'audio lorsque vous mettez à jour le DOM. Sinon, les choses ne seront pas synchronisées la plupart du temps.
Jasper

Réponses:

181

Vous pouvez créer une setTimeoutboucle en utilisant la récursivité:

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

Le problème avec setInterval()et setTimeout()est qu'il n'y a aucune garantie que votre code s'exécutera dans le temps spécifié. En l'utilisant setTimeout()et en l'appelant de manière récursive, vous vous assurez que toutes les opérations précédentes dans le délai d'expiration sont terminées avant le début de la prochaine itération du code.

War10ck
la source
1
Quelle est la différence entre cette méthode et l'utilisation setInterval?
Jasper
4
le problème avec cette approche est que si la fonction prend plus de 1000 ms pour se terminer, tout va mal. ce n'est pas garanti de s'exécuter toutes les 1000 ms. setInterval est.
TJC
@TJC: Cela dépend exactement de ce que vous essayez d'accomplir. Il peut être plus important que la fonction précédente se termine avant la prochaine itération, ou peut-être pas.
Matt Burland
4
@TJC Correct, mais si vos opérations précédentes ne sont pas terminées avant la setInterval()ré-exécution, vos variables et / ou données pourraient se désynchroniser très rapidement. Si j'ajaxe des données par exemple et que le serveur met plus de 1 seconde à répondre, l'utilisation de setInterval()mes données précédentes n'aurait pas fini le traitement avant la poursuite de ma prochaine opération. Avec cette approche, il n'est pas garanti que votre fonction démarre toutes les secondes. Cependant, il est garanti que le traitement de vos données précédentes sera terminé avant le début du prochain intervalle.
War10ck
1
@ War10ck, étant donné un environnement basé sur la musique, j'ai supposé qu'il n'allait pas être utilisé pour définir des variables ou des appels ajax là où l'ordre importait.
TJC
30

Seulement pour compléter. Si vous avez besoin de passer une variable et de l'itérer, vous pouvez faire comme ceci:

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

Production:

1
2
3
...
9
10

Une ligne par seconde.

João Paulo
la source
12

Étant donné qu'aucun temps ne sera très précis, une façon d'utiliser setTimeoutpour être un peu plus précis est de calculer combien de temps le délai a duré depuis la dernière itération, puis d'ajuster l'itération suivante le cas échéant. Par exemple:

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

Donc, la première fois qu'il attendra (au moins) 1000 ms, lorsque votre code sera exécuté, il se peut qu'il soit un peu en retard, disons 1046 ms, donc nous soustrayons 46 ms de notre délai pour le cycle suivant et le prochain délai sera seulement 954 ms. Cela n'empêchera pas la minuterie de se déclencher en retard (c'est normal), mais vous aidera à empêcher les retards de s'accumuler. (Remarque: vous voudrez peut-être vérifier thisDelay < 0ce qui signifie que le retard était plus du double de votre retard cible et que vous avez manqué un cycle - à vous de décider comment vous voulez gérer ce cas)

Bien sûr, cela ne vous aidera probablement pas à synchroniser plusieurs minuteries.Dans ce cas, vous voudrez peut-être savoir comment les contrôler toutes avec la même minuterie.

Donc, en regardant votre code, tous vos retards sont un multiple de 500, vous pouvez donc faire quelque chose comme ceci:

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
la source
1
+1 Approche soignée. Je n'ai jamais pensé à compenser le délai d'expiration de la prochaine course. Cela vous rapprocherait probablement d'une mesure aussi précise que possible, étant donné que JavaScript n'est pas multithread et qu'il n'est pas garanti de se déclencher à un intervalle de temps cohérent.
War10ck
C'est bien! Je vais essayer et vous dire comment ça se passe. Mon code est énorme, donc cela prendra probablement un certain temps
user3084366
Considérez: Avoir ce code déclenché sur une boucle temporelle fixe (même si corrigée, comme ci-dessus) peut en fait être une mauvaise façon de penser au problème. Il se peut que vous deviez en fait régler chaque longueur d'intervalle sur "la prochaine fois que quelque chose doit être joué moins l'heure actuelle".
mwardm
Vous avez créé la meilleure version de la solution que je viens d'écrire. J'aime que vos noms de variables utilisent le langage de la musique et que vous essayez de gérer les retards qui s'accumulent, ce que je n'essaye même pas.
Miguel Valencia
8

La meilleure façon de gérer la synchronisation audio est d'utiliser l'API Web Audio, elle dispose d'une horloge distincte qui est précise indépendamment de ce qui se passe dans le thread principal. Il y a une excellente explication, des exemples, etc. de Chris Wilson ici:

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

Jetez un œil sur ce site pour plus d'API Web Audio, il a été développé pour faire exactement ce que vous recherchez.

Bing
la source
Je souhaite vraiment que plus de gens votent pour cela. Les réponses utilisant setTimeoutvont d'insuffisant à horriblement surcomplexe. Utiliser une fonction native semble être une bien meilleure idée. Si l'API ne répond pas à votre objectif, je vous recommanderais d'essayer de trouver une bibliothèque de planification tierce fiable.
threeve
3

Utilisation setInterval()

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

Ce qui précède s'exécutera alert("Hello");toutes les 3 secondes.

Pedro Lobito
la source
3

Selon votre condition

montre-moi juste un exemple basique d'utilisation de setTimeout pour boucler quelque chose

nous avons l'exemple suivant qui peut vous aider

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
la source
1

J'utilise cette façon dans la vie professionnelle: "Oubliez les boucles communes" dans ce cas et utilisez cette combinaison de "setInterval" inclut "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: Comprenez que le comportement réel de (setTimeOut): ils commenceront tous dans le même temps "les trois bla bla bla commenceront le compte à rebours au même moment" alors faites un timeout différent pour organiser l'exécution.

PS 2: l'exemple pour la boucle de synchronisation, mais pour une boucle de réaction, vous pouvez utiliser des événements, promettez async wait ..

Mohamed Abulnasr
la source
1

Problème de boucle setTimeout avec 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);
} 

Vahid Akhtar
la source
1
Une idée pourquoi il se comporte différemment entre var et let?
Jay
1
@Jay ... La différence entre eux est que var a une portée de fonction et let est une portée de bloc. pour plus de précisions, vous pouvez passer par medium.com/@josephcardillo
Vahid Akhtar
1

Comme quelqu'un d'autre l'a souligné, l'API Web Audio a une meilleure minuterie.

Mais en général, si ces événements se produisent de manière cohérente, que diriez-vous de les mettre tous sur la même minuterie? Je réfléchis au fonctionnement d'un séquenceur pas à pas .

Pratiquement, cela pourrait-il ressembler à quelque chose comme ça?

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 Valence
la source
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);
}

L'émotion mineure
la source
1
Une explication de votre solution serait très appréciée!
ton
-2

Je pense qu'il est préférable de temporiser à la fin de la fonction.

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

Ces trois fonctions sont nécessaires car les variables de la deuxième fonction seront écrasées par la valeur par défaut à chaque fois. Lorsque le pointeur de programme atteint le setTimeout, une étape est déjà calculée. Ensuite, juste l'écran a besoin d'un peu de temps.

BF
la source
-2

Utilisez let au lieu de var dans le code:

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