Développer l'audio d'un jeu avec l'API Web Audio

Introduction

L'audio est un élément essentiel de ce qui rend les expériences multimédias aussi attrayantes. Si vous avez déjà essayé de regarder un film avec le son coupé, vous l’avez probablement remarqué.

Les jeux ne font pas exception ! La musique et les effets sonores sont mes meilleurs souvenirs de jeux vidéo. Bien souvent, près de vingt ans après avoir joué mes favoris, je n'arrive toujours pas à me faire oublier les compositions de Zelda de Koji Kondo et la bande-son de Diablo de Matt Uelmen. La même accroche s'applique aux effets sonores, tels que les réponses de clic sur un bloc qui sont immédiatement reconnaissables de Warcraft et les extraits des classiques Nintendo.

L'audio du jeu présente des défis intéressants. Pour créer une musique de jeu convaincante, les concepteurs doivent s'adapter à l'état de jeu potentiellement imprévisible dans lequel le joueur se trouve. En pratique, certaines parties du jeu peuvent se poursuivre pendant une durée inconnue, les sons peuvent interagir avec l'environnement et se mélanger de manière complexe, par exemple avec des effets de pièce et des positionnements sonores relatifs. Enfin, il peut y avoir un grand nombre de sons lus en même temps, qui doivent tous avoir un bon son ensemble et s'afficher sans nuire aux performances.

Audio de jeux sur le Web

Pour les jeux simples, la balise <audio> peut être suffisante. Cependant, de nombreux navigateurs n'offrent pas une bonne implémentation, ce qui entraîne des problèmes audio et une latence élevée. Il s'agit peut-être d'un problème temporaire, car les fournisseurs mettent tout en œuvre pour améliorer leurs implémentations respectives. Pour obtenir un aperçu de l'état de la balise <audio>, il existe une suite de tests pratique sur areweplayingyet.org.

En examinant plus en détail la spécification de la balise <audio>, il apparaît clairement qu'elle ne peut tout simplement pas être faite avec de nombreuses choses, ce qui n'est pas surprenant, puisqu'elle a été conçue pour la lecture de contenus multimédias. Voici quelques limites:

  • Impossible d'appliquer des filtres au signal sonore
  • Aucun moyen d'accéder aux données PCM brutes
  • Aucun concept de position et de direction des sources et des écouteurs
  • Pas de temporalité précise.

Dans la suite de l'article, nous aborderons certains de ces sujets dans le contexte de l'audio d'un jeu écrit avec l'API Web Audio. Pour une brève présentation de cette API, consultez le tutoriel de démarrage.

Musique de fond

Dans les jeux, la musique de fond est souvent diffusée en boucle.

Si votre boucle est courte et prévisible, cela peut devenir très ennuyeux. Si un joueur est coincé dans une zone ou un niveau et que le même échantillon est lu en continu en arrière-plan, il peut être utile de faire disparaître progressivement la piste afin d'éviter toute frustration. Une autre stratégie consiste à créer des mélanges d'intensités diverses qui enchaînent progressivement les uns avec les autres, en fonction du contexte du jeu.

Par exemple, si votre joueur se trouve dans une zone où une bataille de boss épique mène à une bataille épique, vous pouvez avoir plusieurs mix d'émotions variées, allant de l'ambiance atmosphérique à la prémisse, voire d'intense. Les logiciels de synthèse musicale vous permettent souvent d'exporter plusieurs mix (de la même durée) basés sur un morceau en sélectionnant l'ensemble de pistes à utiliser dans l'exportation. De cette façon, vous obtenez une certaine cohérence interne et évitez les transitions perturbantes lorsque vous passez d'une piste à l'autre en fondu enchaîné.

GarageBand

Ensuite, à l'aide de l'API Web Audio, vous pouvez importer tous ces exemples à l'aide de la classe BufferLoader via XHR (ce sujet est abordé en détail dans l'article de présentation de l'API Web Audio). Le chargement des sons prend du temps. Les éléments utilisés dans le jeu doivent donc être chargés au chargement de la page, au début du niveau ou éventuellement de manière incrémentielle pendant que le joueur joue.

Ensuite, créez une source pour chaque nœud et un nœud de gain pour chaque source, puis connectez le graphe.

Vous pouvez ensuite lire toutes ces sources simultanément sur une boucle. Comme elles ont toutes la même longueur, l'API Web Audio garantit qu'elles resteront alignées. À mesure que le personnage se rapproche ou s'éloigne de la bataille finale contre les boss, le jeu peut faire varier les valeurs de gain pour chacun des nœuds respectifs de la chaîne, à l'aide d'un algorithme de gain de gain comme celui-ci:

// Assume gains is an array of AudioGainNode, normVal is the intensity
// between 0 and 1.
var value = normVal - (gains.length - 1);
// First reset gains on all nodes.
for (var i = 0; i < gains.length; i++) {
    gains[i].gain.value = 0;
}
// Decide which two nodes we are currently between, and do an equal
// power crossfade between them.
var leftNode = Math.floor(value);
// Normalize the value between 0 and 1.
var x = value - leftNode;
var gain1 = Math.cos(x - 0.5*Math.PI);
var gain2 = Math.cos((1.0 - x) - 0.5*Math.PI);
// Set the two gains accordingly.
gains[leftNode].gain.value = gain1;
// Check to make sure that there's a right node.
if (leftNode < gains.length - 1) {
    // If there is, adjust its gain.
    gains[leftNode + 1].gain.value = gain2;
}

Dans l'approche ci-dessus, deux sources jouent en même temps et un fondu enchaîné est effectué entre elles en utilisant des courbes de puissance égales (comme décrit dans l'introduction).

De nombreux développeurs de jeux utilisent aujourd'hui la balise <audio> pour leur musique de fond, car elle est adaptée au streaming de contenu. Vous pouvez maintenant importer le contenu de la balise <audio> dans un contexte audio Web.

Cette technique peut être utile, car la balise <audio> peut fonctionner avec du contenu en streaming, ce qui vous permet de lire immédiatement la musique de fond au lieu d'attendre que tout soit téléchargé. En important le flux dans l'API Web Audio, vous pouvez le manipuler ou l'analyser. L'exemple suivant applique un filtre passe-bas à la musique lue via la balise <audio>:

var audioElement = document.querySelector('audio');
var mediaSourceNode = context.createMediaElementSource(audioElement);
// Create the filter
var filter = context.createBiquadFilter();
// Create the audio graph.
mediaSourceNode.connect(filter);
filter.connect(context.destination);

Pour en savoir plus sur l'intégration de la balise <audio> dans l'API Web Audio, consultez ce court article.

Effets sonores

Les jeux diffusent souvent des effets sonores en réponse à une action de l'utilisateur ou à des changements d'état. Tout comme la musique de fond, les effets sonores peuvent vite devenir agaçants. Pour éviter cela, il est souvent utile d'avoir un ensemble de sons similaires, mais différents, à diffuser. Cela peut aller de légères variations des échantillons de pas à des variations drastiques, comme dans la série Warcraft en réponse aux clics sur les unités.

Autre caractéristique clé des effets sonores : il peut y en avoir plusieurs en même temps. Imaginez que vous soyez en plein milieu d'une fusillade avec plusieurs acteurs tirant des mitraillettes. Chaque mitrailleuse se déclenche plusieurs fois par seconde, provoquant ainsi l'exécution simultanée de dizaines d'effets sonores. La lecture simultanée de plusieurs sources à un moment précis permet à l'API Web Audio de se démarquer.

L'exemple suivant crée une mitrailleuse à partir de plusieurs échantillons de balles en créant plusieurs sources audio dont la lecture est échelonnée dans le temps.

var time = context.currentTime;
for (var i = 0; i < rounds; i++) {
    var source = this.makeSource(this.buffers[M4A1]);
    source.noteOn(time + i - interval);
}

Si toutes les mitraillettes de votre jeu ressemblaient à ça, ce serait assez ennuyeux. Bien sûr, elles varient en fonction du son en fonction de la distance par rapport à la cible et à la position relative (nous y reviendrons plus tard), mais même ce n'est pas suffisant. Heureusement, l'API Web Audio permet d'ajuster facilement l'exemple ci-dessus de deux manières:

  1. Avec un léger décalage dans le temps entre le déclenchement des puces
  2. En modifiant le mécanisme de lecture de chaque échantillon (en changeant également la tonalité) pour mieux simuler le caractère aléatoire du monde réel.

Pour un exemple plus concret de ces techniques, consultez la démonstration de table de billard, qui utilise un échantillonnage aléatoire et fait varier le taux de lecture pour un son de collision de balle plus intéressant.

Son positionnel 3D

Les jeux se déroulent souvent dans un monde comportant des propriétés géométriques, en 2D ou en 3D. Dans ce cas, un son stéréo positionné peut considérablement augmenter l'immersion de l'expérience. Heureusement, l'API Web Audio intègre des fonctionnalités audio de position avec accélération matérielle qui sont assez simples à utiliser. À ce propos, assurez-vous de disposer de haut-parleurs stéréo (de préférence un casque) pour que l'exemple suivant soit pertinent.

Dans l'exemple ci-dessus, un écouteur (icône représentant une personne) se trouve au milieu du canevas, et la souris affecte la position de la source (icône de haut-parleur). L'exemple ci-dessus illustre simplement l'utilisation de AudioPannerNode pour obtenir ce type d'effet. L'idée de base de l'exemple ci-dessus consiste à réagir aux mouvements de la souris en définissant la position de la source audio, comme suit:

PositionSample.prototype.changePosition = function(position) {
    // Position coordinates are in normalized canvas coordinates
    // with -0.5 < x, y < 0.5
    if (position) {
    if (!this.isPlaying) {
        this.play();
    }
    var mul = 2;
    var x = position.x / this.size.width;
    var y = -position.y / this.size.height;
    this.panner.setPosition(x - mul, y - mul, -0.5);
    } else {
    this.stop();
    }
};

Ce qu'il faut savoir sur le traitement de la spatialisation par Web Audio:

  • Par défaut, l'écouteur est à l'origine (0, 0, 0).
  • Les API de position Web Audio ne comportent aucune unité. J'ai donc ajouté un multiplicateur pour améliorer le son de la démonstration.
  • Web Audio utilise les coordonnées cartésiennes y-is-up (à l'opposé de la plupart des systèmes d'infographie). C'est pourquoi je change l'axe des y dans l'extrait ci-dessus

Avancé: cônes de son

Le modèle positionnel est très puissant et assez avancé, en grande partie basé sur OpenAL. Pour en savoir plus, consultez les sections 3 et 4 de la spécification associée ci-dessus.

Modèle de position

Un seul AudioListener est associé au contexte de l'API Web Audio, qui peut être configuré dans l'espace via la position et l'orientation. Chaque source peut être transmise via un AudioPannerNode, qui spaialise le contenu audio d'entrée. Le nœud de panneau dispose d'une position et d'une orientation, ainsi que d'une distance et d'un modèle directionnel.

Le modèle de distance spécifie le niveau de gain en fonction de la proximité par rapport à la source, tandis que le modèle directionnel peut être configuré en spécifiant un cône intérieur et un cône externe, qui déterminent le niveau de gain (généralement négatif) si l'écouteur se trouve dans le cône interne, entre le cône intérieur et extérieur, ou à l'extérieur du cône extérieur.

var panner = context.createPanner();
panner.coneOuterGain = 0.5;
panner.coneOuterAngle = 180;
panner.coneInnerAngle = 0;

Bien que mon exemple soit en 2D, ce modèle se généralise facilement à la troisième dimension. Pour obtenir un exemple de son spatialisé en 3D, consultez cet exemple de position. En plus de la position, le modèle audio Web Audio inclut éventuellement la vitesse des changements de doppler. Cet exemple montre l'effet Doppler plus en détail.

Pour en savoir plus à ce sujet, consultez ce tutoriel détaillé [Mix entre audio positionnel et WebGL][webgl].

Effets et filtres sur la pièce

En réalité, la façon dont le son est perçu dépend en grande partie de la pièce dans laquelle il est entendu. Une même porte qui grince aura un son très différent dans un sous-sol par rapport à une grande porte ouverte. Pour les jeux à forte valeur de production, il est préférable d'imiter ces effets, car la création d'un ensemble d'échantillons distinct pour chaque environnement est excessivement coûteuse et générerait encore plus d'éléments et de données de jeu.

Pour faire simple, le terme audio qui désigne la différence entre le son brut et le son réel est la réponse impulsive. Ces réponses impulsives peuvent être minutieusement enregistrées. Il existe d'ailleurs des sites qui hébergent un grand nombre de ces fichiers de réponse impulsive préenregistrés (stockés sous forme audio) à titre indicatif.

Pour en savoir plus sur la manière dont les réponses impulsives peuvent être créées à partir d'un environnement donné, consultez la section "Configuration de l'enregistrement" de la partie Convolution de la spécification de l'API Web Audio.

Plus important encore pour nos besoins, l'API Web Audio offre un moyen simple d'appliquer ces réponses impulsives à nos sons à l'aide de ConvolverNode.

// Make a source node for the sample.
var source = context.createBufferSource();
source.buffer = this.buffer;
// Make a convolver node for the impulse response.
var convolver = context.createConvolver();
convolver.buffer = this.impulseResponseBuffer;
// Connect the graph.
source.connect(convolver);
convolver.connect(context.destination);

Regardez également cette démonstration des effets de salle sur la page des spécifications de l'API Web Audio, ainsi que cet exemple qui vous permet de contrôler le mixage sec (brut) et humide (traité via convolver) d'un excellent standard Jazz.

Le compte à rebours final

Vous avez créé un jeu, configuré votre position audio et un grand nombre de AudioNodes dans votre graphique, qui sont lus simultanément. Parfait, mais il reste encore une autre chose à prendre en compte:

Étant donné que plusieurs sons s'empilent les uns sur les autres sans normalisation, vous pouvez vous retrouver dans une situation où vous dépassez le seuil de capacité de votre enceinte. Comme les images qui dépassent les limites de leur canevas, les sons peuvent également être coupés si la forme d'onde dépasse son seuil maximal, produisant ainsi une distorsion distincte. Voici à quoi ressemble la forme d'onde:

Coupe

Voici un exemple concret du rognage. La forme d'onde ne semble pas correcte:

Coupe

Il est important d'écouter des distorsions intenses comme celui présenté ci-dessus ou, à l'inverse, des mix trop tamisés qui obligent vos auditeurs à augmenter le volume. Si vous êtes dans cette situation, vous devez vraiment la corriger !

Détecter le rognage

D'un point de vue technique, le rognage se produit lorsque la valeur du signal dans un canal dépasse la plage valide, à savoir entre -1 et 1. Une fois ce problème détecté, il est utile de fournir un retour visuel à ce sujet. Pour ce faire de manière fiable, insérez un JavaScriptAudioNode dans votre graphique. Le graphique audio serait configuré comme suit:

// Assume entire sound output is being piped through the mix node.
var meter = context.createJavaScriptNode(2048, 1, 1);
meter.onaudioprocess = processAudio;
mix.connect(meter);
meter.connect(context.destination);

De plus, le bornement a pu être détecté dans le gestionnaire processAudio suivant:

function processAudio(e) {
    var buffer = e.inputBuffer.getChannelData(0);

    var isClipping = false;
    // Iterate through buffer to check if any of the |values| exceeds 1.
    for (var i = 0; i < buffer.length; i++) {
    var absValue = Math.abs(buffer[i]);
    if (absValue >= 1) {
        isClipping = true;
        break;
    }
    }
}

En général, veillez à ne pas surutiliser JavaScriptAudioNode pour des raisons de performances. Dans ce cas, une autre implémentation de la mesure pourrait interroger un RealtimeAnalyserNode dans le graphique audio pour getByteFrequencyData au moment de l'affichage, comme déterminé par requestAnimationFrame. Cette approche est plus efficace, mais elle manque une grande partie du signal (y compris les endroits où il risque de se rogner), car le rendu se produit au maximum 60 fois par seconde, tandis que le signal audio change beaucoup plus rapidement.

La détection de clips étant très importante, il est probable que nous rencontrions un nœud MeterNode intégré à l'API Web Audio à l'avenir.

Empêcher le rognage

En ajustant le gain sur le AudioGainNode maître, vous pouvez limiter votre mixage à un niveau qui empêche le rognage. Toutefois, dans la pratique, comme les sons joués dans votre jeu peuvent dépendre d'une grande variété de facteurs, il peut être difficile de décider de la valeur de gain principale qui empêche le rognage pour tous les états. En général, vous devez ajuster les gains pour anticiper le pire des cas, mais c'est plus un art qu'une science.

Ajouter un peu de sucre

Les compresseurs sont couramment utilisés dans la production de musique et de jeux pour lisser le signal et contrôler les pics du signal global. Cette fonctionnalité est disponible dans l'univers Web Audio via DynamicsCompressorNode, qui peut être inséré dans votre graphe audio pour générer un son plus puissant, plus riche et plus complet, et pour faciliter le rognage. Citez directement la spécification.

L'utilisation de la compression dynamique est généralement une bonne idée, en particulier dans un environnement de jeu où, comme nous l'avons vu précédemment, vous ne savez pas exactement quels sons seront diffusés ni à quel moment. Plink des laboratoires DinahMoe en est un parfait exemple, car les sons lus dépendent entièrement de vous et des autres participants. Un compresseur est utile dans la plupart des cas, à l'exception de quelques rares cas, où vous devez composer avec des morceaux minutieusement maîtrisés qui ont été réglés pour offrir un son "exactement".

Pour implémenter cela, il suffit d'inclure un DynamicsCompressorNode dans votre graphique audio, généralement en tant que dernier nœud avant la destination :

// Assume the output is all going through the mix node.
var compressor = context.createDynamicsCompressor();
mix.connect(compressor);
compressor.connect(context.destination);

Cet article Wikipédia fournit des informations très détaillées sur la compression dynamique.

Pour résumer, écoutez attentivement les écrêtages et évitez-les en insérant un nœud de gain maître. puis en resserrant l'ensemble du mix à l'aide d'un nœud de compresseur dynamique. Votre graphique audio peut se présenter comme suit:

Résultat final

Conclusion

Voilà qui est, selon moi, les aspects les plus importants du développement audio de jeux à l'aide de l'API Web Audio. Ces techniques vous permettent de créer des expériences audio vraiment attrayantes directement dans votre navigateur. Avant de terminer, laissez-moi vous donner un conseil spécifique au navigateur: veillez à mettre en pause le son si votre onglet passe en arrière-plan à l'aide de l'API Page Visibility, sinon vous créerez une expérience potentiellement frustrante pour l'utilisateur.

Pour en savoir plus sur l'audio sur le Web, consultez l'article d'introduction Premiers pas. Si vous avez une question, consultez les questions fréquentes sur les contenus audio pour le Web si vous y avez déjà répondu. Enfin, si vous avez d'autres questions, posez-les sur Stack Overflow à l'aide du tag web-audio.

Avant de terminer, permettez-moi de vous présenter aujourd'hui quelques utilisations formidables de l'API Web Audio dans des jeux réels: