Inserir Worklet de áudio

O Chrome 64 vem com um novo recurso muito esperado na API Web Audio: o AudioWorklet. Neste artigo, apresentamos o conceito e o uso para quem quer criar um processador de áudio personalizado com código JavaScript. Confira as demonstrações ao vivo no GitHub. Além disso, o próximo artigo da série, Audio Worklet Design Pattern, pode ser uma leitura interessante para criar um app de áudio avançado.

Segundo plano: ScriptProcessorNode

O processamento de áudio na API Web Audio é executado em uma linha de execução separada da linha de execução de interface principal, por isso é executado sem problemas. Para ativar o processamento de áudio personalizado em JavaScript, a API Web Audio propôs um ScriptProcessorNode, que usava manipuladores de eventos para invocar o script do usuário na linha de execução de interface principal.

Há dois problemas nesse design: o processamento de eventos é assíncrono por padrão, e a execução do código acontece na linha de execução principal. O primeiro induz a latência, e o segundo pressiona a linha de execução principal, normalmente lotada com várias tarefas relacionadas à interface e ao DOM, causando a "instabilidade" ou o áudio à "falha". Devido a essa falha fundamental de design, o ScriptProcessorNode foi descontinuado na especificação e substituído pelo AudioWorklet.

conceitos

O Audio Worklet mantém o código JavaScript fornecido pelo usuário na linha de execução de processamento de áudio, ou seja, ele não precisa ir para a linha de execução principal para processar o áudio. Isso significa que o código do script fornecido pelo usuário pode ser executado na linha de execução de renderização de áudio (AudioWorkletGlobalScope) com outros AudioNodes integrados, o que garante nenhuma latência extra e a renderização síncrona.

Escopo global principal e diagrama de escopo da Worklet de áudio
Figura 1

Registro e Instanciação

O uso da Worklet de áudio consiste em duas partes: AudioWorkletProcessor e AudioWorkletNode. Isso é mais do que usar o ScriptProcessorNode, mas é necessário para oferecer aos desenvolvedores o recurso de baixo nível para o processamento de áudio personalizado. O AudioWorkletProcessor representa o processador de áudio real escrito em código JavaScript e reside no AudioWorkletGlobalScope. O AudioWorkletNode é a contraparte do AudioWorkletProcessor e cuida da conexão de e para outros AudioNodes na linha de execução principal. Ele está exposto no escopo global principal e funciona como um AudioNode normal.

Confira um par de snippets de código que demonstram o registro e a instanciação.

// 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 criar um AudioWorkletNode, é preciso pelo menos dois itens: um objeto AudioContext e o nome do processador como uma string. Uma definição do processador pode ser carregada e registrada pela chamada addModule() do novo objeto Audio Worklet. As APIs Worklet, incluindo a Audio Worklet, estão disponíveis apenas em um contexto seguro. Por isso, uma página que as usa precisa ser disponibilizada por HTTPS, embora http://localhost seja considerado seguro para testes locais.

Também é importante notar que você pode subclassificar o AudioWorkletNode para definir um nó personalizado apoiado pelo processador em execução na 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);

O método registerProcessor() no AudioWorkletGlobalScope usa uma string para o nome do processador a ser registrado e a definição da classe. Após a conclusão da avaliação do código do script no escopo global, a promessa de AudioWorklet.addModule() será resolvida notificando os usuários de que a definição de classe está pronta para ser usada no escopo global principal.

AudioParam personalizado

Uma das coisas úteis sobre o AudioNodes é a automação de parâmetros programáveis com AudioParams. Os AudioWorkletNodes podem usá-los para receber parâmetros expostos que podem ser controlados automaticamente pela taxa de áudio.

Diagrama do nó do processador e do nó de worklet de áudio
Fig.2

Os AudioParams definidos pelo usuário podem ser declarados em uma definição de classe AudioWorkletProcessor configurando um conjunto de AudioParamDescriptors. O mecanismo WebAudio subjacente vai coletar essas informações na construção de um AudioWorkletNode e criar e vincular objetos AudioParam ao nó.

/* 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()

O processamento real do áudio acontece no método de callback process() no AudioWorkletProcessor e precisa ser implementado pelo usuário na definição da classe. O mecanismo WebAudio vai invocar essa função de maneira isócrona para alimentar entradas e parâmetros e buscar saídas.

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

Além disso, o valor de retorno do método process() pode ser usado para controlar o ciclo de vida do AudioWorkletNode, para que os desenvolvedores possam gerenciar o consumo de memória. Se false retornar do método process(), o processador será marcado como inativo, e o mecanismo WebAudio não invocará mais o método. Para manter o processador ativo, o método precisa retornar true. Caso contrário, o par de nó/processador será coletado pelo sistema como um lixo.

Comunicação bidirecional com o MessagePort

Às vezes, os AudioWorkletNodes personalizados vão querer expor controles que não mapeiam para o AudioParam. Por exemplo, um atributo type baseado em string pode ser usado para controlar um filtro personalizado. Para essa finalidade e além, o AudioWorkletNode e o AudioWorkletProcessor são equipados com um MessagePort para comunicação bidirecional. Qualquer tipo de dado personalizado pode ser trocado por meio deste canal.

Fig.2
Fig.2

O MessagePort pode ser acessado pelo atributo .port no nó e no processador. O método port.postMessage() do nó envia uma mensagem para o gerenciador port.onmessage do processador associado e vice-versa.

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

Observe também que o MessagePort é compatível com Transferable, o que permite transferir o armazenamento de dados ou um módulo WASM acima do limite da linha de execução. Isso abre inúmeras possibilidades de uso do sistema de Worklet de áudio.

Tutorial: como criar um GainNode

Juntando tudo, aqui está um exemplo completo de GainNode criado sobre o AudioWorkletNode e o 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>

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

Vamos abordar os princípios básicos do sistema de Worklet de áudio. As demonstrações ao vivo estão disponíveis no repositório GitHub da equipe do Chrome WebAudio.

Transição de recursos: de experimental para estável

A Worklet de áudio é ativada por padrão no Chrome 66 ou posterior. No Chrome 64 e 65, o recurso estava por trás da sinalização experimental.