В Chrome 64 появилась долгожданная новая функция Web Audio API — AudioWorklet . В этой статье представлена его концепция и использование для тех, кто хочет создать собственный аудиопроцессор с кодом JavaScript. Пожалуйста, взгляните на живые демо-версии на GitHub. Также следующая статья в серии « Шаблон проектирования аудио-ворлетов » может быть интересной для создания расширенного аудиоприложения.
Фон: ScriptProcessorNode
Обработка звука в API веб-аудио выполняется в отдельном потоке от основного потока пользовательского интерфейса, поэтому она работает без сбоев. Чтобы включить пользовательскую обработку звука в JavaScript, API веб-аудио предложил ScriptProcessorNode, который использовал обработчики событий для вызова пользовательского сценария в основном потоке пользовательского интерфейса.
В этом проекте есть две проблемы: обработка событий по своей конструкции асинхронна, а выполнение кода происходит в основном потоке. Первое вызывает задержку, а второе оказывает давление на основной поток, который обычно переполнен различными задачами, связанными с пользовательским интерфейсом и DOM, что приводит к «подвисаниям» либо пользовательского интерфейса, либо «сбоев» звука. Из-за этого фундаментального недостатка конструкции ScriptProcessorNode
исключен из спецификации и заменен AudioWorklet.
Концепции
Audio Worklet прекрасно сохраняет предоставленный пользователем код JavaScript внутри потока обработки звука, то есть ему не нужно переходить в основной поток для обработки звука. Это означает, что предоставленный пользователем код сценария запускается в потоке рендеринга звука (AudioWorkletGlobalScope) вместе с другими встроенными узлами AudioNodes, что обеспечивает нулевую дополнительную задержку и синхронный рендеринг.
Регистрация и создание экземпляров
Использование Audio Worklet состоит из двух частей: AudioWorkletProcessor и AudioWorkletNode. Это более сложный процесс, чем использование ScriptProcessorNode, но он необходим, чтобы предоставить разработчикам возможность низкоуровневой обработки пользовательского звука. AudioWorkletProcessor представляет собой реальный аудиопроцессор, написанный на коде JavaScript и расположенный в AudioWorkletGlobalScope. AudioWorkletNode является аналогом AudioWorkletProcessor и обеспечивает соединение с другими AudioNodes в основном потоке. Он представлен в основной глобальной области видимости и функционирует как обычный AudioNode.
Вот пара фрагментов кода, демонстрирующих регистрацию и создание экземпляров.
// 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);
});
Для создания AudioWorkletNode требуется как минимум две вещи: объект AudioContext и имя процессора в виде строки. Определение процессора можно загрузить и зарегистрировать с помощью вызова addModule()
нового объекта Audio Worklet. API-интерфейсы Worklet, включая Audio Worklet, доступны только в безопасном контексте , поэтому страница, использующая их, должна обслуживаться через HTTPS, хотя http://localhost
считается безопасным для локального тестирования.
Также стоит отметить, что вы можете создать подкласс AudioWorkletNode, чтобы определить собственный узел, поддерживаемый процессором, работающим в ворлете.
// 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);
Метод registerProcessor()
в AudioWorkletGlobalScope принимает строку с именем регистрируемого процессора и определением класса. После завершения оценки кода сценария в глобальной области обещание AudioWorklet.addModule()
будет выполнено, уведомив пользователей о том, что определение класса готово к использованию в основной глобальной области.
Пользовательский аудиопараметр
Одна из полезных особенностей AudioNodes — автоматизация параметров по расписанию с помощью AudioParams. AudioWorkletNodes может использовать их для получения открытых параметров, которыми можно автоматически управлять скоростью звука.
Пользовательские AudioParams могут быть объявлены в определении класса AudioWorkletProcessor путем настройки набора AudioParamDescriptors. Базовый движок WebAudio будет собирать эту информацию при создании AudioWorkletNode, а затем соответствующим образом создавать и связывать объекты AudioParam с узлом.
/* 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.
}
}
}
Метод AudioWorkletProcessor.process()
Фактическая обработка звука происходит в методе обратного вызова process()
в AudioWorkletProcessor, и она должна быть реализована пользователем в определении класса. Движок WebAudio будет вызывать эту функцию изохронно для подачи входных данных и параметров и получения выходных данных .
/* 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;
}
Кроме того, возвращаемое значение process()
можно использовать для управления временем жизни AudioWorkletNode, чтобы разработчики могли управлять объемом памяти. Возврат false
из process()
пометит процессор как неактивный, и движок WebAudio больше не будет вызывать этот метод. Чтобы процессор оставался работоспособным, метод должен возвращать true
. В противном случае пара узел/процессор в конечном итоге будет удалена системой.
Двунаправленная связь с MessagePort
Иногда пользовательским AudioWorkletNodes может потребоваться предоставить элементы управления, которые не сопоставлены с AudioParam. Например, атрибут строкового type
можно использовать для управления пользовательским фильтром. Для этой и других целей AudioWorkletNode и AudioWorkletProcessor оснащены MessagePort для двунаправленной связи. Через этот канал можно обмениваться любыми пользовательскими данными.
Доступ к MessagePort можно получить через атрибут .port
как на узле, так и на процессоре. Метод узла port.postMessage()
отправляет сообщение обработчику port.onmessage
соответствующего процессора и наоборот.
/* 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);
Также обратите внимание, что MessagePort поддерживает Transferable, что позволяет передавать хранилище данных или модуль WASM через границу потока. Это открывает бесчисленные возможности использования системы Audio Worklet.
Пошаговое руководство: создание GainNode
Собрав все вместе, вот полный пример GainNode, построенный на основе AudioWorkletNode и AudioWorkletProcessor.
Индекс.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>
усиление-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);
Здесь рассматриваются основы системы Audio Worklet. Живые демонстрации доступны в репозитории GitHub команды Chrome WebAudio .
Переход функций: с экспериментальной версии на стабильную
Audio Worklet включен по умолчанию в Chrome 66 или более поздней версии. В Chrome 64 и 65 эта функция находилась под экспериментальным флагом.