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

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

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

ملخّص: Audio Worklet

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

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

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

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

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

تُعدّ المجموعة الأولى مهمة للمطوّرين الذين لديهم استثمارات حالية في رموز معالجة الصوت والمكتبات، فيما يكون العنصر الثاني مهمًا لجميع مستخدمي واجهة برمجة التطبيقات تقريبًا. في عالم 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()

لكي يعمل النمط A بشكل صحيح، يحتاج Emscripten إلى خيارين لإنشاء رمز الغراء WebAssembly الصحيح من أجل الضبط:

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

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

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

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

بيانات الصوت والمقاطع الصوتية في WASM

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

فئة HeapaudioBuffer لتسهيل استخدام كومة WASM
فئة HeapaudioBuffer لتسهيل استخدام كومة WASM

يتوفّر اقتراح مبكر قيد المناقشة لدمج كومة WASM مباشرةً في نظام Audio Worklet. يبدو التخلص من نسخ البيانات المكررة بين ذاكرة JS وكومة WASM أمرًا طبيعيًا، لكن يجب وضع التفاصيل المحددة.

عدم تطابق حجم المخزن المؤقت للتعامل

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

إليك رسم تخطيطي لمُعالِج AudioWorkletProcessor باستخدام مخازن تخزين حلقات بالداخل لاستيعاب دالة WASM التي تأخذ 512 إطارًا داخل وخارجها. (يتم اختيار الرقم 512 هنا بشكل عشوائي).

استخدام RingBuffer داخل طريقة `process()` لـ AudioWorkletProcessor
استخدام RingBuffer ضمن طريقة `process()` AudioWorkletProcessor

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

  1. يرسل AudioWorkletProcessor 128 إطارًا إلى إدخال RingBuffer من خلال الإدخال الخاص به.
  2. نفِّذ الخطوات التالية فقط إذا كان Input RingBuffer أكبر من 512 إطارًا أو يساويه.
    1. اسحب 512 إطارًا من Input RingBuffer.
    2. معالجة 512 إطارًا باستخدام دالة WASM المحددة.
    3. إرسال 512 إطارًا إلى إخراج RingBuffer.
  3. يسحب AudioWorkletProcessor 128 إطارًا من إخراج RingBuffer لملء الإخراج الخاص به.

كما هو موضح في المخطط، يتم تجميع إطارات الإدخال دائمًا في RingBuffer ويتعامل مع فائض التخزين المؤقت من خلال استبدال أقدم كتلة إطار في المخزن المؤقت. وهذا أمر معقول بالنسبة إلى تطبيق صوتي في الوقت الفعلي. وبالمثل، يسحب النظام دائمًا كتلة إطار الإخراج. سيؤدي تدفق المخزن المؤقت (لا تتوفر بيانات كافية) في الإخراج 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 بدعم من SAB لهذا الغرض.

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

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

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

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

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

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

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

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

جارٍ العرض
العرض المتعدّد السلاسل باستخدام SharedArrayBuffers
العرض المتعدّد السلاسل باستخدام SharedArrayBuffers
  1. [AWGS] يتم استدعاء AudioWorkletProcessor.process(inputs, outputs) لكل قيمة كمّية يتم عرضها.
    1. سيتم نقل inputs إلى إدخال SAB.
    2. سيتم ملء outputs عن طريق استخدام البيانات الصوتية في إخراج SAB.
    3. تعديل حالات SAB بمؤشرات مخزن مؤقّت جديدة وفقًا لذلك
    4. وإذا اقتربت مخرجات SAB من الحد الأدنى لمستوى تدفق البيانات، تعرض خدمة Wake Worker المزيد من البيانات الصوتية.
  2. [DWGS] ينتظر العامل (في وضع السكون) إشارة التنشيط الواردة من "AudioWorkletProcessor.process()". عند استيقاظك:
    1. لاسترجاع فهارس التخزين المؤقت من بيانات SAB للولاية
    2. شغِّل دالة العملية باستخدام البيانات من Input 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 والعاملين خيارًا جذابًا للاستكشاف.

المساهمون

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