Inserisci Worklet audio

Hongchan Choi

Chrome 64 include una nuova funzionalità molto attesa nell'API Web Audio: AudioWorklet. Questo articolo ne descrive il concetto e l'utilizzo per gli utenti desiderosi di creare un processore audio personalizzato con codice JavaScript. Dai un'occhiata alle demo dal vivo su GitHub. Potrebbe esserti utile leggere anche l'articolo successivo della serie, Pattern di progettazione del worklet audio, per creare un'app audio avanzata.

Background: ScriptProcessorNode

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

Questa struttura presenta due problemi: la gestione degli eventi è asincrona per definizione e l'esecuzione del codice avviene sul thread principale. La prima provoca la latenza, mentre la seconda fa pressione sul thread principale, comunemente affollato da varie attività relative all'interfaccia utente e al DOM, causando il "jank" dell'interfaccia utente o il "glitch" dell'interfaccia utente. A causa di questo fondamentale difetto di progettazione, ScriptProcessorNode è stato ritirato dalla specifica e sostituito con AudioWorklet.

Concetti

Il worklet audio mantiene perfettamente il codice JavaScript fornito dall'utente all'interno del thread di elaborazione audio, ovvero 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 zero latenza aggiuntiva e rendering sincrono.

Diagramma dell'ambito globale principale e dell'ambito del Worklet audio
Fig.1

Registrazione e creazione di istanze

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

Ecco un paio di snippet di codice che dimostrano la registrazione e l'istituzione.

// 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 sono necessari almeno due elementi: un oggetto AudioContext e il nome del processore come stringa. La chiamata addModule() del nuovo oggetto Worklet audio può caricare e registrare una definizione del processore. Le API Worklet, incluso il Worklet audio, sono disponibili soltanto in un contesto sicuro, pertanto la pagina che le utilizza deve essere pubblicata tramite HTTPS, anche se http://localhost è considerata sicura per i test locali.

Vale anche la pena notare che puoi sottoclasse AudioWorkletNode per definire un nodo personalizzato supportato dal processore in esecuzione sul 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);

Il metodo registerProcessor() in AudioWorkletGlobalScope richiede una stringa per la registrazione del nome del processore e per la definizione della classe. Dopo il completamento 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.

Param Audio personalizzato

Uno degli aspetti utili di AudioNodes è l'automazione pianificabile dei parametri con AudioParams. AudioWorkletNodes può usarle per ottenere parametri esposti che possono essere controllati automaticamente con la frequenza audio.

Diagramma del processore e del nodo di worklet audio
Fig.2

Gli AudioParams definiti dall'utente possono essere dichiarati in una definizione di classe AudioWorkletProcessor configurando un insieme di AudioParamDescriptor. Il motore WebAudio sottostante acquisirà queste informazioni durante la creazione di un AudioWorkletNode e creerà e collegherà di conseguenza gli oggetti AudioParam al nodo.

/* 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'effettiva elaborazione dell'audio avviene nel metodo di callback process() in AudioWorkletProcessor e deve essere implementata dall'utente nella definizione della classe. Il motore WebAudio richiama questa funzione in modo sincronizzato per caricare input e parametri e recuperare gli 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 la durata di AudioWorkletNode in modo che gli sviluppatori possano gestire l'utilizzo della memoria. La restituzione di false dal metodo process() contrassegna 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 nodo/processore sarà garbage collection alla fine raccolta dal sistema.

Comunicazione bidirezionale con MessagePort

A volte AudioWorkletNodes personalizzato potrebbe voler esporre controlli che non sono mappati ad AudioParam. Ad esempio, un attributo type basato su stringa potrebbe essere utilizzato per controllare un filtro personalizzato. A questo scopo e non solo, AudioWorkletNode e AudioWorkletProcessor sono dotati di una MessagePort per la comunicazione bidirezionale. Qualsiasi tipo di dato personalizzato può essere scambiato attraverso questo canale.

Fig.2
Fig.2

È possibile accedere a MessagePort tramite 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!');
});
/* "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);

Inoltre, tieni presente che MessagePort supporta Transferable, che consente di trasferire l'archiviazione dei dati o un modulo WASM oltre il limite del thread. Ciò offre un'infinità di possibilità di utilizzo del sistema di Worklet audio.

Procedura dettagliata: creare un GainNode

Per riassumere, ecco un esempio completo di GainNode basato su AudioWorkletNode e 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>

guadagno-processore.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);

Illustra i concetti fondamentali del sistema Worklet audio. Le demo dal vivo sono disponibili nel repository GitHub di Chrome WebAudio.

Transizione delle funzionalità: da sperimentale a stabile

Il worklet audio è attivo per impostazione predefinita per Chrome 66 o versioni successive. In Chrome 64 e 65, la funzionalità era dietro il flag dell'esperimento.