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

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

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

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

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

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

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

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

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

מוסיקת רקע

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

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

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

Garageband

לאחר מכן, באמצעות 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;
}

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

מפתחי משחקים רבים משתמשים כיום בתג <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);

אפשר לזהות קיצוץ במטפל ה-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 במשחקים אמיתיים היום: