نمط تصميم الوظائف الصوتية

وضّحت المقالة السابقة حول Audio Worklet المفاهيم الأساسية وكيفية الاستخدام. منذ إطلاقه في Chrome 66، تلقّينا العديد من الطلبات للحصول على مزيد من الأمثلة حول كيفية استخدامه في التطبيقات الفعلية. توفّر وحدات Audio Worklet الإمكانات الكاملة لتكنولوجيا WebAudio، ولكن قد يكون من الصعب الاستفادة منها لأنّها تتطلّب معرفة ببرمجة المهام المتزامنة المضمّنة في العديد من واجهات برمجة تطبيقات JavaScript. حتى بالنسبة إلى المطوّرين الملمّين بمنصّة WebAudio، قد يكون من الصعب دمج Audio Worklet مع واجهات برمجة تطبيقات أخرى (مثل WebAssembly).

ستمنح هذه المقالة القارئ فهمًا أفضل لكيفية استخدام ميزة Audio Worklet في الإعدادات الواقعية وتقديم نصائح للاستفادة من إمكاناتها إلى أقصى حدّ. احرص أيضًا على الاطّلاع على أمثلة الرموز البرمجية والعروض التوضيحية المباشرة.

ملخّص: أداة Audio Worklet

قبل البدء، لنلخّص سريعًا المصطلحات والحقائق حول نظام "مهام الصوت" الذي تم تقديمه سابقًا في هذه المشاركة.

  • BaseAudioContext: العنصر الأساسي لواجهة برمجة التطبيقات Web Audio API
  • Audio Worklet: هو أداة تحميل ملفات نصوص برمجية خاصة لعملية Audio Worklet. ينتمي إلى BaseAudioContext. يمكن أن يتضمّن BaseAudioContext وحدة Audio Worklet واحدة. يتم تقييم ملف الرمز البرمجي الذي تم تحميله في AudioWorkletGlobalScope ويتم استخدامه لإنشاء مثيلات AudioWorkletProcessor.
  • AudioWorkletGlobalScope : نطاق JavaScript شامل خاص لعملية Audio Worklet يتم تشغيله على سلسلسة مخصّصة لعرض WebAudio. يمكن أن يتضمّن BaseAudioContext واحدًا من AudioWorkletGlobalScope.
  • AudioWorkletNode : عنصر AudioNode مصمّم لإجراء Audio Worklet. يتم إنشاؤه من BaseAudioContext. يمكن أن يتضمّن BaseAudioContext عدّة AudioWorkletNodes على غرار AudioNodes الأصلية.
  • AudioWorkletProcessor : عنصر مقابل لعنصر AudioWorkletNode. تمثل هذه البنية الأساسية لعنصر AudioWorkletNode الذي يعالج البث الصوتي باستخدام الرمز البرمجي الذي يوفّره المستخدم. يتم إنشاء مثيل له في AudioWorkletGlobalScope عند إنشاء AudioWorkletNode. يمكن أن يتضمّن عنصر AudioWorkletNode عنصرًا واحدًا مطابقًا من نوع AudioWorkletProcessor.

أنماط التصميم

استخدام Audio Worklet مع WebAssembly

WebAssembly هو ملف JavaScript برمجي مثالي لتشغيل AudioWorkletProcessor. يقدّم الجمع بين هاتين الميزتين مجموعة متنوعة من المزايا لمعالجة الصوت على الويب، ولكن أهم ميزتين هما: أ) دمج رمز معالجة الصوت الحالي بتنسيق C/C++ في المنظومة المتكاملة WebAudio و(ب) تجنُّب النفقات العامة لتجميع JS JIT وجمع القمامة في رمز معالجة الصوت.

ويكون الخيار الأول مهمًا للمطوّرين الذين لديهم استثمار حالي في الرموز البرمجية والمكتبات لمعالجة الملفات الصوتية، ولكن الخيار الثاني يكون مهمًا لجميع مستخدمي واجهة برمجة التطبيقات تقريبًا. في عالم WebAudio، تكون المدة الزمنية المسموح بها لبث الصوت الثابت صارمة جدًا: لا تتجاوز 3 مللي ثانية بمعدّل بيانات في الملف الصوتي يبلغ 44.1 كيلوهرتز. حتى إذا حدثت مشكلة بسيطة في رمز معالجة الصوت، يمكن أن يؤدي ذلك إلى حدوث مشاكل. على المطوّر تحسين الرمز البرمجي لسرعة المعالجة، ولكن عليه أيضًا تقليل مقدار القمامة التي يتم إنشاؤها من JavaScript. يمكن أن يكون استخدام WebAssembly حلاً يعالج كلاً من المشكلتين في الوقت نفسه: فهو أسرع ولا يُنشئ أيّ بيانات غير مفيدة من الرمز البرمجي.

يوضّح القسم التالي كيفية استخدام WebAssembly مع Audio Worklet، ويمكن العثور على مثال الرمز المصاحب هنا. للحصول على البرنامج التعليمي الأساسي حول كيفية استخدام Emscripten وWebAssembly (خاصةً код الربط Emscripten)، يُرجى الاطّلاع على هذه المقالة.

الإعداد

يبدو كلّ ذلك رائعًا، ولكن نحتاج إلى بعض التنظيم لإعداد الأمور بشكلٍ صحيح. السؤال الأول الذي يجب طرحه بشأن التصميم هو كيفية إنشاء مثيل لوحدة WebAssembly ومكان إنشائه. بعد جلب رمز التجميع في Emscripten، تتوفّر طريقتان لإنشاء وحدة:

  1. أنشئ مثيلًا لوحدة WebAssembly عن طريق تحميل رمز التجميع في AudioWorkletGlobalScope من خلال audioContext.audioWorklet.addModule().
  2. أنشئ مثيلًا لوحدة WebAssembly في النطاق الرئيسي، ثم أنقِل ال الوحدة من خلال خيارات أداة الإنشاء AudioWorkletNode.

يعتمد القرار إلى حد كبير على التصميم والإعدادات المفضّلة لديك، ولكن الفكرة هي أنّه يمكن لوحدة WebAssembly إنشاء مثيل WebAssembly في AudioWorkletGlobalScope، والذي يصبح نواة لمعالجة الصوت ضمن مثيل AudioWorkletProcessor.

نمط إنشاء وحدات WebAssembly (أ): استخدام طلب addModule()‎.
نمط إنشاء مثيل وحدة WebAssembly (أ): باستخدام .addModule() call

لكي يعمل النمط "أ" بشكل صحيح، يحتاج Emscripten إلى خيارَين لمحاولة توليد رمز WebAssembly الصحيح لإعداداتنا:

-s BINARYEN_ASYNC_COMPILATION=0 -s SINGLE_FILE=1 --post-js mycode.js

تضمن هذه الخيارات تجميع وحدة WebAssembly بشكل متزامن في AudioWorkletGlobalScope. وتُضيف أيضًا تعريف فئة AudioWorkletProcessor في mycode.js حتى يمكن تحميله بعد بدء تشغيل الوحدة. السبب الرئيسي لاستخدام عملية الترجمة المتزامنة هو أنّ حلّ الوعد audioWorklet.addModule() لا ينتظر حلّ الوعود في AudioWorkletGlobalScope. لا يُنصح بشكل عام بالتحميل أو التجميع المتزامن في سلسلة المهام الرئيسية لأنّه يعرقل المهام الأخرى في سلسلة المهام نفسها، ولكن يمكننا هنا تجاوز القاعدة لأنّ عملية compiling تتم في AudioWorkletGlobalScope، التي تعمل خارج سلسلة المهام الرئيسية. (اطّلِع على هذا الرابط لمزيد من المعلومات).

نمط إنشاء وحدة WASM (ب): استخدام أسلوب الإنشاء
    في AudioWorkletNode لنقل البيانات بين الخيوط
نمط إنشاء وحدة WASM (ب): استخدام أسلوب الإنشاء لعنصر AudioWorkletNode للنقل عبر الخيط

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

بيانات محتوى الصوت ومساحة تخزين WASM

لا يعمل رمز WebAssembly إلا على الذاكرة المخصّصة ضمن ملف محتوى WASM مخصّص. للاستفادة من ذلك، يجب استنساخ بيانات الصوت مجددًا ذهابًا وإيابًا بين حزمة WASM وصفائف بيانات الصوت. تعالج فئة HeapAudioBuffer في نموذج التعليمات البرمجية هذه العملية بشكل جيد.

فئة HeapAudioBuffer لتسهيل استخدام ذاكرة WASM
فئة HeapAudioBuffer لتسهيل استخدام ذاكرة WASM heap

هناك اقتراح مبكر قيد المناقشة لدمج حزمة WASM مباشرةً في نظام "وحدات العمل الصوتية" . يبدو أنّ التخلص من عملية تكرار البيانات هذه بين ذاكرة JavaScript وملف برمجي مجمع WASM أمر طبيعي، ولكن يجب تحديد التفاصيل المحدّدة.

التعامل مع عدم تطابق حجم ذاكرة التخزين المؤقت

تم تصميم العنصرَين AudioWorkletNode وAudioWorkletProcessor للعمل مثل عنصر AudioNode العادي، إذ يعالج AudioWorkletNode التفاعل مع الرموز البرمجية الأخرى بينما يتولى AudioWorkletProcessor معالجة الصوت الداخلي. بما أنّه يتم معالجة 128 إطارًا في المرة الواحدة باستخدام AudioNode العادي، يجب أن ينفّذ AudioWorkletProcessor المعالجة نفسها ليصبح ميزة أساسية. هذه إحدى مزايا تصميم Audio Worklet الذي يضمن عدم إدخال وقت استجابة إضافي بسبب التخزين المؤقت الداخلي ضمن AudioWorkletProcessor، ولكن قد تنشأ مشكلة إذا كانت وظيفة المعالجة تتطلّب حجم تخزين مؤقت مختلفًا عن 128 إطارًا. إنّ الحلّ الشائع لهذه الحالة هو استخدام مخزن ذاكرة دوار، والذي يُعرف أيضًا باسم مخزن ذاكرة دائري أو مخزن ذاكرة بأولوية الدخول أولاً (FIFO).

في ما يلي رسم بياني لـ AudioWorkletProcessor يستخدم اثنين من المخازن الدائرية للبيانات داخله للتوافق مع دالة WASM التي تأخذ 512 لقطة للداخل والخارج. (تم اختيار الرقم 512 هنا بشكل عشوائي).

استخدام RingBuffer داخل طريقة process()‎ في AudioWorkletProcessor
استخدام RingBuffer داخل طريقة process()‏ في AudioWorkletProcessor

ستكون الخوارزمية للرسم البياني على النحو التالي:

  1. يُرسِل AudioWorkletProcessor 128 إطارًا إلى Input RingBuffer من إدخاله.
  2. لا تنفِّذ الخطوات التالية إلا إذا كان Input RingBuffer يحتوي على 512 لقطة أو أكثر.
    1. سحب 512 لقطة من Input RingBuffer
    2. عالج 512 لقطة باستخدام دالة WASM المحدّدة.
    3. ادفع 512 إطارًا إلى مخطّط التخزين الدائري للإخراج.
  3. يسحب AudioWorkletProcessor 128 إطارًا من Output RingBuffer ل заполненияOutput.

كما هو موضّح في الرسم البياني، يتم دائمًا تجميع لقطات الإدخال في Input RingBuffer، وهي تعالج تدفّق المخزن المؤقت عن طريق استبدال أقدم قالب لقطة في المخزن المؤقت. وهذا إجراء معقول لاتّخاذ تطبيق معالجة ملف صوتي في الوقت الفعلي. وبالمثل، سيسحِب ال نظام دائمًا مجموعة إطارات الإخراج. سيؤدي انخفاض عدد البيانات في المخزن المؤقت (عدم توفّر بيانات كافية) في "مخزّن الحلقة" للإخراج إلى صمت يتسبب في حدوث خلل في البث.

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

ومع ذلك، من المهمّ فهم أنّ هذا التصميم لا يتوافق إلا مع عدم تطابق حجم ملف التخزين المؤقت ولا يمنح المزيد من الوقت لتشغيل رمز البرنامج النصي المُعطى. إذا لم يتمكّن الرمز من إكمال المهمة خلال المدة الزمنية المحدّدة لعملية التقديم (تقريبًا 3 مللي ثانية عند 44.1 كيلوهرتز)، سيؤثّر ذلك في توقيت بدء الدالة التالية للاستدعاء، ما سيؤدي في النهاية إلى حدوث مشاكل.

يمكن أن يكون دمج هذا التصميم مع WebAssembly معقّدًا بسبب إدارة الذاكرة حول حزمة WASM. في وقت كتابة هذه المقالة، يجب استنساخ البيانات التي تدخل إلى ملف "مكبّر WASM" وتخرج منه، ولكن يمكننا استخدام فئة HeapAudioBuffer لتسهيل إدارة الذاكرة قليلاً. فكرة استخدام الذاكرة التي يخصّصها المستخدم للحدّ من تكرار استنساخ البيانات سيتمّ مناقشتها في المستقبل.

يمكن العثور على فئة RingBuffer هنا.

WebAudio Powerhouse: Audio Worklet وSharedArrayBuffer

النمط الأخير للتصميم في هذه المقالة هو تجميع عدة واجهات برمجة تطبيقات حديثة في مكان واحد، وهي Audio Worklet، SharedArrayBuffer، Atomics وWorker. من خلال هذا الإعداد العميق، يمكن تشغيل برامج الصوت الحالية المكتوبة بلغة برمجة C/C++ في متصفّح ويب مع الحفاظ على تجربة سلسة للمستخدم.

نظرة عامة على نمط التصميم الأخير: Audio Worklet وSharedArrayBuffer وWorker
نظرة عامة على نمط التصميم الأخير: Audio Worklet وSharedArrayBuffer و Worker

تتمثل الميزة الأكبر لهذا التصميم في إمكانية استخدام DedicatedWorkerGlobalScope لمعالجة الصوت فقط. في Chrome، يتم تشغيل WorkerGlobalScope في سلسلة مهام ذات أولوية أقل من سلسلة مهام عرض WebAudio، ولكنّه يتمتع بعدة مزايا مقارنةً بAudioWorkletGlobalScope. يكون DedicatedWorkerGlobalScope أقل تقييدًا من حيث مساحة عرض واجهة برمجة التطبيقات المتوفّرة في النطاق. يمكنك أيضًا توقّع الحصول على دعم أفضل من Emscripten لأنّ Worker API متوفّرة منذ بضع سنوات.

يلعب SharedArrayBuffer دورًا مهمًا لكي يعمل هذا التصميم بكفاءة. على الرغم من أنّ كلّ من Worker وAudioWorkletProcessor مزوّدان برسائل برمجية غير متزامنة (MessagePort)، فإنّه ليس مثاليًا لمعالجة الصوت في الوقت الفعلي بسبب تكرار عملية تخصيص ملف برمجي للذاكرة ووقت استجابة الرسائل. لذلك، نخصّص كتلة ذاكرة مسبقًا يمكن الوصول إليها من كلا الخيطَين لنقل البيانات بسرعة في الاتجاهَين.

من وجهة نظر مستخدمي Web Audio API، قد يبدو هذا التصميم غير مثالي لأنّه يستخدم Audio Worklet بصفتها "وحدة معالجة صوت" بسيطة وينفّذ كل شيء في Worker. ولكن بما أنّ تكلفة إعادة كتابة مشاريع C/C++ في JavaScript قد تكون باهظة أو حتى مستحيلة، يمكن أن يكون هذا التصميم هو مسار التنفيذ الأكثر فعالية لمثل هذه المشاريع.

الحالات والعناصر الأساسية المشتركة

عند استخدام ذاكرة مشترَكة للبيانات الصوتية، يجب تنسيق الوصول من كلا الجانبَين بعناية. إنّ مشاركة الحالات التي يمكن الوصول إليها بشكل ذري هي أحد الحلول لهذه المشكلة. يمكننا الاستفادة من Int32Array المدعوم من "الإعلانات على شبكة البحث" لهذا الغرض.

آلية المزامنة: SharedArrayBuffer وAtomics
آلية المزامنة: SharedArrayBuffer وAtomics

آلية المزامنة: SharedArrayBuffer وAtomics

يمثّل كل حقل من مصفوفة States معلومات حيوية عن ملفّات التخزين المؤقت المشترَكة. أهمّها حقل للمزامنة (REQUEST_RENDER). والفكرة هي أنّ Worker ينتظر تعديل هذا الحقل من قِبل AudioWorkletProcessor ويعالج الصوت عند بدء العمل. بالإضافة إلى SharedArrayBuffer (SAB)، تتيح واجهة برمجة التطبيقات Atomics هذه الآلية.

يُرجى العِلم أنّ مزامنة سلاسل المحادثتَين غير دقيقة. سيتم بدء Worker.process() باستخدام AudioWorkletProcessor.process() method، ولكن لا تنتظر AudioWorkletProcessor حتى تنتهي Worker.process(). هذا هو الغرض من هذا الإجراء، إذ يتم تشغيل AudioWorkletProcessor من خلال دالّة callback الخاصة بالصوت، لذا يجب عدم حظره بشكل متزامن. في أسوأ السيناريوهات، قد يواجه مجرى البث الصوتي تكرارًا أو انقطاعًا، ولكن سيتم استعادته في نهاية المطاف عند استقرار أداء المعالجة.

الإعداد والتشغيل

كما هو موضّح في المخطّط البياني أعلاه، يتضمّن هذا التصميم عدة مكوّنات لتنظيمها: DedicatedWorkerGlobalScope (DWGS) وAudioWorkletGlobalScope (AWGS) وSharedArrayBuffer والخيط الرئيسي. توضِّح الخطوات التالية ما يجب أن يحدث في مرحلة الإعداد.

الإعداد
  1. [Main] يتمّ استدعاء مُنشئ AudioWorkletNode.
    1. أنشئ عاملاً.
    2. سيتم إنشاء AudioWorkletProcessor المرتبط.
  2. [DWGS] ينشئ Worker عنصرَي SharedArrayBuffer. (أحدهما للحالات المشتركة والآخر لبيانات الصوت)
  3. [DWGS] يُرسِل Worker إشارات SharedArrayBuffer إلى AudioWorkletNode.
  4. [الرئيسي] يُرسِل AudioWorkletNode إشارات SharedArrayBuffer إلى AudioWorkletProcessor.
  5. [AWGS] يُعلم AudioWorkletProcessor AudioWorkletNode باكتمال الإعداد.

بعد اكتمال عملية الإعداد، يبدأ AudioWorkletProcessor.process() بالاستدعاء. في ما يلي ما يجب أن يحدث في كل تكرار من حلقة المعالجة.

حلقة العرض
المعالجة المتعدّدة الوحدات باستخدام SharedArrayBuffers
العرض المتعدّد المهام باستخدام SharedArrayBuffers
  1. [AWGS] يتم استدعاء AudioWorkletProcessor.process(inputs, outputs) لكل وحدة عرض.
    1. سيتمّ دفع inputs إلى Input SAB.
    2. سيتم ملء outputs من خلال استخدام بيانات الصوت في Output SAB.
    3. تعديل States SAB باستخدام فهارس التخزين المؤقت الجديدة وفقًا لذلك
    4. إذا اقترب Output SAB من الحدّ الأدنى لمعدل التدفق، يُستخدَم Wake Worker لمعالجة المزيد من بيانات الصوت.
  2. [DWGS] ينتظر Worker (في وضع السكون) إشارة الاستيقاظ من AudioWorkletProcessor.process(). عند الاستيقاظ:
    1. تُستخدَم هذه السمة لجلب فهارس المخزن المؤقت من States SAB.
    2. شغِّل دالة المعالجة باستخدام البيانات من مدخل SAB لملء مستند SAB الناتج.
    3. تعديل حالات SAB باستخدام فهارس المخزن المؤقت وفقًا لذلك
    4. ينتقل إلى وضع السكون وينتظر الإشارة التالية.

يمكن العثور على مثال على الرمز هنا، ولكن يُرجى العلم أنّه يجب تفعيل العلامة التجريبية SharedArrayBuffer لكي يعمل هذا العرض الترويجي. تم كتابة الرمز البرمجي باستخدام رمز JS خالص لتسهيل الأمر، ولكن يمكن استبداله برمز WebAssembly عند الحاجة. يجب التعامل مع هذه الحالة بعناية إضافية من خلال لف إدارة الذاكرة باستخدام فئة HeapAudioBuffer.

الخاتمة

الهدف النهائي من Audio Worklet هو جعل واجهة برمجة التطبيقات Web Audio API "قابلة للتوسيع" حقًا. لقد تطلّب تصميمها جهودًا لعدة سنوات لكي يصبح من الممكن تنفيذ بقية Web Audio API باستخدام Audio Worklet. نتيجةً لذلك، أصبح لدينا الآن تصميم أكثر تعقيدًا، وقد يشكّل ذلك تحديًا غير متوقّع.

لحسن الحظ، فإنّ سبب هذه التعقيدات هو منح المطوّرين المزيد من الصلاحيات. تتيح إمكانية تشغيل WebAssembly على AudioWorkletGlobalScope إمكانيات كبيرة لمعالجة الصوت بأداء عالٍ على الويب. بالنسبة إلى التطبيقات المخصّصة للصوت على نطاق واسع والتي تكون مكتوبة بلغة C أو C++، يمكن أن يكون استخدام Audio Worklet مع SharedArrayBuffers وWorkers خيارًا جذابًا لاستكشافه.

المساهمون

نشكر بشكل خاص "كريس ويلسون" و"جيسون ميلر" و"جوشوا بيل" و"رايموند توي" على مراجعة مسودة هذه المقالة وتقديم ملاحظات مفيدة.