เข้าสู่ Audio Worklet

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

พื้นหลัง: ScriptProcessorNode

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

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

แนวคิด

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

ขอบเขตรวมหลักและแผนภาพขอบเขตของ Audio Worklet
รูปที่ 1

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

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

นี่คือข้อมูลโค้ดคู่หนึ่งที่แสดงให้เห็นการลงทะเบียนและเหตุผล

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

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

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

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

AudioParam ที่กำหนดเอง

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

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

คุณจะประกาศ AudioParams ที่ผู้ใช้กำหนดได้ในคำจำกัดความของคลาส AudioWorkletProcessor ด้วยการตั้งค่าชุด AudioParamDescriptors เครื่องมือ 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 จะเรียกใช้ฟังก์ชันนี้ในรูปแบบอิสระเพื่อฟีดอินพุตและพารามิเตอร์ และดึงข้อมูลเอาต์พุต

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

บางครั้ง AudioWorkletNodes ที่กำหนดเองจะต้องการเปิดเผยการควบคุมที่ไม่ได้แมปกับ 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!');
});
/* "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);

โปรดทราบว่า MessagePort รองรับ Transferable ซึ่งช่วยให้คุณโอนพื้นที่เก็บข้อมูลหรือโมดูล WASM เหนือขอบเขตเทรดได้ วิธีนี้เป็นการเปิดโอกาสให้คุณนำระบบ Audio Worklet ไปใช้ได้นับไม่ถ้วน

คำแนะนำแบบทีละขั้น: การสร้าง GETNode

เมื่อรวมข้อมูลทุกอย่างเข้าด้วยกันแล้ว นี่คือตัวอย่างที่สมบูรณ์ของ GETNode ซึ่งสร้างขึ้นจาก AudioWorkletNode และ 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>

รับโปรเซสเซอร์.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);

ซึ่งครอบคลุมข้อมูลพื้นฐานของระบบ Worklet เสียง เดโมแบบสดมีให้บริการที่ที่เก็บ GitHub ของทีม Chrome WebAudio

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

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