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