Смешивание позиционного звука и WebGL

Ilmari Heikkinen

Введение

В этой статье я расскажу о том, как использовать функцию позиционного звука в API веб-аудио для добавления 3D-звука в ваши сцены WebGL. Чтобы сделать звук более правдоподобным, я также познакомлю вас с эффектами окружающей среды, возможными с помощью API веб-аудио. Чтобы получить более подробное представление об API веб-аудио, прочтите статью Бориса Смуса «Начало работы с API веб-аудио» .

Для создания позиционного звука вы используете AudioPannerNode в API веб-аудио. AudioPannerNode определяет положение, ориентацию и скорость звука. Кроме того, аудиоконтекст API веб-аудио имеет атрибут прослушивателя, который позволяет вам определять положение, ориентацию и скорость прослушивателя. С помощью этих двух вещей вы можете создавать направленные звуки с эффектами Доплера и 3D-панорамированием.

Давайте посмотрим, как выглядит аудиокод для приведенной выше сцены. Это очень простой код 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. Теперь у вас есть подвижный 3D-звук. Позиция прослушивателя аудиоконтекста по умолчанию равна (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, вы можете добавить к звуку эффект Доплера. На странице примеров 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 и прослушивателя аудиоконтекста в каждом кадре.

Воздействие на окружающую среду

После того, как вы настроили позиционный звук, вы можете настроить эффекты окружающей среды для вашего звука, чтобы повысить эффект присутствия вашей 3D-сцены. Предположим, ваша сцена происходит внутри большого собора. При настройках по умолчанию звуки в вашей сцене звучат так, будто вы стоите на улице. Это несоответствие между визуальными эффектами и звуком нарушает погружение и делает сцену менее впечатляющей.

API веб-аудио имеет ConvolverNode, который позволяет вам установить эффект окружающей среды для звука. Добавьте его в график обработки источника звука, и вы сможете настроить звук в соответствии с настройками. В Интернете можно найти образцы импульсных характеристик, которые можно использовать с ConvolverNodes, а также создать свои собственные. Это может быть немного обременительным опытом, поскольку вам нужно записать импульсную характеристику места, которое вы хотите смоделировать, но такая возможность есть, если она вам понадобится.

Использование ConvolverNodes для создания окружающего звука требует перенастройки графа обработки звука. Вместо того, чтобы передавать звук непосредственно на основной том, вам необходимо направить его через ConvolverNode. А поскольку вы можете контролировать силу воздействия окружающей среды, вам также необходимо направить звук вокруг ConvolverNode. Для управления громкостью микса к ConvolverNode и простому звуку необходимо подключить GainNodes.

Последний граф обработки звука, который я использую, содержит звук от объектов, проходящих через 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();
}

Краткое содержание

В этой статье вы узнали, как добавить позиционный звук в 3D-сцены с помощью API веб-аудио. API веб-аудио дает вам возможность установить положение, ориентацию и скорость источников звука и слушателя. Настроив их для отслеживания объектов в вашей 3D-сцене, вы можете создать богатый звуковой ландшафт для своих 3D-приложений.

Чтобы сделать звучание еще более привлекательным, вы можете использовать ConvolverNode в API веб-аудио, чтобы настроить общий звук среды. От соборов до закрытых комнат — вы можете моделировать различные эффекты и среды с помощью API веб-аудио.

Ссылки