Worklet Audio kini tersedia secara default

Chrome 64 dilengkapi dengan fitur baru yang sangat dinantikan di Web Audio API - AudioWorklet. Di sini, Anda akan mempelajari konsep dan penggunaan untuk membuat pemroses audio kustom dengan kode JavaScript. Lihat demo live. Artikel berikutnya dalam seri ini, Pola Desain Worklet Audio, mungkin merupakan bacaan yang menarik untuk membuat aplikasi audio lanjutan.

Latar belakang: ScriptProcessorNode

Pemrosesan audio di Web Audio API berjalan dalam thread terpisah dari thread UI utama, sehingga berjalan dengan lancar. Untuk mengaktifkan pemrosesan audio kustom di JavaScript, Web Audio API mengusulkan ScriptProcessorNode yang menggunakan pengendali peristiwa untuk memanggil skrip pengguna di thread UI utama.

Ada dua masalah dalam desain ini: penanganan peristiwa bersifat asinkron karena desain, dan eksekusi kode terjadi di thread utama. Yang pertama menyebabkan latensi, dan yang kedua menekan thread utama yang biasanya penuh dengan berbagai tugas terkait UI dan DOM yang menyebabkan UI "terganggu" atau audio "bermasalah". Karena kekurangan desain mendasar ini, ScriptProcessorNode tidak digunakan lagi dari spesifikasi dan diganti dengan AudioWorklet.

Konsep

Worklet Audio menyimpan kode JavaScript yang disediakan pengguna dalam thread pemrosesan audio. Artinya, tidak perlu beralih ke thread utama untuk memproses audio. Ini berarti kode skrip yang disediakan pengguna akan dijalankan di thread rendering audio (AudioWorkletGlobalScope) bersama dengan AudioNodes bawaan lainnya, yang memastikan latensi tambahan nol dan rendering sinkron.

Diagram cakupan global utama dan cakupan Worklet Audio
Gambar 1

Pendaftaran dan pembuatan instance

Menggunakan Audio Worklet terdiri dari dua bagian: AudioWorkletProcessor dan AudioWorkletNode. Hal ini lebih rumit daripada menggunakan ScriptProcessorNode, tetapi diperlukan untuk memberi developer kemampuan tingkat rendah untuk pemrosesan audio kustom. AudioWorkletProcessor mewakili pemroses audio sebenarnya yang ditulis dalam kode JavaScript, dan berada di AudioWorkletGlobalScope. AudioWorkletNode adalah pasangan dari AudioWorkletProcessor dan menangani koneksi ke dan dari AudioNodes lain di thread utama. Fungsi ini diekspos dalam cakupan global utama dan berfungsi seperti AudioNode reguler.

Berikut adalah sepasang cuplikan kode yang menunjukkan pendaftaran dan pembuatan instance.

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

Untuk membuat AudioWorkletNode, Anda harus menambahkan objek AudioContext dan nama pemroses sebagai string. Definisi prosesor dapat dimuat dan didaftarkan oleh panggilan addModule() objek Audio Worklet baru. Worklet API termasuk Audio Worklet hanya tersedia dalam konteks aman, sehingga halaman yang menggunakannya harus ditayangkan melalui HTTPS, meskipun http://localhost dianggap aman untuk pengujian lokal.

Anda dapat membuat subclass AudioWorkletNode untuk menentukan node kustom yang didukung oleh prosesor yang berjalan di worklet.

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

Metode registerProcessor() di AudioWorkletGlobalScope menggunakan string untuk nama pemroses yang akan didaftarkan dan definisi class. Setelah evaluasi kode skrip selesai dalam cakupan global, promise dari AudioWorklet.addModule() akan di-resolve untuk memberi tahu pengguna bahwa definisi class siap digunakan dalam cakupan global utama.

Parameter audio kustom

Salah satu hal yang berguna tentang AudioNodes adalah otomatisasi parameter yang dapat dijadwalkan dengan AudioParam. AudioWorkletNodes dapat menggunakannya untuk mendapatkan parameter yang diekspos yang dapat dikontrol pada kecepatan audio secara otomatis.

Diagram node dan prosesor worklet audio
Gambar 2

Parameter audio yang ditentukan pengguna dapat dideklarasikan dalam definisi class AudioWorkletProcessor dengan menyiapkan kumpulan AudioParamDescriptor. Mesin WebAudio yang mendasarinya mengambil informasi ini selama pembuatan AudioWorkletNode, lalu membuat dan menautkan objek AudioParam ke node yang sesuai.

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

Metode AudioWorkletProcessor.process()

Pemrosesan audio yang sebenarnya terjadi di metode callback process() di AudioWorkletProcessor. Ini harus diterapkan oleh pengguna dalam definisi class. Mesin WebAudio memanggil fungsi ini dengan cara isochronous untuk memasukkan input dan parameter serta mengambil output.

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

Selain itu, nilai yang ditampilkan dari metode process() dapat digunakan untuk mengontrol masa aktif AudioWorkletNode sehingga developer dapat mengelola jejak memori. Menampilkan false dari metode process() akan menandai prosesor tidak aktif, dan mesin WebAudio tidak lagi memanggil metode. Agar prosesor tetap aktif, metode harus menampilkan true. Jika tidak, pasangan node dan prosesor akan dihapus oleh sistem pada akhirnya.

Komunikasi dua arah dengan MessagePort

Terkadang, AudioWorkletNode kustom ingin mengekspos kontrol yang tidak dipetakan ke AudioParam, seperti atribut type berbasis string yang digunakan untuk mengontrol filter kustom. Untuk tujuan ini dan lainnya, AudioWorkletNode dan AudioWorkletProcessor dilengkapi dengan MessagePort untuk komunikasi dua arah. Semua jenis data kustom dapat dipertukarkan melalui saluran ini.

Fig.2
Gambar 2

MessagePort dapat diakses dengan atribut .port di node dan prosesor. Metode port.postMessage() node mengirim pesan ke pengendali port.onmessage prosesor terkait dan sebaliknya.

/* 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 mendukung transfer, yang memungkinkan Anda mentransfer penyimpanan data atau modul WASM melalui batas thread. Hal ini membuka kemungkinan yang tak terhitung tentang cara sistem Worklet Audio dapat digunakan.

Panduan: Membuat GainNode

Berikut adalah contoh lengkap GainNode yang dibuat di atas AudioWorkletNode dan AudioWorkletProcessor.

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

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

Bagian ini membahas dasar-dasar sistem Audio Worklet. Demo langsung tersedia di repositori GitHub tim WebAudio Chrome.

Transisi fitur: Eksperimental ke Stabil

Worklet Audio diaktifkan secara default untuk Chrome 66 atau yang lebih baru. Di Chrome 64 dan 65, fitur ini berada di balik flag eksperimental.