worklet של האודיו זמין עכשיו כברירת מחדל

Hongchan Choi

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

רקע: ScriptProcessorNode

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

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

מושגים

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

תרשים של ההיקף הגלובלי הראשי והיקף ה-Audio Worklet
איור 1

רישום ויצירת מכונות

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

פרמטרים מותאמים אישית של אודיו

אחד מהדברים השימושיים ב-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()

עיבוד האודיו בפועל מתבצע בשיטת ה-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

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

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 תומך ב-transferable, שמאפשר להעביר אחסון נתונים או מודול WASM מעבר לגבול של השרשור. כך אפשר להשתמש במערכת של רכיבי ה-Audio Worklet באין-ספור דרכים.

הדרכה: יצירת 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);

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

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

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