Étude de cas : The Sounds of Racer

Introduction

Racer est un test Chrome multijoueur et multi-appareil. Jeu de voitures miniatures rétro sur plusieurs écrans. Sur un téléphone ou une tablette, Android ou iOS. Tout le monde peut y participer. Aucune application Aucun téléchargement Sur le Web mobile uniquement.

Plan8 et nos amis de 14islands ont créé une expérience musicale et sonore dynamique basée sur une composition originale de Giorgio Moroder. Racer propose des sons de moteur et des effets sonores de course réactifs, mais surtout un mix musical dynamique qui se répartit sur plusieurs appareils lorsque des pilotes se joignent à la course. Il s'agit d'une installation multi-enceintes composée de smartphones.

Nous essayons depuis un certain temps de connecter plusieurs appareils. Nous avions effectué des tests musicaux dans lesquels le son était réparti sur différents appareils ou passait d'un appareil à l'autre. Nous étions donc impatients d'appliquer ces idées à Racer.

Plus précisément, nous voulions tester si nous pouvions créer la bande-son sur les appareils à mesure que de plus en plus de joueurs se joignaient au jeu, en commençant par la batterie et la basse, puis en ajoutant de la guitare et des synthés, etc. Nous avons fait des démonstrations musicales et nous sommes lancés dans le codage. L'effet multi-enceintes a été très gratifiant. À ce stade, la synchronisation n'était pas parfaite, mais lorsque nous avons entendu les couches de son se répartir sur les appareils, nous avons su que nous avions trouvé quelque chose de bien.

Créer les sons

Google Creative Lab a défini une direction créative pour le son et la musique. Nous voulions utiliser des synthétiseurs analogiques pour créer les effets sonores plutôt que d'enregistrer les sons réels ou de recourir à des bibliothèques de sons. Nous savions également que, dans la plupart des cas, le haut-parleur de sortie serait un petit haut-parleur de téléphone ou de tablette. Nous avons donc dû limiter le spectre de fréquences des sons pour éviter toute distorsion. Cela s'est avéré être un véritable défi. Lorsque nous avons reçu les premières ébauches musicales de Giorgio, nous avons été soulagés, car sa composition fonctionnait parfaitement avec les sons que nous avions créés.

Bruit du moteur

Le plus grand défi de la programmation des sons a été de trouver le meilleur son de moteur et de sculpter son comportement. Le circuit de course ressemblait à un circuit de Formule 1 ou de Nascar. Les voitures devaient donc donner l'impression d'être rapides et explosives. En même temps, les voitures étaient très petites, donc un son de moteur puissant ne serait pas vraiment en adéquation avec les images. Nous ne pouvions pas diffuser un moteur rugissant dans le haut-parleur du mobile. Nous avons donc dû trouver une autre solution.

Pour trouver l'inspiration, nous avons branché quelques synthétiseurs modulaires de notre ami Jon Ekstrand et nous avons commencé à jouer. Nous avons apprécié ce que nous avons entendu. Voici à quoi cela ressemblait avec deux oscillateurs, de beaux filtres et un LFO.

Nous avions déjà remodelé du matériel analogique avec succès à l'aide de l'API Web Audio. Nous avions donc de grands espoirs et avons commencé à créer un synthétiseur simple dans Web Audio. Un son généré serait le plus réactif, mais il solliciterait la puissance de traitement de l'appareil. Nous devions être extrêmement efficaces pour économiser toutes les ressources possibles afin que les visuels fonctionnent correctement. Nous avons donc changé de technique et avons opté pour la lecture d'échantillons audio.

Synthétiseur modulaire pour trouver l'inspiration pour le son du moteur

Plusieurs techniques peuvent être utilisées pour créer un son de moteur à partir d'échantillons. L'approche la plus courante pour les jeux sur console consiste à créer une couche de plusieurs sons (plus il y en a, mieux c'est) du moteur à différents régimes (avec charge), puis à effectuer un fondu et un changement de hauteur entre eux. Ajoutez ensuite une couche de plusieurs sons du moteur en pleine accélération (sans charge) à la même vitesse de rotation, puis effectuez un fondu et un décalage de hauteur entre les deux. Si vous effectuez correctement le fondu entre ces couches lors du passage des vitesses, le son sera très réaliste, mais uniquement si vous disposez d'un grand nombre de fichiers audio. Le crosspitching ne doit pas être trop large, sinon le son sera très synthétique. Étant donné que nous devions éviter les temps de chargement longs, cette option n'était pas adaptée. Nous avons essayé d'utiliser cinq ou six fichiers audio pour chaque calque, mais le son était décevant. Nous avons dû trouver un moyen de le faire avec moins de fichiers.

La solution la plus efficace s'est avérée être la suivante:

  • Un fichier audio avec accélération et changement de vitesse synchronisés avec l'accélération visuelle de la voiture, se terminant par une boucle programmée à la hauteur / RPM la plus élevée. L'API Web Audio est très efficace pour créer des boucles précises, ce qui nous permet de le faire sans glitchs ni pops.
  • Un fichier audio avec décélération / ralentissement du moteur
  • Enfin, un fichier audio qui diffuse le son d'inactivité en boucle.

Voici à quoi il ressemble :

Image du son du moteur

Pour le premier événement tactile / accélération, nous lirions le premier fichier depuis le début. Si l'utilisateur relâchait la manette des gaz, nous calculerions le temps à partir du point où nous nous trouvions dans le fichier audio au moment du relâchement. Ainsi, lorsque la manette des gaz était à nouveau activée, elle sautait à l'endroit approprié dans le fichier d'accélération après la lecture du deuxième fichier (ralentissement).

function throttleOn(throttle) {
    //Calculate the start position depending 
    //on the current amount of throttle.
    //By multiplying throttle we get a start position 
    //between 0 and 3 seconds.
    var startPosition = throttle * 3;

    var audio = context.createBufferSource();
    audio.buffer = loadedBuffers["accelerate_and_loop"];

    //Sets the loop positions for the buffer source.
    audio.loopStart = 5;
    audio.loopEnd = 9;

    //Starts the buffer source at the current time
    //with the calculated offset.
    audio.start(context.currentTime, startPosition);
}

Essayez

Démarrez le moteur et appuyez sur le bouton "Accélérateur".

<input type="button" id="playstop" value = "Start/Stop Engine" onclick='playStop()'>
<input type="button" id="throttle" value = "Throttle" onmousedown='throttleOn()' onmouseup='throttleOff()'>

Avec seulement trois petits fichiers audio et un moteur de son performant, nous avons décidé de passer au défi suivant.

Obtenir la synchronisation

Avec David Lindkvist de 14islands, nous avons commencé à examiner plus en détail comment faire en sorte que les appareils jouent en parfaite synchronisation. La théorie de base est simple. L'appareil demande l'heure au serveur, tient compte de la latence du réseau, puis calcule le décalage de l'horloge locale.

syncOffset = localTime - serverTime - networkLatency

Grâce à ce décalage, chaque appareil connecté partage le même concept de temps. Facile, n'est-ce pas ? (là encore, en théorie).

Calculer la latence du réseau

Nous pouvons supposer que la latence correspond à la moitié du temps nécessaire pour demander une réponse au serveur et la recevoir:

networkLatency = (receivedTime - sentTime) × 0.5

Le problème avec cette hypothèse est que l'aller-retour vers le serveur n'est pas toujours symétrique, c'est-à-dire que la requête peut prendre plus de temps que la réponse ou inversement. Plus la latence du réseau est élevée, plus cette asymétrie aura un impact important, ce qui entraînera un retard des sons et une lecture non synchronisée avec les autres appareils.

Heureusement, notre cerveau est conçu pour ne pas remarquer si les sons sont légèrement retardés. Des études ont montré qu'il faut un délai de 20 à 30 millisecondes (ms) pour que notre cerveau perçoive les sons comme distincts. Toutefois, à partir de 12 à 15 ms, vous commencez à "sentir" les effets d'un signal retardé, même si vous ne pouvez pas le "percevoir" complètement. Nous avons examiné quelques protocoles de synchronisation temporelle établis, des alternatives plus simples, et avons essayé d'en implémenter certains dans la pratique. Finalement, grâce à l'infrastructure à faible latence de Google, nous avons pu simplement échantillonner une rafale de requêtes et utiliser l'échantillon avec la latence la plus faible comme référence.

Combattre la dérive de l'horloge

Ça a marché ! Nous avions plus de cinq appareils diffusant un battement en parfaite synchronisation, mais seulement pendant un certain temps. Après quelques minutes de lecture, les appareils se décalaient, même si nous avions planifié le son à l'aide de l'heure de contexte de l'API Web Audio, qui est très précise. Le décalage s'accumulait lentement, de quelques millisecondes à la fois, et était indétectable au début. Toutefois, après une lecture prolongée, les couches musicales étaient totalement désynchronisées. Bonjour, erreur de synchronisation de l'horloge.

La solution consistait à resynchroniser l'horloge toutes les quelques secondes, à calculer un nouveau décalage temporel et à l'intégrer facilement au planificateur audio. Pour réduire le risque de changements notables dans la musique en raison du temps de latence du réseau, nous avons décidé d'atténuer le changement en conservant l'historique des derniers décalages de synchronisation et en calculant une moyenne.

Programmer des titres et changer d'arrangement

Créer une expérience sonore interactive signifie que vous ne contrôlez plus le moment où les parties du titre seront diffusées, car vous dépendez des actions des utilisateurs pour modifier l'état actuel. Nous devions nous assurer de pouvoir passer d'un arrangement à un autre dans le titre dans les meilleurs délais. Pour cela, notre planificateur devait pouvoir calculer la durée restante de la barre en cours de lecture avant de passer à l'arrangement suivant. Notre algorithme se présentait comme suit:

  • Client(1) lance le titre.
  • Client(n) demande au premier client quand le titre a commencé.
  • Client(n) calcule un point de référence au moment où le titre a été lancé à l'aide de son contexte Web Audio, en tenant compte de syncOffset et du temps écoulé depuis la création de son contexte audio.
  • playDelta = Date.now() - syncOffset - songStartTime - context.currentTime
  • Client(n) calcule la durée de lecture du titre à l'aide de playDelta. Le planificateur de titres s'en sert pour savoir quelle barre de l'arrangement actuel doit être lue ensuite.
  • playTime = playDelta + context.currentTime nextBar = Math.ceil((playTime % loopDuration) ÷ barDuration) % numberOfBars

Pour des raisons de cohérence, nous avons limité nos arrangements à huit mesures et au même tempo (battements par minute).

Regardez devant vous

Il est toujours important de planifier à l'avance lorsque vous utilisez setTimeout ou setInterval en JavaScript. En effet, l'horloge JavaScript n'est pas très précise, et les rappels planifiés peuvent facilement être faussés de dizaines de millisecondes ou plus par la mise en page, le rendu, le garbage collection et les XMLHTTPRequests. Dans notre cas, nous avons également dû prendre en compte le temps nécessaire pour que tous les clients reçoivent le même événement sur le réseau.

Sprites audio

Combiner des sons dans un seul fichier est un excellent moyen de réduire les requêtes HTTP, à la fois pour HTML Audio et l'API Web Audio. Il s'agit également du meilleur moyen de lire des sons de manière réactive à l'aide de l'objet Audio, car il n'a pas besoin de charger un nouvel objet audio avant de le lire. Nous nous sommes appuyés sur certaines bonnes implémentations existantes. Nous avons étendu notre sprite pour qu'il fonctionne de manière fiable sur iOS et Android, et pour qu'il gère certains cas particuliers où les appareils passent en veille.

Sur Android, les éléments audio continuent de se lire même si vous mettez l'appareil en veille. En mode veille, l'exécution JavaScript est limitée pour préserver l'autonomie de la batterie. Vous ne pouvez pas compter sur requestAnimationFrame, setInterval ou setTimeout pour déclencher des rappels. Cela pose problème, car les sprites audio s'appuient sur JavaScript pour vérifier en permanence si la lecture doit être arrêtée. Pour aggraver le problème, dans certains cas, l'currentTime de l'élément Audio ne se met pas à jour, même si l'audio est toujours en cours de lecture.

Découvrez l'implémentation d'AudioSprite que nous avons utilisée dans Chrome Racer comme solution de remplacement non Web Audio.

Élément audio

Lorsque nous avons commencé à travailler sur Racer, Chrome pour Android n'était pas encore compatible avec l'API Web Audio. La logique d'utilisation d'Audio HTML pour certains appareils et de l'API Web Audio pour d'autres, combinée à la sortie audio avancée que nous souhaitions obtenir, a posé des défis intéressants. Heureusement, ce n'est plus le cas. L'API Web Audio est implémentée dans la version bêta d'Android M28.

  • Problèmes de retard/de synchronisation L'élément audio ne se lance pas toujours exactement au moment où vous le demandez. Étant donné que JavaScript est monothread, le navigateur peut être occupé, ce qui entraîne des retards de lecture pouvant aller jusqu'à deux secondes.
  • En raison des retards de lecture, il n'est pas toujours possible de boucler le contenu de manière fluide. Sur ordinateur, vous pouvez utiliser la double mise en tampon pour obtenir des boucles presque sans interruption, mais ce n'est pas une option sur les appareils mobiles, car :
    • La plupart des appareils mobiles ne diffusent pas plus d'un élément audio à la fois.
    • Volume fixe. Ni Android ni iOS ne vous permettent de modifier le volume d'un objet Audio.
  • Aucun préchargement. Sur les appareils mobiles, l'élément Audio ne commence pas à charger sa source tant que la lecture n'est pas lancée dans un gestionnaire touchStart.
  • Recherche de problèmes. L'obtention de duration ou le paramétrage de currentTime échouera, sauf si votre serveur est compatible avec la plage de bytes HTTP. Faites attention à ce point si vous créez un sprite audio comme nous l'avons fait.
  • L'authentification de base sur MP3 échoue. Certains appareils ne parviennent pas à charger les fichiers MP3 protégés par l'authentification de base, quel que soit le navigateur que vous utilisez.

Conclusions

Nous avons parcouru un long chemin depuis que le bouton de mise en sourdine était la meilleure option pour gérer le son sur le Web. Mais ce n'est que le début, et l'audio Web est sur le point de faire des ravages. Nous n'avons fait qu'effleurer la surface de ce qui est possible en matière de synchronisation de plusieurs appareils. Les téléphones et les tablettes n'avaient pas la puissance de traitement nécessaire pour se lancer dans le traitement du signal et les effets (comme la réverbération). Mais à mesure que les performances des appareils augmentent, les jeux Web pourront également profiter de ces fonctionnalités. C'est le moment idéal pour continuer à repousser les limites du son.