إدخال قائمة تشغيل صوتي

يأتي Chrome 64 مع ميزة جديدة مرتقَبة في Web Audio API، وهي AudioWorklet. تقدم هذه المقالة مفهومها وطريقة استخدامها للأشخاص المهتمّين بإنشاء معالج صوت مخصّص باستخدام رمز JavaScript. يُرجى إلقاء نظرة على العروض التوضيحية المباشرة على GitHub. يمكنك أيضًا قراءة المقالة التالية في هذه السلسلة، نمط تصميم الوظائف الصوتية، لإنشاء تطبيق صوتي متقدّم.

الخلفية: ScriptProcessorNode

إنّ معالجة الصوت في Web Audio API تتم في سلسلة منفصلة عن سلسلة واجهة المستخدم الرئيسية، وبالتالي فهي تعمل بسلاسة. لتفعيل معالجة الصوت المخصّصة في JavaScript، اقترحت Web Audio API استخدام ScriptProcessorNode التي استخدمت معالِجات أحداث لاستدعاء النص البرمجي للمستخدم في سلسلة واجهة المستخدم الرئيسية.

هناك مشكلتان في هذا التصميم: معالجة الحدث غير متزامنة من حيث التصميم، ويتم تنفيذ الرمز في سلسلة التعليمات الرئيسية. تشير الخطوة الأولى إلى وقت الاستجابة، بينما تضغط الخطوة الأخيرة على سلسلة التعليمات الرئيسية التي عادةً ما تزدحم بمختلف المهام ذات الصلة بواجهة المستخدم ونموذج العناصر في المستند (DOM) مما يؤدي إلى حدوث خلل في واجهة المستخدم أو الصوت. وبسبب هذا العيب الأساسي في التصميم، تم إيقاف المواصفات ScriptProcessorNode نهائيًا واستبدالها باستخدام AudioWorklet.

المفاهيم

يحافظ Audio Worklet بشكل جيد على رمز JavaScript الذي يقدّمه المستخدم ضمن سلسلة معالجة الصوت، أي أنّه ليس من الضروري الانتقال إلى سلسلة التعليمات الرئيسية لمعالجة الصوت. ويعني هذا أنّه يتم تشغيل رمز النص البرمجي الذي يقدّمه المستخدم على سلسلة عرض الصوت (AudioWorkletGlobalScope) مع عُقد الصوت المضمّنة الأخرى، ما يضمن عدم حدوث أي وقت استجابة إضافي وعرض متزامن.

مخطّط النطاق العالمي الرئيسي والصوت Worklet
الشكل 1

التسجيل وإنشاء مثيل

يتألف استخدام Audio Worklet من جزأين: AudioWorkletProcessor وAudioWorkletNode. وهذا الأمر أكثر تعقيدًا من استخدام ScriptProcessorNode، ولكنه ضروري لمنح المطوّرين إمكانية منخفضة المستوى لمعالجة الصوت المخصّصة. AudioWorkletProcessor يمثل معالج الصوت الفعلي المكتوب برمز JavaScript، وهو موجود في AudioWorkletGlobalScope. AudioWorkletNode هو نظير لمعالج AudioWorkletProcessor مع مراعاة الاتصال بعُقد الصوت الأخرى ومنها في سلسلة التعليمات الرئيسية. ويتم الكشف عنها في النطاق العالمي الرئيسي والدوال مثل 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، بما في ذلك Audio Worklet، في سياق آمن فقط، وبالتالي يجب عرض الصفحة التي تستخدمها عبر 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() لإعلام المستخدمين بأنّ تعريف الفئة أصبح جاهزًا للاستخدام في النطاق العالمي الرئيسي.

إعدادات صوتية مخصّصة

من الأشياء المفيدة في AudioNodes التشغيل الآلي للمعلمة القابلة للجدولة باستخدام AudioParams. يمكن لـ AudioWorkletNodes استخدام هذه القيم للحصول على المعلمات المعرّضة التي يمكن التحكّم فيها تلقائيًا بمعدّل الصوت.

عقدة الجهاز الصوتي ومخطط المعالجة
الشكل 2

يمكن الإعلان عن مَعلمات AudioParam التي يحدّدها المستخدم في تعريف فئة 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 هذه الدالة بترتيب متساوي اللون لخلاصة المدخلات والمعلمات وجلب المخرجات.

/* 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 بمنفذ 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!');
});
/* "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 تتوافق مع الملفات القابلة للتحويل، والتي تسمح لك بنقل تخزين البيانات أو وحدة WASM عبر حدود سلسلة المحادثات. ويتيح ذلك فرصًا لا حصر لها حول كيفية استخدام نظام 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>

أمر تحصيل-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 التابع لفريق Chrome Web Audio.

نقل الميزات: من ميزة تجريبية إلى ثابتة

تكون ميزة Audio Worklet مفعَّلة تلقائيًا في الإصدار 66 من Chrome أو الإصدارات الأحدث. في إصداري 64 و65 من Chrome، كانت هذه الميزة وراء العلامة التجريبية.