Blog & Astuces

La WebAudio API en 2024

Billet

La WebAudio API est une interface de programmation en JavaScript pour les navigateurs web, qui permet de manipuler des données audio et d'y appliquer des effets. J'utilise cette API pour mon projet Simple Voice Changer qui permet d'appliquer des effets audio à un enregistrement audio (voix) ou un fichier audio.

Cette API est basée sur une norme du W3C et est implémentée dans tous les navigateurs web actuels. Il existe cependant de nombreux détails d'implémentation et bugs qui subsistent entre navigateurs même en 2024, ce qui peut compliquer le développement. Voici quelques uns de ces bugs que j'ai rencontrés lors de mes différents développements sur ce projet.

OfflineAudioContext + ScriptProcessorNode = KO sur Firefox

Le contexte OfflineAudioContext permet d'effectuer un traitement audio le plus rapidement possible et d'obtenir en sortie le buffer audio traité, contrairement au contexte AudioContext classique qui effectue le traitement en temps réel.

En cas d'utilisation d'un noeud audio basé sur ScriptProcessorNode sous Firefox avec un contexte OfflineAudioContext, le buffer en sortie est presque voire totalement vide. Sous Chrome, ce problème ne se pose pas. En utilisant un AudioContext classique sous Firefox, le problème ne se pose cependant plus.

L'API ScriptProcessorNode est dépréciée et remplacée par l'API AudioWorklet. Cependant, certaines bibliothèques logicielles basés sur la WebAudio API utilisent encore le ScriptProcessorNode, comme par exemple soundtouchjs, même si une implémentation en AudioWorklet existe (mais elle est assez gourmande en mémoire RAM).

AudioContext + ScriptProcessorNode = son de mauvaise qualité sous Chrome

De la même manière, les ScriptProcessorNode utilisés avec un AudioContext posent quelques problèmes également sous Chrome. Le son se retrouve haché et de mauvaise qualité par exemple.

Il est possible de corriger le problème en ajustant le buffer size utilisé par le ScriptProcessorNode, mais cela ne fonctionne pas toujours.

De la même manière que le problème précédent, la solution consiste à passer à l'API AudioWorklet.

Comportement de l'API AudioWorklet différent entre Chrome, Firefox et Safari

Parmi un autre petit détail qui peut causer des bugs qui semblent aléatoires, un petit détail d'implémentation de l'API AudioWorklet.

Prenons le code suivant assez simple, qui est un AudioWorklet qui enregistre l'audio en entrée dans un buffer avant de l'envoyer par message à une autre partie du code. Ce code est en partie basé sur le code de mon projet Simple Voice Changer :

class RecorderWorklet extends AudioWorkletProcessor {
    private recording = false;

    constructor() {
        super();
        this.port.onmessage = (event) => {
            if (event.data == "stop") {
                this.recording = false;
            } else if(event.data == "record") {
                this.recording = true;
            }
        };
    }

    static get parameterDescriptors() {
        return [
            { name: "numChannels", defaultValue: 2 }
        ];
    }

    process(inputs: Float32Array[][], outputs: Float32Array[][], parameters: Record<string, Float32Array>): boolean {
        if (!this.recording) return true;

        const input = inputs[0];
        const buffer: Float32Array[] = [];

        if (input && input.length > 0) {
            for (let channel = 0; channel < parameters.numChannels[0]; channel++) {
               buffer.push(input[channel]);
            }

            this.port.postMessage({
                command: "record",
                buffer: buffer
            });
        }

        return true;
    }
}

registerProcessor("recorderWorklet", RecorderWorklet);

Ce code fonctionne correctement sous Chrome, mais pose potentiellement problème sous Firefox. L'erreur est dans la méthode process.

En effet, ici, on se base sur le fait qu'on va effectivement recevoir 2 canaux audio dans le buffer d'entrée.

Le problème est que cela n'est pas le cas sous Firefox et Safari : au tout début de l'exécution du code, lorsque le code ne reçoit que du silence (car nous n'avons pas encore de données audio en entrée à ce moment), le buffer ne contiendra qu'un seul canal audio avec des données vides.

Sous Chrome, nous recevons bien deux canaux audio vides (si en sortie, nous devons avoir 2 canaux également, selon la configuration de l'appareil : c'est souvent le cas).

Pour corriger ce problème, il suffit d'ajouter une condition dans la méthode process :

process(inputs: Float32Array[][], outputs: Float32Array[][], parameters: Record<string, Float32Array>): boolean {
   if (!this.recording) return true;

   const input = inputs[0];
   const buffer: Float32Array[] = [];

   if (input && input.length > 0) {
      for (let channel = 0; channel < parameters.numChannels[0]; channel++) {
            if (input[channel]) {
               buffer.push(input[channel]);
            } else {
               buffer.push(input[0]);
            }
      }

      this.port.postMessage({
            command: "record",
            buffer: buffer
      });
   }

   return true;
}

L'ajout de la condition if (input[channel]) vérifie si le canal audio existe bien entrée avant de l'enregistrer dans le tableau. Si ce n'est pas le cas, on copie le premier canal audio (qui est censé toujours exister).

Conclusion

L'API WebAudio est une API très puissante qui permet de traiter un signal audio en lui appliquant des effets. Elle implique cependant quelques défis à cause des différences d'implémentation entre navigateurs web, qui subsitent encore en 2024.

J'enrichirais à l'avenir cet article avec de nouveaux exemples, si je viens à rencontrer d'autres différences et difficultés d'implémentation.

Commentaires