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

מבוא

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

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

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

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

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

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

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

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

מוזיקת רקע

לרוב, מוזיקת הרקע מתנגנת בלופ.

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

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

רצועת גראז&#39;

לאחר מכן, באמצעות Web Audio API, תוכלו לייבא את כל הדוגמאות האלה באמצעות XHR, באמצעות משהו כמו BufferLoader class (המאמר הזה מוסבר בפירוט במאמר המבוא בנושא 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, אפשר לעיין במאמר הקצר הזה.

אפקטים קוליים

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

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

צליל תלוי מיקום בתלת-ממד

המשחקים מוצגים בדרך כלל בעולם עם מאפיינים גאומטריים מסוימים, בדו-ממד או בתלת-ממד. במקרה כזה, אודיו שממוקם בסטריאו יכול להגביר משמעותית את החווייה הסוחפת. למרבה המזל, 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();
    }
};

דברים שחשוב לדעת על אופן הטיפול של 'אודיו באינטרנט' ביחס לשימוש מרחבי:

  • כברירת מחדל, ה-listener נמצא במקור (0, 0, 0).
  • ממשקי API תלויי מיקום של Web Audio הם חסרי יחידות, לכן הוספתי מכפיל כדי שההדגמה תישמע טוב יותר.
  • Web Audio משתמש בקואורדינטות קרטזית y-is-up (ההיפך מרוב מערכות הגרפיקה במחשב). לכן אני מחליף את ציר ה-Y בקטע שלמעלה

אפשרויות מתקדמות: קונוסים קוליים

המודל תלוי המיקום הוא חזק מאוד ומתקדם, בעיקרו על סמך OpenAL. לפרטים נוספים, קראו את סעיפים 3 ו-4 במפרט המקושר למעלה.

מודל מיקום

קיים AudioListener אחד שמצורף להקשר של Web Audio API , ואפשר להגדיר אותו במרחב באמצעות מיקום וכיוון. ניתן להעביר כל מקור דרך AudioPannerNode, שמציגה מרחבית את האודיו בקלט. לצומת ההזזה יש מיקום וכיוון, וגם מרחק ומודל כיווני.

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

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. באמצעות הטכניקות האלה תוכלו ליצור חוויות אודיו מושכות מאוד ישירות מהדפדפן. לפני שאסיים, אני רוצה לתת לכם טיפ ספציפי לדפדפן: אם הכרטיסייה עוברת לרקע באמצעות ממשק ה-API של חשיפה לדף, זה עלול ליצור חוויה שעלולה להיות מתסכלת למשתמש.

למידע נוסף על Web Audio, כדאי לקרוא את המאמר המבואר יותר. אם יש לך שאלה, אפשר לעיין בשאלות נפוצות בנושא אודיו באינטרנט. לבסוף, אם יש לכם שאלות נוספות, שאלו אותן ב-Stack Overflow באמצעות התג web-audio.

לפני שאסיים, אני רוצה לתת לך מידע על שימושים מדהימים ב-WebAudio API במשחקים אמיתיים: