O objeto de áudio agora está disponível por padrão

O Chrome 64 vem com um novo recurso muito esperado na API Web Audio: AudioWorklet. Aqui você vai aprender conceitos e uso para criar um processador de áudio personalizado com código JavaScript. Confira as demonstrações ao vivo. O próximo artigo da série, Padrão de design de worklet de áudio, pode ser uma leitura interessante para criar um app de áudio avançado.

Contexto: ScriptProcessorNode

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

Há dois problemas nesse design: o processamento de eventos é assíncrono por design, 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, que geralmente está lotada com várias tarefas relacionadas à interface e ao DOM, fazendo com que a interface apresente "jank" ou o áudio apresente "glitch". Devido a essa falha fundamental de design, ScriptProcessorNode foi descontinuado da especificação e substituído por AudioWorklet.

Conceitos

O Audio Worklet mantém o código JavaScript fornecido pelo usuário dentro da linha de execução de processamento de áudio. Isso significa que ele não precisa pular para a linha de execução principal para processar o áudio. Isso significa que o código do script fornecido pelo usuário é executado na linha de execução de renderização de áudio (AudioWorkletGlobalScope) com outras AudioNodes integradas, o que garante zero latência adicional e renderização síncrona.

Diagrama do escopo global principal e do escopo do Audio Worklet
Fig.1

Registro e instanciação

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

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, adicione um objeto AudioContext e o nome do processador como uma string. Uma definição de processador pode ser carregada e registrada pela chamada addModule() do novo objeto Audio Worklet. As APIs de worklet, incluindo a Audio Worklet, só estão disponíveis em um contexto seguro. Portanto, uma página que as usa precisa ser veiculada por HTTPS, embora http://localhost seja considerado seguro para testes locais.

É possível criar uma subclasse de AudioWorkletNode para definir um nó personalizado com suporte do processador em execução no 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);

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 da classe está pronta para ser usada no escopo global principal.

Parâmetros de áudio personalizados

Uma das vantagens dos AudioNodes é a automação de parâmetros programáveis com AudioParam. Os AudioWorkletNodes podem usar esses parâmetros para receber parâmetros expostos que podem ser controlados automaticamente na taxa de áudio.

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

Os parâmetros de áudio definidos pelo usuário podem ser declarados em uma definição de classe AudioWorkletProcessor configurando um conjunto de AudioParamDescriptor. O motor da WebAudio detecta essas informações durante a construção de um AudioWorkletNode e, em seguida, cria e vincula objetos AudioParam ao nó de acordo.

/* 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 de áudio real acontece no método de callback process() no AudioWorkletProcessor. Ele precisa ser implementado por um usuário na definição da classe. O mecanismo do WebAudio invoca essa função de forma isocrômica 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 a vida útil de AudioWorkletNode, para que os desenvolvedores possam gerenciar a pegada de memória. O retorno de false do método process() marca o processador como inativo, e o mecanismo WebAudio não invoca mais o método. Para manter o processador ativo, o método precisa retornar true. Caso contrário, o par de nó e processador será coletado pelo sistema no futuro.

Comunicação bidirecional com MessagePort

Às vezes, um AudioWorkletNode personalizado quer expor controles que não são mapeados para AudioParam, como um atributo type baseado em string usado para controlar um filtro personalizado. Para esse e outros fins, AudioWorkletNode e AudioWorkletProcessor são equipados com um MessagePort para comunicação bidirecional. Qualquer tipo de dados personalizados pode ser trocado por esse canal.

Fig.2
Fig.2

A MessagePort pode ser acessada com o 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!');
});
/* "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);

O MessagePort oferece suporte a transferência, o que permite transferir o armazenamento de dados ou um módulo WASM pelo limite da linha de execução. Isso abre inúmeras possibilidades de como o sistema Audio Worklet pode ser usado.

Tutorial: criar um GainNode

Confira um exemplo completo de GainNode criado com base em AudioWorkletNode e AudioWorkletProcessor.

O arquivo 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>

O arquivo 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);

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

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

O Audio Worklet é ativado por padrão no Chrome 66 ou mais recente. No Chrome 64 e 65, o recurso estava por trás da flag experimental.