מקרה לדוגמה – The Sounds of Racer

מבוא

Racer הוא ניסוי של Chrome, רב-משתתפים ומרוב מכשירים. משחק מכוניות בסגנון רטרו שמשחקים בו על גבי מסכים. בטלפונים או בטאבלטים, ב-Android או ב-iOS. כל אחד יכול להצטרף. אין אפליקציות. ללא הורדות. רק האינטרנט לנייד.

Plan8, יחד עם החברים שלנו ב-14islands, יצרו את חוויית המוזיקה והסאונד הדינמית על סמך יצירה מקורית של ג'ורג'ו מורודר. מרוץ מרוצים כולל צלילי מנוע רספונסיביים ואפקטים קוליים למרוץ, אבל חשוב יותר מכך, מיקס מוזיקה דינמי שמפיץ את עצמו בכמה מכשירים כשהמרוץ מצטרפים. מדובר בהתקנה של מספר רמקולים שמורכבת מסמארטפונים.

חיבור של כמה מכשירים יחד הוא עניין שדיברנו עליו לא מעט זמן. ערכנו ניסויים במוזיקה שבהם האודיו יתפצל במכשירים שונים או יעבור בין מכשירים, לכן היינו להוטים ליישם את הרעיונות האלה ב-Rracer.

באופן ספציפי יותר, רצינו לבדוק אם נוכל ליצור את טראק המוזיקה בכל המכשירים ככל שיותר ויותר אנשים הצטרפו למשחק - מתחילים בתופים ובבס, מוסיפים גיטרה, סינתיסייזרים וכן הלאה. ערכנו כמה הדגמות של מוזיקה וצללנו לתחום התכנות. האפקט של ריבוי דוברים היה מתגמל מאוד. לא היה לנו כל הסנכרון שצריך באותו זמן, אבל כששמענו את שכבות הקול מפוזרים על פני כל המכשירים, ידענו שאנחנו על משהו טוב.

יצירת הצלילים

Google Creative Lab ניסתה כיוון יצירתי עבור הצליל והמוזיקה. רצינו להשתמש בסינתיסייזרים אנלוגיים כדי ליצור את האפקטים הקוליים, במקום להקליט את הצלילים האמיתיים או להשתמש בספריות סאונד. ידענו גם שהרמקול של הפלט יהיה, ברוב המקרים, רמקול זעיר של טלפון או טאבלט, כך שספקטרום התדרים צריך להיות מוגבל כדי למנוע עיוות של הרמקולים. נראה שזה אתגר למדי. כשקיבלנו את טיוטות המוזיקה הראשונות מג'ורג'יו, זו הרגשה הקלה כי הלחן שלו עבד מצוין עם הצלילים שיצרנו.

צליל מנוע

האתגר הגדול ביותר בתכנות הצלילים היה למצוא את צליל המנוע הטוב ביותר ולפסל את ההתנהגות שלו. מסלול המרוצים היה דומה למסלול פורמולה 1 או מסלול נאסקאר, כך שהמכוניות היו מהירות ונפיצות. במקביל, המכוניות היו ממש קטנות, כך שצליל מנוע גדול לא היה פשוט לחבר את הצליל לרכיבים החזותיים. ממילא לא יכולנו להפעיל מנוע שואג חזק ברמקול הנייד, אז נאלצנו למצוא משהו אחר.

כדי לקבל השראה, הכנו רשימה עם אוסף הסית'ים המודולריים של חבר שלנו, ג'ון אקסטרנד, והתחלנו להתבלבל. אהבנו את מה ששמענו. כך זה נשמע עם שני מתנדים, כמה פילטרים נחמדים ו-LFO.

הציוד האנלוגי שופץ בהצלחה בעבר באמצעות ממשק Web Audio API, כך שהיו לנו תקוות רבות והתחלנו ליצור סינתיסייזר פשוט ב-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()'>

לכן, עם שלושה קובצי קול קטנים בלבד ומנוע שנשמע טוב, החלטנו לעבור לאתגר הבא.

הסנכרון מתבצע

יחד עם דייוויד לינדקוויסט מ-14 האיים, התחלנו לחקור יותר לעומק כדי לגרום למכשירים יושמעו בסנכרון מושלם. התיאוריה הבסיסית היא פשוטה. המכשיר מבקש מהשרת מה השעה שלו, לוקח בחשבון את זמן האחזור של הרשת, ואז מחשב את קיזוז השעון המקומי.

syncOffset = localTime - serverTime - networkLatency

בעזרת ההיסט הזה, כל מכשיר מחובר חולק עיקרון זמן זהה. קל, נכון? (שוב, בתיאוריה).

חישוב זמן האחזור של הרשת

אנחנו עשויים להניח שזמן האחזור הוא חצי מהזמן שנדרש כדי לשלוח בקשה ולקבל תגובה מהשרת:

networkLatency = (receivedTime - sentTime) × 0.5

הבעיה בהנחה הזו היא שהמעבר לשרת הלוך ושוב אינו תמיד סימטרי, כלומר , ייתכן שהבקשה תימשך זמן רב יותר מהתגובה, או להפך. ככל שזמן האחזור של הרשת ארוך יותר, ההשפעה של האסימטריה הזו גדולה יותר, וכתוצאה מכך צלילים מתעכבים ויושמעו לא מסונכרנים עם מכשירים אחרים.

למרבה המזל, המוח שלנו מחובר כך שלא יבחין אם הצלילים מתעכבים קצת. מחקרים הראו שנדרש עיכוב של 20 עד 30 אלפיות השנייה לפני שהמוח שלנו מבחין בצלילים נפרדים. עם זאת, בערך 12 עד 15 אלפיות השנייה אתם מתחילים "להרגיש" את ההשפעות של אות מאוחר, גם אם לא מצליחים 'לקלוט' אותו במלואו. חקרנו כמה פרוטוקולים קיימים לסנכרון זמן, חלופות פשוטות יותר, וניסינו ליישם חלק מהם. בסופו של דבר - הודות לתשתית של Google לזמן אחזור קצר - הצלחנו פשוט לדגום רצף של בקשות ולהשתמש בדוגמה עם זמן האחזור הנמוך ביותר כחומר עזר.

סטיית שעון לחימה

זה הצליח! היו לנו יותר מ-5 מכשירים שהפעילו דופק בסנכרון מושלם - אבל רק לזמן מה. לאחר הפעלה של מספר דקות, מכשירים התרחקו זה מזה, למרות שתזמננו את האודיו באמצעות זמן ההקשר המדויק ביותר של Web Audio API. הפרש הזמן נצבר באיטיות, בכמה אלפיות שנייה בלבד בכל פעם, ובהתחלה לא ניתן לזהות אותו, אבל השכבות של המוזיקה לא היו מסונכרנות כלל לאחר ההפעלה למשך פרקי זמן ארוכים יותר. שלום, סטיית שעון.

הפתרון היה לסנכרן מחדש כל כמה שניות, לחשב הבדלי שעון חדשים ולהזין אותו בצורה חלקה במתזמן האודיו. כדי להפחית את הסיכון לשינויים משמעותיים במוזיקת המוזיקה בגלל עיכוב בחיבור לרשת, החלטנו לסכך את השינוי על ידי שמירת היסטוריה של הפרשי הסנכרון האחרונים ולחשב את הממוצע.

תזמון שירים וסידורי החלפה

המשמעות של חוויית צליל אינטראקטיבית היא שלא ניתן יותר להחליט מתי חלקים מהשיר יושמעו, כי תלוי בפעולות המשתמש כדי לשנות את המצב הנוכחי. היינו צריכים לוודא שנוכל לעבור בין עיבודים בשיר בזמן המתאים, כך שהמתזמן שלנו היה צריך לחשב כמה נותר מהקטע שמתנגן כרגע לפני המעבר לעיבוד הבא. האלגוריתם שלנו נראה בערך כך:

  • השיר מתחיל לפעול על ידי Client(1).
  • הלקוח הראשון שואל על ידי Client(n) מתי השיר התחיל.
  • Client(n) מחשב נקודת התייחסות לתקופה שבה השיר התחיל באמצעות ההקשר של Web Audio שלו, תוך התחשבות ב-SyncOFFset והזמן שחלף מאז יצירת ההקשר האודיו שלו.
  • 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. במקרה שלנו היינו צריכים גם לקחת בחשבון את הזמן שלוקח לכל הלקוחות לקבל את אותו אירוע ברשת.

אודיו Sprite

שילוב צלילים בקובץ אחד הוא דרך מצוינת לצמצם את בקשות ה-HTTP, הן ל-HTML Audio והן ל-Web Audio API. זו גם הדרך הטובה ביותר להשמיע צלילים ורספונסיבית באמצעות אובייקט האודיו, כי אין צורך לטעון אובייקט אודיו חדש לפני ההפעלה. כבר קיימות מספר הטמעות טובות שהשתמשנו בהן כנקודת התחלה. הרחבנו את ה-Sprite שלנו כך שיפעל באופן מהימן גם ב-iOS וגם ב-Android, וגם לטפל במקרים מוזרים שבהם מכשירים ישנים.

ב-Android, רכיבי האודיו ממשיכים לפעול גם אם מעבירים את המכשיר למצב שינה. במצב שינה, הביצוע של JavaScript מוגבל לצריכת הסוללה, ואי אפשר להסתמך על requestAnimationFrame, setInterval או setTimeout כדי להפעיל קריאות חוזרות (callback). זו בעיה כי תמונות Sprite של אודיו מסתמכות על JavaScript כדי להמשיך לבדוק אם צריך להפסיק את ההפעלה. כדי להחמיר את המצב, במקרים מסוימים currentTime של רכיב האודיו לא מתעדכן למרות שהאודיו עדיין פועל.

כדאי לנסות את ההטמעה של AudioSprite שבה השתמשנו ב-Chrome Racer כחלופה לאודיו שלא קשור לאינטרנט.

רכיב אודיו

כשהתחלנו לעבוד על Racer, Chrome ל-Android עדיין לא תמך ב-Web Audio API. הלוגיקה של השימוש באודיו ב-HTML במכשירים מסוימים, Web Audio API באחרים, בשילוב עם פלט האודיו המתקדם שרצינו להשיג, שנועד להתמודד עם כמה אתגרים מעניינים. למרבה המזל, עכשיו כל ההיסטוריה היא שם. Web Audio API מוטמע בגרסת הבטא של Android M28.

  • עיכובים/בעיות תזמון. רכיב האודיו לא תמיד יופעל בדיוק כשאמרתם לו לפעול. מכיוון ש-JavaScript פועל בשרשור יחיד, הדפדפן עשוי להיות עמוס, דבר שיגרום לעיכוב של עד שתי שניות בהפעלה.
  • המשמעות של השהיית ההפעלה היא שלא תמיד אפשר ליהנות מהפעלה חלקה בלופ. במחשב אפשר להשתמש באחסון נתונים זמני כפול כדי ליצור לולאות חסרות פערים, אבל במכשירים ניידים האפשרות הזו לא זמינה כי:
    • ברוב המכשירים הניידים לא ניתן להפעיל יותר מרכיב אודיו אחד בו-זמנית.
    • נפח קבוע. אין אפשרות לשנות את עוצמת הקול של אובייקט אודיו ב-Android או ב-iOS.
  • אין טעינה מראש. במכשירים ניידים, רכיב האודיו לא יתחיל לטעון את המקור שלו אלא אם תופעל הפעלה ב-handler של touchStart.
  • חיפוש בעיות. קבלה של duration או הגדרה של currentTime ייכשלו, אלא אם השרת שלך תומך ב-HTTP Byte-Range. שימו לב לקטע הזה אם אתם בונים Sprite של אודיו כמו שאנחנו עשינו.
  • האימות הבסיסי ב-MP3 נכשל. מכשירים מסוימים לא מצליחים לטעון קובצי MP3 המוגנים על ידי 'אימות בסיסי', ללא קשר לדפדפן שבו אתם משתמשים.

מסקנות

עברנו כברת דרך ארוכה מאז הלחיצה על לחצן ההשתקה וזוהי האפשרות הטובה ביותר להתמודד עם צלילים באינטרנט, אבל זו רק ההתחלה, והאודיו באינטרנט עומד להשתבש. נגענו רק בפני השטח של מה שאפשר לעשות בכל הנוגע לסנכרון של מכשירים מרובים. לא היה לנו בטלפונים ובטאבלטים את עוצמת העיבוד המתאימה כדי לצלול לעיבוד אותות ולאפקטים (כמו הדהוד), אבל ככל שביצועי המכשיר ישפרו, משחקים מבוססי-אינטרנט ייהנו גם מהיתרונות של התכונות האלה. אלה זמנים מרגשים שבהם ניתן להשתמש במגוון רחב של סאונד.