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 theo vị trí trong Web Audio API để thêm âm thanh 3D vào cảnh WebGL của bạn. Để giúp âm thanh đáng tin hơn, tôi cũng sẽ giới thiệu cho bạn các hiệu ứng môi trường có thể có với Web Audio API. Để tìm hiểu kỹ hơn về API Web âm thanh, hãy tham khảo bài viết Bắt đầu với API Web âm thanh của Boris Smus.

Để tạo âm thanh theo vị trí, bạn cần 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 Web Audio có thuộc tính trình nghe cho phép bạn xác định vị trí, hướng và tốc độ của trình nghe. Hai tính năng này giúp bạn có thể tạo âm thanh định hướng bằng hiệu ứng doppler và kéo hình 3D.

Hãy xem đoạn mã âm thanh trong cảnh trên. Đây là mã API âm thanh rất cơ bản. Bạn tạo một nhóm các nút Audio API và kết nối chúng với nhau. Nút âm thanh là những âm thanh riêng lẻ, bộ điều khiển âm lượng, nút hiệu ứng, 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 theo bối cảnh âm thanh để biểu đồ đó 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 theo 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 với loa. Nguồn âm thanh ở bên trái trình nghe sẽ to hơn ở loa trái và ngược lại ở loa phải.

Để bắt đầu, hãy tạo một nguồn âm thanh rồi đính kèm vào AudioPannerNode. Sau đó, đặt vị trí của AudioPannerNode. Giờ đây, bạn sẽ có âm thanh 3D có thể di chuyển. Vị trí trình nghe ngữ cảnh âm thanh là ở mức (0,0,0) theo mặc định, vì vậy khi được sử dụng theo cách này, vị trí AudioPannerNode có liên quan đến vị trí camera. Bất cứ khi nào di chuyển camera, bạn cần cập nhật vị trí AudioPannerNode. Để xác định vị trí AudioPannerNode so với thế giới, bạn cần thay đổi vị trí của trình nghe ngữ cảnh âm thanh sang vị trí camera của mì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à nối dây 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 ví dụ bên dưới.

...
// 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í trình nghe của ngữ cảnh âm thanh sao cho 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

Bây giờ, chúng ta đã nắm được vị trí của trình nghe và AudioPannerNode, hãy cùng chú ý đến tốc độ của chúng. Bằng cách thay đổi các thuộc tính vận 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ụ về hiệu ứng Doppler hay trên trang ví dụ về API Web Audio.

Cách dễ nhất để có được tốc độ cho trình nghe và AudioPannerNode là theo dõi vị trí trên mỗi khung hình của chúng. 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 vật thể, lấy vị trí hiện tại trừ đ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 gần nhất. Sau đâ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 mà nguồn âm thanh trỏ tới và hướng nghe của người nghe. Với hướng, bạn có thể mô phỏng các nguồn âm thanh 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 có hướng 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 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 âm thanh và nhân vec3(0,0,1) với nó để xem nó kết thúc ở đâu. Đối với hướng của trình nghe theo bối 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ơ hướng lên, vì vectơ này cần biết góc cuộn của đầu trình nghe. Để tính toán hướng của trình nghe, hãy lấy phần xoay của ma trận 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.

Để có hiệu ứng cho hướng âm thanh, bạn cũng cần xác định hình nón cho âm thanh. Nón âm thanh có góc trong, góc ngoài và khuếch đại ngoài. Âm thanh phát ở mức âm lượng bình thường bên trong góc trong và dần thay đổi mức tăng thành mức tăng âm bên ngoài khi bạn tiếp cận góc ngoài. Ở góc ngoài, âm thanh sẽ phát ra hiệu ứng khuếch đại ngoài.

Việc theo dõi hướng trong Three.js phức tạp hơn một chút vì nó liên quan đến một số toán vectơ và xóa phần dịch của ma trận thế giới 4x4. Tuy nhiên, có không 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 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 nón âm thanh cho âm thanh của bạn, bạn đặt các thuộc tính thích hợp của nút kéo. Các góc hình nón được tính theo độ và có giá trị từ 0 đến 360.

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

Tất cả ở cùng một nơi

Kết hợp tất cả lại với nhau, trình nghe ngữ cảnh âm thanh sẽ tuân theo vị trí, hướng và vận tốc của camera 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í, tốc độ và hướng của AudioPannerNodes cũng như 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ể đặt hiệu ứng môi trường cho âm thanh để tăng độ sống động cho cảnh 3D. Giả sử cảnh của bạn được đặt bên trong một thánh đường lớn. Theo chế độ cài đặt mặc định, âm thanh trong cảnh nghe giống như bạn đang đứng ngoài trời. Sự chênh lệch giữa hình ảnh và âm thanh này phá vỡ sự sống động và làm cho cảnh của bạn kém ấn tượng hơn.

Web Audio API có ConvolverNode cho phép bạn đặt hiệu ứng môi trường cho âm thanh. Hãy thêm nguồn âm thanh này vào sơ đồ xử lý của nguồn âm thanh rồi bạn sẽ có thể điều chỉnh âm thanh sao 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 và bạn cũng có thể tạo của riêng mình. Trải nghiệm này có thể hơi cồng kềnh vì bạn cần ghi lại phản hồi xung động của địa điểm bạn muốn mô phỏng, nhưng bạn vẫn có thể sử dụng chức năng này nếu cần.

Để sử dụng ConvolverNodes để phát âm thanh môi trường, bạn phải đấu 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 chuyển âm thanh qua ConvolverNode. Ngoài ra, nếu muốn kiểm soát cường độ của hiệu ứng môi trường, 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 phải đính kèm GainNodes.

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 truyền 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 kết nối với GainNode riêng để điều khiển âm lượng âm thanh kết hợp. Đầu ra của GainNodes đượ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ẫ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ư vớ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 theo vị trí vào cảnh 3D bằng API Web âm thanh. Web Audio API cho phép bạn thiết lập vị trí, hướng và tốc độ của nguồn âm thanh cũng như trình nghe. Bằng cách đặt các yếu tố đó để theo dõi vật thể trong cảnh 3D, bạn có thể tạo không gian âm thanh phong phú cho các ứng dụng 3D của mình.

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

Tài liệu tham khảo