Wprowadzenie
Z tego artykułu dowiesz się, jak używać funkcji dźwięku przestrzennego w interfejsie Web Audio API, aby dodawać dźwięk 3D do scen WebGL. Aby dźwięk był bardziej wiarygodny, pokażę Ci też efekty dźwiękowe, które można uzyskać dzięki interfejsowi Web Audio API. Aby uzyskać bardziej szczegółowe wprowadzenie do Web Audio API, przeczytaj artykuł Początkujący: Web Audio API autorstwa Borisa Smusa.
Aby uzyskać dźwięk pozycyjny, użyj węzła AudioPannerNode w interfejsie Web Audio API. Węzeł AudioPannerNode określa położenie, orientację i szybkość dźwięku. Dodatkowo kontekst audio interfejsu Web Audio API zawiera atrybut listener, który umożliwia określenie pozycji, orientacji i prędkości słuchacza. Dzięki tym dwóm elementom możesz tworzyć dźwięki kierunkowe z efektami Dopplera i panoramą 3D.
Zobaczmy, jak wygląda kod audio w przypadku tej sceny. To jest bardzo podstawowy kod interfejsu Audio API. Utwórz kilka węzłów interfejsu Audio API i połącz je ze sobą. Węzły audio to pojedyncze dźwięki, elementy sterujące głośnością, węzły efektów i analizatory. Po utworzeniu tego grafu musisz go podłączyć do miejsca docelowego kontekstu audio, aby był słyszalny.
// 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 pozycjonowany wykorzystuje położenie źródeł dźwięku i położenie słuchacza, aby określić, jak miksować dźwięk na głośniki. Źródło dźwięku po lewej stronie słuchacza będzie głośniejsze w lewym głośniku, a na odwrót po prawej stronie.
Aby rozpocząć, utwórz źródło dźwięku i dołącz je do węzła AudioPannerNode. Następnie ustaw pozycję węzła AudioPannerNode. Teraz masz ruchomy dźwięk 3D. Domyślnie pozycja odbiorcy kontekstu audio wynosi (0,0,0), więc gdy jest używana w ten sposób, pozycja AudioPannerNode jest względna do pozycji kamery. Za każdym razem, gdy przesuniesz kamerę, musisz zaktualizować pozycję węzła AudioPannerNode. Aby ustawić pozycję węzła AudioPannerNode względem świata, musisz zmienić pozycję słuchacza kontekstu dźwięku na pozycję kamery.
Aby skonfigurować śledzenie pozycji, musimy utworzyć węzeł AudioPannerNode i podłączyć go do głównego poziomu 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żdym ujęciu aktualizuj pozycje węzłów AudioPannerNodes. W przykładach poniżej będę używać 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ę słuchacza, ustaw pozycję słuchacza w kontekście audio tak, aby odpowiadała pozycji 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ść
Teraz, gdy znamy pozycje słuchacza i węzła AudioPannerNode, skupmy się na ich prędkościach. Zmiana właściwości prędkości odbiorcy i węzła AudioPannerNode pozwala dodać do efektu Dopplera. Na stronie z przykładami API Web Audio 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 ich pozycji w każdej klatce. Prędkość obiektu słuchacza to bieżąca pozycja kamery pomniejszona o jej pozycję w poprzedniej klatce. Podobnie prędkość węzła AudioPannerNode to jego bieżąca pozycja pomniejszona o jego poprzednią pozycję.
Prędkość można śledzić, uzyskując poprzednią pozycję obiektu, odejmując ją od bieżącej pozycji i dzieląc wynik przez czas upływający 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 skierowane jest źródło dźwięku, oraz kierunek, w którym zwrócona jest twarz słuchacza. Dzięki orientacji możesz symulować kierunkowe źródła dźwięku. Weźmy na przykład głośnik kierunkowy. Jeśli stoisz przed głośnikiem, dźwięk będzie głośniejszy niż wtedy, gdy stoisz za głośnikiem. Co ważniejsze, musisz określić orientację słuchacza, aby określić, z którego boku dochodzą dźwięki. Dźwięk z lewej strony musi się przełączyć na prawą, gdy się odwrócisz.
Aby uzyskać wektor orientacji dla węzła AudioPannerNode, musisz wziąć część obrotu z macierzy modelu obiektu 3D emitującego dźwięk i pomnożyć ją przez wektor vec3(0,0,1), aby zobaczyć, gdzie w końcu jest skierowany. Aby określić orientację słuchowego kontekstu, musisz uzyskać wektor orientacji kamery. Orientacja słuchacza wymaga też wektora skierowanego w górę, ponieważ musi znać kąt odchylenia głowy słuchacza. Aby obliczyć orientację słuchacza, pobierz część obrotu z macierzy widoku kamery i pomnóż ją przez wektor vec3(0,0,1) dla orientacji oraz wektor vec3(0,-1,0) dla wektora skierowanego w górę.
Aby orientacja miała wpływ na dźwięki, musisz też zdefiniować stożek dla dźwięku. Kształt stożka dźwięku ma kąt wewnętrzny, kąt zewnętrzny i wzmocnienie zewnętrzne. Dźwięk odtwarzany jest z normalną głośnością wewnątrz kąta wewnętrznego, a w miarę zbliżania się do kąta zewnętrznego stopniowo zmienia się wzmocnienie do wzmocnienia zewnętrznego. Poza zewnętrznym kątem dźwięk jest odtwarzany z zewnętrznym wzmocnieniem.
Śledzenie orientacji w Three.js jest nieco bardziej skomplikowane, ponieważ wymaga wykonania kilku obliczeń wektorowych i ustawienia na 0 części przekształceń w macierz świata 4 x 4. Nadal nie ma zbyt wielu linii 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 również wektora w górę, więc musisz pomnożyć wektor w górę przez macierz przekształcenia.
...
// 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, musisz ustawić odpowiednie właściwości węzła panner. Kąty stożka są podawane w stopniach w zakresie od 0 do 360.
...
sound.panner.coneInnerAngle = innerAngleInDegrees;
sound.panner.coneOuterAngle = outerAngleInDegrees;
sound.panner.coneOuterGain = outerGainFactor;
...
Razem
Podsumowując, Listener audio context śledzi pozycję, orientację i prędkość kamery, a AudioPannerNodes śledzi pozycje, orientacje i prędkości odpowiednich źródeł dźwięku. W każdej klatce musisz zaktualizować pozycje, prędkości i orientacje węzłów AudioPannerNodes oraz odbiornika kontekstu audio.
Efekty środowiskowe
Po skonfigurowaniu dźwięku pozycyjnego możesz ustawić efekty dźwiękowe związane z otoczeniem, aby zwiększyć realizm sceny 3D. Załóżmy, że Twoja scena rozgrywa się w dużej katedrze. W ustawieniach domyślnych dźwięki w scenie brzmią tak, jakbyś stała na zewnątrz. Ta rozbieżność między obrazem a dźwiękiem powoduje przerwanie immersji i sprawia, że scena staje się mniej imponująca.
Interfejs API Web Audio zawiera węzeł ConvolverNode, który umożliwia ustawienie efektu otoczenia dla dźwięku. Dodaj go do wykresu przetwarzania dla źródła dźwięku, aby dostosować dźwięk do ustawienia. W internecie można znaleźć próbki odpowiedzi impulsowej, których można używać z ConvolverNodes. Można też tworzyć własne. Może to być nieco kłopotliwe, ponieważ musisz nagrać odpowiedź impulsową miejsca, które chcesz symulować. Jeśli jednak tego potrzebujesz, ta funkcja jest dostępna.
Korzystanie z elementów ConvolverNodes do generowania dźwięku otoczenia wymaga przeprojektowania grafu przetwarzania dźwięku. Zamiast przekazywać dźwięk bezpośrednio do głównego wolumenu, musisz go przekierować przez węzeł ConvolverNode. Ponieważ możesz chcieć kontrolować natężenie efektu środowiskowego, musisz też przekierować dźwięk wokół węzła ConvolverNode. Aby móc kontrolować głośność miksów, do węzła ConvolverNode i zwykłego dźwięku należy dołączyć węzły GainNode.
Ostatni użyty przeze mnie schemat przetwarzania dźwięku zawiera dźwięk z obiektów przechodzących przez GainNode używany jako przepuszczający mikser. Z miksera przekazuję dźwięk do węzła ConvolverNode i innego węzła GainNode, który służy do sterowania głośnością zwykłego dźwięku. Węzeł ConvolverNode jest podłączony do własnego węzła GainNode, aby sterować głośnością convolwed audio. Wyjścia GainNodes są połączone z głównym regulatorem 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 uruchomić węzeł ConvolverNode, musisz załadować próbkę odpowiedzi impulsowej do bufora i użyć jej w węźle ConvolverNode. Ładowanie próbki odbywa się w taki sam sposób jak w przypadku zwykłych próbek dźwięku. Poniżej znajdziesz przykład jednej z możliwości:
...
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ć dźwięk pozycyjny do scen 3D za pomocą interfejsu Web Audio API. Interfejs Web Audio API umożliwia ustawianie pozycji, orientacji i prędkości źródeł dźwięku oraz słuchacza. Ustawiając je tak, aby śledziły obiekty w scenie 3D, możesz tworzyć bogate dźwięki w aplikacjach 3D.
Aby jeszcze bardziej uatrakcyjnić dźwięk, możesz użyć węzła ConvolverNode w interfejsie Web Audio API, aby skonfigurować ogólny dźwięk środowiska. Korzystając z interfejsu Web Audio API, możesz symulować różne efekty i otoczenia, od katedr po zamknięte pomieszczenia.
Pliki referencyjne
- Specyfikacja Web Audio API
- Odpowiedź impulsu
- Three.js
- Przykład fajnego dźwięku przestrzennego 3D
- Na stronie Przykłady dźwięku w internecie znajdziesz wiele przydatnych przykładów korzystania z funkcji Web Audio API.