Ingresar al Worklet de audio

Chrome 64 incluye una nueva función muy esperada en la API de Web Audio: AudioWorklet. En este artículo, se presentan su concepto y uso para quienes deseen crear un procesador de audio personalizado con código JavaScript. Consulta las demostraciones en vivo en GitHub. También es posible que el siguiente artículo de la serie, Audio Worklet Design Pattern, sea una lectura interesante para compilar una app de audio avanzada.

Segundo plano: ScriptProcessorNode

El procesamiento de audio en la API de Web Audio se ejecuta en un subproceso independiente del de IU principal, por lo que se ejecuta sin problemas. Para habilitar el procesamiento de audio personalizado en JavaScript, la API de Web Audio propuso un ScriptProcessorNode que utilizaba controladores de eventos para invocar la secuencia de comandos del usuario en el subproceso de IU principal.

Este diseño tiene dos problemas: el control de eventos es asíncrono por diseño y la ejecución del código ocurre en el subproceso principal. El primero induce la latencia y el segundo presiona al subproceso principal que, por lo general, está lleno de varias tareas relacionadas con la IU y el DOM, lo que provoca que la IU se “bloquee” o el audio se “falle”. Debido a esta falla de diseño fundamental, ScriptProcessorNode dejó de estar disponible en la especificación y se reemplazó por AudioWorklet.

Conceptos

Worklet de audio mantiene bien el código JavaScript proporcionado por el usuario dentro del subproceso de procesamiento de audio, es decir, no tiene que pasar al subproceso principal para procesar el audio. Esto significa que el código de secuencia de comandos proporcionado por el usuario se ejecuta en el subproceso de renderización de audio (AudioWorkletGlobalScope) junto con otros AudioNodes integrados, lo que garantiza cero latencia adicional y renderización síncrona.

Alcance global principal y diagrama del alcance del Worklet de audio
Fig.1

Registro y creación de instancias

El uso de Audio Worklet consta de dos partes: AudioWorkletProcessor y AudioWorkletNode. Esto es más complejo que usar ScriptProcessorNode, pero es necesario para brindarles a los desarrolladores la capacidad de bajo nivel de procesamiento de audio personalizado. AudioWorkletProcessor representa el procesador de audio real escrito en código JavaScript y se aloja en AudioWorkletGlobalScope. AudioWorkletNode es la contraparte de AudioWorkletProcessor y se encarga de la conexión desde y hacia otros AudioNodes en el subproceso principal. Se expone en el alcance global principal y funciona como un AudioNode normal.

Estos son un par de fragmentos de código que demuestran el registro y la creación de instancias.

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

Para crear un objeto AudioWorkletNode, se requieren al menos dos elementos: un objeto AudioContext y el nombre del procesador como una cadena. Se puede cargar y registrar una definición de procesador mediante la llamada addModule() del nuevo objeto de Audio Worklet. Las APIs de Worklet, incluido el Worklet de audio, solo están disponibles en un contexto seguro, por lo que una página que las use debe entregarse a través de HTTPS, aunque http://localhost se considera un sitio seguro para las pruebas locales.

También vale la pena señalar que puedes crear una subclase de AudioWorkletNode para definir un nodo personalizado respaldado por el procesador que se ejecuta en el 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);

El método registerProcessor() de AudioWorkletGlobalScope toma una cadena para que se registre el nombre del procesador y la definición de la clase. Después de completar la evaluación del código de secuencia de comandos en el alcance global, se resolverá la promesa de AudioWorklet.addModule() y se notificará a los usuarios que la definición de la clase está lista para usarse en el alcance global principal.

Parámetro de audio personalizado

Uno de los aspectos útiles de AudioNodes es la automatización de parámetros programables con AudioParams. AudioWorkletNodes puede usarlos para obtener parámetros expuestos que se pueden controlar automáticamente a la velocidad de audio.

Diagrama del procesador y el nodo del worklet de audio
Fig.2

Se pueden declarar los AudioParams definidos por el usuario en una definición de clase AudioWorkletProcessor configurando un conjunto de AudioParamDescriptors. El motor de WebAudio subyacente recopilará esta información tras la construcción de un AudioWorkletNode y, luego, creará y vinculará los objetos AudioParam al nodo según corresponda.

/* 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étodo AudioWorkletProcessor.process()

El procesamiento de audio real se realiza en el método de devolución de llamada process() en AudioWorkletProcessor, y el usuario debe implementarlo en la definición de la clase. El motor de WebAudio invocará esta función de forma isocrónica para ingresar entradas y parámetros, y recuperar salidas.

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

Además, el valor que se muestra del método process() se puede usar para controlar la vida útil de AudioWorkletNode a fin de que los desarrolladores puedan administrar el uso de memoria. Si se muestra false desde el método process(), se marcará el procesador inactivo, y el motor de WebAudio ya no invocará el método. Para mantener el procesador activo, el método debe mostrar true. De lo contrario, el sistema recolectará el par nodo-procesador eventualmente.

Comunicación bidireccional con MessagePort

En ocasiones, los objetos AudioWorkletNodes personalizados querrán exponer controles que no se asignan a AudioParam. Por ejemplo, se podría usar un atributo type basado en cadenas para controlar un filtro personalizado. Para este propósito y más allá, AudioWorkletNode y AudioWorkletProcessor están equipados con un MessagePort para la comunicación bidireccional. Cualquier tipo de datos personalizados se puede intercambiar a través de este canal.

Fig.2
Fig.2

Se puede acceder a MessagePort a través del atributo .port tanto en el nodo como en el procesador. El método port.postMessage() del nodo envía un mensaje al controlador port.onmessage del procesador asociado y 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);

Además, ten en cuenta que MessagePort es compatible con Transferable, que te permite transferir almacenamiento de datos o un módulo WASM sobre el límite del subproceso. Esto abre una infinidad de posibilidades sobre cómo se puede utilizar el sistema de Worklet de audio.

Explicación: Cómo compilar un GainNode

A modo de resumen, aquí hay un ejemplo completo de GainNode compilado sobre AudioWorkletNode y 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>

get-procesador.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);

Esto abarca los aspectos básicos del sistema Worklet de audio. Las demostraciones en vivo están disponibles en el repositorio de GitHub del equipo de Chrome WebAudio.

Transición de funciones: de la experiencia experimental a la estable

Worklet de audio está habilitado de forma predeterminada para Chrome 66 o versiones posteriores. En Chrome 64 y 65, la función estaba detrás del parámetro experimental.