Premiers pas avec l'API Web Audio

Avant l'élément <audio> HTML5, Flash ou un autre plug-in était nécessaire pour briser le silence du Web. Bien que l'audio sur le Web ne nécessite plus de plug-in, la balise audio présente des limites importantes pour l'implémentation de jeux et d'applications interactives sophistiqués.

L'API Web Audio est une API JavaScript de haut niveau permettant de traiter et de synthétiser l'audio dans les applications Web. L'objectif de cette API est d'inclure les fonctionnalités des moteurs audio de jeu modernes et certaines des tâches de mixage, de traitement et de filtrage que l'on trouve dans les applications de production audio pour ordinateur de bureau modernes. Vous trouverez ci-dessous une présentation détaillée de l'utilisation de cette API puissante.

Un AudioContext permet de gérer et de lire tous les sons. Pour produire un son à l'aide de l'API Web Audio, créez une ou plusieurs sources audio et connectez-les à la destination audio fournie par l'instance AudioContext. Cette connexion n'a pas besoin d'être directe et peut passer par un nombre illimité de nœuds AudioNodes intermédiaires, qui agissent en tant que modules de traitement pour le signal audio. Ce routage est décrit plus en détail dans la spécification Web Audio.

Une seule instance de AudioContext peut prendre en charge plusieurs entrées audio et des graphiques audio complexes. Nous n'en aurons donc besoin que d'une seule pour chaque application audio que nous créons.

L'extrait de code suivant crée un AudioContext:

var context;
window.addEventListener('load', init, false);
function init() {
    try {
    context = new AudioContext();
    }
    catch(e) {
    alert('Web Audio API is not supported in this browser');
    }
}

Pour les anciens navigateurs basés sur WebKit, utilisez le préfixe webkit, comme avec webkitAudioContext.

De nombreuses fonctionnalités intéressantes de l'API Web Audio, telles que la création d'AudioNodes et le décodage des données de fichiers audio, sont des méthodes de AudioContext.

Chargement des sons

L'API Web Audio utilise un AudioBuffer pour les sons de courte à moyenne durée. L'approche de base consiste à utiliser XMLHttpRequest pour extraire les fichiers audio.

L'API permet de charger des données de fichiers audio dans plusieurs formats, tels que WAV, MP3, AAC, OGG et autres. La compatibilité des navigateurs avec les différents formats audio varie.

L'extrait de code suivant montre comment charger un extrait audio :

var dogBarkingBuffer = null;
var context = new AudioContext();

function loadDogSound(url) {
    var request = new XMLHttpRequest();
    request.open('GET', url, true);
    request.responseType = 'arraybuffer';

    // Decode asynchronously
    request.onload = function() {
    context.decodeAudioData(request.response, function(buffer) {
        dogBarkingBuffer = buffer;
    }, onError);
    }
    request.send();
}

Les données du fichier audio sont binaires (et non textuelles). Nous définissons donc le responseType de la requête sur 'arraybuffer'. Pour en savoir plus sur ArrayBuffers, consultez cet article sur XHR2.

Une fois les données du fichier audio (non décodées) reçues, vous pouvez les conserver pour un décodage ultérieur ou les décoder immédiatement à l'aide de la méthode AudioContext decodeAudioData(). Cette méthode prend la ArrayBuffer des données du fichier audio stockées dans request.response et la décode de manière asynchrone (sans bloquer le thread d'exécution JavaScript principal).

Lorsque decodeAudioData() a terminé, il appelle une fonction de rappel qui fournit les données audio PCM décodées sous forme de AudioBuffer.

Lecture de sons

Graphique audio simple
Graphique audio simple

Une fois qu'un ou plusieurs AudioBuffers sont chargés, nous sommes prêts à lire des sons. Supposons que nous venons de charger un AudioBuffer avec le son d'un chien qui aboie et que le chargement est terminé. Nous pouvons ensuite lire ce tampon avec le code suivant.

var context = new AudioContext();

function playSound(buffer) {
    var source = context.createBufferSource(); // creates a sound source
    source.buffer = buffer;                    // tell the source which sound to play
    source.connect(context.destination);       // connect the source to the context's destination (the speakers)
    source.noteOn(0);                          // play the source now
}

Cette fonction playSound() peut être appelée chaque fois qu'un utilisateur appuie sur une touche ou clique sur un élément avec la souris.

La fonction noteOn(time) permet de planifier facilement la lecture précise du son pour les jeux et d'autres applications critiques. Toutefois, pour que cette planification fonctionne correctement, assurez-vous que vos tampons audio sont préchargés.

Extraire l'API Web Audio

Bien entendu, il serait préférable de créer un système de chargement plus général qui ne soit pas codé en dur pour charger ce son spécifique. Il existe de nombreuses approches pour gérer les nombreux sons courts à moyens qu'une application ou un jeu audio utiliseraient. Voici une méthode utilisant un BufferLoader (qui ne fait pas partie de la norme Web).

Vous trouverez ci-dessous un exemple d'utilisation de la classe BufferLoader. Créons deux AudioBuffers et, dès qu'ils sont chargés, lisons-les en même temps.

window.onload = init;
var context;
var bufferLoader;

function init() {
    context = new AudioContext();

    bufferLoader = new BufferLoader(
    context,
    [
        '../sounds/hyper-reality/br-jam-loop.wav',
        '../sounds/hyper-reality/laughter.wav',
    ],
    finishedLoading
    );

    bufferLoader.load();
}

function finishedLoading(bufferList) {
    // Create two sources and play them both together.
    var source1 = context.createBufferSource();
    var source2 = context.createBufferSource();
    source1.buffer = bufferList[0];
    source2.buffer = bufferList[1];

    source1.connect(context.destination);
    source2.connect(context.destination);
    source1.noteOn(0);
    source2.noteOn(0);
}

Gérer le temps: jouer des sons avec du rythme

L'API Web Audio permet aux développeurs de planifier précisément la lecture. Pour illustrer cela, configurons une piste de rythme simple. Le modèle de batterie le plus connu est probablement le suivant:

Un rythme de batterie rock simple
Un simple pattern de batterie rock

dans lequel un charleston est joué toutes les croches, et que la grosse caisse et la caisse claire sont jouées en alternance toutes les quarts, en 4/4.

En supposant que nous ayons chargé les tampons kick, snare et hihat, le code permettant d'effectuer cette opération est simple:

for (var bar = 0; bar < 2; bar++) {
    var time = startTime + bar * 8 * eighthNoteTime;
    // Play the bass (kick) drum on beats 1, 5
    playSound(kick, time);
    playSound(kick, time + 4 * eighthNoteTime);

    // Play the snare drum on beats 3, 7
    playSound(snare, time + 2 * eighthNoteTime);
    playSound(snare, time + 6 * eighthNoteTime);

    // Play the hi-hat every eighth note.
    for (var i = 0; i < 8; ++i) {
    playSound(hihat, time + i * eighthNoteTime);
    }
}

Ici, nous ne faisons qu'une seule répétition au lieu de la boucle illimitée que nous voyons dans la partition. La fonction playSound est une méthode qui lit un tampon à un moment spécifié, comme suit:

function playSound(buffer, time) {
    var source = context.createBufferSource();
    source.buffer = buffer;
    source.connect(context.destination);
    source.noteOn(time);
}

Régler le volume d'un son

L'une des opérations les plus basiques que vous pouvez effectuer sur un son est de modifier son volume. À l'aide de l'API Web Audio, nous pouvons acheminer notre source vers sa destination via un AudioGainNode afin de manipuler le volume:

Graphique audio avec un nœud de gain
Graphique audio avec un nœud de gain

Pour configurer cette connexion, procédez comme suit:

// Create a gain node.
var gainNode = context.createGainNode();
// Connect the source to the gain node.
source.connect(gainNode);
// Connect the gain node to the destination.
gainNode.connect(context.destination);

Une fois le graphique configuré, vous pouvez modifier le volume par programmation en manipulant gainNode.gain.value comme suit:

// Reduce the volume.
gainNode.gain.value = 0.5;

Fondu entre deux sons

Supposons maintenant que nous ayons un scénario un peu plus complexe, dans lequel nous jouons plusieurs sons, mais que nous voulons créer un fondu croisé. Il s'agit d'un cas courant dans une application de type DJ, où nous disposons de deux platines et souhaitons pouvoir faire un panoramique d'une source sonore à une autre.

Vous pouvez le faire à l'aide du graphique audio suivant:

Graphique audio avec deux sources connectées via des nœuds de gain
Graphique audio avec deux sources connectées via des nœuds de gain

Pour configurer cela, nous créons simplement deux AudioGainNodes et connectons chaque source via les nœuds, à l'aide d'une fonction semblable à celle-ci:

function createSource(buffer) {
    var source = context.createBufferSource();
    // Create a gain node.
    var gainNode = context.createGainNode();
    source.buffer = buffer;
    // Turn on looping.
    source.loop = true;
    // Connect source to gain.
    source.connect(gainNode);
    // Connect gain to destination.
    gainNode.connect(context.destination);

    return {
    source: source,
    gainNode: gainNode
    };
}

Fondu avec puissance égale

Une approche naïve de fondu linéaire présente une baisse du volume lorsque vous balayez les échantillons.

Un fondu enchaîné linéaire
Un fondu linéaire

Pour résoudre ce problème, nous utilisons une courbe de puissance égale, dans laquelle les courbes de gain correspondantes sont non linéaires et se croisent à une amplitude plus élevée. Cela réduit les baisses de volume entre les régions audio, ce qui permet d'obtenir un fondu plus régulier entre les régions dont le niveau peut être légèrement différent.

Un fondu croisé de puissance égale.
Un fondu croisé de puissance égale

Mélange des titres d'une playlist

Une autre application de fondu enchaîné courante concerne une application de lecteur de musique. Lorsque le titre change, nous voulons atténuer le titre actuel et atténuer le nouveau pour éviter une transition brutale. Pour ce faire, programmez un fondu enchaîné dans le futur. Nous pourrions utiliser setTimeout pour effectuer cette planification, mais ce n'est pas précis. Avec l'API Web Audio, nous pouvons utiliser l'interface AudioParam pour planifier des valeurs futures pour des paramètres tels que la valeur de gain d'un AudioGainNode.

Ainsi, étant donné une playlist, nous pouvons passer d'un titre à un autre en planifiant une diminution du gain sur le titre en cours de lecture et une augmentation du gain sur le titre suivant, tous deux légèrement avant la fin de la lecture du titre en cours:

function playHelper(bufferNow, bufferLater) {
    var playNow = createSource(bufferNow);
    var source = playNow.source;
    var gainNode = playNow.gainNode;
    var duration = bufferNow.duration;
    var currTime = context.currentTime;
    // Fade the playNow track in.
    gainNode.gain.linearRampToValueAtTime(0, currTime);
    gainNode.gain.linearRampToValueAtTime(1, currTime + ctx.FADE_TIME);
    // Play the playNow track.
    source.noteOn(0);
    // At the end of the track, fade it out.
    gainNode.gain.linearRampToValueAtTime(1, currTime + duration-ctx.FADE_TIME);
    gainNode.gain.linearRampToValueAtTime(0, currTime + duration);
    // Schedule a recursive track change with the tracks swapped.
    var recurse = arguments.callee;
    ctx.timer = setTimeout(function() {
    recurse(bufferLater, bufferNow);
    }, (duration - ctx.FADE_TIME) - 1000);
}

L'API Web Audio fournit un ensemble pratique de méthodes RampToValue pour modifier progressivement la valeur d'un paramètre, comme linearRampToValueAtTime et exponentialRampToValueAtTime.

Bien que la fonction de temporisation de la transition puisse être sélectionnée parmi les fonctions linéaires et exponentielles intégrées (comme ci-dessus), vous pouvez également spécifier votre propre courbe de valeur via un tableau de valeurs à l'aide de la fonction setValueCurveAtTime.

Appliquer un effet de filtre simple à un son

Graphique audio avec un BiquadFilterNode
Graphique audio avec un BiquadFilterNode

L'API Web Audio vous permet de transmettre le son d'un nœud audio à un autre, créant ainsi une chaîne de processeurs potentiellement complexe pour ajouter des effets complexes à vos formes sonores.

Pour ce faire, vous pouvez placer les BiquadFilterNode entre la source et la destination audio. Ce type de nœud audio peut exécuter divers filtres de faible ordre qui peuvent être utilisés pour créer des égaliseurs graphiques et même des effets plus complexes, principalement pour sélectionner les parties du spectre de fréquences d'un son à mettre en valeur et celles à atténuer.

Les types de filtres compatibles sont les suivants:

  • Filtre passe-bas
  • Filtre passe-haut
  • Filtre de bande passante
  • Filtre de l'étagère inférieure
  • Filtre de l'étagère supérieure
  • Filtre de pic
  • Filtre Notch
  • Filtre "Toutes les cartes"

Tous les filtres incluent des paramètres permettant de spécifier un certain niveau de gain, la fréquence d'application du filtre et un facteur de qualité. Le filtre passe-bas conserve la plage de fréquences inférieures, mais ignore les fréquences élevées. Le point d'arrêt est déterminé par la valeur de la fréquence, et le facteur Q, qui n'a pas d'unité, détermine la forme du graphique. Le gain ne concerne que certains filtres, tels que les filtres à pente basse et les filtres à pic, et non ce filtre passe-bas.

Configurez un filtre passe-bas simple pour n'extraire que les basses d'un échantillon audio:

// Create the filter
var filter = context.createBiquadFilter();
// Create the audio graph.
source.connect(filter);
filter.connect(context.destination);
// Create and specify parameters for the low-pass filter.
filter.type = 0; // Low-pass filter. See BiquadFilterNode docs
filter.frequency.value = 440; // Set cutoff to 440 HZ
// Playback the sound.
source.noteOn(0);

En général, les commandes de fréquence doivent être ajustées pour fonctionner à l'échelle logarithmique, car l'audition humaine elle-même fonctionne selon le même principe (A4 correspond à 440 Hz et A5 à 880 Hz). Pour en savoir plus, consultez la section concernant la fonction FilterSample.changeFrequency dans le lien du code source ci-dessus.

Enfin, notez que l'exemple de code vous permet de connecter et de déconnecter le filtre, en modifiant dynamiquement le graphique AudioContext. Nous pouvons déconnecter AudioNodes du graphique en appelant node.disconnect(outputNumber). Par exemple, pour réacheminer le graphique d'un filtre à une connexion directe, procédez comme suit:

// Disconnect the source and filter.
source.disconnect(0);
filter.disconnect(0);
// Connect the source directly.
source.connect(context.destination);

Écouter d'autres titres

Nous avons abordé les bases de l'API, y compris le chargement et la lecture d'échantillons audio. Nous avons créé des graphiques audio avec des nœuds et des filtres de gain, ainsi que des sons programmés et des ajustements de paramètres audio pour activer certains effets sonores courants. À ce stade, vous êtes prêt à créer des applications audio Web géniales.

Si vous cherchez l'inspiration, de nombreux développeurs ont déjà créé des chefs-d'œuvre à l'aide de l'API Web Audio. Voici quelques-unes de mes préférées:

  • AudioJedit, un outil de montage audio dans le navigateur qui utilise des liens permanents SoundCloud.
  • ToneCraft, un séquenceur de sons qui crée des sons en empilant des blocs 3D.
  • Plink, un jeu de création musicale collaboratif utilisant Web Audio et Web Sockets.