Le Worklet audio est désormais disponible par défaut

Hongchan Choi

Chrome 64 est livré avec une nouvelle fonctionnalité très attendue dans l'API Web Audio : AudioWorklet. Vous allez découvrir les concepts et l'utilisation permettant de créer un processeur audio personnalisé avec du code JavaScript. Regardez les démonstrations en direct. L'article suivant de la série, Modèle de conception de worklets audio, peut être intéressant à lire pour créer une application audio avancée.

Contexte: ScriptProcessorNode

Le traitement audio dans l'API Web Audio s'exécute dans un thread distinct du thread d'interface utilisateur principal. Il s'exécute donc de manière fluide. Pour activer le traitement audio personnalisé en JavaScript, l'API Web Audio proposait un ScriptProcessorNode qui utilisait des gestionnaires d'événements pour appeler le script utilisateur dans le thread d'UI principal.

Cette conception présente deux problèmes: la gestion des événements est asynchrone par conception, et l'exécution du code se produit sur le thread principal. Le premier induit la latence, et le second exerce une pression sur le thread principal, qui est généralement encombré de diverses tâches liées à l'UI et au DOM, ce qui entraîne des à-coups dans l'UI ou des glitchs audio. En raison de ce défaut de conception fondamental, ScriptProcessorNode est obsolète dans la spécification et remplacé par AudioWorklet.

Concepts

Le worklet audio conserve le code JavaScript fourni par l'utilisateur dans le thread de traitement audio. Cela signifie qu'il n'a pas besoin de passer au thread principal pour traiter l'audio. Cela signifie que le code de script fourni par l'utilisateur peut s'exécuter sur le thread de rendu audio (AudioWorkletGlobalScope) avec d'autres AudioNodes intégrés, ce qui garantit une latence et un rendu synchrones nuls.

Schéma du champ d'application global principal et du champ d'application du worklet audio
Fig.1

Enregistrement et instanciation

L'utilisation d'un Audio Worklet se compose de deux parties: AudioWorkletProcessor et AudioWorkletNode. Cette approche est plus complexe que l'utilisation de ScriptProcessorNode, mais elle est nécessaire pour fournir aux développeurs la fonctionnalité de bas niveau pour le traitement audio personnalisé. AudioWorkletProcessor représente le processeur audio réel écrit en code JavaScript et se trouve dans AudioWorkletGlobalScope. AudioWorkletNode est le pendant de AudioWorkletProcessor et gère la connexion à partir et à destination d'autres AudioNodes dans le thread principal. Il est exposé dans le champ d'application global principal et fonctionne comme un AudioNode standard.

Voici deux extraits de code qui illustrent l'enregistrement et l'instanciation.

// The code in the main global scope.
class MyWorkletNode extends AudioWorkletNode {
  constructor(context) {
    super(context, 'my-worklet-processor');
  }
}

let context = new AudioContext();

context.audioWorklet.addModule('processors.js').then(() => {
  let node = new MyWorkletNode(context);
});

Pour créer un AudioWorkletNode, vous devez ajouter un objet AudioContext et le nom du processeur sous forme de chaîne. Une définition de processeur peut être chargée et enregistrée par l'appel addModule() du nouvel objet Audio Worklet. Les API Worklet, y compris le Worklet audio, ne sont disponibles que dans un contexte sécurisé. Par conséquent, une page qui les utilise doit être diffusée via HTTPS, bien que http://localhost soit considéré comme sécurisé pour les tests locaux.

Vous pouvez sous-classer AudioWorkletNode pour définir un nœud personnalisé pris en charge par le processeur exécuté sur le worklet.

// This is the "processors.js" file, evaluated in AudioWorkletGlobalScope
// upon audioWorklet.addModule() call in the main global scope.
class MyWorkletProcessor extends AudioWorkletProcessor {
  constructor() {
    super();
  }

  process(inputs, outputs, parameters) {
    // audio processing code here.
  }
}

registerProcessor('my-worklet-processor', MyWorkletProcessor);

La méthode registerProcessor() de AudioWorkletGlobalScope utilise une chaîne pour le nom du processeur à enregistrer et la définition de la classe. Une fois l'évaluation du code de script terminée dans le champ d'application global, la promesse de AudioWorklet.addModule() sera résolue, ce qui indiquera aux utilisateurs que la définition de la classe est prête à être utilisée dans le champ d'application global principal.

Paramètres audio personnalisés

L'un des avantages des AudioNodes est l'automatisation des paramètres planifiable avec AudioParam. Les AudioWorkletNodes peuvent les utiliser pour obtenir des paramètres exposés qui peuvent être contrôlés automatiquement à la fréquence audio.

Schéma du nœud et du processeur de worklet audio
Fig.2

Les paramètres audio définis par l'utilisateur peuvent être déclarés dans une définition de classe AudioWorkletProcessor en configurant un ensemble de AudioParamDescriptor. Le moteur WebAudio sous-jacent récupère ces informations lors de la création d'un AudioWorkletNode, puis crée et lie des objets AudioParam au nœud en conséquence.

/* A separate script file, like "my-worklet-processor.js" */
class MyWorkletProcessor extends AudioWorkletProcessor {

  // Static getter to define AudioParam objects in this custom processor.
  static get parameterDescriptors() {
    return [{
      name: 'myParam',
      defaultValue: 0.707
    }];
  }

  constructor() { super(); }

  process(inputs, outputs, parameters) {
    // |myParamValues| is a Float32Array of either 1 or 128 audio samples
    // calculated by WebAudio engine from regular AudioParam operations.
    // (automation methods, setter) Without any AudioParam change, this array
    // would be a single value of 0.707.
    const myParamValues = parameters.myParam;

    if (myParamValues.length === 1) {
      // |myParam| has been a constant value for the current render quantum,
      // which can be accessed by |myParamValues[0]|.
    } else {
      // |myParam| has been changed and |myParamValues| has 128 values.
    }
  }
}

Méthode AudioWorkletProcessor.process()

Le traitement audio se produit dans la méthode de rappel process() dans AudioWorkletProcessor. Il doit être implémenté par un utilisateur dans la définition de la classe. Le moteur WebAudio appelle cette fonction de manière isochrone pour alimenter les entrées et les paramètres, et récupérer les sorties.

/* AudioWorkletProcessor.process() method */
process(inputs, outputs, parameters) {
  // The processor may have multiple inputs and outputs. Get the first input and
  // output.
  const input = inputs[0];
  const output = outputs[0];

  // Each input or output may have multiple channels. Get the first channel.
  const inputChannel0 = input[0];
  const outputChannel0 = output[0];

  // Get the parameter value array.
  const myParamValues = parameters.myParam;

  // if |myParam| has been a constant value during this render quantum, the
  // length of the array would be 1.
  if (myParamValues.length === 1) {
    // Simple gain (multiplication) processing over a render quantum
    // (128 samples). This processor only supports the mono channel.
    for (let i = 0; i < inputChannel0.length; ++i) {
      outputChannel0[i] = inputChannel0[i] * myParamValues[0];
    }
  } else {
    for (let i = 0; i < inputChannel0.length; ++i) {
      outputChannel0[i] = inputChannel0[i] * myParamValues[i];
    }
  }

  // To keep this processor alive.
  return true;
}

De plus, la valeur renvoyée par la méthode process() peut être utilisée pour contrôler la durée de vie de AudioWorkletNode afin que les développeurs puissent gérer l'espace mémoire. Le fait de renvoyer false à partir de la méthode process() marque le processeur comme inactif, et le moteur WebAudio n'appelle plus la méthode. Pour maintenir le processeur actif, la méthode doit renvoyer true. Sinon, la paire de nœuds et de processeurs est finalement collectée par le système.

Communication bidirectionnelle avec MessagePort

Parfois, un AudioWorkletNode personnalisé souhaite exposer des commandes qui ne sont pas mappées sur AudioParam, comme un attribut type basé sur une chaîne utilisé pour contrôler un filtre personnalisé. À cette fin et au-delà, AudioWorkletNode et AudioWorkletProcessor sont équipés d'un MessagePort pour la communication bidirectionnelle. Tout type de données personnalisées peut être échangé via ce canal.

Fig.2
Fig.2

Vous pouvez accéder à MessagePort avec l'attribut .port à la fois sur le nœud et sur le processeur. La méthode port.postMessage() du nœud envoie un message au gestionnaire port.onmessage du processeur associé et inversement.

/* The code in the main global scope. */
context.audioWorklet.addModule('processors.js').then(() => {
  let node = new AudioWorkletNode(context, 'port-processor');
  node.port.onmessage = (event) => {
    // Handling data from the processor.
    console.log(event.data);
  };

  node.port.postMessage('Hello!');
});
/* "processors.js" file. */
class PortProcessor extends AudioWorkletProcessor {
  constructor() {
    super();
    this.port.onmessage = (event) => {
      // Handling data from the node.
      console.log(event.data);
    };

    this.port.postMessage('Hi!');
  }

  process(inputs, outputs, parameters) {
    // Do nothing, producing silent output.
    return true;
  }
}

registerProcessor('port-processor', PortProcessor);

MessagePort est compatible avec les transferts, ce qui vous permet de transférer le stockage de données ou un module WASM au-delà de la limite de thread. Cela ouvre d'innombrables possibilités d'utilisation du système Audio Worklet.

Tutoriel: Créer un GainNode

Voici un exemple complet de GainNode basé sur AudioWorkletNode et AudioWorkletProcessor.

Le fichier index.html:

<!doctype html>
<html>
<script>
  const context = new AudioContext();

  // Loads module script with AudioWorklet.
  context.audioWorklet.addModule('gain-processor.js').then(() => {
    let oscillator = new OscillatorNode(context);

    // After the resolution of module loading, an AudioWorkletNode can be
    // constructed.
    let gainWorkletNode = new AudioWorkletNode(context, 'gain-processor');

    // AudioWorkletNode can be interoperable with other native AudioNodes.
    oscillator.connect(gainWorkletNode).connect(context.destination);
    oscillator.start();
  });
</script>
</html>

Le fichier gain-processor.js:

class GainProcessor extends AudioWorkletProcessor {

  // Custom AudioParams can be defined with this static getter.
  static get parameterDescriptors() {
    return [{ name: 'gain', defaultValue: 1 }];
  }

  constructor() {
    // The super constructor call is required.
    super();
  }

  process(inputs, outputs, parameters) {
    const input = inputs[0];
    const output = outputs[0];
    const gain = parameters.gain;
    for (let channel = 0; channel < input.length; ++channel) {
      const inputChannel = input[channel];
      const outputChannel = output[channel];
      if (gain.length === 1) {
        for (let i = 0; i < inputChannel.length; ++i)
          outputChannel[i] = inputChannel[i] * gain[0];
      } else {
        for (let i = 0; i < inputChannel.length; ++i)
          outputChannel[i] = inputChannel[i] * gain[i];
      }
    }

    return true;
  }
}

registerProcessor('gain-processor', GainProcessor);

Cela couvre les principes fondamentaux du système Audio Worklet. Des démonstrations en direct sont disponibles dans le dépôt GitHub de l'équipe WebAudio de Chrome.

Transition de la fonctionnalité: version expérimentale vers version stable

Les worklets audio sont activés par défaut pour Chrome 66 ou version ultérieure. Dans les versions 64 et 65 de Chrome, la fonctionnalité était disponible via le flag expérimental.