Worklet de audio ahora está disponible de forma predeterminada

Chrome 64 incluye una nueva función muy esperada en la API de Web Audio: AudioWorklet. Aquí aprenderás conceptos y usos para crear un procesador de audio personalizado con código JavaScript. Mira las demostraciones en vivo. El siguiente artículo de la serie, Patrón de diseño de worklet de audio, podría ser una lectura interesante para compilar una app de audio avanzada.

En segundo plano: ScriptProcessorNode

El procesamiento de audio en la API de Web Audio se ejecuta en un subproceso independiente del subproceso principal de la IU, 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 usaba controladores de eventos para invocar la secuencia de comandos del usuario en el subproceso principal de la IU.

Hay dos problemas en este diseño: el manejo de eventos es asíncrono por diseño, y la ejecución de código se produce en el subproceso principal. El primero induce la latencia y el segundo presiona el subproceso principal, que suele estar repleto de varias tareas relacionadas con la IU y el DOM, lo que hace que la IU se "trague" o que el audio tenga "errores". Debido a esta falla de diseño fundamental, ScriptProcessorNode dejó de estar disponible en la especificación y se reemplazó por AudioWorklet.

Conceptos

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

Diagrama del alcance global principal y del alcance de la Audio Worklet
Figura 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 para el procesamiento de audio personalizado. AudioWorkletProcessor representa el procesador de audio real escrito en código JavaScript y reside en AudioWorkletGlobalScope. AudioWorkletNode es la contraparte de AudioWorkletProcessor y se encarga de la conexión hacia y desde otros AudioNodes en el subproceso principal. Se expone en el alcance global principal y funciona como un AudioNode normal.

A continuación, se incluyen dos fragmentos de código que demuestran el registro y la instanciación.

// 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 AudioWorkletNode, debes agregar un objeto AudioContext y el nombre del procesador como una cadena. La llamada addModule() del nuevo objeto Audio Worklet puede cargar y registrar una definición de procesador. Las APIs de Worklet, incluida la Audio Worklet, solo están disponibles en un contexto seguro, por lo que una página que las usa debe entregarse a través de HTTPS, aunque http://localhost se considera seguro para pruebas locales.

Puedes crear una subclase de AudioWorkletNode para definir un nodo personalizado respaldado por el procesador que se ejecuta en la 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);

El método registerProcessor() en AudioWorkletGlobalScope toma una cadena para el nombre del procesador que se registrará y la definición de la clase. Después de completar la evaluación del código de la 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ámetros de audio personalizados

Una de las funciones útiles de AudioNodes es la automatización de parámetros programables con AudioParam. AudioWorkletNodes puede usarlos para obtener parámetros expuestos que se pueden controlar automáticamente a la velocidad de audio.

Diagrama del nodo y el procesador de worklet de audio
Figura 2

Los parámetros de audio definidos por el usuario se pueden declarar en una definición de clase AudioWorkletProcessor si se configura un conjunto de AudioParamDescriptor. El motor subyacente de WebAudio recoge esta información durante la construcción de un AudioWorkletNode y, luego, crea y vincula 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. Un usuario debe implementarlo en la definición de la clase. El motor de WebAudio invoca esta función de forma isócrona para alimentar 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 el ciclo de vida de AudioWorkletNode, de modo que los desarrolladores puedan administrar el espacio en memoria. Devolver false desde el método process() marca el procesador como inactivo y el motor WebAudio ya no invoca el método. Para mantener el procesador activo, el método debe mostrar true. De lo contrario, el sistema recogerá el par de nodos y procesadores en algún momento.

Comunicación bidireccional con MessagePort

A veces, un AudioWorkletNode personalizado desea exponer controles que no se asignan a AudioParam, como un atributo type basado en cadenas que se usa 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. Se puede intercambiar cualquier tipo de datos personalizados a través de este canal.

Fig.2
Figura 2

Se puede acceder a MessagePort con el atributo .port en el nodo y 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!');
});
/* "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 admite elementos transferibles, lo que te permite transferir el almacenamiento de datos o un módulo WASM a través del límite del subproceso. Esto abre innumerables posibilidades sobre cómo se puede usar el sistema de Audio Worklet.

Explicación: Cómo compilar un GainNode

Este es un ejemplo completo de GainNode compilado sobre AudioWorkletNode y AudioWorkletProcessor.

El archivo 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>

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

Aquí se abordan los aspectos básicos del sistema de Audio Worklet. Las demostraciones en vivo están disponibles en el repositorio de GitHub del equipo de WebAudio de Chrome.

Transición de funciones: De experimental a estable

La función Audio Worklet está habilitada de forma predeterminada en Chrome 66 o versiones posteriores. En Chrome 64 y 65, la función estaba detrás de la marca experimental.