Wpisz Worklet audio

Chrome 64 ma bardzo oczekiwaną nową funkcję Web Audio API – AudioWorklet. W tym artykule omawiamy tę koncepcję i zastosowanie z myślą o osobach, które chcą utworzyć niestandardowy procesor dźwięku za pomocą kodu JavaScript. Obejrzyj prezentacje na żywo na GitHubie. Ciekawą lekcją na temat tworzenia zaawansowanej aplikacji audio może być też następny artykuł z serii Audio Worklet Design Pattern.

Tło: ScriptProcessorNode

Przetwarzanie dźwięku w interfejsie Web Audio API działa w innym wątku niż główny wątek UI, dzięki czemu działa płynnie. Aby umożliwić niestandardowe przetwarzanie dźwięku w języku JavaScript, interfejs API Web Audio zaproponował obiekt ScriptProcessorNode, który wykorzystywał moduły obsługi zdarzeń do wywoływania skryptu użytkownika w głównym wątku interfejsu użytkownika.

W tym projekcie występują 2 problemy: obsługa zdarzeń jest z założenia asynchroniczna, a wykonanie kodu odbywa się w wątku głównym. Pierwsze powoduje opóźnienie, a drugi uciska główny wątek, który jest zwykle zatłoczony różnymi zadaniami związanymi z interfejsem użytkownika i DOM, co powoduje „zacinanie się” lub „zakłócenia” w dźwięku. Z powodu tej podstawowej wady projektowej parametr ScriptProcessorNode został wycofany ze specyfikacji i zastąpiony elementem AudioWorklet.

Pojęcia

Audio Worklet dobrze utrzymuje dostarczony przez użytkownika kod JavaScript w wątku przetwarzania dźwięku, tzn. nie musi przechodzić do wątku głównego, aby przetworzyć dźwięk. Oznacza to, że dostarczony przez użytkownika kod skryptu jest uruchamiany w wątku renderowania dźwięku (AudioWorkletGlobalScope) wraz z innymi wbudowanymi węzłami AudioNodes, co zapewnia brak dodatkowych opóźnień i renderowanie synchroniczne.

Diagram głównego zakresu globalnego i zakresu Worklet audio
Rys.1

Rejestracja i tworzenie instancji

Korzystanie z Worklet audio składa się z 2 części: AudioWorkletProcessor i AudioWorkletNode. Potrzebne jest to bardziej niż w przypadku języka ScriptProcessorNode, ale trzeba dać programistom niskopoziomową możliwość niestandardowego przetwarzania dźwięku. AudioWorkletProcessor to rzeczywisty procesor audio napisany w kodzie JavaScript, znajdujący się w obiekcie AudioWorkletGlobalScope. AudioWorkletNode jest odpowiednikiem komponentu AudioWorkletProcessor, który dba o połączenie z innymi węzłami audio w wątku głównym oraz od nich. Jest on dostępny w głównym zakresie globalnym i działa jak zwykły węzeł audio.

Oto 2 fragmenty kodu, które ilustrują rejestrację i możliwość zainicjowania procesu.

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

Utworzenie elementu AudioWorkletNode wymaga co najmniej 2 elementów: obiektu AudioContext i nazwy procesora w postaci ciągu znaków. Definicję procesora można wczytać i zarejestrować za pomocą wywołania addModule() nowego obiektu Audio Worklet. Interfejsy Worklet API, w tym Audio Worklet, są dostępne tylko w kontekście bezpiecznym, więc korzystająca z nich strona musi być wyświetlana przy użyciu protokołu HTTPS, chociaż http://localhost jest uważany za bezpieczną do testów lokalnych.

Warto też pamiętać, że można utworzyć podklasę AudioWorkletNode, aby zdefiniować węzeł niestandardowy oparty na procesorze działającym w workletze.

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

Metoda registerProcessor() w AudioWorkletGlobalScope pobiera ciąg znaków określający nazwę procesora, który ma zostać zarejestrowany, oraz definicję klasy. Po zakończeniu oceny kodu skryptu w zakresie globalnym obietnica dostawcy AudioWorklet.addModule() zostanie zrealizowana, a użytkownicy zobaczą powiadomienie, że definicja klasy jest gotowa do użycia w głównym zakresie globalnym.

Niestandardowy parametr audio

Jedną z przydatnych funkcji AudioNodes jest możliwość zaplanowania automatyzacji parametrów za pomocą AudioParams. AudioWorkletNodes może ich używać do uzyskiwania narażonych parametrów, które można automatycznie kontrolować z wysoką szybkością dźwięku.

Schemat węzła Worklet audio i procesora
Rys.2

Zdefiniowane przez użytkownika parametry AudioParam można zadeklarować w definicji klasy AudioWorkletProcessor, konfigurując zbiór parametrów AudioParamDescriptors. Główny mechanizm WebAudio pobierze te informacje podczas tworzenia obiektu AudioWorkletNode, a następnie utworzy obiekty AudioParamde i odpowiednio je połą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 audioWorkletProcessor i musi być zaimplementowane przez użytkownika w definicji klasy. Mechanizm WebAudio wywoła tę funkcję w formie izochronicznej, 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 wartość zwracana metody process() może służyć do kontrolowania czasu życia obiektu AudioWorkletNode, dzięki czemu deweloperzy mogą zarządzać wykorzystaniem pamięci. Zwrócenie false z metody process() spowoduje oznaczenie procesora jako nieaktywnego, a mechanizm WebAudio nie będzie już wywoływać tej metody. Aby procesor mógł działać, metoda musi zwracać wartość true. W przeciwnym razie para węzeł-procesor będzie ostatecznie zbierana przez system.

Dwukierunkowa komunikacja za pomocą MessagePort

Czasami niestandardowe węzły AudioWorkletNodes chcą udostępniać elementy sterujące, które nie są mapowane na AudioParam. Na przykład oparty na ciągach znaków atrybut type może służyć do sterowania filtrem niestandardowym. W tym celu i w innych zastosowaniach interfejsy AudioWorkletNode i AudioWorkletProcessor są wyposażone w MessagePort, który umożliwia komunikację dwukierunkową. Za jego pomocą można wymieniać dowolne dane niestandardowe.

Fig.2
Rys.2

Dostęp do MessagePort można uzyskać za pomocą atrybutu .port zarówno w węźle, jak i procesorze. Metoda port.postMessage() węzła wysyła komunikat do modułu obsługi port.onmessage powiązanego procesora i odwrotnie.

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

Pamiętaj też, że MessagePort obsługuje funkcję Transferable, która umożliwia przenoszenie miejsca na dane lub modułu WASM na granicach wątku. Otwiera to niezliczone możliwości wykorzystania systemu Audio Worklet.

Przewodnik: tworzenie węzła GainNode

Po połączeniu wszystkich elementów mamy kompletny przykład GainNode oparty na elementach AudioWorkletNode i 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>

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

Omawiamy podstawowe zasady systemu Audio Worklet. Wersje demonstracyjne na żywo są dostępne w repozytorium zespołu Chrome WebAudio na GitHubie.

Przełączanie funkcji: z eksperymentu na stabilny

Worklet Audio jest domyślnie włączony w Chrome 66 i nowszych wersjach. W Chrome 64 i 65 funkcja ta znajdowała się za flagą eksperymentalnej.