פיתוח אודיו של משחק באמצעות ממשק ה-API של Web Audio

מבוא

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

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

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

אודיו של משחקים באינטרנט

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

עם זאת, כשמתעמקים במפרט התגים של <audio>, ברור שיש הרבה דברים שפשוט אי אפשר לעשות איתו, וזה לא מפתיע, כי הוא מיועד להפעלת מדיה. בין המגבלות:

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

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

מוסיקת רקע

במשחקים רבים יש מוזיקה ברקע שמופעלת בלופ.

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

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

רצועת חנייה

לאחר מכן, באמצעות Web Audio API, תוכלו לייבא את כל הדוגמאות האלה באמצעות משהו כמו המחלקה BufferLoader דרך 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;
}

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

מפתחי משחקים רבים משתמשים כיום בתג <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 אפשר לשנות בקלות את הדוגמה שלמעלה בשתי דרכים:

  1. עם שינוי קל בזמן בין הירי של הכדורים
  2. שינוי של 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 כולל גם מהירות לתנועות דופלר. בדוגמה הזו מוצג אפקט דופלר בפירוט רב יותר.

למידע נוסף בנושא הזה, אפשר לעיין במדריך המפורט בנושא [מיקס של אודיו מיקומי ו-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);

וניתן לזהות חיתוך ב-handler הבא של 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 במשחקים אמיתיים: