جدولة التسجيلات الصوتية على الويب بدقة
مقدمة
إنّ إدارة الوقت هي أحد أكبر التحديات التي تواجهك عند إنشاء برامج صوتية وموسيقية رائعة باستخدام منصة الويب. هناك سؤال يختلف عن "وقت كتابة الرمز" (وهو ما يحدث في الوقت على مدار الساعة) إذ إنّ كيفية التعامل بشكل صحيح مع الساعة الصوتية من المواضيع الأقل فهمًا حول Web Audio. يحتوي كائن Web Audio AudioContext على خاصية currentTime التي تعرض ساعة الصوت هذه.
بالنسبة إلى التطبيقات الموسيقية لصوت الويب، على وجه الخصوص، ليس فقط كتابة برامج التسلسل والتركيب، ولكن أي استخدام إيقاعي للأحداث الصوتية، مثل آلات الطبل والألعاب والتطبيقات الأخرى، من المهم جدًا تحديد توقيتات متسقة ودقيقة للأحداث الصوتية، وليس فقط بدء الأصوات وإيقافها، ولكن أيضًا تحديد موعد التغييرات في الصوت (مثل تغيير التردد أو مستوى الصوت). قد يكون من المفضّل أحيانًا تنظيم أحداث ذات ترتيب عشوائي لبعض الوقت، على سبيل المثال في العرض التوضيحي للمسدسات الآلية في مقالة تطوير Game Audio باستخدام Web Audio API، ولكنّنا نحتاج عادةً إلى أن يكون توقيت النوتات الموسيقية متّسقًا ودقيقًا.
لقد سبق أن أوضحنا لك كيفية جدولة الملاحظات باستخدام معلمة الوقت NoteOn وNoteOff لأسلوبَي Web Audio وnoteOff (تُعرف الآن باسم "البدء والإيقاف") في بدء استخدام Web Audio وفي تطوير Game Audio باستخدام Web Audio API، ولكننا لم نستكشف بالتفصيل السيناريوهات الأكثر تعقيدًا، مثل تشغيل التسلسلات الموسيقية أو الإيقاعات الطويلة. للتعمق في ذلك، نحتاج أولاً إلى خلفية بسيطة عن الساعات.
The Best of Times - the Web Audio Clock
توفّر واجهة برمجة التطبيقات Web Audio API إمكانية الوصول إلى ساعة الأجهزة في النظام الفرعي للصوت. تظهر هذه الساعة في كائن AudioContext من خلال خاصية .currentTime، كعدد عائم بالثواني منذ إنشاء AudioContext. يتيح ذلك لهذه الساعة (التي يُشار إليها لاحقًا باسم "الساعة الصوتية") أن تكون عالية الدقة، فقد تم تصميمها لتتمكّن من تحديد المواءمة على مستوى عيّنة صوت فردية، حتى مع معدّل أخذ العينات المرتفع. ونظرًا لأن هناك حوالي 15 رقمًا عشريًا للدقة في "الدقة المزدوجة"، فحتى لو كانت ساعة الصوت تعمل لأيام، لا يزال من المفترض أن تحتوي على الكثير من وحدات البت المتبقية للإشارة إلى عينة معينة حتى مع معدل عينة مرتفع.
تُستخدَم ساعة الصوت لجدولة المَعلمات وأحداث الصوت في Web Audio API، وذلك لاستخدام start() وstop() بالطبع، ولكن أيضًا لطرق set*ValueAtTime() في AudioParams. يتيح لنا ذلك إعداد أحداث صوتية مُجدوَلة بدقة مسبقًا. في الواقع، من المغري ضبط كل الإعدادات في Web Audio على أنّها أوقات بدء/إيقاف، ولكن في الواقع، هناك مشكلة في ذلك.
على سبيل المثال، انظر إلى مقتطف الرمز المصغر هذا من مقدمة Web Audio، والذي ينشئ شريطين لنمط نمط القبعة الثامنة:
for (var bar = 0; bar < 2; bar++) {
var time = startTime + bar * 8 * eighthNoteTime;
// Play the hi-hat every eighth note.
for (var i = 0; i < 8; ++i) {
playSound(hihat, time + i * eighthNoteTime);
}
سيعمل هذا الرمز بشكل رائع. ومع ذلك، إذا أردت تغيير الإيقاع في منتصف هذين الشريطَين أو إيقاف التشغيل قبل انتهاء الشريطَين، لن تتمكّن من ذلك. (لقد رأيت مطوّرين يُجريون إجراءات مثل إدراج عقدة كسب بين AudioBufferSourceNodes المُجدوَلة مسبقًا والإخراج، وذلك فقط لكي يتمكّنوا من كتم صوت المحتوى الخاص بهم).
باختصار، لأنّك ستحتاج إلى المرونة في تغيير الإيقاع أو المَعلمات، مثل معدّل التكرار أو الكسب (أو إيقاف الجدولة تمامًا)، لا تريد إضافة الكثير من أحداث الصوت إلى "قائمة المحتوى التالي"، أو بتعبير أدق، لا تريد النظر إلى المستقبل البعيد جدًا، لأنّك قد تريد تغيير هذا الجدول الزمني بالكامل.
The Worst of Times - ساعة JavaScript
لدينا أيضًا ساعة JavaScript المحبوبة جدًا والمُساءَة كثيرًا، والتي يتم تمثيلها من خلال Date.now() وsetTimeout(). الجانب الجيد من ساعة JavaScript هو أنّها تتضمّن طريقتَي window.setTimeout() وwindow.setInterval() المفيدتَين جدًا للتواصل معنا لاحقًا، ما يتيح لنا أن يطلب النظام من الرمز البرمجي الردّ علينا في أوقات محدّدة.
الجانب السلبي لساعة JavaScript هو أنّها ليست دقيقة جدًا. في البداية، تعرض دالة Date.now() قيمة بالمللي ثانية، أي عدد صحيح بالمللي ثانية، لذا فإنّ أفضل دقة يمكنك الحصول عليها هي ملي ثانية واحدة. لا يُعدّ ذلك أمرًا سيئًا للغاية في بعض السياقات الموسيقية، فربما لا تلاحظ أي فرق إذا بدأت النغمة قبل أو بعد ملي ثانية، ولكن حتى عند استخدام معدّل منخفض نسبيًا لأجهزة الصوت يبلغ 44.1 كيلوهرتز، يكون ذلك أبطأ بمقدار 44.1 مرة لاستخدامه كساعة لجدولة الصوت. يُرجى العِلم أنّ حذف أي عيّنات قد يؤدي إلى حدوث مشاكل في الصوت، لذا إذا أردنا ربط عيّنات معًا، يجب أن تكون متسلسلة بدقة.
إنّ مواصفات الوقت العالي الدقة القادمة تمنحنا الوقت الحالي بدقة أكبر من خلال window.performance.now();، وقد تم تنفيذها (مع إضافة بادئة) في العديد من المتصفحات الحالية. يمكن أن يساعد ذلك في بعض الحالات، على الرغم من أنّه ليس ذا صلة فعلية بأكثر أجزاء واجهات برمجة التطبيقات لتحديد الوقت في JavaScript سوءًا.
إنّ أسوأ جزء في واجهات برمجة التطبيقات الخاصة بتحديد الوقت في JavaScript هو أنّه على الرغم من أنّ دقة المللي ثانية في دالة Date.now() لا تبدو سيئة جدًا، إلا أنّه يمكن بسهولة تحريف الموعد النهائي الفعلي لأحداث الموقّت في JavaScript (من خلال window.setTimeout() أو window.setInterval) بمقدار عشرات المللي ثانية أو أكثر من خلال التنسيق والعرض وجمع المهملات وXMLHTTPRequest وطلبات الاستدعاء الأخرى، أي أي عدد من الإجراءات التي تحدث في سلسلة التنفيذ الرئيسية. هل تذكر أنّني ذكرت "أحداث الصوت" التي يمكننا جدولتها باستخدام Web Audio API؟ تتم معالجة كل هذه العناصر في سلسلة مهام منفصلة، لذا حتى إذا توقّفت السلسلة الرئيسية مؤقتًا عن العمل بسبب تنسيق معقّد أو مهمة طويلة أخرى، سيستمر تشغيل الصوت في الأوقات المحدّدة له بالضبط. وفي الواقع، حتى إذا توقّفت عند نقطة توقّف في أداة تصحيح الأخطاء، سيواصل خيط الصوت تشغيل الأحداث المُجدوَلة.
استخدام JavaScript setTimeout() في التطبيقات الصوتية
بما أنّه يمكن بسهولة إيقاف سلسلة المهام الرئيسية لعدة مللي ثوانٍ في المرة الواحدة، لا يُنصح باستخدام setTimeout في JavaScript لبدء تشغيل أحداث الصوت مباشرةً، لأنّه في أفضل الأحوال سيتم تشغيل الملاحظات خلال مللي ثانية أو نحو ذلك من الوقت الذي يجب أن يتم تشغيلها فيه، وفي أسوأ الأحوال سيتم تأخيرها لفترة أطول. والأسوأ من ذلك، بالنسبة إلى التسلسلات الإيقاعية، لن يتم تشغيلها على فترات زمنية دقيقة لأنّ التوقيت سيكون حساسًا للعناصر الأخرى التي تحدث في سلسلة JavaScript الرئيسية.
لتوضيح ذلك، كتبتُ نموذجًا لتطبيق مترونوم "سيئ"، أي تطبيق يستخدم setTimeout مباشرةً لجدولة النغمات، ويُجري أيضًا الكثير من عمليات التنسيق. افتح هذا التطبيق وانقر على "تشغيل"، ثم عدِّل حجم النافذة بسرعة أثناء تشغيل الفيديو، وستلاحظ أنّ التوقيت متذبذب بشكل ملحوظ (يمكنك سماع أنّ الإيقاع غير ثابت). قد تقول: "لكن هذا مصطنع". بالطبع، ولكن هذا لا يعني أنّه لا يحدث في العالم الحقيقي أيضًا. حتى واجهة المستخدم الثابتة نسبيًا ستواجه مشاكل في التوقيت في setTimeout بسبب عمليات الإرسال، على سبيل المثال، لاحظتُ أنّ تغيير حجم النافذة بسرعة سيؤدي إلى حدوث خلل ملحوظ في توقيت WebkitSynth الممتاز. الآن، تخيل ما سيحدث عند محاولة الانتقال بسلاسة إلى مقطع موسيقي كامل مع الصوت، ويمكنك بسهولة تخيل مدى تأثير ذلك في تطبيقات الموسيقى المعقّدة في العالم الواقعي.
من أكثر الأسئلة التي يتم طرحها بشكل متكرّر هي "لماذا لا يمكنني الحصول على طلبات استدعاء من أحداث الصوت؟". على الرغم من أنّه قد تكون هناك استخدامات لهذه الأنواع من طلبات الاستدعاء، إلا أنّها لن تحلّ المشكلة المحدّدة المطروحة. من المهمّ فهم أنّه سيتم تشغيل هذه الأحداث في سلسلة مهام JavaScript الرئيسية، لذا ستخضع لجميع التأخيرات المحتملة نفسها مثل setTimeout، أي أنّه يمكن تأخيرها لبعض الوقت المجهول والمتغيّر بالمللي ثانية من الوقت الدقيق الذي تم تحديده قبل معالجتها فعليًا.
إذًا، ما الذي يمكننا فعله؟ أفضل طريقة للتعامل مع التوقيت هي إعداد تعاون بين مؤقتات JavaScript (setTimeout() أو setInterval() أو requestAnimationFrame()، المزيد حول ذلك لاحقًا) وجدولة الأجهزة الصوتية.
الحصول على توقيت صخري صلب من خلال النظر للمستقبل
لنعود إلى العرض التوضيحي للموقّت الإيقاعي. لقد كتبتُ النسخة الأولى من هذا العرض التوضيحي البسيط للموقّت الإيقاعي بشكل صحيح لشرح أسلوب تحديد المواعيد المشترَكة هذا. (يتوفر الرمز أيضًا على Github) يشغّل هذا العرض الترويجي أصواتًا صفرية (يتم إنشاؤها بواسطة مذبذب) بدقة عالية في كل نغمة سداسية أو ثامن أو ربع، مع تغيير درجة الصوت حسب الإيقاع. ويتيح لك أيضًا تغيير الإيقاع وفاصل النغمات أثناء تشغيل المحتوى أو إيقاف التشغيل في أي وقت، وهي ميزة رئيسية لأي أداة تسلسل إيقاعي في العالم الحقيقي. سيكون من السهل جدًا إضافة رمز لتغيير الأصوات التي يستخدمها هذا المقياس الإيقاعي أثناء التشغيل أيضًا.
إنّ الطريقة التي يتم بها السماح بالتحكّم في السرعة مع الحفاظ على توقيت ثابت هي التعاون: وهو موقّت setTimeout يتم تشغيله مرة كل فترة، ويُعدّ جدولة Web Audio في المستقبل للمقاطع الصوتية الفردية. يتحقّق موقّت setTimeout بشكل أساسي ممّا إذا كان يجب جدولة أي ملاحظات "قريبًا" استنادًا إلى الإيقاع الحالي، ثمّ يحدّد موعدها، على النحو التالي:
من الناحية العملية، قد تتأخر استدعاءات setTimeout() ، لذلك قد يتلاشى توقيت استدعاءات الجدولة (ويتحوّل، بناءً على كيفية استخدامك لـ setTimeout) بمرور الوقت. وعلى الرغم من أنّ الأحداث في هذا المثال تبدأ بمسافة 50 ملي ثانية تقريبًا، إلّا أنّها غالبًا ما تكون أكثر من ذلك بقليل (وأحيانًا أكثر). ومع ذلك، خلال كلّ مكالمة، نحدّد جدولاً زمنيًا لأحداث Web Audio ليس فقط لأيّ نغمات يجب تشغيلها الآن (مثل النغمة الأولى)، ولكن أيضًا لأيّ نغمات يجب تشغيلها بين الآن والفاصل الزمني التالي.
في الواقع، لا نريد فقط النظر إلى المستقبل من خلال الفاصل الزمني بين طلبات setTimeout() بدقة، بل نحتاج أيضًا إلى بعض التداخل في الجدولة بين طلب الموقّت هذا والطلب التالي، وذلك لاستيعاب أسوأ حالة لسلوك الخيط الرئيسي، أي أسوأ حالة لجمع المهملات أو التنسيق أو العرض أو أي رمز آخر يحدث في الخيط الرئيسي يؤخّر طلب الموقّت التالي. نحتاج أيضًا إلى مراعاة وقت جدولة كتلة الصوت، وهو مقدار الصوت الذي يحتفظ به نظام التشغيل في المخزن المؤقت للمعالجة، والذي يختلف باختلاف أنظمة التشغيل والأجهزة، من أرقام مفردة قليلة من مللي ثانية إلى حوالي 50 ملّي ثانية. لكلّ استدعاء setTimeout() معروض أعلاه فاصل أزرق يعرض النطاق الكامل للمرات التي سيحاول فيها جدولة الأحداث. على سبيل المثال، قد يتم تشغيل الحدث الرابع للصوت على الويب المُجدوَل في المخطّط البياني أعلاه "متأخرًا" إذا انتظرنا تشغيله إلى أن يحدث استدعاء setTimeout التالي، إذا كان استدعاء setTimeout هذا بعد بضع مللي ثوانٍ فقط. في الحياة الواقعية، يمكن أن يكون الاضطراب في هذه الأوقات أكثر حدة من ذلك، ويصبح هذا التداخل أكثر أهمية عندما يصبح تطبيقك أكثر تعقيدًا.
يؤثر وقت الاستجابة الإجمالي للنظرة إلى المستقبل في مدى دقة التحكّم في الإيقاع (وعناصر التحكّم الأخرى في الوقت الفعلي). ويشكّل الفاصل الزمني بين طلبات الجدولة توازنًا بين الحد الأدنى من وقت الاستجابة ومعدّل تكرار تأثير الرمز البرمجي في المعالج. ويحدِّد مدى تداخل واجهة النظر مع وقت بدء الفاصل الزمني التالي مدى مرونة تطبيقك على مختلف الأجهزة، وعندما يصبح أكثر تعقيدًا (وقد يستغرق تنسيق البيانات وجمع البيانات غير المرغوب فيها وقتًا أطول). بوجه عام، للمرونة في مواجهة الأجهزة وأنظمة التشغيل الأبطأ، يُفضَّل أن يكون لديك واجهة عامة كبيرة وفاصل زمني قصير بشكل معقول. يمكنك تعديل تداخلات أقصر وفواصل أطول لمعالجة عدد أقل من معاودة الاتصال، ولكن في مرحلة ما، قد تبدأ في سماع أنّ وقت الاستجابة الطويل يتسبب في حدوث تغييرات في الإيقاع، وغير ذلك.
يوضّح مخطّط التوقيت التالي ما يفعله الرمز التجريبي لمقياس الإيقاع في الواقع: لديه فاصل زمني setTimeout 25ms، ولكنّه يتداخل بشكلٍ أكثر مرونة: سيتم جدولة كلّ طلب خلال 100ms التالية. الجانب السلبي لهذا الإجراء هو أنّ التغييرات في الإيقاع وما إلى ذلك ستستغرق 10% من الثانية لكي تسري، ولكنّنا أكثر مرونة في التعامل مع الانقطاعات:
في الواقع، يمكنك ملاحظة أنّه حدثت انقطاع في setTimeout في منتصف هذا المثال. كان من المفترض أن نتلقّى طلب استدعاء setTimeout بعد 270 ملي ثانية تقريبًا، ولكن تأخّر لسبب ما حتى 320 ملي ثانية تقريبًا، أي بعد 50 ملي ثانية من الوقت المحدّد. ومع ذلك، حافظت مدة الاستجابة الكبيرة للنظرة إلى المستقبل على مواصلة التوقيت بدون أي مشكلة، ولم يفوتنا أيّ إيقاع، على الرغم من أنّنا رفعنا معدّل الإيقاع قبل ذلك مباشرةً لتشغيل المقاطع الموسيقية الستة عشر بمعدّل 240 نبضة في الدقيقة (أعلى من معدّلات الإيقاعات القوية للأسطوانات الموسيقية).
من الممكن أيضًا أن تؤدي كلّ مكالمة إلى جدولة ملاحظات متعددة. لنلقِ نظرة على ما يحدث إذا استخدمنا فاصلًا زمنيًا أطول للجدولة (250 ملي ثانية للنظر إلى الأمام، مع فاصل زمني قدره 200 ملي ثانية) وزيادة في وتيرة الأداء في المنتصف:
توضِّح هذه الحالة أنّ كلّ طلب setTimeout() قد ينتهي بجدولة أحداث صوتية متعدّدة. في الواقع، هذا المقياس الإيقاعي هو تطبيق بسيط يعزف ملاحظة واحدة في كل مرة، ولكن يمكنك بسهولة معرفة كيف يعمل هذا الأسلوب مع آلة الطبول (التي تتضمّن غالبًا ملاحظات متعدّدة متزامنة) أو آلة تسلسل الأصوات (التي قد تتضمّن غالبًا فواصل زمنية غير منتظمة بين النغمات).
من الناحية العملية، ستحتاج إلى ضبط الفاصل الزمني للتخطيط ووقت النظر إلى المستقبل لمعرفة مدى تأثّره بالتنسيق وجمع المهملات والعمليات الأخرى التي تحدث في سلسلة مهام تنفيذ JavaScript الرئيسية، وضبط درجة دقة التحكّم في السرعة وما إلى ذلك. على سبيل المثال، إذا كان لديك تنسيق معقّد جدًا يحدث بشكل متكرّر، قد تحتاج إلى زيادة وقت النظر إلى المستقبل. النقطة الأساسية هي أنّنا نريد أن يكون مقدار "الجدول الزمني المُسبَق" الذي نُجريه كبيرًا بما يكفي لتجنُّب أي تأخيرات، ولكن ليس كبيرًا جدًا لدرجة أن يتسبب في تأخير ملحوظ عند تعديل عنصر التحكّم في الإيقاع. حتى الحالة أعلاه تتضمّن تداخلًا صغيرًا جدًا، لذا لن تكون مرنة جدًا على جهاز بطيء يستخدم تطبيق ويب معقّدًا. من الأفضل البدء بوقت "نظرة إلى المستقبل" يبلغ 100 ملي ثانية، مع ضبط الفواصل الزمنية على 25 ملي ثانية. قد يستمر حدوث مشاكل في التطبيقات المعقدة على الأجهزة التي تواجه الكثير من وقت الاستجابة في نظام الصوت، وفي هذه الحالة عليك زيادة وقت النظر إلى المستقبل، أو إذا كنت بحاجة إلى تحكم أكثر صرامة مع فقدان بعض المرونة، استخدِم وقت نظر إلى المستقبل أقصر.
التعليمة البرمجية الأساسية لعملية الجدولة في الدالة Scheduler() -
while (nextNoteTime < audioContext.currentTime + scheduleAheadTime ) {
scheduleNote( current16thNote, nextNoteTime );
nextNote();
}
تحصل هذه الدالة على الوقت الحالي لجهاز الصوت، وتقارنه بالوقت المخصّص للنغمة التالية في التسلسل. في معظم الأحيان*، لن يؤدي هذا الإجراء إلى أيّ شيء في هذا السيناريو الدقيق (لأنّه لا تتوفّر "نغمات" مترونوم في انتظار جدولتها، ولكن عندما تنجح، ستجدول هذه النغمة باستخدام Web Audio API، وتنتقل إلى النغمة التالية.
تتحمّل الدالة scheduleNote() مسؤولية تحديد موعد تشغيل "النغمة" التالية في Web Audio. في هذه الحالة، استخدمتُ المذبذبات لإصدار أصوات صفير بمعدّلات تردد مختلفة. يمكنك بسهولة إنشاء عقد AudioBufferSource وضبط ذاكرات التخزين المؤقتة على أصوات الطبول أو أي أصوات أخرى تريدها.
currentNoteStartTime = time;
// create an oscillator
var osc = audioContext.createOscillator();
osc.connect( audioContext.destination );
if (! (beatNumber % 16) ) // beat 0 == low pitch
osc.frequency.value = 220.0;
else if (beatNumber % 4) // quarter notes = medium pitch
osc.frequency.value = 440.0;
else // other 16th notes = high pitch
osc.frequency.value = 880.0;
osc.start( time );
osc.stop( time + noteLength );
بعد جدولة أجهزة الاهتزاز هذه وتوصيلها، يمكن لهذا الرمز أن ينسى أمرها تمامًا؛ ستبدأ في العمل ثم تتوقف، ثم يتم جمع البيانات غير المرغوب فيها تلقائيًا.
تتحمّل الدالة nextNote() مسؤولية الانتقال إلى العلامة الموسيقية الستة عشر التالية، أي ضبط المتغيّرين nextNoteTime وcurrent16thNote على العلامة الموسيقية التالية:
function nextNote() {
// Advance current note and time by a 16th note...
var secondsPerBeat = 60.0 / tempo; // picks up the CURRENT tempo value!
nextNoteTime += 0.25 * secondsPerBeat; // Add 1/4 of quarter-note beat length to time
current16thNote++; // Advance the beat number, wrap to zero
if (current16thNote == 16) {
current16thNote = 0;
}
}
هذه العملية بسيطة جدًا، ولكن من المهمّ معرفة أنّه في مثال تحديد الجدول الزمني هذا، لا نتتبّع "وقت التسلسل"، أي الوقت منذ بدء المقياس الإيقاعي. ما عليك سوى تذكُّر وقت تشغيل آخر ملاحظة ومعرفة وقت تشغيل الملاحظة التالية. بهذه الطريقة، يمكننا تغيير الإيقاع (أو إيقاف اللعب) بسهولة كبيرة.
يتم استخدام أسلوب الجدولة هذا من قِبل عدد من تطبيقات الصوت الأخرى على الويب، مثل Web Audio Drum Machine وAcid Defender game الممتعة للغاية، بالإضافة إلى أمثلة أكثر تفصيلاً على الصوت، مثل العرض التجريبي لتأثيرات Granular Effects.
نظام توقيت آخر
والآن، كما يعرف أي موسيقي جيد، تحتاج كل التطبيقات الصوتية إلى مزيد من الموقتات. تجدر الإشارة إلى أنّ الطريقة الصحيحة لتنفيذ العرض المرئي هي استخدام نظام توقيت ثالث.
لماذا نحتاج إلى نظام توقيت آخر؟ يتم مزامنة هذا المقياس مع العرض المرئي، أي معدّل تحديث الرسومات، من خلال واجهة برمجة التطبيقات requestAnimationFrame. بالنسبة إلى رسم المربّعات في مثال المقياس الموسيقي، قد لا يبدو هذا الأمر مهمًا جدًا، ولكن مع زيادة تعقيد الرسومات، يصبح من الضروري استخدام requestAnimationFrame() للمزامنة مع معدّل التحديث المرئي، ومن السهل استخدام هذه الدالة منذ البداية تمامًا كما هو الحال مع setTimeout(). باستخدام الرسومات المتزامنة المعقّدة جدًا (مثل العرض الدقيق للمقاطع الموسيقية الكثيفة أثناء تشغيلها في حزمة موسيقية)، ستوفّر لك دالة requestAnimationFrame() أفضل مزامنة للرسومات والصوت.
لقد تتبّعنا المقاطع الصوتية في قائمة الانتظار في أداة تحديد المواعيد:
notesInQueue.push( { note: beatNumber, time: time } );
يمكن العثور على التفاعل مع الوقت الحالي للميترونيومي في طريقة draw()، والتي يتمّ استدعاؤها (باستخدام requestAnimationFrame) كلّما كان نظام الرسومات جاهزًا لإجراء تعديل:
var currentTime = audioContext.currentTime;
while (notesInQueue.length && notesInQueue[0].time < currentTime) {
currentNote = notesInQueue[0].note;
notesInQueue.splice(0,1); // remove note from queue
}
مرة أخرى، ستلاحظ أننا نتحقق من ساعة النظام الصوتي، لأن هذه هي الساعة التي نريد المزامنة معها، بما أنّها ستشغّل النغمات في الواقع، وذلك لمعرفة ما إذا كان يجب رسم مربع جديد أم لا. في الواقع، لا نستخدم الطوابع الزمنية لـ requestAnimationFrame إطلاقًا، لأنّنا نستخدم ساعة النظام الصوتي لمعرفة أين وصلنا في الوقت المناسب.
بالطبع، كان بإمكاني أن أتخطى استخدام استدعاء setطلع() تمامًا، ووضع أداة جدولة الملاحظات في استدعاء requestAnimationFrame، ثم سنعود إلى مؤقتين مرة أخرى. لا بأس في ذلك أيضًا، ولكن من المهم أن تفهم أن requestAnimationFrame هو مجرد بديل لـ setTimeout() في هذه الحالة. ستظل ترغب في دقة جدولة Web Audio للملاحظات الفعلية.
الخاتمة
نأمل أن يكون هذا الدليل التعليمي مفيدًا في شرح الساعات والموقّتات وكيفية إنشاء توقيت رائع في تطبيقات الصوت على الويب. ويمكن استخدام هذه التقنيات نفسها بسهولة لإنشاء مشغّلات تسلسلات وآلات طبول وغير ذلك. إلى اللقاء في المرة القادمة.