Войти в аудио-ворлет

В Chrome 64 появилась долгожданная новая функция Web Audio API — AudioWorklet . В этой статье представлена ​​его концепция и использование для тех, кто хочет создать собственный аудиопроцессор с кодом JavaScript. Пожалуйста, взгляните на живые демо-версии на GitHub. Также следующая статья в серии « Шаблон проектирования аудио-ворлетов » может быть интересной для создания расширенного аудиоприложения.

Фон: ScriptProcessorNode

Обработка звука в API веб-аудио выполняется в отдельном потоке от основного потока пользовательского интерфейса, поэтому она работает без сбоев. Чтобы включить пользовательскую обработку звука в JavaScript, API веб-аудио предложил ScriptProcessorNode, который использовал обработчики событий для вызова пользовательского сценария в основном потоке пользовательского интерфейса.

В этом проекте есть две проблемы: обработка событий по своей конструкции асинхронна, а выполнение кода происходит в основном потоке. Первое вызывает задержку, а второе оказывает давление на основной поток, который обычно переполнен различными задачами, связанными с пользовательским интерфейсом и DOM, что приводит к «подвисаниям» либо пользовательского интерфейса, либо «сбоев» звука. Из-за этого фундаментального недостатка конструкции ScriptProcessorNode исключен из спецификации и заменен AudioWorklet.

Концепции

Audio Worklet прекрасно сохраняет предоставленный пользователем код JavaScript внутри потока обработки звука, то есть ему не нужно переходить в основной поток для обработки звука. Это означает, что предоставленный пользователем код сценария запускается в потоке рендеринга звука (AudioWorkletGlobalScope) вместе с другими встроенными узлами AudioNodes, что обеспечивает нулевую дополнительную задержку и синхронный рендеринг.

Основная глобальная область видимости и диаграмма области действия Audio Worklet
Рисунок 1

Регистрация и создание экземпляров

Использование 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 может использовать их для получения открытых параметров, которыми можно автоматически управлять скоростью звука.

Узел аудиоворлета и схема процессора
Рис.2

Пользовательские 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 для двунаправленной связи. Через этот канал можно обмениваться любыми пользовательскими данными.

Рис.2
Рис.2

Доступ к 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 эта функция находилась под экспериментальным флагом.