שילוב אודיו מקומי ו-WebGL

Ilmari Heikkinen

מבוא

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

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

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

// Detect if the audio context is supported.
window.AudioContext = (
  window.AudioContext ||
  window.webkitAudioContext ||
  null
);

if (!AudioContext) {
  throw new Error("AudioContext not supported!");
} 

// Create a new audio context.
var ctx = new AudioContext();

// Create a AudioGainNode to control the main volume.
var mainVolume = ctx.createGain();
// Connect the main volume node to the context destination.
mainVolume.connect(ctx.destination);

// Create an object with a sound source and a volume control.
var sound = {};
sound.source = ctx.createBufferSource();
sound.volume = ctx.createGain();

// Connect the sound source to the volume control.
sound.source.connect(sound.volume);
// Hook up the sound volume control to the main volume.
sound.volume.connect(mainVolume);

// Make the sound source loop.
sound.source.loop = true;

// Load a sound file using an ArrayBuffer XMLHttpRequest.
var request = new XMLHttpRequest();
request.open("GET", soundFileName, true);
request.responseType = "arraybuffer";
request.onload = function(e) {

  // Create a buffer from the response ArrayBuffer.
  ctx.decodeAudioData(this.response, function onSuccess(buffer) {
    sound.buffer = buffer;

    // Make the sound source use the buffer and start playing it.
    sound.source.buffer = sound.buffer;
    sound.source.start(ctx.currentTime);
  }, function onFailure() {
    alert("Decoding the audio buffer failed");
  });
};
request.send();

מיקום

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

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

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

...
sound.panner = ctx.createPanner();
// Instead of hooking up the volume to the main volume, hook it up to the panner.
sound.volume.connect(sound.panner);
// And hook up the panner to the main volume.
sound.panner.connect(mainVolume);
...

בכל פריים, צריך לעדכן את המיקומים של AudioPannerNodes. אני אשתמש ב-Three.js בדוגמאות הבאות.

...
// In the frame handler function, get the object's position.
object.position.set(newX, newY, newZ);
object.updateMatrixWorld();
var p = new THREE.Vector3();
p.setFromMatrixPosition(object.matrixWorld);

// And copy the position over to the sound of the object.
sound.panner.setPosition(p.x, p.y, p.z);
...

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

...
// Get the camera position.
camera.position.set(newX, newY, newZ);
camera.updateMatrixWorld();
var p = new THREE.Vector3();
p.setFromMatrixPosition(camera.matrixWorld);

// And copy the position over to the listener.
ctx.listener.setPosition(p.x, p.y, p.z);
...

מהירות

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

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

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

...
var dt = secondsSinceLastFrame;

var p = new THREE.Vector3();
p.setFromMatrixPosition(object.matrixWorld);
var px = p.x, py = p.y, pz = p.z;

object.position.set(newX, newY, newZ);
object.updateMatrixWorld();

var q = new THREE.Vector3();
q.setFromMatrixPosition(object.matrixWorld);
var dx = q.x-px, dy = q.y-py, dz = q.z-pz;

sound.panner.setPosition(q.x, q.y, q.z);
sound.panner.setVelocity(dx/dt, dy/dt, dz/dt);
...

כיוון

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

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

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

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

...
var vec = new THREE.Vector3(0,0,1);
var m = object.matrixWorld;

// Save the translation column and zero it.
var mx = m.elements[12], my = m.elements[13], mz = m.elements[14];
m.elements[12] = m.elements[13] = m.elements[14] = 0;

// Multiply the 0,0,1 vector by the world matrix and normalize the result.
vec.applyProjection(m);
vec.normalize();

sound.panner.setOrientation(vec.x, vec.y, vec.z);

// Restore the translation column.
m.elements[12] = mx;
m.elements[13] = my;
m.elements[14] = mz;
...

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

...
// The camera's world matrix is named "matrix".
var m = camera.matrix;

var mx = m.elements[12], my = m.elements[13], mz = m.elements[14];
m.elements[12] = m.elements[13] = m.elements[14] = 0;

// Multiply the orientation vector by the world matrix of the camera.
var vec = new THREE.Vector3(0,0,1);
vec.applyProjection(m);
vec.normalize();

// Multiply the up vector by the world matrix.
var up = new THREE.Vector3(0,-1,0);
up.applyProjection(m);
up.normalize();

// Set the orientation and the up-vector for the listener.
ctx.listener.setOrientation(vec.x, vec.y, vec.z, up.x, up.y, up.z);

m.elements[12] = mx;
m.elements[13] = my;
m.elements[14] = mz;
...

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

...
sound.panner.coneInnerAngle = innerAngleInDegrees;
sound.panner.coneOuterAngle = outerAngleInDegrees;
sound.panner.coneOuterGain = outerGainFactor;
...

הכול ביחד

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

השפעות סביבתיות

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

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

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

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

...
var ctx = new webkitAudioContext();
var mainVolume = ctx.createGain();

// Create a convolver to apply environmental effects to the audio.
var convolver = ctx.createConvolver();

// Create a mixer that receives sound from the panners.
var mixer = ctx.createGain();

sounds.forEach(function(sound){
  sound.panner.connect(mixer);
});

// Create volume controllers for the plain audio and the convolver.
var plainGain = ctx.createGain();
var convolverGain = ctx.createGain();

// Send audio from the mixer to plainGain and the convolver node.
mixer.connect(plainGain);
mixer.connect(convolver);

// Hook up the convolver to its volume control.
convolver.connect(convolverGain);

// Send audio from the volume controls to the main volume control.
plainGain.connect(mainVolume);
convolverGain.connect(mainVolume);

// Finally, connect the main volume to the audio context's destination.
volume.connect(ctx.destination);
...

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

...
loadBuffer(ctx, "impulseResponseExample.wav", function(buffer){
  convolver.buffer = buffer;
  convolverGain.gain.value = 0.7;
  plainGain.gain.value = 0.3;
})
...
function loadBuffer(ctx, filename, callback) {
  var request = new XMLHttpRequest();
  request.open("GET", soundFileName, true);
  request.responseType = "arraybuffer";
  request.onload = function() {
    // Create a buffer and keep the channels unchanged.
    ctx.decodeAudioData(request.response, callback, function() {
      alert("Decoding the audio buffer failed");
    });
  };
  request.send();
}

סיכום

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

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

קובצי עזר