יש להזין worklet של אודיו

Hongchan Choi

Chrome 64 מגיע עם תכונה חדשה שכולם מצפים לה ב-Web Audio API – AudioWorklet. במאמר הזה נסביר את המושג שלו והשימוש בו בשביל אנשים שרוצים ליצור מעבד אודיו בהתאמה אישית באמצעות קוד JavaScript. אתם מוזמנים לצפות בהדגמות חיות ב-GitHub. בנוסף, המאמר הבא בסדרה, Audio Worklet Design Pattern, עשוי להיות מעניין לפיתוח של אפליקציית אודיו מתקדמת.

רקע: ScriptProcessorNode

עיבוד האודיו ב-Web Audio API פועל בשרשור נפרד מה-thread הראשי של ממשק המשתמש, כך שהוא פועל בצורה חלקה. כדי להפעיל עיבוד אודיו מותאם אישית ב-JavaScript, ממשק ה-Web Audio API הציע ScriptProcessorNode שמשתמש במטפלים באירועים כדי להפעיל סקריפט של משתמש ב-thread הראשי של ממשק המשתמש.

יש שתי בעיות בעיצוב הזה: הטיפול באירועים הוא אסינכרוני מלכתחילה, והפעלת הקוד מתבצעת ב-thread הראשי. האפשרות הראשונה גורמת לזמן אחזור, והשני לוחצים על ה-thread הראשי שלרוב עמוס במשימות שונות שקשורות לממשק המשתמש ול-DOM, שגורמות לממשק המשתמש להיות 'jank' או לאודיו 'לתקלה'. בגלל הפגם הבסיסי הזה בעיצוב, השדה ScriptProcessorNode הוצא משימוש במפרט והוחלף ב-AudioWorklet.

מושגים

Audio Worklet שומר בצורה יפה את קוד ה-JavaScript שסופק על ידי המשתמש, והכל בתוך ה-thread של עיבוד האודיו – כלומר, לא צריך לדלג ל-thread הראשי כדי לעבד את האודיו. המשמעות היא שקוד הסקריפט שסופק על ידי המשתמש פועל בשרשור של עיבוד האודיו (AudioWorkletGlobalScope) יחד עם AudioNodes מובנים אחרים, מה שמבטיח אפס זמן אחזור נוסף ועיבוד סינכרוני.

תרשים של היקף גלובלי ראשי והיקף worklet של אודיו
איור 1

רישום וייצור

השימוש ב-Audio Worklet מורכב משני חלקים: AudioWorkletProcessor ו-AudioWorkletNode. התהליך הזה יותר כרוך בשימוש ב-ScriptProcessorNode, אבל הוא נדרש כדי לספק למפתחים יכולת ברמה נמוכה לעיבוד אודיו בהתאמה אישית. AudioWorkletProcessor מייצג את מעבד האודיו בפועל שנכתב בקוד JavaScript, והוא נמצא ב-AudioWorkletGlobalScope. AudioWorkletNode היא המקבילה של AudioWorkletProcessor, ומטפלת בחיבור אל AudioNodes אחר ב-thread הראשי או בחיבור מהם. הוא נחשף בהיקף הגלובלי הראשי, והוא פועל כמו 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 נדרשים לפחות שני דברים: אובייקט AudioContext ושם המעבד כמחרוזת. ניתן לטעון ולרשום הגדרה של מעבד באמצעות הקריאה addModule() של אובייקט Audio Worklet החדש. ממשקי Worklet API, כולל Audio Worklet, זמינים רק בהקשר מאובטח, ולכן דפים שמשתמשים בהם חייבים להיות מוצגים ב-HTTPS, אבל http://localhost נחשב בטוח לבדיקה מקומית.

חשוב גם לציין שאפשר לסווג את AudioWorkletNode למחלקה משנית כדי להגדיר צומת בהתאמה אישית שמגובה על ידי המעבד שפועל על 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);

השיטה registerProcessor() ב-AudioWorkletGlobalScope משתמשת במחרוזת כדי לרשום את שם המעבד (CPU) ואת הגדרת הכיתה. לאחר השלמת ההערכה של קוד הסקריפט בהיקף הגלובלי, ההבטחה מ-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())

עיבוד האודיו בפועל מתרחש בשיטת הקריאה החוזרת (callback) 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. אחרת, זוג הצמתים/המעבד יהיה אשפה שנאספת על ידי המערכת בסופו של דבר.

תקשורת דו-כיוונית עם MessagePort

לפעמים, קובץ AudioWorkletNodes בהתאמה אישית יחשוף פקדים שלא ממופים ל-AudioParam. לדוגמה, אפשר להשתמש במאפיין type שמבוסס על מחרוזות כדי לשלוט במסנן מותאם אישית. למטרה זו ומעבר לכך, AudioWorkletNode ו-AudioWorkletProcessor ניתן להעביר כל סוג של נתונים מותאמים אישית דרך הערוץ הזה.

Fig.2
איור 2

אפשר לגשת ל-MessagePort דרך המאפיין .port גם בצומת וגם במעבד. השיטה port.postMessage() של הצומת שולחת הודעה ל-handler של 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 מעבר לגבולות ה-thread. כך פותחים הרבה אפשרויות לאופן השימוש במערכת Audio Worklet.

הדרכה מפורטת: בניית GainNode

לסיכום, הנה דוגמה מלאה ל-GainNode שמבוססת על 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>

get-CPU.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);

נושא זה מתייחס ליסודות של מערכת Audio worklet. הדגמות בזמן אמת זמינות במאגר GitHub של צוות Chrome WebAudio.

מעבר תכונה: מניסיוני לגרסה יציבה

Audio Worklet מופעל כברירת מחדל ב-Chrome מגרסה 66 ואילך. בגרסאות 64 ו-65 של Chrome, התכונה הופיעה מאחורי הדגל הניסיוני.