Masukkan Worklet Audio

Chrome 64 hadir dengan fitur baru yang sangat dinantikan di Web Audio API - AudioWorklet. Artikel ini memperkenalkan konsep dan penggunaannya bagi pengguna yang ingin membuat pemroses audio kustom dengan kode JavaScript. Lihat demo live di GitHub. Selain itu, artikel berikutnya dari seri ini, Pola Desain Worklet Audio, mungkin merupakan bacaan menarik untuk membangun aplikasi audio tingkat lanjut.

Latar Belakang: ScriptProcessorNode

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

Ada dua masalah dalam desain ini: penanganan peristiwanya asinkron secara 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, sehingga menyebabkan UI "jank" atau audio menjadi "gangguan". Karena cacat desain dasar ini, ScriptProcessorNode tidak digunakan lagi dari spesifikasi dan diganti dengan AudioWorklet.

Konsep

Worklet Audio menyimpan kode JavaScript yang disediakan pengguna, semuanya dalam thread pemrosesan audio, dengan baik sehingga tidak harus melompat ke thread utama untuk memproses audio. Artinya, kode skrip yang disediakan pengguna dapat dijalankan di thread rendering audio (AudioWorkletGlobalScope) bersama dengan AudioNodes bawaan lainnya, yang memastikan nol latensi tambahan dan rendering sinkron.

Diagram cakupan global utama dan cakupan Worklet Audio
Gambar.1

Pendaftaran dan Pembuatan Instance

Penggunaan Worklet Audio terdiri atas dua bagian: AudioWorkletProcessor dan AudioWorkletNode. Ini lebih rumit dibandingkan menggunakan ScriptProcessorNode, tetapi diperlukan untuk memberikan kemampuan tingkat rendah kepada developer untuk pemrosesan audio kustom. AudioWorkletProcessor mewakili prosesor audio sebenarnya yang ditulis dalam kode JavaScript, dan ada di AudioWorkletGlobalScope. AudioWorkletNode adalah kebalikan dari AudioWorkletProcessor dan menangani koneksi ke dan dari AudioNode lain di thread utama. Alat ini ekspos dalam cakupan dan fungsi global utama seperti AudioNode biasa.

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

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

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

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

Metode registerProcessor() dalam AudioWorkletGlobalScope mengambil string untuk nama prosesor yang akan didaftarkan dan definisi class. Setelah menyelesaikan evaluasi kode skrip dalam cakupan global, promise dari AudioWorklet.addModule() akan diselesaikan dengan memberi tahu pengguna bahwa definisi class siap digunakan dalam cakupan global utama.

AudioParam Kustom

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

Diagram prosesor dan node worklet audio
Gambar.2

AudioParams yang ditentukan pengguna dapat dideklarasikan dalam definisi class AudioWorkletProcessor dengan menyiapkan kumpulan AudioParamDescriptors. Mesin WebAudio yang mendasarinya akan mengambil informasi ini setelah pembuatan AudioWorkletNode, lalu akan 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 dalam metode callback process() di AudioWorkletProcessor dan harus diterapkan oleh pengguna dalam definisi class. Mesin WebAudio akan memanggil fungsi ini secara isokronus untuk memasukkan input dan parameter ke feed, 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 akan memanggil metode lagi. Agar prosesor tetap aktif, metode ini harus menampilkan true. Jika tidak, pasangan node/prosesor pada akhirnya akan dibersihkan sebagai sampah memori oleh sistem.

Komunikasi Dua Arah dengan MessagePort

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

Fig.2
Gambar.2

MessagePort dapat diakses melalui atribut .port di node dan pemroses. Metode port.postMessage() node mengirim pesan ke pengendali port.onmessage pemroses yang 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!');
});
/* "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);

Perhatikan juga bahwa MessagePort mendukung Transferable, yang memungkinkan Anda mentransfer penyimpanan data atau modul WASM melalui batas thread. Hal ini membuka berbagai kemungkinan tentang cara penggunaan sistem Worklet Audio.

Panduan: membuat GainNode

Dengan menggabungkan semuanya, berikut adalah contoh lengkap GainNode yang dibuat di atas AudioWorkletNode dan 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);

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

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 penanda eksperimental.