Kết hợp âm thanh theo vị trí và WebGL

Ilmari Heikkinen

Giới thiệu

Trong bài viết này, tôi sẽ nói về cách sử dụng tính năng âm thanh vị trí trong Web Audio API để thêm âm thanh 3D vào các cảnh WebGL. Để giúp âm thanh trở nên đáng tin cậy hơn, tôi cũng sẽ giới thiệu với bạn về những tác động môi trường có thể xảy ra với Web Audio API. Để tìm hiểu kỹ hơn về API Web âm thanh, hãy xem bài viết Bắt đầu sử dụng API Web âm thanh của Boris Smus.

Để tạo âm thanh định vị, bạn hãy sử dụng AudioPannerNode trong API Web âm thanh. AudioPannerNode xác định vị trí, hướng và vận tốc của âm thanh. Ngoài ra, ngữ cảnh âm thanh của API Âm thanh trên web có một thuộc tính trình nghe cho phép bạn xác định vị trí, hướng và vận tốc của trình nghe. Với hai yếu tố này, bạn có thể tạo âm thanh định hướng bằng hiệu ứng Doppler và hiệu ứng kéo 3D.

Hãy xem mã âm thanh cho cảnh trên. Đây là mã API Audio rất cơ bản. Bạn tạo một loạt nút Audio API và kết nối các nút đó với nhau. Các nút âm thanh là các âm thanh riêng lẻ, bộ điều khiển âm lượng, nút hiệu ứng và trình phân tích, v.v. Sau khi tạo biểu đồ này, bạn cần kết nối biểu đồ đó với đích đến ngữ cảnh âm thanh để có thể nghe được.

// 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();

Vị trí

Âm thanh vị trí sử dụng vị trí của nguồn âm thanh và vị trí của người nghe để xác định cách phối âm thanh cho loa. Nguồn âm thanh ở bên trái người nghe sẽ to hơn ở loa trái và ngược lại đối với bên phải.

Để bắt đầu, hãy tạo một nguồn âm thanh và đính kèm nguồn đó vào AudioPannerNode. Sau đó, hãy đặt vị trí của AudioPannerNode. Bây giờ, bạn đã có âm thanh 3D di chuyển được. Theo mặc định, vị trí trình nghe ngữ cảnh âm thanh là (0,0,0). Vì vậy, khi được sử dụng theo cách này, vị trí AudioPannerNode sẽ tương ứng với vị trí của máy ảnh. Bất cứ khi nào di chuyển máy ảnh, bạn cần cập nhật vị trí AudioPannerNode. Để đặt vị trí AudioPannerNode tương ứng với thế giới, bạn cần thay đổi vị trí trình nghe ngữ cảnh âm thanh thành vị trí máy ảnh.

Để thiết lập tính năng theo dõi vị trí, chúng ta cần tạo một AudioPannerNode và kết nối AudioPannerNode đó với âm lượng chính.

...
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);
...

Trên mỗi khung hình, hãy cập nhật vị trí của AudioPannerNodes. Tôi sẽ sử dụng Three.js trong các ví dụ dưới đây.

...
// 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);
...

Để theo dõi vị trí của trình nghe, hãy đặt vị trí của trình nghe trong ngữ cảnh âm thanh khớp với vị trí của máy ảnh.

...
// 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);
...

Vận tốc

Giờ đây, chúng ta đã có vị trí của trình nghe và AudioPannerNode, hãy chuyển sự chú ý của chúng ta đến tốc độ của các vị trí này. Bằng cách thay đổi các thuộc tính tốc độ của trình nghe và AudioPannerNode, bạn có thể thêm hiệu ứng Doppler vào âm thanh. Có một số ví dụ hay về hiệu ứng Doppler trên trang ví dụ của API Âm thanh trên web.

Cách dễ nhất để lấy tốc độ cho trình nghe và AudioPannerNode là theo dõi vị trí của chúng trên mỗi khung hình. Vận tốc của trình nghe bằng vị trí hiện tại của camera trừ đi vị trí của camera trong khung hình trước. Tương tự, tốc độ của AudioPannerNode là vị trí hiện tại trừ đi vị trí trước đó.

Bạn có thể theo dõi vận tốc bằng cách lấy vị trí trước đó của đối tượng, trừ vị trí đó khỏi vị trí hiện tại rồi chia kết quả cho thời gian đã trôi qua kể từ khung hình cuối cùng. Dưới đây là cách thực hiện trong 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);
...

Hướng

Hướng là hướng nguồn âm thanh đang trỏ đến và hướng người nghe đang đối diện. Với hướng, bạn có thể mô phỏng các nguồn âm thanh định hướng. Ví dụ: hãy nghĩ đến một loa định hướng. Nếu bạn đứng trước loa, âm thanh sẽ to hơn so với khi bạn đứng sau loa. Quan trọng hơn, bạn cần hướng của trình nghe để xác định âm thanh phát ra từ phía nào của trình nghe. Âm thanh phát ra từ bên trái cần chuyển sang bên phải khi bạn quay xe.

Để có vectơ định hướng cho AudioPannerNode, bạn cần lấy phần xoay của ma trận mô hình của đối tượng 3D phát ra âm thanh và nhân vec3(0,0,1) với nó để xem nơi cuối cùng trỏ đến. Đối với hướng trình nghe ngữ cảnh, bạn cần lấy vectơ hướng của máy ảnh. Hướng của trình nghe cũng cần có một vectơ lên, vì trình nghe cần biết góc cuộn của đầu trình nghe. Để tính hướng của trình nghe, hãy lấy phần xoay của ma trận chế độ xem của máy ảnh và nhân vec3(0,0,1) cho hướng và vec3(0,-1,0) cho vectơ lên.

Để hướng có ảnh hưởng đến âm thanh, bạn cũng cần xác định hình nón cho âm thanh. Hình nón âm thanh có góc trong, góc ngoài và mức tăng âm ngoài. Âm thanh phát ở âm lượng bình thường bên trong góc trong và dần thay đổi độ lợi thành độ lợi bên ngoài khi bạn tiến đến góc ngoài. Bên ngoài góc ngoài, âm thanh phát ở mức tăng ngoài.

Việc theo dõi hướng trong Three.js sẽ phức tạp hơn một chút vì liên quan đến một số toán vectơ và đặt phần dịch của ma trận thế giới 4x4 về 0. Tuy nhiên, vẫn không có nhiều dòng mã.

...
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;
...

Việc theo dõi hướng của máy ảnh cũng yêu cầu vectơ hướng lên, vì vậy, bạn cần nhân vectơ lên với ma trận biến đổi.

...
// 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;
...

Để đặt hình nón âm thanh cho âm thanh, bạn cần đặt các thuộc tính thích hợp của nút panner. Góc hình nón được tính bằng độ và chạy từ 0 đến 360.

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

Tất cả cùng nhau

Khi kết hợp tất cả, trình nghe ngữ cảnh âm thanh sẽ tuân theo vị trí, hướng và tốc độ của máy ảnh, còn AudioPannerNodes sẽ tuân theo vị trí, hướng và tốc độ của các nguồn âm thanh tương ứng. Bạn cần cập nhật vị trí, vận tốc và hướng của AudioPannerNodes và trình nghe ngữ cảnh âm thanh trên mọi khung hình.

Tác động môi trường

Sau khi thiết lập âm thanh theo vị trí, bạn có thể thiết lập các hiệu ứng môi trường cho âm thanh để tăng sự sống động cho cảnh 3D. Giả sử cảnh của bạn được đặt bên trong một nhà thờ lớn. Ở chế độ cài đặt mặc định, âm thanh trong cảnh của bạn sẽ giống như bạn đang đứng ngoài trời. Sự khác biệt này giữa hình ảnh và âm thanh làm giảm sự sống động và khiến cảnh của bạn kém ấn tượng.

Web Audio API có một ConvolverNode cho phép bạn đặt hiệu ứng môi trường cho âm thanh. Thêm tham số này vào biểu đồ xử lý cho nguồn âm thanh và bạn có thể điều chỉnh âm thanh cho phù hợp với chế độ cài đặt. Bạn có thể tìm thấy các mẫu phản hồi xung trên web mà bạn có thể sử dụng với ConvolverNodes, đồng thời bạn cũng có thể tạo mẫu của riêng mình. Việc này có thể hơi rườm rà vì bạn cần ghi lại phản hồi xung của vị trí mà bạn muốn mô phỏng, nhưng bạn có thể sử dụng tính năng này nếu cần.

Việc sử dụng ConvolverNodes để tạo âm thanh môi trường đòi hỏi phải kết nối lại biểu đồ xử lý âm thanh. Thay vì truyền âm thanh trực tiếp đến âm lượng chính, bạn cần định tuyến âm thanh đó thông qua ConvolverNode. Và vì bạn có thể muốn kiểm soát độ mạnh của hiệu ứng môi trường, nên bạn cũng cần định tuyến âm thanh xung quanh ConvolverNode. Để kiểm soát âm lượng kết hợp, ConvolverNode và âm thanh thuần tuý cần đính kèm các GainNode.

Biểu đồ xử lý âm thanh cuối cùng mà tôi đang sử dụng có âm thanh từ các đối tượng đi qua GainNode được dùng làm bộ trộn truyền qua. Từ bộ trộn, tôi truyền âm thanh vào ConvolverNode và một GainNode khác, dùng để điều khiển âm lượng của âm thanh thuần tuý. ConvolverNode được nối vào GainNode của riêng nó để điều khiển âm lượng âm thanh được kết hợp. Đầu ra của các GainNode được kết nối với bộ điều khiển âm lượng chính.

...
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 hoạt động, bạn cần tải một mẫu phản hồi xung vào vùng đệm và yêu cầu ConvolverNode sử dụng mẫu đó. Quá trình tải mẫu diễn ra theo cách tương tự như quy trình tải mẫu âm thanh thông thường. Dưới đây là ví dụ về một cách thực hiện:

...
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();
}

Tóm tắt

Trong bài viết này, bạn đã tìm hiểu cách thêm âm thanh vị trí vào cảnh 3D bằng Web Audio API. API Âm thanh trên web cung cấp cho bạn một cách để đặt vị trí, hướng và vận tốc của nguồn âm thanh và trình nghe. Bằng cách thiết lập các đối tượng đó để theo dõi các đối tượng trong cảnh 3D, bạn có thể tạo ra một không gian âm thanh phong phú cho các ứng dụng 3D.

Để trải nghiệm âm thanh trở nên hấp dẫn hơn, bạn có thể sử dụng ConvolverNode trong API Âm thanh trên web để thiết lập âm thanh chung của môi trường. Từ nhà thờ lớn cho đến phòng kín, bạn có thể mô phỏng nhiều hiệu ứng và môi trường khác nhau bằng Web Audio API.

Tài liệu tham khảo