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.
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.
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.
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.