Premiers pas avec l'API Web Audio

Avant l'élément HTML5 <audio>, Flash ou un autre plug-in était nécessaire pour briser le silence sur le 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 sophistiqués et d'applications interactives.

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 des fonctionnalités présentes dans les moteurs audio de jeux modernes, ainsi que certaines tâches de mixage, de traitement et de filtrage que l'on trouve dans les applications de production audio de bureau modernes. Voici une introduction simple à l'utilisation de cette API puissante.

Premiers pas avec AudioContext

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 sonores et connectez-les à la destination sonore fournie par l'instance AudioContext. Cette connexion n'a pas besoin d'être directe et peut passer par un nombre quelconque d'AudioNodes intermédiaires qui agissent comme des modules de traitement du signal audio. Ce routage est décrit plus en détail dans la spécification Web Audio specification.

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

L'extrait 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 webkit préfixe, 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 d'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 récupérer les fichiers audio.

L'API est compatible avec le chargement des données de fichiers audio dans plusieurs formats, tels que WAV, MP3, AAC, OGG et autres. La prise en charge des différents formats audio par les navigateursvarie.

L'extrait suivant montre comment charger un échantillon sonore :

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 les ArrayBuffers, consultez cet article sur XHR2.

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

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

Lecture des sons

Une fois qu'un ou plusieurs AudioBuffers sont chargés, nous sommes prêts à lire les 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'une personne appuie sur une touche ou clique sur un élément avec la souris.

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

Appliquer une abstraction à l'API Web Audio

Bien sûr, 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 de courte à moyenne durée qu'une application ou un jeu audio utiliserait. Voici une méthode utilisant un BufferLoader (qui ne fait pas partie de la norme Web).

Voici 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 : lire des sons avec un rythme

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

Un modèle de batterie rock simple

dans lequel un charleston est joué toutes les huitièmes notes, et la grosse caisse et la caisse claire sont jouées en alternance tous les quarts, en 4/4.

En supposant que nous ayons chargé les tampons kick, snare et hihat, le code à utiliser 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);
}

Modifier le volume d'un son

L'une des opérations les plus élémentaires 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 :

Organigramme simple représentant le flux audio de la source au nœud de gain, puis à la destination.

Cette configuration de connexion peut être obtenue 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 le gainNode.gain.value comme suit :

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

Fondu enchaîné entre deux sons

Supposons maintenant un scénario légèrement plus complexe, dans lequel nous lisons plusieurs sons, mais nous voulons effectuer un fondu enchaîné entre eux. Il s'agit d'un cas courant dans une application de type DJ, où nous avons deux platines et voulons pouvoir passer d'une source sonore à une autre.

Cela peut être fait avec le graphique audio suivant :

Graphique audio avec deux sources connectées à des nœuds de gain distincts, qui sont ensuite routés vers la même destination.

Pour configurer cela, il suffit de créer deux AudioGainNodes et de connecter chaque source via les nœuds, à l'aide d'une fonction comme 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 enchaîné à puissance égale

Une approche de fondu enchaîné linéaire naïve présente une baisse de volume lorsque vous effectuez un panoramique entre les échantillons.

Fondu enchaîné linéaire représenté par deux lignes droites complémentaires mappées en tant que niveaux d'amplitude au fil du temps.
Un fondu enchaîné 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 minimise les baisses de volume entre les régions audio, ce qui permet d'obtenir un fondu enchaîné plus uniforme entre les régions qui peuvent être légèrement différentes en termes de niveau.

Fondu enchaîné à puissance égale représenté par deux lignes courbes mappées en tant que niveaux d'amplitude au fil du temps.
Un fondu enchaîné à puissance égale

Fondu enchaîné de playlist

Une autre application courante de fondu enchaîné est celle d'une application de lecteur de musique. Lorsqu'une chanson change, nous voulons que la piste actuelle s'estompe et que la nouvelle s'affiche, afin d'éviter une transition brutale. Pour ce faire, planifiez un fondu enchaîné à l'avenir. Bien que nous puissions utiliser setTimeout pour effectuer cette planification, elle n'est pas précise. Avec l'API Web Audio, nous pouvons utiliser l'interface AudioParam pour planifier les valeurs futures de paramètres tels que la valeur de gain d'un AudioGainNode.

Ainsi, étant donné une playlist, nous pouvons passer d'une piste à l'autre en planifiant une diminution du gain sur la piste en cours de lecture et une augmentation du gain sur la piste suivante, les deux légèrement avant la fin de la lecture de la piste actuelle :

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 RampToValue méthodes pour modifier progressivement la valeur d'un paramètre, tel que linearRampToValueAtTime et exponentialRampToValueAtTime.

Bien que la fonction de synchronisation 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

Organigramme simple représentant le flux audio de la source vers BiquadFilterNode, puis vers la destination.

L'API Web Audio vous permet de canaliser 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, placez des BiquadFilterNodes entre votre source sonore et votre destination. Ce type de nœud audio peut effectuer divers filtres de bas ordre qui peuvent être utilisés pour créer des égaliseurs graphiques et des effets encore plus complexes, principalement pour sélectionner les parties du spectre de fréquences d'un son à mettre en évidence et celles à atténuer.

Les types de filtres compatibles incluent les suivants :

  • Filtre passe-bas
  • Filtre passe-haut
  • Filtre passe-bande
  • Filtre passe-bas
  • Filtre passe-haut
  • Filtre de crête
  • Filtre coupe-bande
  • Filtre passe-tout

Tous les filtres incluent des paramètres permettant de spécifier une certaine quantité de gain, la fréquence à laquelle appliquer le filtre et un facteur de qualité. Le filtre passe-bas conserve la plage de fréquences inférieure, mais supprime les hautes fréquences. Le point de rupture est déterminé par la valeur de fréquence, et le facteur Q est sans unité et détermine la forme du graphique. Le gain n'affecte que certains filtres, tels que les filtres passe-bas et de crête, et non ce filtre passe-bas.

Configurons un filtre passe-bas simple pour extraire uniquement les bases d'un échantillon sonore :

// 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 sur une échelle logarithmique, car l'audition humaine fonctionne selon le même principe (c'est-à-dire que le la4 est de 440 Hz et le la5 de 880 Hz). Pour en savoir plus, consultez 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 des AudioNodes du graphique en appelant node.disconnect(outputNumber). Par exemple, pour rediriger le graphique d'un filtre vers une connexion directe, nous pouvons procéder comme suit :

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

Pour aller plus loin

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 de gain et des filtres, et planifié des sons et des ajustements de paramètres audio pour activer certains effets sonores courants. Vous êtes maintenant prêt à créer des applications audio Web de qualité.

Si vous cherchez de l'inspiration, de nombreux développeurs ont déjà créé d'excellents projets à l'aide de l'API Web Audio. Voici quelques-uns de mes préférés :

  • AudioJedit, un outil de découpage sonore dans le navigateur qui utilise des liens permanents SoundCloud.
  • ToneCraft, un séquenceur sonore dans lequel les sons sont créés en empilant des blocs 3D.
  • Plink, un jeu de création musicale collaborative utilisant Web Audio et Web Sockets.