Łączenie pozycjonowania dźwięku z WebGL

Ilmari Heikkinen

Wstęp

W tym artykule opowiem, jak za pomocą funkcji pozycjonowania dźwięku w interfejsie Web Audio API dodawać dźwięk 3D do scen WebGL. Aby dźwięk stał się bardziej wiarygodny, przedstawię też wpływ interfejsu API Web Audio na środowisko. Aby przybliżyć Ci interfejs Web Audio API, zapoznaj się z artykułem Boris Smus: Getting started with Web Audio API (Pierwsze kroki z interfejsem Web Audio API).

Do tego celu użyj AudioPannerNode w interfejsie Web Audio API. Węzeł AudioPannerNode określa położenie, orientację i prędkość dźwięku. Dodatkowo kontekst audio interfejsu API Web Audio ma atrybut detektor, który pozwala określić położenie, orientację i prędkość detektora. Dzięki tym dwóm rzeczom możesz tworzyć dźwięki kierunkowe z efektami dopplera i panoramą 3D.

Zobaczmy, jak wygląda kod dźwiękowy powyższej sceny. To bardzo podstawowy kod interfejsu Audio API. Tworzysz grupę węzłów interfejsu Audio API i łączysz je ze sobą. Węzły audio to osobne dźwięki, kontrolery głośności, węzły efektów, analizatory itd. Po utworzeniu tego wykresu musisz go połączyć z miejscem docelowym kontekstu audio, aby aktywować go.

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

Pozycja

Dźwięk pozycyjny określa sposób miksowania dźwięku z głośnikami na podstawie położenia źródeł dźwięku i słuchacza. Źródło dźwięku po lewej stronie będzie głośniejsze dla lewego głośnika i odwrotnie dla prawego głośnika.

Zacznij od utworzenia źródła dźwięku i podłącz je do AudioPannerNode. Następnie ustaw pozycję węzła AudioPannerNode. Teraz możesz uzyskać ruchomy dźwięk w 3D. Położenie detektora kontekstu audio jest domyślnie ustawione na (0,0,0), więc jeśli użyjesz go w ten sposób, położenie węzła AudioPannerNode zależy od pozycji kamery. Za każdym razem, gdy poruszasz kamerą, musisz zaktualizować położenie węzła AudioPannerNode. Aby ustawić położenie węzła AudioPannerNode względem świata, zmień położenie odbiornika kontekstu audio na pozycję kamery.

Aby skonfigurować śledzenie pozycji, musimy utworzyć węzeł AudioPannerNode i podłączyć go do głównej głośności.

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

W każdej klatce zaktualizuj położenie węzłów AudioPannerNodes. W poniższych przykładach użyję 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);
...

Aby śledzić pozycję odbiornika, ustaw pozycję detektora kontekstu audio tak, aby pasował do położenia kamery.

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

Prędkość

Skoro znamy już pozycje słuchaczy i węzła AudioPannerNode, możemy zwrócić uwagę na ich prędkości. Zmieniając właściwości prędkości odbiornika i AudioPannerNode, możesz dodać do dźwięku efekt dopplera. Na stronie z przykładami interfejsu Web Audio API znajdziesz kilka ciekawych przykładów efektu dopplera.

Najprostszym sposobem na uzyskanie prędkości dla słuchacza i węzła AudioPannerNode jest śledzenie pozycji ich położenia na klatce. Prędkość detektora to położenie kamery pomniejszonej o położenie kamery w poprzedniej klatce. Podobnie prędkość węzła AudioPannerNode to jego bieżąca pozycja minus jego poprzednia pozycja.

Śledzenie prędkości można wykonać, ustalając poprzednie położenie obiektu, odejmując je od bieżącej pozycji i dzieląc wynik przez czas, który upłynął od ostatniej klatki. Oto jak to zrobić w 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);
...

Orientacja

Orientacja to kierunek, w którym wskazuje źródło dźwięku, oraz kierunek, w którym skierowany jest słuchacz. W przypadku orientacji możesz symulować kierunkowe źródła dźwięku. Weźmy na przykład głośnik kierunkowy. Jeśli staniesz przed głośnikiem, dźwięk będzie głośniejszy niż gdy staniesz za głośnikiem. Co ważniejsze, potrzebna jest orientację, z której strony pochodzą dźwięki. Dźwięk dobiegający z lewej strony musi przełączyć się w prawo, gdy się obrócisz.

Aby uzyskać wektor orientacji dla węzła AudioPannerNode, należy pomnożyć przez niego obrotową część macierzy modelu emisyjnego obiektu 3D i pomnożyć przez niego wektor vec3(0,0,1), aby sprawdzić, gdzie to się kończy. Aby określić orientację odbiornika kontekstu, musisz pobrać wektor orientacji kamery. Orientacja nasłuchu wymaga również wektora w górę, ponieważ musi znać kąt obrotu głowy słuchacza. Aby obliczyć orientację odbiornika, pobierz obrotową część macierzy widoku kamery i pomnóż vec3(0;0;1) dla orientacji i wektor3(0;-1,0) dla wektoru kierunkowego.

Aby orientacja wpływała na dźwięki, musisz też określić stożek dźwięku. Stożek dźwiękowy przyjmuje kąt wewnętrzny, zewnętrzny i wzmocnienie zewnętrzne. Dźwięk odtwarza się z normalną głośnością wewnątrz kąta wewnętrznego i stopniowo zmienia wzmocnienie na zewnętrzne, gdy zbliżasz się do kąta zewnętrznego. Na zewnątrz zewnętrzny dźwięk jest odtwarzany z wzmocnieniem zewnętrznym.

Śledzenie orientacji w Three.js jest nieco trudniejsze, ponieważ wymaga wykonania pewnych działań matematycznych w postaci wektorów i wyzerowania części translacyjnej macierzy 4 x 4. Mimo to niewiele linijek kodu.

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

Śledzenie orientacji kamery wymaga też wektora górnego, więc trzeba pomnożyć wektor w górę przez macierz przekształceń.

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

Aby ustawić stożek dźwięku dla dźwięku, ustaw odpowiednie właściwości węzła Panner. Kąty stożka są wyrażone w stopniach i mają zakres od 0 do 360.

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

Wszystko razem

Po połączeniu detektor kontekstu audio śledzi pozycję, orientację i prędkość kamery, a węzły AudioPanner dostosowują się do pozycji, orientacji i prędkości swoich źródeł dźwięku. W każdej klatce musisz zaktualizować położenie, prędkości i orientacje węzłów AudioPannerNodes oraz detektor kontekstu audio.

Wpływ na środowisko

Po skonfigurowaniu dźwięku pozycjonującego możesz ustawić efekty środowiskowe swojego dźwięku, aby zwiększyć wciągającą scenę 3D. Załóżmy, że scena rozgrywa się w okazałej katedrze. W przypadku domyślnych ustawień dźwięki w scenie brzmią tak, jakby stoisz na zewnątrz. Ta rozbieżność między obrazem a dźwiękiem sprawia, że scena staje się mniej imponująca.

Interfejs Web Audio API ma interfejs ConvolverNode, który umożliwia ustawienie wpływu dźwięku na środowisko. Dodaj go do wykresu przetwarzania dla źródła dźwięku, aby dopasować dźwięk do ustawienia. Próbki reakcji impulsowych znajdziesz w internecie. Możesz je wykorzystać z instancji ConvolverNodes. Możesz też utworzyć własne. Może to być nieco uciążliwe, ponieważ musisz zarejestrować impuls w odniesieniu do miejsca, które chcesz symulować, ale masz taką możliwość.

Używanie ConvolverNodes do obsługi dźwięków środowiskowych wymaga zmiany grafu przetwarzania dźwięku. Zamiast przesyłać dźwięk bezpośrednio do głównego poziomu głośności, trzeba go skierować do węzła ConvolverNode. Być może chcesz kontrolować siłę oddziaływania na środowisko, ale musisz też skierować dźwięk wokół węzła ConvolverNode. Aby sterować głośnością miksu, musisz mieć do niego przyłączone węzły ConvolverNode i zwykły dźwięk.

Ostateczny wykres przetwarzania dźwięku, którego używam, zawiera dźwięk z obiektów przechodzących przez GainNode, używany jako mikser typu „przelotny mikser”. Z miksera przekazuję dźwięk do węzła ConvolverNode i kolejnego GainNode, który służy do sterowania głośnością zwykłego dźwięku. Element ConvolverNode jest podłączony do własnego węzła GainNode, aby sterować głośnością głośnego dźwięku. Dane wyjściowe węzłów GainNodes są podłączone do głównego kontrolera głośności.

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

Aby obiekt ConvolverNode działał, musisz wczytać próbkę odpowiedzi impulsowej do bufora i umożliwić korzystanie z niego przez ConvolverNode. Ładowanie odbywa się w taki sam sposób jak w przypadku normalnych próbek dźwięku. Oto przykład:

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

Podsumowanie

Z tego artykułu dowiesz się, jak dodać do scen 3D dźwięk pozycjonujący za pomocą interfejsu Web Audio API. Interfejs Web Audio API umożliwia ustawianie położenia, orientacji i prędkości źródeł dźwięku oraz słuchaczy. Korzystając z funkcji śledzenia obiektów w scenie 3D, można stworzyć bogate środowisko dźwiękowe do zastosowania w aplikacjach 3D.

Aby wrażenia dźwiękowe były jeszcze bardziej atrakcyjne, możesz użyć elementu ConvolverNode w interfejsie Web Audio API do skonfigurowania ogólnego dźwięku z otoczenia. Za pomocą interfejsu Web Audio API możesz symulować różne efekty i środowiska – od katedry po zamknięte pomieszczenia.

Źródła