دراسة حالة - The Sounds of Racer

مقدمة

Racer هو عبارة عن تجربة Chrome متعددة اللاعبين. هي فئة ألعاب سيارات سلوت بنمط كلاسيكي يتم لعبها على مختلف الشاشات. على الهواتف أو الأجهزة اللوحية، سواء Android أو iOS يمكن لأي مستخدم الانضمام. ليس هناك أي تطبيقات. لم يتم تنزيل أي محتوى. ويب على الأجهزة الجوّالة فقط.

تعاونت فرقة Plan8 مع أصدقائنا في موقع 14islands لإنشاء تجربة موسيقية وصوتية ديناميكية تستند إلى مقطوعة موسيقية أصلية للفنان "جورجيو مورودر". يتميّز المتسابق بأصوات المحركات سريعة الاستجابة والمؤثرات الصوتية الخاصة بالسباقات، ولكن الأهم من ذلك هو مزيج موسيقي ديناميكي يتم توزيعه على عدة أجهزة عند انضمام المتسابقين. وهو مثبت على مكبرات صوت متعددة ويتألف من هواتف ذكية.

كنا نتساءل عن إمكانية ربط عدة أجهزة معًا لبعض الوقت. أجرينا تجارب موسيقية حيث كان الصوت ينقسم على أجهزة مختلفة أو يتنقل بين الأجهزة، لذلك أردنا تطبيق هذه الأفكار على Racer.

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

إنشاء الأصوات

كان مختبر Google الإبداعي قد حدد اتجاهًا إبداعيًا للصوت والموسيقى. أردنا استخدام أدوات المزج التناظرية لإنشاء المؤثرات الصوتية بدلاً من تسجيل الأصوات الحقيقية أو اللجوء إلى مكتبات الصوت. وعرفنا أيضًا أن مكبّر الصوت الناتج سيكون، في معظم الحالات، مكبّر صوت هاتفًا أو جهازًا لوحيًا صغيرًا، لذا يجب أن تكون الأصوات محدودة في نطاق التردد لتجنب تشوّه السماعات. وثبت أن هذا كان تحديًا كبيرًا. شعرنا بالسعادة عندما تلقّينا أوّل مسودات موسيقية من "جورجيو"؛ لأنّ مقطوعته الموسيقية كانت متوافقة مع الأصوات التي أنشأناها.

صوت المحرّك

كان التحدي الأكبر في برمجة الأصوات هو العثور على أفضل صوت للمحرك وتحديد سلوكه. كان مضمار السباق يشبه مضمار F1 أو ناسكار، لذلك كان على السيارات تشعر بالسرعة والانفجار. في الوقت نفسه، كانت السيارات صغيرة بالفعل لذا لن يعمل صوت المحرك الكبير على ربط الصوت بالعناصر المرئية. لم يكن لدينا محرك ضايق يعمل في مكبر صوت الهاتف المحمول على أي حال، ولذلك كان علينا اكتشاف شيء آخر.

للحصول على الإلهام، اخترنا بعضًا من مجموعة آلات مزج المركبات التابعة لصديقنا جون إكستراند وبدأنا في إدخال بعض الأفكار حولها. وقد أحببنا ما سمعناه. هكذا كان يبدو الصوت مع جهازَي مذبذبَين، أحدهما يحتوي على فلاتر رائعة وLFO.

تمت إعادة تشكيل المعدات التناظرية بنجاح كبير باستخدام واجهة برمجة تطبيقات Web Audio، لذا كانت لدينا آمال كبيرة وبدأنا في إنشاء توليف بسيط في Web Audio. سيكون الصوت الذي يتم إنشاؤه هو الأكثر استجابة ولكنه سيزيد من قوة معالجة الجهاز. احتجنا إلى توفير جميع الموارد التي يمكننا استخدامها لكي تعمل العناصر المرئية بسلاسة. لهذا السبب، غيّرنا الأسلوب المستخدَم في الفيديوهات بدلاً من تشغيل عيّنات صوتية.

جهاز مزج نموذجي لاستلهام صوت المحرّك

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

ثبت أنّ أكثر الحلول فعالية هي:

  • ملف صوتي واحد تتم مزامنته مع التسارع وتغيير المعدات مع التسارع المرئي للسيارة منتهيًا بحلقة مبرمجة أعلى درجة / سرعة في الدقيقة تتميز Web Audio API بأنها جيدة جدًا في التكرار الدقيق، لذا يمكننا إجراء ذلك بدون أعطال أو أعطال.
  • ملف صوتي واحد يتضمّن تباطؤًا / سرعة المحرك
  • وأخيرًا، يشغّل ملف صوتي واحد الصوت الذي يتم تشغيله بشكل ثابت أو غير نشط بشكل متكرر.

تبدو هكذا

الرسم الصوتي للمحرّك

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

function throttleOn(throttle) {
    //Calculate the start position depending 
    //on the current amount of throttle.
    //By multiplying throttle we get a start position 
    //between 0 and 3 seconds.
    var startPosition = throttle * 3;

    var audio = context.createBufferSource();
    audio.buffer = loadedBuffers["accelerate_and_loop"];

    //Sets the loop positions for the buffer source.
    audio.loopStart = 5;
    audio.loopEnd = 9;

    //Starts the buffer source at the current time
    //with the calculated offset.
    audio.start(context.currentTime, startPosition);
}

التجربة

شغِّل المحرك واضغط على زر "التخفيف".

<input type="button" id="playstop" value = "Start/Stop Engine" onclick='playStop()'>
<input type="button" id="throttle" value = "Throttle" onmousedown='throttleOn()' onmouseup='throttleOff()'>

فمع توفّر ثلاثة ملفات صوتية صغيرة فقط ومحرك صوت جيد، قرّرنا الانتقال إلى التحدي التالي.

الحصول على المزامنة

بالتعاون مع "ديفيد ليندكفيست" من 14islands، بدأنا نبحث بشكل أكبر عن إمكانية تشغيل الأجهزة في تناغم مثالية. النظرية الأساسية بسيطة. يطلب الجهاز من الخادم وقته وعوامل وقت استجابة الشبكة ثم يحسب معادلة الساعة المحلية.

syncOffset = localTime - serverTime - networkLatency

وبهذه الإزاحة، يتشارك كل جهاز متصل في مفهوم الوقت نفسه. الأمر سهل، أليس كذلك؟ (نكرّر أنه من الناحية النظرية).

جارٍ احتساب وقت استجابة الشبكة

قد نفترض أنّ وقت الاستجابة هو نصف الوقت الذي يستغرقه طلب استجابة من الخادم وتلقّيها:

networkLatency = (receivedTime - sentTime) × 0.5

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

لحسن الحظ، فإن دماغنا لا يلاحظ ما إذا تأخرت الأصوات قليلاً. أظهرت الدراسات أن التأخير يستغرق من 20 إلى 30 مللي ثانية (مللي ثانية) قبل أن يدرك دماغنا الأصوات على أنها منفصلة. ومع ذلك، في غضون 12 إلى 15 ملي ثانية تقريبًا، ستبدأ في "الشعور" بتأثيرات تأخر الإشارة حتى إذا لم تتمكن من "إدراكها" بشكل كامل. لقد فحصنا اثنين من بروتوكولات مزامنة الوقت المعمول بها، وبدائل أبسط، وحاولنا تنفيذ بعضها عمليًا. في النهاية، بفضل البنية الأساسية لوقت الاستجابة المنخفض في Google، استطعنا ببساطة أخذ عيّنة من الطلبات المتسلسلة واستخدام النموذج ذي الاستجابة الأقل كمرجع.

عقارب الساعة

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

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

جدولة الأغنية وتبديل الترتيبات

وعند إنشاء تجربة صوتية تفاعلية، لن يعود بإمكانك التحكّم في موعد تشغيل أجزاء من الأغنية، لأنّ هذه الحالة تعتمد على إجراءات المستخدم لتغيير الحالة الحالية. كان علينا التأكّد من أنّه يمكننا التبديل بين الترتيبات في الأغنية في الوقت المناسب، ما يعني أنّ نظام الجدولة لدينا كان عليه أن يكون قادرًا على حساب الكمية المتبقية من الشريط المفعَّل حاليًا قبل التبديل إلى الترتيب التالي. انتهى المطاف بالخوارزمية إلى البحث عن شيء مثل هذا:

  • يبدأ "Client(1)" الأغنية.
  • "Client(n)" يسأل العميل الأول عن تاريخ بدء الأغنية.
  • يحتسب Client(n) نقطة مرجعية إلى وقت بدء الأغنية باستخدام سياق Web Audio، مع مراعاة Sync Offset، والوقت الذي انقضى منذ إنشاء سياقها الصوتي.
  • playDelta = Date.now() - syncOffset - songStartTime - context.currentTime
  • يحسب Client(n) مدة تشغيل الأغنية باستخدام playDelta. تستخدم أداة جدولة الأغاني هذا الجدول لتحديد الشريط الذي يجب تشغيله تاليًا في الترتيب الحالي.
  • playTime = playDelta + context.currentTime nextBar = Math.ceil((playTime % loopDuration) ÷ barDuration) % numberOfBars

من أجل الحفاظ على السلامة، وضعنا حدودًا بأن تكون طول الترتيبات دائمًا ثمانية بارات ولها نفس الإيقاع (الإيقاعات في الدقيقة).

يُرجى النظر للأمام.

من المهم دائمًا تحديد موعد مسبق عند استخدام setTimeout أو setInterval في JavaScript. وهذا يرجع إلى أن ساعة JavaScript ليست دقيقة للغاية، ويمكن بسهولة أن تنحرف عمليات الاستدعاء المجدولة بعشرات من المللي ثانية أو أكثر حسب التنسيق والعرض وجمع البيانات غير المهمّة وXMLHTTPRequests. في حالتنا، كان علينا أيضًا أن نراعي الوقت الذي يستغرقه جميع العملاء لتلقي نفس الحدث عبر الشبكة.

تركيبات صوتية

يعتبر دمج الأصوات في ملف واحد طريقة رائعة لتقليل طلبات HTTP، لكل من HTML Audio وWeb Audio API. وتُعدّ هذه الميزة أيضًا أفضل طريقة لتشغيل الأصوات بشكل متجاوب باستخدام عنصر الصوت، وذلك لعدم الحاجة إلى تحميل عنصر صوتي جديد قبل التشغيل. هناك حاليًا بعض عمليات التنفيذ الجيدة التي استخدمناها كنقطة بداية. لقد وسّعنا نطاق ابتكاراتنا للعمل بشكل موثوق على كلّ من iOS وAndroid بالإضافة إلى التعامل مع بعض الحالات النادرة التي تكون فيها الأجهزة في وضع السكون.

على Android، يستمر تشغيل عناصر الصوت حتى في حال ضبط الجهاز على "وضع السكون". في وضع السكون، تقتصر عملية تنفيذ JavaScript على الحفاظ على طاقة البطارية، ولا يمكنك الاعتماد على requestAnimationFrame أو setInterval أو setTimeout لتنشيط عمليات معاودة الاتصال. هذه مشكلة لأن الصور المدمجة الصوتية تعتمد على JavaScript لمواصلة التحقق مما إذا كان يجب إيقاف التشغيل أم لا. وما يزيد الأمر سوءًا، في بعض الحالات، لا يتم تحديث currentTime للعنصر الصوتي بالرغم من استمرار تشغيل الصوت.

راجع تطبيق AudioSprite الذي استخدمناه في Chrome Racer كإجراء احتياطي غير تابع للويب.

عنصر صوتي

عندما بدأنا العمل على Racer، لم يكن Chrome لنظام Android متوافقًا بعد مع واجهة برمجة تطبيقات Web Audio. لقد ساعدَنا منطق استخدام HTML Audio لبعض الأجهزة، وWeb Audio API على أجهزة أخرى، مع إخراج الصوت المتقدم الذي أردنا تحقيقه من خلال طرح بعض التحديات المثيرة للاهتمام. لحسن الحظ، أصبح هذا كله للتاريخ الآن. يتم تنفيذ Web Audio API في الإصدار التجريبي من الإصدار M28 من نظام التشغيل Android.

  • مشاكل في التأخير أو التوقيت لا يتم دائمًا تشغيل عنصر الصوت بدقة عندما تطلب تشغيله. ونظرًا إلى أنّ JavaScript يتضمّن سلسلة تعليمات واحدة، قد يكون المتصفّح مشغولاً، ما يؤدي إلى تأخير في التشغيل لمدة تصل إلى ثانيتَين.
  • تعني التأخيرات في التشغيل أنّ التكرار السلس قد لا يكون ممكنًا دائمًا. على الكمبيوتر المكتبي، يمكنك استخدام ميزة التخزين المؤقت المزدوج لتحقيق تكرارات بسيطة إلى حد ما، ولكن لا يتوفّر هذا الخيار على الأجهزة الجوّالة للسببَين التاليين:
    • لن تُشغِّل معظم الأجهزة الجوّالة أكثر من عنصر صوتي واحد في الوقت نفسه.
    • مستوى صوت ثابت. لا يسمح لك نظاما التشغيل Android أو iOS بتغيير مستوى صوت عنصر صوتي.
  • بدون تحميل مسبق. على الأجهزة الجوّالة، لن يبدأ العنصر الصوتي في تحميل مصدره ما لم يتم بدء التشغيل في معالِج touchStart.
  • البحث عن المشاكل سيتعذر الحصول على duration أو إعداد currentTime ما لم يكن الخادم يتيح استخدام نطاق بايت HTTP. انتبه من هذا إذا كنت تنشئ صوتًا كما فعلنا.
  • تعذّرت المصادقة الأساسية على MP3. تتعذّر على بعض الأجهزة تحميل ملفات MP3 المحمية من خلال المصادقة الأساسية، بغض النظر عن المتصفّح الذي تستخدمه.

الاستنتاجات

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