مقدمه
Racer یک آزمایش Chrome چند نفره و چند دستگاهی است. یک بازی ماشین اسلات به سبک یکپارچهسازی با سیستمعامل که در سراسر صفحه نمایش اجرا می شود. در گوشی یا تبلت، اندروید یا iOS. هر کسی می تواند بپیوندد. بدون برنامه بدون دانلود. فقط وب موبایل.
Plan8 به همراه دوستانمان در 14islands تجربه موسیقی و صدای پویا را بر اساس آهنگسازی اصلی جورجیو مورودر ایجاد کردند. Racer دارای صداهای موتور پاسخگو، جلوه های صوتی مسابقه، اما مهمتر از آن یک ترکیب موسیقی پویا است که با پیوستن مسابقه دهندگان، خود را در چندین دستگاه توزیع می کند. این یک نصب چند بلندگو است که از تلفن های هوشمند تشکیل شده است.
اتصال چند دستگاه به یکدیگر چیزی بود که ما مدتی با آن فریب میدادیم. ما آزمایشهای موسیقی را انجام داده بودیم که در آن صدا در دستگاههای مختلف تقسیم میشد یا بین دستگاهها میپرید، بنابراین مشتاق بودیم این ایدهها را در Racer اعمال کنیم.
به طور خاص، ما میخواستیم آزمایش کنیم که آیا میتوانیم آهنگ موسیقی را در همه دستگاهها ایجاد کنیم، زیرا افراد بیشتر و بیشتری به بازی میپیوندند - با درام و بیس شروع کردند، سپس گیتار و سینتها و غیره را اضافه کردند. ما چند دموی موسیقی انجام دادیم و وارد کدنویسی شدیم. جلوه چند بلندگو واقعاً ارزشمند بود. ما در این مرحله تمام همگامسازی را نداشتیم، اما وقتی لایههای صدا را که روی دستگاهها پخش میشوند شنیدیم، متوجه شدیم که به چیز خوبی رسیدهایم.
ایجاد صداها
آزمایشگاه خلاق گوگل یک جهت خلاقانه برای صدا و موسیقی مشخص کرده بود. ما می خواستیم از سینت سایزرهای آنالوگ برای ایجاد جلوه های صوتی به جای ضبط صداهای واقعی یا توسل به کتابخانه های صوتی استفاده کنیم. ما همچنین میدانستیم که بلندگوی خروجی در بیشتر موارد، یک بلندگوی کوچک تلفن یا تبلت است، بنابراین صداها باید در طیف فرکانسی محدود میشوند تا از اعوجاج بلندگوها جلوگیری شود. ثابت شد که این یک چالش است. وقتی اولین پیش نویس های موسیقی را از جورجیو دریافت کردیم، مایه آرامش بود، زیرا آهنگسازی او با صداهایی که ما ساخته بودیم کاملاً کار می کرد.
صدای موتور
بزرگترین چالش در برنامه نویسی صداها یافتن بهترین صدای موتور و شکل دادن به رفتار آن بود. مسیر مسابقه شبیه پیست F1 یا نسکار بود، بنابراین ماشینها باید سریع و انفجاری را احساس میکردند. در عین حال، ماشینها واقعا کوچک بودند، بنابراین صدای موتور بزرگ نمیتوانست صدا را به تصاویر وصل کند. به هر حال نمیتوانستیم یک موتور خروشان در بلندگوی موبایل داشته باشیم، بنابراین باید چیز دیگری را کشف میکردیم.
برای الهام گرفتن، مجموعهای از ترکیبهای مصنوعی مدولار دوستمان جان اکستراند را به هم متصل کردیم و شروع کردیم به سر و کله زدن. ما از آنچه شنیدیم خوشمان آمد. این همان چیزی است که با دو اسیلاتور، چند فیلتر خوب و LFO به نظر می رسید.
چرخ دنده آنالوگ قبلا با موفقیت زیادی با استفاده از Web Audio API بازسازی شده است، بنابراین ما امید زیادی داشتیم و شروع به ایجاد یک ترکیب ساده در Web Audio کردیم. صدای تولید شده پاسخگوترین صدا خواهد بود اما قدرت پردازش دستگاه را کاهش می دهد. برای اینکه تصاویر بصری به خوبی اجرا شوند، باید بسیار ناب باشیم تا همه منابعی را که میتوانیم ذخیره کنیم. بنابراین ما تکنیک را به نفع پخش نمونه های صوتی تغییر دادیم.

چندین تکنیک وجود دارد که می تواند برای ایجاد صدای موتور از نمونه ها استفاده شود. رایجترین رویکرد برای بازیهای کنسول، داشتن لایهای از صداهای متعدد (هرچه بیشتر، بهتر) موتور در دورهای مختلف (با بار) و سپس crossfade و crosspitch بین آنهاست. سپس یک لایه از صداهای چندگانه موتور را اضافه کنید که فقط دور موتور (بدون بار) در همان RPM می چرخد و بین آن ها نیز ضربدری و ضربدری ایجاد کنید. هنگام تعویض دنده، اگر به درستی انجام شود، متقاطع بین این لایهها بسیار واقعی به نظر میرسد، اما تنها در صورتی که حجم زیادی فایل صوتی داشته باشید. ضربدری نمی تواند خیلی عریض باشد یا بسیار مصنوعی به نظر می رسد. از آنجایی که مجبور بودیم از زمان بارگذاری طولانی اجتناب کنیم، این گزینه برای ما خوب نبود. ما با پنج یا شش فایل صوتی برای هر لایه امتحان کردیم، اما صدا ناامید کننده بود. باید راهی با فایل های کمتر پیدا می کردیم.
موثرترین راه حل ثابت شد که:
- یک فایل صوتی با شتاب و تعویض دنده با شتاب بصری خودرو که به یک حلقه برنامه ریزی شده در بالاترین گام / 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 میلی ثانیه، شما شروع به "احساس" اثرات یک سیگنال تاخیری خواهید کرد، حتی اگر قادر به "درک" کامل آن نباشید. ما چند پروتکل همگامسازی زمان، جایگزینهای سادهتر را بررسی کردیم و سعی کردیم برخی از آنها را در عمل پیادهسازی کنیم. در پایان - به لطف زیرساخت تاخیر کم گوگل - ما به سادگی توانستیم یک سری درخواست ها را نمونه برداری کنیم و از نمونه ای با کمترین تاخیر به عنوان مرجع استفاده کنیم.
مبارزه با رانش ساعت
کار کرد! ما بیش از 5 دستگاه داشتیم که یک پالس را با هماهنگی کامل پخش می کردند - اما فقط برای مدتی. پس از چند دقیقه بازی، دستگاهها از هم دور میشوند، حتی اگر صدا را با استفاده از زمان بسیار دقیق Web Audio API برنامهریزی کنیم. تأخیر به آرامی، تنها چند میلی ثانیه در یک زمان و در ابتدا غیرقابل تشخیص جمع میشود، اما منجر به عدم هماهنگی لایههای موسیقی پس از پخش برای مدت زمان طولانیتر میشود. سلام، ساعت دریفت.
راه حل این بود که هر چند ثانیه یکبار مجدداً همگام سازی کنید، یک افست ساعت جدید را محاسبه کنید و به طور یکپارچه آن را به زمانبندی صدا وارد کنید. برای کاهش خطر تغییرات قابل توجه در موسیقی به دلیل تاخیر شبکه، تصمیم گرفتیم با نگه داشتن تاریخچه آخرین افست های همگام سازی و محاسبه میانگین، این تغییر را هموار کنیم.
برنامه ریزی آهنگ و تغییر ترتیبات
ایجاد یک تجربه صوتی تعاملی به این معنی است که دیگر کنترل زمان پخش قطعات آهنگ را ندارید، زیرا برای تغییر وضعیت فعلی به اقدامات کاربر وابسته هستید. ما باید مطمئن میشدیم که میتوانیم بین تنظیمهای آهنگ بهموقع جابهجا شویم، به این معنی که برنامهریز ما باید قبل از تغییر به تنظیم بعدی میتوانست محاسبه کند که چه مقدار از نوار در حال پخش فعلی باقی مانده است. الگوریتم ما در نهایت چیزی شبیه به این شد:
-
Client(1)
آهنگ را شروع می کند. -
Client(n)
از اولین مشتری می پرسد که آهنگ چه زمانی شروع شده است. -
Client(n)
یک نقطه مرجع را محاسبه می کند که آهنگ با استفاده از زمینه صوتی وب آن، فاکتور در syncOffset و مدت زمانی که از ایجاد زمینه صوتی آن گذشته است، شروع شده است. -
playDelta = Date.now() - syncOffset - songStartTime - context.currentTime
-
Client(n)
مدت زمان اجرای آهنگ را با استفاده از playDelta محاسبه می کند. زمانبندی آهنگ از این مورد استفاده میکند تا بداند کدام نوار در تنظیم فعلی باید در مرحله بعدی پخش شود. -
playTime = playDelta + context.currentTime nextBar = Math.ceil((playTime % loopDuration) ÷ barDuration) % numberOfBars
بهخاطر سلامت عقل، ترتیبهایمان را محدود کردیم که همیشه هشت میله باشد و سرعت یکسانی (ضربان در دقیقه) داشته باشد.
به جلو نگاه کن
هنگام استفاده از setTimeout
یا setInterval
در جاوا اسکریپت، همیشه مهم است که از قبل برنامه ریزی کنید . این به این دلیل است که ساعت جاوا اسکریپت خیلی دقیق نیست و تماسهای برنامهریزیشده را میتوان به راحتی دهها میلیثانیه یا بیشتر با طرحبندی، رندر، جمعآوری زباله و درخواستهای XMLHTTPR منحرف کرد. در مورد ما همچنین باید مدت زمانی را که طول می کشد تا همه مشتریان یک رویداد را از طریق شبکه دریافت کنند، در نظر می گرفتیم.
جن های صوتی
ترکیب صداها در یک فایل یک راه عالی برای کاهش درخواست های HTTP، هم برای HTML Audio و هم برای Web Audio API است. همچنین بهترین راه برای پخش صداها به صورت پاسخگو با استفاده از شیء صوتی است، زیرا لازم نیست قبل از پخش یک شیء صوتی جدید بارگذاری شود. در حال حاضر چند پیاده سازی خوب وجود دارد که ما از آنها به عنوان نقطه شروع استفاده کردیم. ما sprite خود را گسترش دادهایم تا در iOS و Android بهطور قابل اعتماد کار کند و همچنین برخی موارد عجیب و غریب را که دستگاهها به خواب میروند، مدیریت کنیم.
در Android، حتی اگر دستگاه را در حالت خواب قرار دهید، عناصر صوتی به پخش ادامه میدهند. در حالت خواب، اجرای جاوا اسکریپت برای حفظ باتری محدود شده است و نمیتوانید به requestAnimationFrame
، setInterval
یا setTimeout
اعتماد کنید. این یک مشکل است زیرا اسپرایت های صوتی به جاوا اسکریپت برای بررسی اینکه آیا پخش باید متوقف شود یا خیر متکی هستند. بدتر از همه، در برخی موارد، currentTime
عنصر صوتی بهروزرسانی نمیشود، اگرچه صدا همچنان در حال پخش است.
اجرای AudioSprite را که در Chrome Racer بهعنوان نسخه بازگشتی صوتی غیر وب استفاده کردیم، بررسی کنید.
عنصر صوتی
وقتی ما کار روی Racer را شروع کردیم، Chrome for Android هنوز از Web Audio API پشتیبانی نمی کرد. منطق استفاده از HTML Audio برای برخی دستگاهها، Web Audio API برای برخی دیگر، همراه با خروجی صوتی پیشرفتهای که میخواستیم به آن برسیم، برای چالشهای جالبی ساخته شده است. خوشبختانه، این همه تاریخ است. Web Audio API در اندروید M28 بتا پیاده سازی شده است.
- مشکلات تاخیر/زمان عنصر صوتی همیشه دقیقاً زمانی که به آن میگویید پخش نمیشود، پخش نمیشود. از آنجایی که جاوا اسکریپت تک رشته ای است، ممکن است مرورگر مشغول باشد و باعث تاخیر در پخش تا دو ثانیه شود.
- تأخیر در پخش به این معنی است که حلقه صاف همیشه امکان پذیر نیست. در دسکتاپ میتوانید از بافر مضاعف برای دستیابی به حلقههای تا حدودی بدون شکاف استفاده کنید، اما در دستگاههای تلفن همراه این یک گزینه نیست، زیرا:
- اکثر دستگاه های تلفن همراه بیش از یک عنصر صوتی را همزمان پخش نمی کنند.
- حجم ثابت نه اندروید و نه iOS به شما اجازه نمیدهند که حجم یک شیء صوتی را تغییر دهید.
- بدون پیش بارگذاری در دستگاههای تلفن همراه، عنصر Audio شروع به بارگیری منبع خود نمیکند مگر اینکه پخش در یک کنترلکننده
touchStart
آغاز شود. - به دنبال مسائل دریافت
duration
یا تنظیمcurrentTime
ناموفق خواهد بود مگر اینکه سرور شما از HTTP-Byte-Range پشتیبانی کند. اگر مانند ما در حال ساخت یک اسپرایت صوتی هستید، مراقب این مورد باشید. - تأیید اولیه در MP3 ناموفق است. برخی از دستگاه ها نمی توانند فایل های MP3 محافظت شده توسط Basic Auth را بارگیری کنند ، مهم نیست از کدام مرورگری استفاده می کنید.
نتیجه گیری
ما از زدن دکمه بیصدا بهعنوان بهترین گزینه برای مقابله با صدا برای وب، راه درازی را پیمودهایم، اما این تنها آغاز کار است و صدای وب به شدت در حال پخش شدن است. ما فقط سطح کارهایی را که می توان در مورد همگام سازی چندین دستگاه انجام داد، لمس کردیم. ما در تلفنها و تبلتها قدرت پردازشی برای پردازش سیگنال و افکتها (مانند Reverb) نداشتیم، اما با افزایش عملکرد دستگاه، بازیهای مبتنی بر وب نیز از این ویژگیها استفاده خواهند کرد. این زمان های هیجان انگیزی برای ادامه دادن به امکانات صدا است.