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.
Rejestracja i tworzenie instancji
Korzystanie z elementu Audio Worklet składa się z 2 części: AudioWorkletProcessor
i AudioWorkletNode
. 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.
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 AudioWorkletNode
i AudioWorkletProcessor
są wyposażone w MessagePort
do komunikacji dwukierunkowej. Za pomocą tego kanału można wymieniać dowolne dane niestandardowe.
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 AudioWorkletNode
i AudioWorkletProcessor
.
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.