Étude de cas : The Sounds of Racer

Présentation

Racer est une expérience Chrome Experiments, multi-joueur et multi-appareil. Jeu de machine à sous de style rétro sur différents écrans. Sur les téléphones ou les tablettes, Android ou iOS. Tout le monde peut rejoindre le groupe. Aucune appli. Aucun téléchargement. Uniquement le Web mobile.

Plan8 et nos amis de 14islands ont créé une expérience musicale et sonore dynamique basée sur une composition originale de Giorgio Mooder. Le jeu de course propose des sons de moteur réactif, des effets sonores de course, mais surtout un mix de musique dynamique qui se répartit sur plusieurs appareils à mesure que les coureurs se rejoignent. Il s'agit d'une installation multi-enceintes composée de smartphones.

Connecter plusieurs appareils entre eux était quelque chose que nous fassions depuis un moment. Nous avions fait des expériences musicales où le son se diviserait sur différents appareils ou passerait 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 un morceau de musique sur tous 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 la guitare et les synthétiseurs, et ainsi de suite. Nous avons fait des démos musicales et nous sommes plongés dans le codage. L'effet multi-enceintes était vraiment gratifiant. Nous n'avions pas la synchronisation parfaite à ce stade, mais lorsque nous avons entendu les couches de son éparpillées sur les appareils, nous savions que nous avions quelque chose de bien.

Créer les sons

Google Creative Lab avait 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 vrais sons ou de recourir à des bibliothèques de sons. Nous savions également que le haut-parleur de sortie serait, dans la plupart des cas, un tout petit haut-parleur de téléphone ou de tablette. Il fallait donc limiter le spectre de fréquences 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, c'est un soulagement, car sa composition s'accordait parfaitement avec les sons que nous avions créés.

Bruit du moteur

Pour programmer ces sons, le plus difficile était de trouver le meilleur son de moteur et de façonner son comportement. Le circuit ressemblait à un circuit de F1 ou de Nascar, les voitures devaient donc être rapides et explosives. En même temps, les voitures étaient très petites, si bien qu'un gros son de moteur ne permettrait pas vraiment de relier le son aux éléments visuels. Nous ne pouvions pas de toute façon jouer un rugissant moteur sur l'enceinte mobile, nous avons donc dû trouver autre chose.

Pour trouver l'inspiration, nous avons choisi la collection de synthétiseurs modulaires de notre ami Jon Ekstrand, et nous avons commencé à nous débattre. Nous avons bien aimé ce que nous avons entendu. Voilà à quoi cela ressemblait avec deux oscillateurs, des filtres intéressants et un LFO.

L'équipement analogique a déjà été remanié 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 en Web Audio. Un son généré est le plus réactif, mais il sollicite la puissance de traitement de l'appareil. Nous devions être extrêmement maigres pour économiser toutes les ressources que nous pouvions afin que les éléments visuels fonctionnent correctement. Nous avons donc remplacé la technique par la lecture d'échantillons audio.

Synthé modulaire pour trouver l'inspiration

Plusieurs techniques peuvent être utilisées pour créer un son de moteur à partir d'échantillons. Dans les jeux de console, l'approche la plus courante consiste à ajouter une couche de plusieurs sons (plus il est bon, mieux c'est) du moteur à différents tr/min (avec charge), puis un fondu enchaîné et un écart entre les deux. Ajoutez ensuite une couche de plusieurs sons du moteur pour monter en marche (sans charge) au même TPM, et créer un fondu enchaîné et un diapason entre les deux. Le fondu enchaîné entre ces couches lors du changement de vitesse, s'il est effectué correctement, donne un résultat très réaliste, mais seulement si vous disposez d'un grand nombre de fichiers audio. Le transversal ne doit pas être trop large, car il risque d'être très synthétique. Étant donné que nous devions éviter de longs délais de chargement, cette option ne nous convenait pas. Nous avons essayé avec cinq ou six fichiers audio par couche, mais le son était décevant. Nous avons dû trouver un moyen avec moins de fichiers.

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

  • Un fichier audio avec des accélérations et un changement de vitesse, synchronisé avec l'accélération visuelle de la voiture, se terminant par une boucle programmée à la trempe/tr/min la plus élevée. L'API Web Audio est très efficace pour effectuer des boucles avec précision, afin d'éviter les problèmes.
  • Un fichier audio avec un ralentissement ou un ralentissement du moteur.
  • Enfin, un fichier audio lisant en boucle le son immobile ou inactif.

ressemblant à ceci

Graphique sonore du moteur

Pour le premier événement tactile ou l'accélération, nous lisons le premier fichier depuis le début. Si le lecteur relâchait la limitation, nous calculions le temps à partir de là où nous nous trouvions dans le fichier audio au moment du lancement. Ainsi, lorsque la limitation se remettait, il se trouvait au bon endroit dans le fichier d'accélération après la lecture du second fichier (avec l'inverse).

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 d'accélération.

<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 bonne qualité, nous avons décidé de passer au défi suivant.

Synchroniser

En collaboration avec David Lindkvist, originaire de 14 îles, nous avons commencé à chercher une synchronisation parfaite sur tous les appareils. La théorie de base est simple. L'appareil demande l'heure au serveur, prend en compte la latence du réseau, puis calcule le décalage de l'horloge locale.

syncOffset = localTime - serverTime - networkLatency

Avec ce décalage, chaque appareil connecté partage le même concept de temps. Simple, 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 et recevoir une réponse du serveur:

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, ce qui signifie que la requête peut prendre plus de temps que la réponse, et inversement. Plus la latence du réseau est élevée, plus l'impact de cette asymétrie est important : les sons sont décalés et désynchronisés avec les autres appareils.

Heureusement, notre cerveau est conçu pour ne pas remarquer si les sons sont légèrement décalés. Des études ont démontré qu'un délai de 20 à 30 millisecondes (ms) est nécessaire pour que notre cerveau perçoive les sons comme des sons distincts. Cependant, entre 12 et 15 ms environ, vous commencerez à "ressentir" les effets d'un signal retardé, même si vous ne parvenez pas à le "percevoir" pleinement. Nous avons étudié quelques protocoles de synchronisation de l'heure et des alternatives plus simples, et nous avons essayé de mettre en œuvre certains d'entre eux dans la pratique. Au final, 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 des horloges

Ça a marché ! La synchronisation parfaite de plus de 5 appareils s'est effectuée de manière parfaitement synchronisée, mais seulement pendant un certain temps. Après quelques minutes de lecture, les appareils s'écartent, même si nous planifions 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 seulement quelques millisecondes à la fois et indétectables au début, mais entraînant une désynchronisation totale des couches musicales après des périodes de jeu plus longues. Bonjour, la dérive de l'horloge.

La solution consistait à effectuer une nouvelle synchronisation toutes les quelques secondes, à calculer un nouveau décalage d'horloge et à l'intégrer facilement au planificateur audio. Pour réduire le risque de modifications notables de la musique dues à un retard sur le réseau, nous avons décidé de les atténuer en conservant un historique des derniers décalages de synchronisation et en calculant une moyenne.

Programmation d'un titre et changement d'arrangement

En créant une expérience audio interactive, vous ne pouvez plus contrôler le moment où des parties du titre doivent être lues, car vous dépendez des actions des utilisateurs pour modifier l'état actuel. Nous devions nous assurer que nous pouvions changer d'arrangement dans un titre en temps opportun, ce qui signifie que notre planificateur devait être en mesure de calculer la quantité restante de la mesure en cours de lecture avant de passer à l'arrangement suivant. Notre algorithme a fini par ressembler à ceci:

  • 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 à partir du moment où le titre a été lancé à l'aide de son contexte audio Web, en tenant compte de syncOffset et du temps écoulé depuis la création du contexte audio.
  • playDelta = Date.now() - syncOffset - songStartTime - context.currentTime
  • Client(n) calcule la durée d'écoute du titre à l'aide de playDelta. Le planificateur de titres l'utilise pour savoir quelle mesure de l'arrangement actuelle passer ensuite.
  • playTime = playDelta + context.currentTime nextBar = Math.ceil((playTime % loopDuration) ÷ barDuration) % numberOfBars

Par souci de clarté, nous avons limité nos arrangements à une longueur de huit mesures et au même tempo (battements par minute).

Regardez devant vous

Il est toujours important de planifier votre action à 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 plusieurs dizaines de millisecondes, voire plus, par la mise en page, le rendu, la récupération de mémoire et les requêtes XMLHTTPRequest. 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.

Lutins audio

Combiner des sons en un seul fichier est un excellent moyen de réduire le nombre de requêtes HTTP, à la fois pour l'audio HTML et pour l'API Web Audio. C'est également la meilleure façon de lire du son de manière réactive à l'aide de l'objet audio, car il n'a pas besoin de charger un nouvel objet audio avant la lecture. Il existe déjà de bonnes implémentations que nous avons utilisées comme point de départ. Nous avons étendu notre lutin pour qu'il fonctionne de manière fiable sur iOS et Android, et qu'il gère quelques cas particuliers où les appareils s'endorment.

Sur Android, la lecture des éléments audio continue même si vous mettez l'appareil en mode veille. En mode veille, l'exécution JavaScript est limitée afin de préserver la batterie, et vous ne pouvez pas compter sur requestAnimationFrame, setInterval ou setTimeout pour déclencher des rappels. Ce problème est dû au fait que les lutins audio s'appuient sur JavaScript pour vérifier en permanence si la lecture doit être interrompue. Pour ne rien arranger, dans certains cas, l'élément currentTime de l'élément audio n'est pas mis à jour alors que le contenu audio est toujours en cours de lecture.

Découvrez l'implémentation d'AudioSprite que nous avons utilisée dans Chrome Racer en tant que remplacement audio non Web.

É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 de l'audio HTML pour certains appareils, l'API Web Audio pour d'autres, associée à la sortie audio avancée que nous voulions réaliser, a permis de relever certains défis intéressants. Heureusement, tout cela fait partie de l'histoire. L'API Web Audio est implémentée dans la version bêta d'Android M28.

  • Retards/problèmes de synchronisation. L'élément audio n'est pas toujours lu exactement quand vous lui demandez de le lire. JavaScript étant à thread unique, le navigateur peut être occupé, ce qui peut entraîner des retards de lecture pouvant atteindre deux secondes.
  • En effet, les retards de lecture empêchent toujours une lecture en boucle fluide. Sur ordinateur, vous pouvez utiliser la double mise en mémoire tampon pour créer des boucles relativement fluides. Toutefois, ce n'est pas possible sur les appareils mobiles, pour les raisons suivantes :
    • En général, les appareils mobiles ne diffusent qu'un seul é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 commencera à charger sa source que si la lecture est lancée dans un gestionnaire touchStart.
  • Rechercher des problèmes L'obtention de duration ou la définition de currentTime échouera, sauf si votre serveur prend en charge la plage d'octets HTTP. Faites attention à celui-ci si vous créez un lutin audio comme nous l'avons fait.
  • Échec de l'authentification de base sur le fichier MP3. Quel que soit le navigateur que vous utilisez, certains appareils ne parviennent pas à charger les fichiers MP3 protégés par l'authentification de base.

Conclusions

Nous avons beaucoup progressé depuis que l'option "Couper le son" était la meilleure option pour traiter du son sur le Web, mais ce n'est que le début, et l'audio Web est sur le point de vibrer. Nous n'avons fait que parler des possibilités de synchronisation de plusieurs appareils. Les téléphones et les tablettes ne disposaient pas de la puissance de traitement nécessaire pour nous pencher sur le traitement du signal et les effets (comme la réverbération), mais à mesure que les performances des appareils augmentent, les jeux Web exploiteront également ces fonctionnalités. Nous vivons une période passionnante où nous continuons à repousser les limites du son.