Accéder au Worklet audio

Hongchan Choi

Chrome 64 intègre une nouvelle fonctionnalité très attendue dans l'API Web Audio : AudioWorklet. Cet article présente son concept et son utilisation aux personnes souhaitant créer un processeur audio personnalisé avec du code JavaScript. Consultez les démonstrations en direct sur GitHub. Le prochain article de la série, Audio Worklet Design Pattern (Modèle de conception de Worklet Audio), peut également être intéressant pour la création d'une application audio avancée.

Arrière-plan: ScriptProcessorNode

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

Cette conception présente deux problèmes: la gestion des événements est asynchrone par nature, et l'exécution du code a lieu sur le thread principal. Le premier entraîne la latence, tandis que le second exerce une pression sur le thread principal, généralement encombré par diverses tâches liées à l'UI et au DOM, entraînant des"à-coups " dans l'interface utilisateur ou des problèmes 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 parfaitement le code JavaScript fourni par l'utilisateur dans le thread de traitement audio. En d'autres termes, 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 supplémentaire et un rendu synchrone.

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

Enregistrement et instanciation

L'utilisation du Worklet audio se compose de deux parties: AudioWorkletProcessor et AudioWorkletNode. Cette méthode est plus complexe que celle d'utiliser ScriptProcessorNode, mais elle est nécessaire pour donner aux développeurs la capacité de bas niveau pour un traitement audio personnalisé. AudioWorkletProcessor représente le processeur audio réel écrit en code JavaScript et se trouve dans AudioWorkletGlobalScope. AudioWorkletNode est l'équivalent d'AudioWorkletProcessor et se charge de la connexion vers et depuis 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'inscription 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);
});

La création d'un AudioWorkletNode nécessite au moins deux éléments: un objet AudioContext et le nom du processeur en tant que chaîne. Une définition de processeur peut être chargée et enregistrée par l'appel addModule() du nouvel objet Worklet audio. Les API de Worklet comprenant un 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 en local.

Notez également que vous pouvez sous-classer AudioWorkletNode pour définir un nœud personnalisé reposant sur le processeur exécuté sur le worklet.

// This is "processor.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 en informant les utilisateurs que la définition de la classe est prête à être utilisée dans le champ d'application global principal.

AudioParam personnalisé

L'automatisation des paramètres programmables avec AudioParams est utile. Les AudioWorkletNodes peuvent utiliser ces éléments pour obtenir des paramètres exposés qui peuvent être contrôlés automatiquement au niveau du débit audio.

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

Les AudioParams définis par l'utilisateur peuvent être déclarés dans une définition de classe AudioWorkletProcessor en configurant un ensemble de AudioParamDescriptors. Le moteur WebAudio sous-jacent récupère ces informations lors de la construction d'un AudioWorkletNode, puis crée des objets AudioParam et les associe 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 réel s'effectue dans la méthode de rappel process() de AudioWorkletProcessor et doit être implémenté par l'utilisateur dans la définition de classe. Le moteur WebAudio appellera cette fonction de manière isochrone pour alimenter les entrées et les paramètres, et extraire 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;
}

En outre, la valeur renvoyée par la méthode process() peut être utilisée pour contrôler la durée de vie d'AudioWorkletNode afin que les développeurs puissent gérer l'espace mémoire utilisé. Le renvoi de false à partir de la méthode process() marque le processeur inactif et le moteur WebAudio n'appelle plus la méthode. Pour maintenir le processeur actif, la méthode doit renvoyer true. Sinon, le système finit par récupérer la mémoire de la paire nœud/processeur.

Communication bidirectionnelle avec MessagePort

Les AudioWorkletNodes personnalisés souhaitent parfois exposer des commandes qui ne correspondent pas à AudioParam. Par exemple, un attribut type basé sur une chaîne peut être 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

MessagePort est accessible via l'attribut .port sur le nœud et 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!');
});
/* "processor.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);

Notez également que MessagePort est compatible avec les services transférables, ce qui vous permet de transférer un stockage de données ou un module WASM au-delà de la limite des threads. Cela offre d'innombrables possibilités d'utilisation du système de Worklet audio.

Tutoriel: créer un GainNode

En résumé, voici un exemple complet de GainNode basé sur AudioWorkletNode et AudioWorkletProcessor.

Index.html

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

  // Loads module script via 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>

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);

Il aborde les principes de base du système de Worklet audio. Les démonstrations en direct sont disponibles sur le dépôt GitHub de l'équipe Chrome WebAudio.

Transition des fonctionnalités: version expérimentale vers version stable

Le Worklet audio est activé par défaut dans Chrome 66 ou version ultérieure. Dans Chrome 64 et 65, cette fonctionnalité se trouvait derrière le drapeau expérimental.