المزج بين الصوت الموضعي وWebGL

مقدمة

في هذه المقالة، سأتحدث عن كيفية استخدام ميزة الصوت المكاني في Web Audio API لإضافة صوت ثلاثي الأبعاد إلى مَشاهد WebGL. لجعل الصوت أكثر إقناعًا، سأعرّفك أيضًا على المؤثرات البيئية التي يمكن إجراؤها باستخدام Web Audio API. للحصول على مقدمة أكثر تفصيلاً عن Web Audio API، اطّلِع على مقالة البدء باستخدام Web Audio API التي كتبها "بوريس سموس".

لإنشاء صوت موضعي، يمكنك استخدام AudioPannerNode في واجهة برمجة تطبيقات Web Audio. تحدِّد AudioPannerNode موضع الصوت واتجاهه وسرعته. بالإضافة إلى ذلك، يحتوي سياق الصوت في Web Audio API على سمة مستمع تتيح لك تحديد موضع المستمع واتجاهه وسرعته. باستخدام هذين المكوّنين، يمكنك إنشاء أصوات اتجاهية باستخدام تأثيرات دوبلر والتمويه الصوتي الثلاثي الأبعاد.

لنطّلِع على شكل رمز الصوت للمشهد أعلاه. هذا رمز برمجي أساسي جدًا لواجهة برمجة التطبيقات 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);
...

السرعة المتّجهة

الآن بعد أن حصلنا على مواضع المستمع وAudioPannerNode، لنركّز على سرعاتهما. من خلال تغيير سمات السرعة للمستمع و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) به لمعرفة الاتجاه الذي يشير إليه. بالنسبة إلى اتجاه مستمع السياق، عليك الحصول على متجه اتجاه الكاميرا. يحتاج اتجاه المستمع أيضًا إلى متجه للأعلى، لأنّه يحتاج إلى معرفة زاوية دوران رأس المستمع. لاحتساب اتجاه المستمع، احصل على جزء التدوير من مصفوفة عرض الكاميرا واضرب 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;
...

لضبط مخروط الصوت للصوت، يجب عليك تعيين الخصائص المناسبة لعقدة البانر. يتم التعبير عن زوايا الأسطوانة بالدرجات وتتراوح بين 0 و360.

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

معًا

باختصار، يتتبّع مستمع سياق الصوت موضع الكاميرا واتجاهها وسرعتها، وتتتبّع عناصر AudioPannerNodes مواضع مصادر الصوت واتجاهاتها وسرعاتها. تحتاج إلى تعديل مواضع وسرعات واتّجاهات AudioPannerNodes وأداة استماع سياق الصوت على كل إطار.

التأثيرات البيئية

بعد إعداد ميزة الصوت الموضعي، يمكنك ضبط التأثيرات البيئية للصوت لتقديم تجربة غامرة للمشهد الثلاثي الأبعاد. لنفترض أن المشهد يقع داخل كاتدرائية كبيرة. في الإعدادات التلقائية، تبدو الأصوات في المشهد وكأنّك تقف في الهواء الطلق. ويؤدي هذا التناقض بين العناصر المرئية والصوتية إلى إيقاف تجربة الغمر في الفيديو وجعل المشهد أقل روعة.

تحتوي واجهة برمجة التطبيقات Web Audio API على ConvolverNode التي تتيح لك ضبط التأثير البيئي للصوت. أضِفه إلى الرسم البياني للمعالجة لمصدر الصوت وستتمكّن من ضبط الصوت بما يناسب الإعداد. يمكنك العثور على عيّنات استجابة النبضات على الويب واستخدامها مع ConvolverNodes، ويمكنك أيضًا إنشاء عيّنات بنفسك. قد تكون تجربة مرهقة بعض الشيء حيث تحتاج إلى تسجيل الاستجابة التحفيزية للمكان الذي ترغب في محاكاته، ولكن الإمكانية متاحة إذا كنت بحاجة إليها.

يتطلب استخدام ConvolverNodes لإنشاء صوت محيطي إعادة توصيل الرسم البياني لمعالجة الصوت. بدلاً من تمرير الصوت مباشرةً إلى مستوى الصوت الرئيسي، عليك توجيهه من خلال ConvolverNode. ولأنك قد تحتاج إلى التحكم في قوة التأثير البيئي، ستحتاج أيضًا إلى توجيه الصوت حول ConvolverNode. للتحكم في مستويات صوت المزيج، يجب ربط ConvolverNode والصوت العادي بـ WinNodes.

يحتوي الرسم البياني النهائي لمعالجة الصوت الذي أستخدمه على الصوت من العناصر التي تمر عبر 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، بدءًا من الكاتدرائيات وانتهاءً بالغرف المغلقة.

المراجع