Audio Worklet พร้อมให้ใช้งานตามค่าเริ่มต้นแล้ว

Chrome 64 มาพร้อมฟีเจอร์ใหม่ใน Web Audio API ที่ทุกคนรอคอยอย่าง AudioWorklet ในส่วนนี้ คุณจะได้เรียนรู้แนวคิดและการใช้งานเพื่อสร้างโปรแกรมประมวลผลเสียงที่กําหนดเองด้วยโค้ด JavaScript ดูการสาธิตแบบสด บทความถัดไปในชุดนี้คือรูปแบบการออกแบบเวิร์กเลตเสียง ซึ่งอาจเป็นสิ่งที่น่าสนใจในการอ่านเพื่อสร้างแอปเสียงขั้นสูง

เบื้องหลัง: ScriptProcessorNode

การประมวลผลเสียงใน Web Audio API จะทำงานในเธรดแยกต่างหากจากเธรด UI หลัก เพื่อให้ทำงานได้อย่างราบรื่น Web Audio API เสนอ ScriptProcessorNode ซึ่งใช้ตัวแฮนเดิลเหตุการณ์เพื่อเรียกใช้สคริปต์ของผู้ใช้ในเธรด UI หลัก เพื่อเปิดใช้การประมวลผลเสียงที่กําหนดเองใน JavaScript

การออกแบบนี้มี 2 ปัญหา ได้แก่ การจัดการเหตุการณ์เป็นแบบแอซิงโครนัสโดยการออกแบบ และการเรียกใช้โค้ดเกิดขึ้นในเธรดหลัก รายการแรกจะทำให้เกิดเวลาในการตอบสนอง และรายการที่ 2 จะกดดันชุดข้อความหลักที่มักเต็มไปด้วยงานต่างๆ ที่เกี่ยวข้องกับ UI และ DOM ซึ่งทำให้ UI "กระตุก" หรือเสียง "ขัดข้อง" ข้อบกพร่องพื้นฐานด้านการออกแบบนี้ทำให้ ScriptProcessorNode เลิกใช้งานจากข้อกำหนดและแทนที่ด้วย AudioWorklet

แนวคิด

เวิร์กเลตเสียงจะเก็บโค้ด JavaScript ที่ผู้ใช้ระบุไว้ทั้งหมดไว้ในเธรดการประมวลผลเสียง ซึ่งหมายความว่าไม่ต้องข้ามไปที่แธรดหลักเพื่อประมวลผลเสียง ซึ่งหมายความว่าโค้ดสคริปต์ที่ผู้ใช้ระบุจะทำงานในเธรดการแสดงผลเสียง (AudioWorkletGlobalScope) พร้อมกับ AudioNodes ในตัวอื่นๆ ซึ่งจะช่วยลดเวลาในการตอบสนองและการแสดงผลแบบซิงโครนัส

แผนภาพขอบเขตระดับบนสุดและขอบเขตของเวิร์กเลตเสียง
รูปที่ 1

การลงทะเบียนและการสร้างอินสแตนซ์

การใช้เวิร์กเลตเสียงประกอบด้วย 2 ส่วน ได้แก่ AudioWorkletProcessor และ AudioWorkletNode วิธีนี้ซับซ้อนกว่าการใช้ ScriptProcessorNode แต่จำเป็นต้องใช้เพื่อให้นักพัฒนาซอฟต์แวร์มีความสามารถระดับล่างสำหรับการประมวลผลเสียงที่กำหนดเอง AudioWorkletProcessor แสดงถึงตัวประมวลผลเสียงจริงที่เขียนด้วยโค้ด JavaScript และอยู่ใน AudioWorkletGlobalScope AudioWorkletNode เป็นคู่ของ AudioWorkletProcessor และดูแลการเชื่อมต่อจาก AudioNodes อื่นๆ ในชุดข้อความหลัก โดยจะแสดงในขอบเขตส่วนกลางหลักและทํางานเหมือน AudioNode ปกติ

ต่อไปนี้คือข้อมูลโค้ด 2 รายการที่แสดงการลงทะเบียนและการสร้างอินสแตนซ์

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

หากต้องการสร้าง AudioWorkletNode คุณต้องเพิ่มออบเจ็กต์ AudioContext และชื่อตัวประมวลผลเป็นสตริง คุณสามารถโหลดและลงทะเบียนคําจํากัดความของโปรเซสเซอร์ได้โดยการเรียกใช้ addModule() ของออบเจ็กต์เวิร์กเลตเสียงใหม่ Worklet API รวมถึง Audio Worklet ใช้ได้เฉพาะในบริบทที่ปลอดภัย ดังนั้นหน้าเว็บที่ใช้ Worklet API เหล่านี้ต้องแสดงผ่าน HTTPS แม้ว่า http://localhost จะถือว่าปลอดภัยสําหรับการทดสอบในเครื่องก็ตาม

คุณสามารถกำหนดคลาสย่อยของ AudioWorkletNode เพื่อกำหนดโหนดที่กำหนดเองซึ่งได้รับการสนับสนุนโดยโปรเซสเซอร์ที่ทำงานในเวิร์กเลต

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

เมธอด registerProcessor() ใน AudioWorkletGlobalScope จะรับสตริงสำหรับชื่อของโปรเซสเซอร์ที่จะลงทะเบียนและคำจำกัดความของคลาส หลังจากการประเมินโค้ดสคริปต์ในขอบเขตส่วนกลางเสร็จสมบูรณ์แล้ว ระบบจะแก้ไขการสัญญาจาก AudioWorklet.addModule() โดยแจ้งให้ผู้ใช้ทราบว่าการกําหนดคลาสพร้อมใช้งานในขอบเขตส่วนกลางหลักแล้ว

พารามิเตอร์เสียงที่กำหนดเอง

ประโยชน์อย่างหนึ่งของ AudioNodes คือการทำงานอัตโนมัติด้วยพารามิเตอร์ที่ตั้งเวลาได้โดยใช้ AudioParam AudioWorkletNodes สามารถใช้ข้อมูลเหล่านี้เพื่อรับพารามิเตอร์ที่แสดงซึ่งควบคุมอัตราเสียงได้โดยอัตโนมัติ

โหนดเวิร์กเลตเสียงและแผนภาพโปรเซสเซอร์
รูปที่ 2

คุณประกาศพารามิเตอร์เสียงที่ผู้ใช้กำหนดได้ในAudioWorkletProcessor คําจํากัดความของคลาสโดยการตั้งค่าชุด AudioParamDescriptor เครื่องมือ WebAudio พื้นฐานจะรับข้อมูลนี้ระหว่างการสร้าง AudioWorkletNode จากนั้นจะสร้างและลิงก์ออบเจ็กต์ AudioParam กับโหนดดังกล่าว

/* 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() วิธี

การประมวลผลเสียงจริงจะเกิดขึ้นในเมธอดการเรียกคืน process() ใน AudioWorkletProcessor ผู้ใช้ต้องติดตั้งใช้งานในคลาสการกําหนด เครื่องมือ WebAudio จะเรียกใช้ฟังก์ชันนี้ในลักษณะแบบ Isochronous เพื่อส่งอินพุตและพารามิเตอร์ รวมถึงดึงข้อมูลเอาต์พุต

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

นอกจากนี้ คุณยังใช้ผลลัพธ์ของเมธอด process() เพื่อควบคุมอายุการใช้งานของ AudioWorkletNode เพื่อให้นักพัฒนาแอปจัดการร่องรอยหน่วยความจำได้ การแสดงผล false จากเมธอด process() จะทําให้เครื่องประมวลผลไม่ทำงาน และเครื่องมือ WebAudio จะไม่เรียกใช้เมธอดนั้นอีก วิธีการต้องแสดงผลเป็น true เพื่อให้โปรเซสเซอร์ทำงานต่อไป มิฉะนั้น ระบบจะเก็บขยะคู่โหนดและโปรเซสเซอร์ในท้ายที่สุด

การสื่อสารแบบ 2 ทิศทางด้วย MessagePort

บางครั้ง AudioWorkletNode ที่กําหนดเองต้องการแสดงการควบคุมที่ไม่ได้แมปกับ AudioParam เช่น แอตทริบิวต์ type ที่อิงตามสตริงซึ่งใช้ควบคุมตัวกรองที่กําหนดเอง AudioWorkletNode และ AudioWorkletProcessor มี MessagePort สำหรับการสื่อสารแบบ 2 ทิศทางเพื่อวัตถุประสงค์นี้และวัตถุประสงค์อื่นๆ คุณสามารถแลกเปลี่ยนข้อมูลที่กำหนดเองทุกประเภทผ่านช่องทางนี้ได้

Fig.2
รูปที่ 2

คุณสามารถเข้าถึง MessagePort ได้ด้วยแอตทริบิวต์ .port ทั้งในโหนดและตัวประมวลผล เมธอด port.postMessage() ของโหนดจะส่งข้อความไปยังตัวแฮนเดิล port.onmessage ของโปรแกรมประมวลผลที่เกี่ยวข้อง และในทางกลับกัน

/* 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 รองรับการโอนได้ ซึ่งช่วยให้คุณโอนพื้นที่เก็บข้อมูลหรือโมดูล WASM ข้ามขอบเขตของเธรดได้ ซึ่งเปิดโอกาสในการใช้งานระบบเวิร์กเลตเสียงได้นับไม่ถ้วน

คำแนะนำแบบทีละขั้นตอน: สร้าง GainNode

ต่อไปนี้คือตัวอย่างที่สมบูรณ์ของ GainNode ที่สร้างขึ้นจาก AudioWorkletNode และ AudioWorkletProcessor

ไฟล์ 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>

ไฟล์ 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);

หัวข้อนี้ครอบคลุมพื้นฐานของระบบเวิร์กเลตเสียง คุณสามารถดูการสาธิตเวอร์ชันที่ใช้จริงได้ที่ที่เก็บ GitHub ของทีม Chrome WebAudio

การเปลี่ยนผ่านฟีเจอร์จากเวอร์ชันทดลองเป็นเวอร์ชันเสถียร

โดยค่าเริ่มต้นแล้ว Chrome เวอร์ชัน 66 ขึ้นไปจะเปิดใช้เวิร์กเลตเสียง ใน Chrome เวอร์ชัน 64 และ 65 ฟีเจอร์นี้อยู่หลัง Flag ทดลอง