Worklet audio jest teraz domyślnie dostępny

Chrome 64 zawiera długo wyczekiwaną nową funkcję w interfejsie Web Audio API – AudioWorklet. Tutaj poznasz pojęcia i sposoby tworzenia niestandardowego procesora audio za pomocą kodu JavaScript. Zapoznaj się z prezentacjami na żywo. Następny artykuł z tej serii, Audio Worklet Design Pattern, może być interesujący, jeśli chcesz tworzyć zaawansowane aplikacje audio.

Tło: węzeł ScriptProcessorNode

Przetwarzanie dźwięku w interfejsie Web Audio API odbywa się w osobnym wątku od głównego wątku interfejsu użytkownika, dzięki czemu działa płynnie. Aby umożliwić niestandardowe przetwarzanie dźwięku w JavaScript, interfejs Web Audio API zaproponował węzeł ScriptProcessorNode, który używał metod obsługi zdarzeń do wywoływania skryptu użytkownika w głównym wątku interfejsu użytkownika.

W tej architekturze występują 2 problemy: obsługa zdarzeń jest asynchroniczna i wykonanie kodu odbywa się w głównym wątku. Pierwsza z nich powoduje opóźnienie, a druga obciąża wątek główny, który jest często wypełniony różnymi zadaniami związanymi z interfejsem użytkownika i DOM, co powoduje „zacinanie” interfejsu lub „zacinanie” dźwięku. Z powodu tej fundamentalnej wady konstrukcyjnej specyfikacja ScriptProcessorNode została wycofana i zastąpiona przez AudioWorklet.

Pojęcia

Worklet audio przechowuje kod JavaScriptu przesłany przez użytkownika w wątku przetwarzania audio. Oznacza to, że nie musi przełączać się na główny wątek, aby przetworzyć dźwięk. Oznacza to, że kod skryptu dostarczony przez użytkownika jest uruchamiany w wątku renderowania dźwięku (AudioWorkletGlobalScope) wraz z innymi wbudowanymi AudioNodes, co zapewnia brak dodatkowego opóźnienia i synchroniczne renderowanie.

Diagram zakresu głównego i globalnego oraz zakresu elementu audio
Fig.1

Rejestracja i tworzenie instancji

Korzystanie z elementu Audio Worklet składa się z 2 części: AudioWorkletProcessorAudioWorkletNode. Jest to bardziej skomplikowane niż użycie węzła ScriptProcessorNode, ale jest to konieczne, aby zapewnić deweloperom możliwość korzystania z niestandardowych funkcji przetwarzania dźwięku. AudioWorkletProcessor reprezentuje rzeczywisty procesor audio napisany w języku JavaScript i znajduje się w komponencie AudioWorkletGlobalScope. AudioWorkletNode jest odpowiednikiem AudioWorkletProcessor i obsługuje połączenia z innymi AudioNodes w głównym wątku. Jest ona dostępna w głównym zakresie globalnym i działa jak zwykła funkcja AudioNode.

Oto 2 fragmenty kodu, które pokazują rejestrację i tworzenie instancji.

// 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);
});

Aby utworzyć AudioWorkletNode, musisz dodać obiekt AudioContext i nazwę procesora w postaci ciągu znaków. Definicję procesora można załadować i zarejestrować za pomocą wywołania addModule() nowego obiektu Audio Worklet. Interfejsy API workletów, w tym worklet audio, są dostępne tylko w bezpiecznym kontekście, dlatego strona, która ich używa, musi być wyświetlana w protokole HTTPS, choć http://localhost jest uważany za bezpieczny do testowania lokalnego.

Możesz utworzyć podklasę AudioWorkletNode, aby zdefiniować węzeł niestandardowy obsługiwany przez procesor działający na worklecie.

// 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);

Metoda registerProcessor() w klasie AudioWorkletGlobalScope przyjmuje ciąg znaków z nazwą procesora do zarejestrowania oraz definicję klasy. Po zakończeniu oceny kodu skryptu w zakresie globalnym spełnienie obietnicy z AudioWorklet.addModule() powiadomi użytkowników, że definicja klasy jest gotowa do użycia w głównym zakresie globalnym.

Niestandardowe parametry dźwięku

Jedną z przydatnych funkcji AudioNodes jest możliwość zaplanowania automatyzacji parametrów za pomocą AudioParam. Elementy AudioWorkletNodes mogą ich używać do uzyskiwania parametrów, które można kontrolować automatycznie z częstotliwością dźwięku.

Schemat węzła i procesora workletu audio
Fig.2

Parametry dźwięku zdefiniowane przez użytkownika można zadeklarować w definicji klasy AudioWorkletProcessor, konfigurując zestaw AudioParamDescriptor. Podrzędny mechanizm WebAudio pobiera te informacje podczas tworzenia węzła AudioWorkletNode, a następnie tworzy obiekty AudioParam i odpowiednio je łączy z węzłem.

/* 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() metoda

Rzeczywiste przetwarzanie dźwięku odbywa się w metodzie wywołania zwrotnego process() w komponencie AudioWorkletProcessor. Musi być ona implementowana przez użytkownika w ramach definicji klasy. Silnik WebAudio wywołuje tę funkcję w sposób asynchroniczny, aby przekazywać dane wejściowe i parametry oraz pobierać dane wyjściowe.

/* 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;
}

Dodatkowo zwracana wartość metody process() może służyć do kontrolowania czasu trwania AudioWorkletNode, dzięki czemu deweloperzy mogą zarządzać śladem pamięci. Zwracanie wartości false z metody process() powoduje, że przetwarzanie staje się nieaktywne, a silnik WebAudio nie wywołuje już tej metody. Aby procesor pozostał aktywny, metoda musi zwracać wartość true. W przeciwnym razie para węzeł i procesor zostanie ostatecznie zebrana przez system.

Komunikacja dwukierunkowa z MessagePort

Czasami niestandardowa funkcja AudioWorkletNode chce udostępnić elementy sterujące, które nie są mapowane na AudioParam, np. atrybut type oparty na ciągu znaków, który służy do sterowania filtrem niestandardowym. W tym celu urządzenia AudioWorkletNodeAudioWorkletProcessor są wyposażone w MessagePort do komunikacji dwukierunkowej. Za pomocą tego kanału można wymieniać dowolne dane niestandardowe.

Fig.2
Fig.2

Do MessagePort można uzyskać dostęp za pomocą atrybutu .port zarówno w węźle, jak i w procesorze. Metoda port.postMessage() węzła wysyła wiadomość do powiązanego procesora do obsługi port.onmessage i na odwrót.

/* 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);

MessagePort obsługuje przenośność, co umożliwia przenoszenie danych lub modułu WASM przez granicę wątku. Otwiera to niezliczone możliwości wykorzystania systemu Audio Worklet.

Przykład: tworzenie węzła GainNode

Oto pełny przykład węzła GainNode utworzonego na podstawie węzłów AudioWorkletNodeAudioWorkletProcessor.

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

Plik 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);

Ten artykuł zawiera podstawy systemu Audio Worklet. Prezentacje na żywo są dostępne w repozytorium GitHub zespołu Chrome WebAudio.

Przejście z wersji eksperymentalnej na stabilną

Worklet audio jest domyślnie włączony w Chrome 66 lub nowszej. W Chrome 64 i 65 ta funkcja była dostępna za pomocą flagi eksperymentalnej.