وضّحت المقالة السابقة حول 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، تتوفّر طريقتان لإنشاء وحدة:
- أنشئ مثيلًا لوحدة WebAssembly عن طريق تحميل رمز التجميع في
AudioWorkletGlobalScope من خلال
audioContext.audioWorklet.addModule()
. - أنشئ مثيلًا لوحدة WebAssembly في النطاق الرئيسي، ثم أنقِل ال الوحدة من خلال خيارات أداة الإنشاء AudioWorkletNode.
يعتمد القرار إلى حد كبير على التصميم والإعدادات المفضّلة لديك، ولكن الفكرة هي أنّه يمكن لوحدة WebAssembly إنشاء مثيل WebAssembly في AudioWorkletGlobalScope، والذي يصبح نواة لمعالجة الصوت ضمن مثيل AudioWorkletProcessor.
لكي يعمل النمط "أ" بشكل صحيح، يحتاج 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، التي تعمل خارج سلسلة المهام
الرئيسية. (اطّلِع على
هذا الرابط
لمزيد من المعلومات).
يمكن أن يكون النمط (ب) مفيدًا إذا كان مطلوبًا تنفيذ مهام صعبة غير متزامنة. ويستخدم سلسلة المحادثات الرئيسية لجلب رمز التجميع من الخادم و compiling the module. بعد ذلك، سيتم نقل وحدة WASM من خلال المنشئ لـ AudioWorkletNode. يكون هذا النمط أكثر منطقية عندما يكون عليك تحميل الوحدة ديناميكيًا بعد أن يبدأ AudioWorkletGlobalScope بعرض مجرى المحتوى الموسّع الصوتي. استنادًا إلى حجم الوحدة، يمكن أن يؤدي تجميعها في منتصف عملية المعالجة إلى حدوث مشاكل في البث.
بيانات محتوى الصوت ومساحة تخزين WASM
لا يعمل رمز WebAssembly إلا على الذاكرة المخصّصة ضمن ملف محتوى WASM مخصّص. للاستفادة من ذلك، يجب استنساخ بيانات الصوت مجددًا ذهابًا وإيابًا بين حزمة WASM وصفائف بيانات الصوت. تعالج فئة HeapAudioBuffer في نموذج التعليمات البرمجية هذه العملية بشكل جيد.
هناك اقتراح مبكر قيد المناقشة لدمج حزمة WASM مباشرةً في نظام "وحدات العمل الصوتية" . يبدو أنّ التخلص من عملية تكرار البيانات هذه بين ذاكرة JavaScript وملف برمجي مجمع WASM أمر طبيعي، ولكن يجب تحديد التفاصيل المحدّدة.
التعامل مع عدم تطابق حجم ذاكرة التخزين المؤقت
تم تصميم العنصرَين AudioWorkletNode وAudioWorkletProcessor للعمل مثل عنصر AudioNode العادي، إذ يعالج AudioWorkletNode التفاعل مع الرموز البرمجية الأخرى بينما يتولى AudioWorkletProcessor معالجة الصوت الداخلي. بما أنّه يتم معالجة 128 إطارًا في المرة الواحدة باستخدام AudioNode العادي، يجب أن ينفّذ AudioWorkletProcessor المعالجة نفسها ليصبح ميزة أساسية. هذه إحدى مزايا تصميم Audio Worklet الذي يضمن عدم إدخال وقت استجابة إضافي بسبب التخزين المؤقت الداخلي ضمن AudioWorkletProcessor، ولكن قد تنشأ مشكلة إذا كانت وظيفة المعالجة تتطلّب حجم تخزين مؤقت مختلفًا عن 128 إطارًا. إنّ الحلّ الشائع لهذه الحالة هو استخدام مخزن ذاكرة دوار، والذي يُعرف أيضًا باسم مخزن ذاكرة دائري أو مخزن ذاكرة بأولوية الدخول أولاً (FIFO).
في ما يلي مخطّط بياني لـ AudioWorkletProcessor يستخدم اثنين من وحدات التخزين الدائري داخل لتلبية وظيفة WASM التي تأخذ 512 لقطة للداخل والخارج. (تم اختيار الرقم 512 هنا بشكل عشوائي).
ستكون الخوارزمية للرسم البياني على النحو التالي:
- يُرسِل AudioWorkletProcessor 128 إطارًا إلى Input RingBuffer من إدخاله.
- لا تنفِّذ الخطوات التالية إلا إذا كان Input RingBuffer يحتوي على 512 لقطة أو أكثر.
- سحب 512 لقطة من Input RingBuffer
- عالج 512 لقطة باستخدام دالة WASM المحدّدة.
- ادفع 512 لقطة إلى مخطّط التخزين الدائري للإخراج.
- يسحب AudioWorkletProcessor 128 إطارًا من Output RingBuffer ل заполненияOutput.
كما هو موضّح في الرسم البياني، يتم دائمًا تجميع لقطات الإدخال في Input RingBuffer، ويعمل هذا العنصر على معالجة فائض المخزن المؤقت عن طريق استبدال أقدم قالب لقطة في المخزن المؤقت. وهذا إجراء معقول اتّخاذه في تطبيق معالجة ملف صوتي في الوقت الفعلي. وبالمثل، سيسحِب ال نظام دائمًا مجموعة إطارات الإخراج. سيؤدي انخفاض عدد البيانات في المخزن المؤقت (عدم توفّر بيانات كافية) في "مخزّن الحلقة" للإخراج إلى صمت يتسبب في حدوث خلل في البث.
يكون هذا النمط مفيدًا عند استبدال ScriptProcessorNode (SPN) بأحد العناصر التالية: AudioWorkletNode. بما أنّ واجهة SPN تتيح للمطوّر اختيار حجم التخزين المؤقت بين 256 و16384 إطارًا، قد يكون من الصعب استبدال SPN بواجهة AudioWorkletNode ، ويشكّل استخدام التخزين المؤقت الدائري حلاً بديلاً جيدًا. ويعدّ مسجل المحتوى السمعي مثالاً رائعًا يمكن إنشاؤه استنادًا إلى هذا التصميم.
ومع ذلك، من المهمّ فهم أنّ هذا التصميم لا يتوافق إلا مع عدم تطابق حجم ملف التخزين المؤقت ولا يمنح المزيد من الوقت لتشغيل رمز البرنامج النصي المُعطى. إذا لم يتمكّن الرمز من إكمال المهمة خلال المدة الزمنية المحدّدة لعملية التقديم (تقريبًا 3 مللي ثانية عند 44.1 كيلوهرتز)، سيؤثّر ذلك في توقيت بدء الدالة التالية للاستدعاء، ما سيؤدي في النهاية إلى حدوث مشاكل.
يمكن أن يكون دمج هذا التصميم مع WebAssembly معقّدًا بسبب إدارة الذاكرة حول حزمة WASM. في وقت كتابة هذه المقالة، يجب استنساخ البيانات التي تدخل إلى ملف "مكبّر WASM" وتخرج منه، ولكن يمكننا استخدام فئة HeapAudioBuffer لتسهيل إدارة الذاكرة قليلاً. فكرة استخدام الذاكرة التي يخصّصها المستخدم للحدّ من تكرار استنساخ البيانات سيتمّ مناقشتها في المستقبل.
يمكن العثور على فئة RingBuffer هنا.
أدوات WebAudio القوية: Audio Worklet وSharedArrayBuffer
النمط الأخير للتصميم في هذه المقالة هو تجميع عدة واجهات برمجة تطبيقات حديثة في مكان واحد، وهي Audio Worklet وSharedArrayBuffer وAtomics وWorker. من خلال هذا الإعداد العميق، يمكن تشغيل برامج الصوت الحالية المكتوبة بلغة برمجة C/C++ في متصفّح ويب مع الحفاظ على تجربة سلسة للمستخدم.
تتمثل الميزة الأكبر لهذا التصميم في إمكانية استخدام 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
يمثّل كل حقل من مصفوفة States معلومات حيوية عن ملفّات التخزين المؤقت المشترَكة. أهمّها حقل للمزامنة
(REQUEST_RENDER
). والفكرة هي أنّ Worker ينتظر تعديل هذا الحقل
من قِبل AudioWorkletProcessor ويعالج الصوت عند بدء العمل. بالإضافة إلى
SharedArrayBuffer (SAB)، تتيح واجهة برمجة التطبيقات Atomics هذه الآلية.
يُرجى العِلم أنّ مزامنة سلاسل المحادثتَين غير دقيقة. سيتم بدء
Worker.process()
باستخدام طريقة AudioWorkletProcessor.process()
، ولكن لا تنتظر AudioWorkletProcessor حتى تنتهي
Worker.process()
. هذا هو الغرض من تصميم AudioWorkletProcessor، إذ يتم تشغيله من خلال دالّة callback الخاصة بالصوت، لذا يجب عدم حظره بشكل متزامن. في أسوأ السيناريوهات، قد يواجه مجرى البث الصوتي تكرارًا أو انقطاعًا، ولكن سيتم استعادته في نهاية المطاف عند استقرار أداء المعالجة.
الإعداد والتشغيل
كما هو موضّح في المخطّط البياني أعلاه، يتضمّن هذا التصميم عدة مكوّنات لتنظيمها: DedicatedWorkerGlobalScope (DWGS) وAudioWorkletGlobalScope (AWGS) وSharedArrayBuffer والخيط الرئيسي. توضِّح الخطوات التالية ما يجب أن يحدث في مرحلة الإعداد.
الإعداد
- [Main] يتمّ استدعاء مُنشئ AudioWorkletNode.
- أنشئ عاملاً.
- سيتم إنشاء AudioWorkletProcessor المرتبط.
- [DWGS] ينشئ Worker عنصرَي SharedArrayBuffer. (أحدهما للحالات المشتركة والآخر لبيانات الصوت)
- [DWGS] يُرسِل Worker إشارات SharedArrayBuffer إلى AudioWorkletNode.
- [الرئيسي] يُرسِل AudioWorkletNode إشارات SharedArrayBuffer إلى AudioWorkletProcessor.
- [AWGS] يُعلم AudioWorkletProcessor AudioWorkletNode باكتمال الإعداد.
بعد اكتمال عملية الإعداد، يبدأ AudioWorkletProcessor.process()
بالاستدعاء. في ما يلي ما يجب أن يحدث في كل تكرار من حلقة المعالجة.
حلقة العرض
- [AWGS] يتم استدعاء
AudioWorkletProcessor.process(inputs, outputs)
لكل وحدة عرض.- سيتمّ نقل
inputs
إلى Input SAB. - سيتم ملء
outputs
من خلال استخدام بيانات الصوت في Output SAB. - تعديل States SAB باستخدام فهارس التخزين المؤقت الجديدة وفقًا لذلك
- إذا اقترب Output SAB من الحدّ الأدنى لمعدل تدفق البيانات، يُستخدَم Wake Worker لمعالجة المزيد من بيانات الصوت.
- سيتمّ نقل
- [DWGS] ينتظر Worker (في وضع السكون) إشارة الاستيقاظ من
AudioWorkletProcessor.process()
. عند الاستيقاظ:- تُستخدَم هذه السمة لجلب فهارس المخزن المؤقت من States SAB.
- شغِّل دالة المعالجة باستخدام بيانات من مدخلات جدول بيانات العملاء لملءجدول بيانات العملاء الناتج.
- تعديل حالات SAB باستخدام فهارس المخزن المؤقت وفقًا لذلك
- ينتقل إلى وضع السكون وينتظر الإشارة التالية.
يمكن العثور على مثال على الرمز هنا، ولكن يُرجى العلم أنّه يجب تفعيل العلامة التجريبية SharedArrayBuffer لكي يعمل هذا العرض الترويجي. تم كتابة الرمز البرمجي باستخدام رمز JS خالص لتسهيل الأمر، ولكن يمكن استبداله برمز WebAssembly عند الحاجة. يجب التعامل مع هذه الحالة بعناية إضافية من خلال لف إدارة الذاكرة باستخدام فئة HeapAudioBuffer.
الخاتمة
الهدف النهائي من Audio Worklet هو جعل واجهة برمجة التطبيقات Web Audio API "قابلة للتوسيع" حقًا. لقد تطلّب تصميمها جهودًا لعدة سنوات لكي يصبح من الممكن تنفيذ بقية Web Audio API باستخدام Audio Worklet. نتيجةً لذلك، أصبح لدينا الآن تصميم أكثر تعقيدًا، وقد يشكّل ذلك تحديًا غير متوقّع.
لحسن الحظ، فإنّ سبب هذه التعقيدات هو منح المطوّرين المزيد من الصلاحيات. تتيح إمكانية تشغيل WebAssembly على AudioWorkletGlobalScope إمكانيات كبيرة لمعالجة الصوت بأداء عالٍ على الويب. بالنسبة إلى التطبيقات المخصّصة للصوت على نطاق واسع والتي تكون مكتوبة بلغة C أو C++، يمكن أن يكون استخدام Audio Worklet مع SharedArrayBuffers وWorkers خيارًا جذابًا لاستكشافه.
المساهمون
نشكر بشكل خاص "كريس ويلسون" و"جيسون ميلر" و"جوشوا بيل" و"رايموند توي" على مراجعة مسودة هذه المقالة وتقديم ملاحظات مفيدة.