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