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

Introduction

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

Les jeux ne font pas exception ! Mes souvenirs les plus chers des jeux vidéo sont liés à la musique et aux effets sonores. Bien souvent, près de 20 ans après avoir joué à mes titres préférés, je n'arrive toujours pas à entendre les compositions de Zelda de Koji Kondo et la bande-son de Diablo de Matt Uelmen. Le même caractère accrocheur s'applique aux effets sonores, comme les réponses au clic des unités instantanément reconnaissables de Warcraft et les échantillons des classiques de Nintendo.

L'audio des jeux 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 se trouve le joueur. En pratique, certaines parties du jeu peuvent durer une durée inconnue, les sons peuvent interagir avec l'environnement et se mélanger de manière complexe, comme les effets de pièce et le positionnement relatif du son. Enfin, un grand nombre de sons peuvent être lus en même temps, et tous doivent sonner ensemble et s'afficher sans pénaliser les performances.

Audio de jeu sur le Web

Pour les jeux simples, l'utilisation de la balise <audio> peut suffire. Toutefois, de nombreux navigateurs offrent de mauvaises implémentations, ce qui entraîne des problèmes audio et une latence élevée. Il s'agit probablement d'un problème temporaire, car les fournisseurs s'efforcent d'améliorer leurs implémentations respectives. Pour avoir un aperçu de l'état de la balise <audio>, consultez la suite de tests sur areweplayingyet.org.

En examinant plus en détail la spécification du tag <audio>, il apparaît clairement qu'il existe de nombreuses choses qui ne peuvent tout simplement pas être faites, ce qui n'est pas surprenant, car il a été conçu pour la lecture de contenus multimédias. Voici quelques-unes des limites:

  • Impossible d'appliquer des filtres au signal audio
  • Aucun moyen d'accéder aux données PCM brutes
  • Aucune notion de position et de direction des sources et des auditeurs
  • Pas de synchronisation précise.

Dans la suite de l'article, je vais aborder certains de ces sujets dans le contexte de l'audio de 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

Les jeux comportent souvent une musique de fond en boucle.

Cela peut être très agaçant si la boucle est courte et prévisible. Si un joueur est bloqué dans une zone ou un niveau, et que le même extrait est diffusé en continu en arrière-plan, il peut être utile d'atténuer progressivement le son pour éviter toute frustration supplémentaire. Une autre stratégie consiste à utiliser des mélanges d'intensités variables qui se fondent progressivement dans l'autre, en fonction du contexte du jeu.

Par exemple, si votre joueur se trouve dans une zone de combat épique contre les boss, vous pouvez avoir plusieurs combinaisons dont la palette émotionnelle peut être atmosphérique, préjudiciable ou intense. Les logiciels de synthèse musicale vous permettent souvent d'exporter plusieurs mix (de même durée) basés sur un morceau en sélectionnant l'ensemble de pistes à utiliser dans l'exportation. Vous obtiendrez ainsi une certaine cohérence interne et éviterez les transitions choquantes lorsque vous passez d'un titre à un autre.

Garageband

Ensuite, à l'aide de l'API Web Audio, vous pouvez importer tous ces échantillons à l'aide d'un élément tel que la classe BufferLoader via XHR (cela est abordé en détail dans l'article d'introduction sur l'API Web Audio). Le chargement des sons prend du temps. Par conséquent, les éléments utilisés dans le jeu doivent être chargés lors du chargement de la page, au début du niveau ou peut-être de manière incrémentielle pendant que le joueur joue.

Vous allez ensuite créer une source pour chaque nœud, un nœud de gain pour chaque source et connecter le graphique.

Vous pouvez ensuite lire toutes ces sources simultanément en boucle. Étant donné qu'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 du combat final contre le 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 comme suit:

// 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 sont lues en même temps, et nous effectuons un fondu enchaîné 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 bien adaptée au streaming de contenu. Vous pouvez désormais importer le contenu de la balise <audio> dans un contexte Web Audio.

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'avoir à attendre que tout soit téléchargé. En intégrant le flux à l'API Web Audio, vous pouvez le manipuler ou l'analyser. L'exemple suivant applique un filtre passe-bas à la musique diffusée 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> à l'API Web Audio, consultez cet article court.

Effets sonores

Les jeux diffusent souvent des effets sonores en réponse à une entrée de l'utilisateur ou à des changements d'état du jeu. Cependant, comme la musique de fond, les effets sonores peuvent devenir très vite gênants. Pour éviter cela, il est souvent utile de disposer d'un ensemble de sons similaires, mais différents. Cela peut aller de légères variations d'échantillons de pas à des variations drastiques, comme on le voit dans la série Warcraft en réponse au clic sur des unités.

Autre caractéristique clé des effets sonores dans les jeux : ils peuvent être nombreux simultanément. Imaginez que vous êtes au milieu d'une fusillade avec plusieurs acteurs tirant des mitraillettes. Chaque mitrailleuse se déclenche plusieurs fois par seconde, entraînant l'exécution de dizaines d'effets sonores en même temps. La lecture simultanée de sons provenant de plusieurs sources à synchronisation précise est l'un des points forts de l'API Web Audio.

L'exemple suivant crée une rafale de mitrailleuse à partir de plusieurs échantillons de balles individuels en créant plusieurs sources sonores dont la lecture est décalé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);
}

Maintenant, si toutes les mitrailleuses de votre jeu sonnaient exactement comme ça, ce serait assez ennuyeux. Bien entendu, ils varient en fonction du son en fonction de la distance de la cible et de la position relative (nous y reviendrons plus tard), mais même cela peut ne pas suffire. Heureusement, l'API Web Audio permet de modifier facilement l'exemple ci-dessus de deux manières:

  1. avec un décalage subtil entre les tirs
  2. En modifiant le playbackRate de chaque échantillon (et en modifiant également la hauteur) pour mieux simuler le caractère aléatoire du monde réel.

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

Son spatial 3D

Les jeux se déroulent souvent dans un monde doté de certaines propriétés géométriques, en 2D ou en 3D. Si tel est le cas, l'audio stéréo peut considérablement augmenter l'immersion de l'expérience. Heureusement, l'API Web Audio est fournie avec des fonctionnalités audio positionnelles accélérées par matériel intégrées qui sont très simples à utiliser. Par ailleurs, assurez-vous d'avoir des haut-parleurs stéréo (de préférence des écouteurs) pour que l'exemple suivant ait du sens.

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 est un exemple simple d'utilisation d'AudioPannerNode pour obtenir ce type d'effet. L'idée de base de l'exemple ci-dessus est de répondre au mouvement 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 dans Web Audio:

  • L'écouteur se trouve à l'origine (0, 0, 0) par défaut.
  • Les API de positionnement Web Audio n'ont pas d'unité. J'ai donc introduit un multiplicateur pour améliorer le son de la démo.
  • Web Audio utilise les coordonnées cartésiennes "y est en haut" (à l'opposé de la plupart des systèmes de graphisme). C'est pourquoi je permute l'axe des y dans l'extrait ci-dessus

Avancé: cônes sonores

Le modèle basé sur la position est très puissant et très avancé, et repose en grande partie sur OpenAL. Pour en savoir plus, consultez les sections 3 et 4 de la spécification indiqué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 spatialise l'audio d'entrée. Le nœud de panoramique possède une position et une orientation, ainsi qu'un modèle de distance et de direction.

Le modèle de distance spécifie le niveau de gain en fonction de la proximité avec la source, tandis que le modèle directionnel peut être configuré en spécifiant un cône intérieur et un cône extérieur, qui déterminent la quantité 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 externe.

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 de son Web Audio inclut également la vitesse pour les décalages Doppler. Cet exemple illustre plus en détail l'effet Doppler.

Pour en savoir plus à ce sujet, consultez ce tutoriel détaillé sur le [mixage de l'audio positionnel et de WebGL][webgl].

Effets et filtres pour les pièces

En réalité, la façon dont le son est perçu dépend grandement de la pièce dans laquelle il est entendu. Le même grincement de porte sera très différent dans un sous-sol par rapport à un grand hall ouvert. Les jeux à forte valeur de production voudront imiter ces effets, car créer un ensemble d'échantillons distinct pour chaque environnement est prohibitif, et entraînerait encore plus d'éléments et une plus grande quantité de données de jeu.

En termes audio, la réponse impulsionnelle désigne la différence entre le son brut et la façon dont il sonne dans la réalité. Ces réponses impulsionnelles peuvent être enregistrées avec beaucoup de minutie. En fait, de nombreux sites hébergent ces fichiers de réponse impulsionnelle préenregistrés (stockés en tant qu'audio) pour votre commodité.

Pour en savoir plus sur la création de réponses impulsionnelles à 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 du 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);

Consultez également cette démonstration des effets de pièce 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 un convolver) d'un grand standard de jazz.

Le compte à rebours final

Vous avez donc créé un jeu et configuré votre audio positionnel. Vous disposez maintenant d'un grand nombre d'AudioNodes dans votre graphe, qui sont tous lus simultanément. Parfait, mais il y a encore une chose à prendre en compte:

Étant donné que plusieurs sons se superposent 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 du canevas, les sons peuvent également être coupés si la forme d'onde dépasse son seuil maximal, ce qui produit une distorsion distincte. La forme d'onde ressemble à ceci:

Clipping

Voici un exemple concret de découpage. La forme d'onde ne s'affiche pas correctement:

Clipping

Il est important d'écouter des distorsions fortes comme celle ci-dessus, ou à l'inverse, des mixages trop atténués qui obligent vos auditeurs à augmenter le volume. Si vous vous trouvez dans cette situation, vous devez absolument y remédier.

Détecter les coupures

D'un point de vue technique, le lissage se produit lorsque la valeur du signal dans un canal dépasse la plage valide, à savoir entre -1 et 1. Une fois cette situation détectée, il est utile de fournir un retour visuel pour indiquer qu'elle se produit. Pour ce faire de manière fiable, ajoutez un JavaScriptAudioNode à votre graphique. Le graphique audio est 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);

Un écrasement peut ê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 du rendu, comme déterminé par requestAnimationFrame. Cette approche est plus efficace, mais passe à côté de la plupart du signal (y compris les endroits où il risque d'être tronqué), car le rendu a lieu au maximum 60 fois par seconde, tandis que le signal audio change beaucoup plus rapidement.

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

Éviter les coupures

En ajustant le gain sur le nœud AudioGainNode maître, vous pouvez atténuer votre mixage à un niveau qui empêche le forçage. Toutefois, dans la pratique, les sons utilisés dans votre jeu peuvent dépendre d'une grande variété de facteurs. Il peut donc être difficile de décider de la valeur de gain principale qui empêche le bornement de tous les états. En règle générale, vous devez ajuster les gains pour anticiper le pire des cas, mais il s'agit davantage d'un art que d'une science.

Ajouter un peu de sucre

Les compresseurs sont couramment utilisés dans la production musicale et de jeux pour lisser le signal et contrôler les pics du signal global. Cette fonctionnalité est disponible dans le monde de l'audio Web via DynamicsCompressorNode, qui peut être inséré dans votre graphique audio pour obtenir un son plus fort, plus riche et plus complet, et également pour limiter le clipping. Citant directement la spécification, ce nœud

L'utilisation de la compression dynamique est généralement une bonne idée, en particulier dans un jeu, où, comme indiqué précédemment, vous ne savez pas exactement quels sons seront diffusés et quand. Plink des ateliers DinahMoe en est un parfait exemple, car les sons écoutés dépendent entièrement de vous et des autres participants. Un compresseur est utile dans la plupart des cas, sauf dans de rares cas où vous travaillez sur des pistes masterisées avec soin qui ont déjà été réglées pour un son "juste parfait".

La mise en œuvre consiste simplement à inclure un DynamicsCompressorNode dans votre graphe 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);

Pour en savoir plus sur la compression dynamique, consultez cet article de Wikipédia.

Pour résumer, écoutez attentivement les coupures et évitez-les en insérant un nœud de gain maître. Ensuite, resserrez l'ensemble à l'aide d'un nœud de compression de dynamiques. Votre graphique audio peut ressembler à ceci:

Résultat final

Conclusion

Voyons quels sont, selon moi, les aspects les plus importants du développement audio de jeu à l'aide de l'API Web Audio. Grâce à ces techniques, vous pouvez créer des expériences audio vraiment attrayantes directement dans votre navigateur. Avant de vous quitter, 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 risquez de créer une expérience potentiellement frustrante pour votre utilisateur.

Pour en savoir plus sur Web Audio, consultez l'article de démarrage plus introductif. Si vous avez une question, vérifiez si elle a déjà été répondue dans les questions fréquentes sur Web Audio. Enfin, si vous avez d'autres questions, posez-les sur Stack Overflow à l'aide de la balise web-audio.

Avant de vous quitter, je vais vous présenter quelques utilisations exceptionnelles de l'API Web Audio dans des jeux réels: