L'audio Worklet è ora disponibile per impostazione predefinita

Hongchan Choi

Chrome 64 include una nuova funzionalità molto attesa nell'API Web Audio: AudioWorklet. Qui apprenderai i concetti e le modalità di utilizzo per creare un elaboratore audio personalizzato con codice JavaScript. Dai un'occhiata alle demo dal vivo. L'articolo successivo della serie, Audio Worklet Design Pattern, potrebbe essere una lettura interessante per la creazione di un'app audio avanzata.

Informazioni di sfondo: ScriptProcessorNode

L'elaborazione audio nell'API Web Audio viene eseguita in un thread separato dal thread dell'interfaccia utente principale, quindi funziona senza problemi. Per abilitare l'elaborazione audio personalizzata in JavaScript, l'API Web Audio ha proposto un nodo ScriptProcessor che utilizzava gestori eventi per richiamare lo script utente nel thread dell'interfaccia utente principale.

Questo design presenta due problemi: la gestione degli eventi è asincrona per progettazione e l'esecuzione del codice avviene nel thread principale. Il primo induce la latenza, mentre il secondo mette sotto pressione il thread principale, che è spesso affollato da varie attività relative all'interfaccia utente e al DOM, causando "balbuzie" nell'interfaccia utente o "glitch" nell'audio. A causa di questo difetto di progettazione fondamentale, ScriptProcessorNode è deprecato dalla specifica e sostituito da AudioWorklet.

Concetti

Audio Worklet mantiene il codice JavaScript fornito dall'utente all'interno del thread di elaborazione audio. Ciò significa che non deve passare al thread principale per elaborare l'audio. Ciò significa che il codice dello script fornito dall'utente viene eseguito sul thread di rendering audio (AudioWorkletGlobalScope) insieme ad altri AudioNodes integrati, il che garantisce una latenza aggiuntiva pari a zero e un rendering sincrono.

Diagramma dell'ambito globale principale e dell'ambito del worklet audio
Figura 1

Registrazione e istanziazione

L'utilizzo di Audio Worklet è costituito da due parti: AudioWorkletProcessor e AudioWorkletNode. Questa operazione è più complessa rispetto all'utilizzo di ScriptProcessorNode, ma è necessaria per offrire agli sviluppatori la funzionalità di basso livello per l'elaborazione audio personalizzata. AudioWorkletProcessor rappresenta l'effettivo elaboratore audio scritto in codice JavaScript e si trova in AudioWorkletGlobalScope. AudioWorkletNode è la controparte di AudioWorkletProcessor e si occupa della connessione verso e da altri AudioNodes nel thread principale. È esposto nell'ambito globale principale e funziona come un normale AudioNode.

Ecco un paio di snippet di codice che mostrano la registrazione e l'inizializzazione.

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

Per creare un AudioWorkletNode, devi aggiungere un oggetto AudioContext e il nome del processore come stringa. Una definizione del processore può essere caricata e registrata dalla chiamata addModule() del nuovo oggetto Audio Worklet. Le API Worklet, tra cui Audio Worklet, sono disponibili solo in un contesto sicuro, pertanto una pagina che le utilizza deve essere pubblicata tramite HTTPS, anche se http://localhost è considerato sicuro per i test locali.

Puoi creare una sottoclasse di AudioWorkletNode per definire un nodo personalizzato supportato dal processore in esecuzione nel 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);

Il metodo registerProcessor() in AudioWorkletGlobalScope accetta una stringa per il nome del processore da registrare e la definizione della classe. Al termine della valutazione del codice dello script nell'ambito globale, la promessa di AudioWorklet.addModule() verrà risolta informando gli utenti che la definizione della classe è pronta per essere utilizzata nell'ambito globale principale.

Parametri audio personalizzati

Una delle caratteristiche utili di AudioNodes è l'automazione dei parametri programmabili con AudioParam. AudioWorkletNodes può utilizzarli per ottenere parametri esposti che possono essere controllati automaticamente alla frequenza audio.

Diagramma del nodo e del processore del worklet audio
Figura 2

I parametri audio definiti dall'utente possono essere dichiarati in una definizione di classe AudioWorkletProcessor impostando un insieme di AudioParamDescriptor. Il motore WebAudio sottostante recupera queste informazioni durante la costruzione di un AudioWorkletNode, quindi crea e collega gli oggetti AudioParam al nodo di conseguenza.

/* 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.
    }
  }
}

Metodo AudioWorkletProcessor.process()

L'elaborazione audio effettiva avviene nel metodo di callback process() in AudioWorkletProcessor. Deve essere implementato da un utente nella definizione della classe. Il motore WebAudio invoca questa funzione in modo isocrono per fornire input e parametri e recuperare output.

/* 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;
}

Inoltre, il valore restituito del metodo process() può essere utilizzato per controllare il ciclo di vita di AudioWorkletNode in modo che gli sviluppatori possano gestire l'impronta in memoria. Il ritorno di false dal metodo process() contrassegnata il processore come inattivo e il motore WebAudio non richiama più il metodo. Per mantenere attivo il processore, il metodo deve restituire true. In caso contrario, la coppia di nodi e processori viene ritirata dal sistema.

Comunicazione bidirezionale con MessagePort

A volte, un AudioWorkletNode personalizzato vuole esporre controlli che non sono mappati a AudioParam, ad esempio un attributo type basato su stringhe utilizzato per controllare un filtro personalizzato. A questo scopo e non solo,AudioWorkletNode e AudioWorkletProcessor sono dotati di unMessagePort per la comunicazione bidirezionale. Qualsiasi tipo di dati personalizzati può essere scambiato tramite questo canale.

Fig.2
Figura 2

Puoi accedere a MessagePort con l'attributo .port sia sul nodo sia sul processore. Il metodo port.postMessage() del nodo invia un messaggio al gestore port.onmessage del processore associato e viceversa.

/* 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 supporta trasferibili, che consente di trasferire lo spazio di archiviazione dei dati o un modulo WASM oltre il confine del thread. Ciò apre innumerevoli possibilità su come utilizzare il sistema di worklet audio.

Procedura dettagliata: crea un GainNode

Ecco un esempio completo di GainNode basato su AudioWorkletNode e AudioWorkletProcessor.

Il file 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>

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

Questo articolo illustra i concetti fondamentali del sistema Audio Worklet. Le demo in tempo reale sono disponibili nel repository GitHub del team di Chrome WebAudio.

Transizione della funzionalità: da sperimentale a stabile

Il worklet audio è abilitato per impostazione predefinita per Chrome 66 o versioni successive. In Chrome 64 e 65, la funzionalità era nascosta dietro il flag sperimentale.