מבוא
האודיו הוא חלק משמעותי מהסיבה לחוויית הצפייה המרתקת במולטימדיה. אם ניסיתם פעם לצפות בסרט בלי קול, סביר להניח שזה קרה לכם.
משחקים הם לא יוצאי דופן! הזיכרונות הכי טובים שלי ממשחקי וידאו הם מהמוזיקה והאפקטים הקוליים. עכשיו, כמעט שני עשורים אחרי ששיחקתי במשחקים האהובים עלי, עדיין לא הצלחתי להוציא מהראש את הלחנים של Koji Kondo ל-Zelda ואת פסקול האטמוספרי של Matt Uelmen ל-Diablo. אותו עיקרון רלוונטי גם לאפקטים קוליים, כמו התגובות המיידיות של יחידות הקליקים מ-Warcraft ודגימות מהמשחקים הקלאסיים של Nintendo.
אודיו של משחקים מציב כמה אתגרים מעניינים. כדי ליצור מוזיקה משכנעת למשחק, מעצבים צריכים להתאים את עצמם למצב המשחק שעלול להיות בלתי צפוי, שבו השחקן נמצא. בפועל, חלקים מהמשחק יכולים להימשך למשך לא ידוע, צלילים יכולים לקיים אינטראקציה עם הסביבה ולהתערבב בדרכים מורכבות, כמו אפקטים של חדרים ומיקום יחסי של צלילים. לבסוף, יכול להיות שיישמע מספר גדול של צלילים בו-זמנית, וכל אחד מהם צריך להישמע טוב ביחד ולהירנדור בלי לפגוע בביצועים.
אודיו של משחקים באינטרנט
במשחקים פשוטים, יכול להיות ששימוש בתג <audio>
יספיק. עם זאת, בדפדפנים רבים יש הטמעות גרועות, וכתוצאה מכך יש שיבושים באודיו וזמן אחזור ארוך. אנחנו מקווים שזו בעיה זמנית, כי הספקים משקיעים מאמצים רבים בשיפור ההטמעות שלהם. כדי לקבל הצצה למצב של התג <audio>
, יש חבילת בדיקות שימושית באתר areweplayingyet.org.
עם זאת, כשבודקים לעומק את מפרט התג <audio>
, מתברר שיש הרבה דברים שפשוט אי אפשר לעשות איתו, וזה לא מפתיע כי הוא תוכנן להפעלת מדיה. בין המגבלות:
- אי אפשר להחיל מסננים על אות הצליל
- אין דרך לגשת לנתוני PCM הגולמיים
- אין מושג של מיקום וכיוון של מקורות ומאזינים
- אין תזמון מפורט.
בהמשך המאמר אעמיק בחלק מהנושאים האלה בהקשר של אודיו במשחקים שנכתב באמצעות Web Audio API. במדריך למתחילים מוסבר על ממשק ה-API הזה.
מוסיקת רקע
במשחקים רבים יש מוזיקה ברקע שמופעלת בלופ.
זה יכול להיות מאוד מעצבן אם הלולאה קצרה וצפויה. אם שחקן תקוע באזור או ברמה מסוימים, ואותו טראק מופעל ברציפות ברקע, כדאי להפחית בהדרגה את עוצמת הטראק כדי למנוע תסכול נוסף. שיטה נוספת היא ליצור שילובים של עוצמות שונות שמתמזגים בהדרגה זה בזה, בהתאם להקשר של המשחק.
לדוגמה, אם השחקן נמצא באזור עם קרב מאסף מרהיב, יכול להיות שתשתמשו בכמה רמיקסים עם טווח רגשי משתנה, החל מאווירה ועד לחזות את העתיד או ליצירת מצב אינטנסיבי. בדרך כלל, תוכנות סינתזה של מוזיקה מאפשרות לייצא כמה רמיקסים (באותו אורך) על סמך קטע מסוים, על ידי בחירת קבוצת הטראקים שבהם רוצים להשתמש בייצוא. כך תוכלו לשמור על עקביות פנימית מסוימת ולהימנע ממעברים חדים בזמן המעבר בין טראקים.

לאחר מכן, באמצעות Web Audio API, תוכלו לייבא את כל הדגימות האלה באמצעות משהו כמו BufferLoader class דרך XHR (הנושא הזה מוסבר בהרחבה במאמר המבוא על Web Audio API). טעינת צלילים אורכת זמן, לכן נכסים שמשמשים במשחק צריכים להיטען כשהדף נטען, בתחילת השלב או אולי באופן מצטבר בזמן שהשחקן משחק.
בשלב הבא יוצרים מקור לכל צומת וצומת רווח לכל מקור ומחברים את התרשים.
לאחר מכן תוכלו להשמיע את כל המקורות האלה בו-זמנית בלופ, ומכיוון שהם באורך זהה, Web Audio API יבטיח שהם יישארו מותאמים. ככל שהדמות מתקרבת או מתרחקת מהקרב האחרון מול הבוס, המשחק יכול לשנות את ערכי הרווח לכל אחד מהצומתים המתאימים בשרשרת, באמצעות אלגוריתם של סכום הרווח, כמו זה:
// Assume gains is an array of AudioGainNode, normVal is the intensity
// between 0 and 1.
var value = normVal - (gains.length - 1);
// First reset gains on all nodes.
for (var i = 0; i < gains.length; i++) {
gains[i].gain.value = 0;
}
// Decide which two nodes we are currently between, and do an equal
// power crossfade between them.
var leftNode = Math.floor(value);
// Normalize the value between 0 and 1.
var x = value - leftNode;
var gain1 = Math.cos(x - 0.5*Math.PI);
var gain2 = Math.cos((1.0 - x) - 0.5*Math.PI);
// Set the two gains accordingly.
gains[leftNode].gain.value = gain1;
// Check to make sure that there's a right node.
if (leftNode < gains.length - 1) {
// If there is, adjust its gain.
gains[leftNode + 1].gain.value = gain2;
}
בגישה שלמעלה, שני מקורות מופעלים בו-זמנית, ואנחנו מבצעים מעבר חלק ביניהם באמצעות עקומות הספק שוות (כפי שמתואר במבוא).
הקישור החסר: תג אודיו ל-Web Audio
מפתחי משחקים רבים משתמשים כיום בתג <audio>
למוזיקה ברקע, כי הוא מתאים במיוחד לתוכן בסטרימינג. עכשיו אפשר להעביר תוכן מהתג <audio>
להקשר של Web Audio.
השיטה הזו יכולה להיות שימושית כי התג <audio>
יכול לפעול עם תוכן בסטרימינג, וכך תוכלו להשמיע את המוזיקה ברקע באופן מיידי במקום להמתין להורדה של כל התוכן. כשמביאים את המקור לנתונים ל-Web Audio API, אפשר לבצע בו מניפולציות או לנתח אותו. בדוגמה הבאה חל מסנן מסוג 'שידור פס נמוך' על המוזיקה שמושמעת דרך התג <audio>
:
var audioElement = document.querySelector('audio');
var mediaSourceNode = context.createMediaElementSource(audioElement);
// Create the filter
var filter = context.createBiquadFilter();
// Create the audio graph.
mediaSourceNode.connect(filter);
filter.connect(context.destination);
לסקירה מלאה יותר על שילוב התג <audio>
עם Web Audio API, אפשר לעיין במאמר הקצר הזה.
אפקטים קוליים
במשחקים, אפקטים של צלילים מושמעים לעיתים קרובות בתגובה לקלט של המשתמש או לשינויים במצב המשחק. עם זאת, כמו מוזיקת רקע, אפקטים קוליים יכולים להפוך למעצבנים מאוד מהר. כדי למנוע זאת, מומלץ להכין מאגר של צלילים דומים אך שונים. אלה יכולות להיות וריאציות קלות של דגימות של צעדים, או וריאציות דרסטיות, כמו בתגובה ללחיצה על יחידות בסדרת Warcraft.
מאפיין מרכזי נוסף של אפקטים קוליים במשחקים הוא שיכולים להיות הרבה מהם בו-זמנית. נסו לדמיין שאתם נמצאים באמצע קרב יריות עם כמה שחקנים שמשתמשים במקלעים. כל מקלע יורה כמה פעמים בשנייה, וכתוצאה מכך מושמעים בו-זמנית עשרות אפקטים קוליים. ה-Web Audio API מצוין בהפעלת קול ממספר מקורות בו-זמנית, עם תזמון מדויק.
בדוגמה הבאה נוצרת ירייה ממקלע מכמה דגימות של כדורים נפרדים, על ידי יצירת כמה מקורות קול שההפעלה שלהם מושהית בזמן.
var time = context.currentTime;
for (var i = 0; i < rounds; i++) {
var source = this.makeSource(this.buffers[M4A1]);
source.noteOn(time + i - interval);
}
עכשיו, אם כל המקלעים במשחק שלכם יישמעו בדיוק כך, זה יהיה די משעמם. כמובן שהם ישתנו בהתאם לסאונד, בהתאם למרחק מהיעד ולמיקום היחסי (נפרט על כך בהמשך), אבל יכול להיות שגם זה לא יספיק. למרבה המזל, באמצעות Web Audio API אפשר לשנות בקלות את הדוגמה שלמעלה בשתי דרכים:
- עם שינוי קל בזמן בין הירי של הכדורים
- שינוי של playbackRate של כל דגימה (שינוי של הטון גם כן) כדי לדמות טוב יותר את האקראיות של העולם האמיתי.
לדוגמה מעשית יותר של השיטות האלה בפעולה, אפשר לעיין בהדגמה של שולחן ביליארד, שבה נעשה שימוש במדגם אקראי ובשינוי של playbackRate כדי ליצור צליל מעניין יותר של התנגשות בין כדורים.
צליל מיקומי תלת-ממדי
משחקים מתרחשים לרוב בעולם עם מאפיינים גיאומטריים מסוימים, ב-2D או ב-3D. במקרה כזה, אודיו במיקום סטריאו יכול לשפר משמעותית את חוויית הצפייה. למרבה המזל, Web Audio API כולל תכונות מובנות של אודיו מיקומי שמואץ בחומרה, והשימוש בהן פשוט למדי. דרך אגב, כדי שהדוגמה הבאה תהיה מובנת, צריך לוודא שיש לכם רמקולים סטריאופוניים (רצוי אוזניות).
בדוגמה שלמעלה, יש מאזין (סמל אדם) באמצע הלוח, והעכבר משפיע על המיקום של המקור (סמל דובר). הדוגמה שלמעלה היא דוגמה פשוטה לשימוש ב-AudioPannerNode כדי להשיג אפקט כזה. הרעיון הבסיסי של הדוגמה שלמעלה הוא להגיב לתנועת העכבר על ידי הגדרת המיקום של מקור האודיו, באופן הבא:
PositionSample.prototype.changePosition = function(position) {
// Position coordinates are in normalized canvas coordinates
// with -0.5 < x, y < 0.5
if (position) {
if (!this.isPlaying) {
this.play();
}
var mul = 2;
var x = position.x / this.size.width;
var y = -position.y / this.size.height;
this.panner.setPosition(x - mul, y - mul, -0.5);
} else {
this.stop();
}
};
דברים שכדאי לדעת על הטיפול של Web Audio במיקום סטריאו:
- כברירת מחדל, המאזין נמצא במקור (0, 0, 0).
- ממשקי ה-API למיקום של Web Audio הם ללא יחידה, לכן הוספתי מכפיל כדי לשפר את הצליל של הדמו.
- ב-Web Audio נעשה שימוש בקואורדינטות קרטוזיות שבהן ציר y הוא למעלה (בניגוד לרוב מערכות הגרפיקה הממוחשבת). לכן החלפתי את ציר ה-y בקטע הקוד שלמעלה
מתקדם: חרוטות קול
המודל המבוסס על מיקום הוא חזק מאוד ומתקדם למדי, והוא מבוסס במידה רבה על OpenAL. פרטים נוספים זמינים בסעיפים 3 ו-4 במפרט שמקושר למעלה.

יש אובייקט AudioListener אחד שמצורף להקשר של Web Audio API, ואפשר להגדיר אותו במרחב באמצעות המיקום והכיוון. אפשר להעביר כל מקור דרך AudioPannerNode, שממיר את האודיו של הקלט למרחבי. לצומת ה-panner יש מיקום וכיוון, וגם מודל מרחק וכיוון.
מודל המרחק מציין את כמות ההגברה בהתאם לקרבה למקור, ואילו מודל הכיוון מאפשר להגדיר חרוט פנימי וחיצוני, שמגדירים את כמות ההגברה (בדרך כלל שלילית) אם המאזין נמצא בתוך החרוט הפנימי, בין החרוט הפנימי לחרוט החיצוני או מחוץ לחרוט החיצוני.
var panner = context.createPanner();
panner.coneOuterGain = 0.5;
panner.coneOuterAngle = 180;
panner.coneInnerAngle = 0;
הדוגמה שלי היא דו-ממדית, אבל אפשר להכליל בקלות את המודל הזה למאפיין שלישי. דוגמה לקול במרחב תלת-ממדי מופיעה בטעימת מיקום הזו. בנוסף למיקום, מודל האודיו של Web Audio כולל גם מהירות לתנודות דופלר (Doppler). בדוגמה הזו מוצג אפקט דופלר בפירוט רב יותר.
למידע נוסף בנושא הזה, אפשר לעיין במדריך המפורט בנושא [מיקס של אודיו מיקומי ו-WebGL][webgl].
אפקטים ומסננים של חדרים
בפועל, האופן שבו קול נתפס תלוי במידה רבה בחדר שבו הקול נשמע. אותה דלת חורקת תשמע שונה מאוד במרתף, בהשוואה לאולם גדול ופתוח. במשחקים עם ערך ייצור גבוה כדאי לחקות את האפקטים האלה, כי יצירת קבוצה נפרדת של נכסי דגימה לכל סביבה היא יקרה מאוד, ותגרום ליצירת עוד יותר נכסים ולכמות גדולה יותר של נתוני משחק.
באופן כללי, המונח באודיו להבדל בין האודיו הגולמי לבין האופן שבו הוא נשמע בפועל הוא תגובת הדחף. אפשר להקליט את תגובות הלם כאלה ביסודיות, ולמעשה יש אתרים שמארחים הרבה מקובצי תגובות הלם שהוקלטו מראש (ששמורים כאודיו) לנוחותכם.
מידע נוסף על יצירת תגובות דחף בסביבה נתונה זמין בקטע 'הגדרת ההקלטה' בחלק Convolution במפרט של Web Audio API.
חשוב יותר לצרכים שלנו, ש-Web Audio API מספק דרך קלה להחיל את תגובות הפולסים האלה על הצלילים שלנו באמצעות ConvolverNode.
// Make a source node for the sample.
var source = context.createBufferSource();
source.buffer = this.buffer;
// Make a convolver node for the impulse response.
var convolver = context.createConvolver();
convolver.buffer = this.impulseResponseBuffer;
// Connect the graph.
source.connect(convolver);
convolver.connect(context.destination);
מומלץ גם לצפות בהדגמה הזו של אפקטי חדר בדף המפרט של Web Audio API, וגם בדוגמה הזו שמאפשרת לכם לשלוט בערבוב של גרסה יבשה (גולמית) וגרסה רטובה (שעברה עיבוד באמצעות קולבטור) של סטנדרט ג'אז נהדר.
הספירה לאחור הסופית
אז יצרתם משחק, הגדרתם את האודיו המיקומי ועכשיו יש לכם מספר גדול של AudioNodes בתרשים, והכול מופעל בו-זמנית. מצוין, אבל יש עוד דבר אחד שכדאי לחשוב עליו:
מכיוון שקולות מרובים מוערמים זה על גבי זה ללא נורמליזציה, יכול להיות שתגיעו למצב שבו תחרגו מהסף של יכולת הרמקול. בדומה לתמונות שחורגות מגבולות הקנבס, גם צלילים יכולים לעבור קליפ אם צורת הגל חורגת מהסף המקסימלי שלה, וכתוצאה מכך נוצר עיוות בולט. צורת הגל נראית כך:

הנה דוגמה אמיתית לחיתוך בפעולה. צורת הגל נראית לא טובה:

חשוב להאזין לעיוותים חריפים כמו זה שלמעלה, או להפך, למיקסים חלשים מדי שמאלצים את המאזינים להגביר את עוצמת הקול. אם המצב הזה קורה לכם, חשוב מאוד לתקן אותו.
זיהוי קיצוץ
מבחינה טכנית, חיתוך קורה כשערך האות בכל ערוץ חורג מהטווח החוקי, כלומר בין -1 ל-1. אחרי שמזהים את הבעיה, כדאי לתת משוב חזותי על כך שהיא מתרחשת. כדי לעשות זאת בצורה מהימנה, מוסיפים לתרשים JavaScriptAudioNode. תרשים האודיו יוגדר באופן הבא:
// Assume entire sound output is being piped through the mix node.
var meter = context.createJavaScriptNode(2048, 1, 1);
meter.onaudioprocess = processAudio;
mix.connect(meter);
meter.connect(context.destination);
אפשר לזהות קיצוץ במטפל ה-processAudio
הבא:
function processAudio(e) {
var buffer = e.inputBuffer.getChannelData(0);
var isClipping = false;
// Iterate through buffer to check if any of the |values| exceeds 1.
for (var i = 0; i < buffer.length; i++) {
var absValue = Math.abs(buffer[i]);
if (absValue >= 1) {
isClipping = true;
break;
}
}
}
באופן כללי, חשוב להיזהר שלא להשתמש יותר מדי ב-JavaScriptAudioNode
מסיבות שקשורות לביצועים. במקרה כזה, הטמעה חלופית של המדידה יכולה לבדוק RealtimeAnalyserNode
בתרשים האודיו עבור getByteFrequencyData
, בזמן העיבוד, כפי שנקבע על ידי requestAnimationFrame
. הגישה הזו יעילה יותר, אבל היא מחמיצה את רוב האות (כולל מקומות שבהם הוא עלול להיות חתוך), כי היצירה מתבצעת לכל היותר 60 פעמים בשנייה, בעוד שאות האודיו משתנה מהר הרבה יותר.
זיהוי קליפים חשוב מאוד, ולכן סביר להניח שנראה בעתיד צומת מובנה של MeterNode
Web Audio API.
מניעת חיתוך
על ידי שינוי הגבר ב-AudioGainNode הראשי, תוכלו להפחית את עוצמת המיקס לרמה שמונעת קיצוץ. עם זאת, בפועל, מכיוון שהצלילים שמתנגנים במשחק עשויים להיות תלויים במגוון עצום של גורמים, יכול להיות שיהיה קשה לקבוע את ערך הגבר הראשי שימנע קיצוץ בכל המצבים. באופן כללי, מומלץ לשנות את הרווחים כדי לצפות לתרחיש הגרוע ביותר, אבל זו יותר אומנות מאשר מדע.
מוסיפים קצת סוכר
בדרך כלל משתמשים בקומפרסורים בהפקת מוזיקה ומשחקים כדי להחליק את האות ולשלוט בפסגות באות הכולל. הפונקציונליות הזו זמינה בעולם Web Audio באמצעות הפונקציה DynamicsCompressorNode
, שאפשר להוסיף לתרשים האודיו כדי לקבל צליל חזק יותר, עשיר יותר ומלא יותר, וגם כדי למנוע קיצוץ.
ציטוט ישיר של המפרט, הצומת הזה
בדרך כלל מומלץ להשתמש בלחץ דינמי, במיוחד בסביבת משחקים, שבה, כפי שצוין קודם, לא יודעים בדיוק אילו צלילים יושמעו ומתי. Plink מ-DinahMoe labs הוא דוגמה מצוינת לכך, כי הצלילים שמושמעים תלויים בכם ובמשתתפים האחרים. קומפרסור שימושי ברוב המקרים, מלבד מקרים נדירים שבהם מדובר בטראקים שעברו עיבוד מאסטר בקפידה והצליל שלהם כבר 'מושלם'.
כדי להטמיע את זה, פשוט צריך לכלול את הקוד DynamicsCompressorNode בתרשים האודיו, בדרך כלל כנקודה האחרונה לפני היעד:
// Assume the output is all going through the mix node.
var compressor = context.createDynamicsCompressor();
mix.connect(compressor);
compressor.connect(context.destination);
המאמר הזה בוויקיפדיה מכיל מידע מפורט על דחיסת דינמיקה.
לסיכום, חשוב להקשיב היטב כדי לזהות קליפים ולמנוע אותם על ידי הוספת צומת של הגברה ראשית. לאחר מכן, צמצום המיקס כולו באמצעות צומת של דינמיק קומפרסור. תרשים האודיו עשוי להיראות כך:

סיכום
זהו סיכום של מה שלדעתי הם ההיבטים החשובים ביותר בפיתוח אודיו למשחקים באמצעות Web Audio API. בעזרת הטכניקות האלה תוכלו ליצור חוויות אודיו מרתקות ממש בדפדפן. לפני שנסיים, רציתי לתת לכם טיפ ספציפי לדפדפן: חשוב להשהות את הצליל אם הכרטיסייה עוברת לרקע באמצעות page visibility API, אחרת אתם עלולים ליצור חוויה מתסכלת למשתמש.
למידע נוסף על Web Audio, אפשר לעיין במאמר למתחילים. אם יש לכם שאלה, כדאי לבדוק אם היא כבר נענתה בשאלות הנפוצות בנושא Web Audio. לסיום, אם יש לכם שאלות נוספות, אתם יכולים לפרסם אותן ב-Stack Overflow באמצעות התג web-audio.
לפני שאסיים, רציתי לציין כמה שימושים מדהימים של Web Audio API במשחקים אמיתיים היום:
- Field Runners, וכן סקירה על חלק מהפרטים הטכניים.
- Angry Birds, שהמעבר ל-Web Audio API בוצע לאחרונה. מידע נוסף זמין בסקירה הזו.
- Skid Racer, שבו האודיו המרחבי מנוצל בצורה יעילה.