Premiers pas avec l'API Web Audio

Avant l'ajout de 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 la mise en œuvre de jeux sophistiqués et d'applications interactives.

L'API Web Audio est une API JavaScript de haut niveau qui permet de traiter et de synthétiser des contenus audio dans des applications Web. L'objectif de cette API est d'inclure les fonctionnalités des moteurs audio de jeux modernes ainsi que certaines des tâches de mixage, de traitement et de filtrage disponibles dans les applications de production audio de bureau modernes. Vous trouverez ci-dessous une introduction à 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 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 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 de Web Audio.

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

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 de 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 longueur courte à moyenne. L'approche de base consiste à utiliser XMLHttpRequest pour récupérer 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. Les navigateurs compatibles avec différents formats audio varient.

L'extrait de code suivant montre comment charger un échantillon 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 du texte). Nous avons donc défini 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 decodeAudioData() AudioContext. Cette méthode prend le ArrayBuffer des données du fichier audio stocké dans request.response et le décode de manière asynchrone (sans bloquer le thread d'exécution JavaScript principal).

Lorsque decodeAudioData() est terminé, il appelle une fonction de rappel qui fournit les données audio PCM décodées en tant que AudioBuffer.

Lecture de sons

Graphique audio simple
Graphique audio simple

Une fois qu'un ou plusieurs AudioBuffers sont chargés, nous pouvons diffuser des sons. Supposons que nous venons de charger un AudioBuffer avec le son d'un aboiement 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) permet de programmer facilement une lecture précise du son pour les jeux et d'autres applications urgentes. Toutefois, pour que cette programmation fonctionne correctement, assurez-vous que vos tampons de son sont préchargés.

Extraire l'API Web Audio

Bien sûr, il est préférable de créer un système de chargement plus général, qui n'est pas codé en dur pour charger ce son spécifique. Il existe de nombreuses approches pour gérer les nombreux sons de longueur courte à moyenne qu'une application ou un jeu audio utiliseraient. Voici un exemple d'utilisation de BufferLoader (qui ne fait pas partie de la norme Web).

Voici un exemple d'utilisation de la classe BufferLoader. Créons deux AudioBuffers et, une fois 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 en rythme

L'API Web Audio permet aux développeurs de planifier la lecture avec précision. Pour illustrer cela, créons un morceau de rythme simple. Le modèle de kit de batterie le plus connu est probablement le suivant:

Un motif de tambour de rock simple
Modèle de tambour rock simple

où un chariot est joué toutes les huits et un coup de pied et de caisse enregistreuse en alternance chaque quart, 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);
}

Modifier le volume d'un son

L'une des opérations les plus élémentaires que vous pouvez effectuer sur un son consiste à régler le 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
Audiographe avec un nœud de gain

Cette configuration de connexion peut être effectuée 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 de manière programmatique en manipulant le gainNode.gain.value comme suit:

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

Fondu enchaîné entre deux sons

Supposons maintenant que nous ayons un scénario légèrement plus complexe, dans lequel nous écoutons plusieurs sons, mais que nous voulions créer un fondu entre eux. Il s'agit d'un cas courant dans une application de type DJ, où nous avons deux platines et que nous voulons pouvoir faire un panoramique d'une source audio à une autre.

Pour ce faire, utilisez le graphique audio suivant:

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

Pour ce faire, il nous suffit de créer deux AudioGainNodes et de connecter 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 enchaîné à puissance égale

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

Un fondu enchaîné linéaire
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 s'entrecroisent à une amplitude supérieure. Cela minimise les baisses de volume entre les régions audio, ce qui se traduit par un fondu enchaîné plus homogène entre les régions, dont le niveau peut être légèrement différent.

Un fondu enchaîné à puissance égale.
Fondu enchaîné de puissance égale

Fondu enchaîné de playlists

Une autre application de fondu enchaîné courante est pour une application de lecteur de musique. Lorsqu'un titre change, nous souhaitons faire disparaître le titre actuel en fondu et la nouvelle en fondu, afin d'éviter une transition marrante. Pour ce faire, planifiez un fondu enchaîné vers une date ultérieure. Bien que nous puissions utiliser setTimeout pour effectuer cette planification, cette méthode n'est pas précise. Avec l'API Web Audio, nous pouvons utiliser l'interface AudioParam pour planifier des valeurs futures pour des paramètres tels que la valeur d'acquisition d'un AudioGainNode.

Ainsi, pour une playlist, nous pouvons passer d'un titre à l'autre en planifiant une baisse de plus sur le titre en cours de lecture et une augmentation du gain sur le titre suivant, les 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 durée de transition puisse être choisie 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 diffuser le son d'un nœud audio à un autre, créant ainsi une chaîne de processeurs potentiellement complexes pour ajouter des effets complexes à vos formes sonores.

Pour ce faire, vous pouvez placer les BiquadFilterNodes entre la source audio et la destination. Ce type de nœud audio peut effectuer divers filtres d'ordre faible 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 à accentuer et celles à neutraliser.

Voici les types de filtres pris en charge:

  • Filtre Passe-bas
  • Filtre Passe-haut
  • Filtre Passe-bande
  • Filtre de tablette basse
  • Filtre de tablette haute
  • Filtre d'amplification
  • Filtre à encoche
  • Filtre "Toutes les cartes"

Tous les filtres incluent des paramètres permettant de spécifier un certain 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 élimine les hautes fréquences. Le point d'arrêt est déterminé par la valeur de fréquence. Le facteur Q, sans unité, détermine la forme du graphe. Le gain n'affecte que certains filtres, tels que les filtres de base basse et de crête, et non ce filtre passe bas.

Configurons un simple filtre passe-bas pour n'extraire que 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 contrôles de la fréquence doivent être ajustés pour fonctionner sur une échelle logarithmique, car l'audition humaine fonctionne sur le même principe (A4 correspond à 440 Hz et A5 à 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 de manière dynamique le graphique AudioContext. Nous pouvons déconnecter AudioNodes du graphique en appelant node.disconnect(outputNumber). Par exemple, pour rediriger le graphe par 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);

Écoute prolongée

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.

Si vous cherchez l'inspiration, de nombreux développeurs ont déjà développé du travail avec l'API Web Audio. Certains de mes préférés incluent:

  • AudioJedit, un outil d'insertion de sons intégré au navigateur qui utilise les liens permanents SoundCloud.
  • ToneCraft, un séquenceur de sons dans lequel les sons sont créés en empilant des blocs 3D.
  • Plink, un jeu de création musicale collaboratif qui utilise Web Audio et WebSockets.