Movatterモバイル変換


[0]ホーム

URL:


  1. 面向开发者的 Web 技术
  2. Web API
  3. Web Audio API
  4. Web audio 空间化基础

此页面由社区从英文翻译而来。了解更多并加入 MDN Web Docs 社区。

View in EnglishAlways switch to English

Web audio 空间化基础

正如大量的各种声音处理(或者其他)选择是不够的,WebAduioAPI 也包含了一些工具,可以让你模仿听众在声源周围移动时的声音差异,例如当你在 3D 游戏声源中移动时声音的平移。官方名词称为空间化,这篇文章将会介绍如何实现这样一个系统的基础知识。

空间化的基础知识

在 Web Audio 中,复杂的 3D 空间化是使用PannerNode 创建的,用外行人的话来说就是一个使音频出现在 3D 空间中的很酷的数学。想象一下声音从你头上飞过,爬到你身后,在你面前移动。诸如此类的事情。

它对 WebXR 和游戏非常有用。在 3D 空间中,它是实现逼真的音频效果的唯一方式。像three.jsA-frame 这样的库在处理声音时就利用了它的潜力。值得注意的是,你不必在完整的 3D 空间中移动声音 - 你可以只使用 2D 平面,因此如果你计划实现一个 2D 游戏,这依然是你要寻找的节点。

备注:还有一个设计用于处理创建简单的左右立体声平移效果的StereoPannerNode 。这使用起来更简单,但显然无处可用。如果你只想要一个简单的立体声平移效果,我们的StereoPannerNode 示例请参阅源码)应该可以为你提供所需的一切。

3D boombox 演示

为了演示 3D 空间化,我们在Using the Web Audio API 指南中的 boombox 演示的基础上创建一个修改版本。参见3D spatialization demo live (同时也可以看source code

A simple UI with a rotated boombox and controls to move it left and right and in and out, and rotate it.

音箱放置于房间中(由浏览器视区边缘定义),在本 demo 中我们可以通过提供的控件移动和旋转它。当我们移动音箱时,它产生的声音会相应的改变,当它在移动到房间的左侧或右侧时声音平移,或当它远离用户时变得安静,或旋转使得扬声器背离它们等。这是通过给PannerNode 对象实例设置不同的与该运动有关的属性来实现模拟空间化。

备注:如果你使用耳机或者其他某种环绕声系统连接计算机,体验会更好。

创建 audio 收听者

让我们开始!BaseAudioContextAudioContext 扩展自该接口)有一个 listener 属性,返回一个AudioListener 对象。这代表着场景收听者,通常是使用者(用户)。你可以定义他 (她) 们在空间中的位置和他 (她) 们面向的方向。他 (她) 们保持静止。pannerNode 可以计算出声音相对于收听者位置的位置。

让我们创建我们的上下文和监听器,并设置收听者的位置来模拟一个看向(探索)我们房间的人:

js
const AudioContext = window.AudioContext || window.webkitAudioContext;const audioCtx = new AudioContext();const listener = audioCtx.listener;const posX = window.innerWidth / 2;const posY = window.innerHeight / 2;const posZ = 300;listener.positionX.value = posX;listener.positionY.value = posY;listener.positionZ.value = posZ - 5;

我们可以使用positionX 将收听者向左/右移动,使用positionY 向上/下移动,或使用positionZ 移出/入房间。在这里,我们将收听者设置在视口中间同时稍微位于音箱的前方。我们还可以设置收听者面对的方向。这些默认值工作良好:

js
listener.forwardX.value = 0;listener.forwardY.value = 0;listener.forwardZ.value = -1;listener.upX.value = 0;listener.upY.value = 1;listener.upZ.value = 0;

这些 forward 属性代表了收听者前进方向的 3D 坐标位置(例如他/她们面向的方向),而 up 属性表示了收听者头顶的 3D 坐标位置。这两种属性值可以很好的设定方位。

创建 panner 节点

让我们创建PannerNode节点,这有很多与之相关的属性。让我们来一一看看:

首先我们可以设置panningModel。这是用于在 3D 空间中定位音频的空间化算法。我们可以将其设置为:

equalpower — 计算出默认和一般方式的平移。

HRTF — 这代表 'Head-related transfer function' ,在弄清楚声音的位置时,会考虑人脑(对声音的处理)。

非常聪明的东西,让我们使用HRTF 模型!

js
const pannerModel = "HRTF";

属性coneInnerAngleconeOuterAngle 指定音量发出的位置。默认情况下,两者都是 360 度。我们可以定义音箱扬声器拥有较小的锥体。内锥是总是模拟增益(音量)最大值的地方,外锥是增益开始下降的地方。

增益通过coneOuterGain 值来减少。让我们创建之后将会用于这些参数的常量:

js
const innerCone = 60;const outerCone = 90;const outerGain = 0.3;

下一个参数是distanceModel — 这只能设置为linear,inverse, 或者exponential。这些是不同的算法,用于在音频源远离收听者时减小音频源的音量。我们将使用linear,因为它很简单:

js
const distanceModel = "linear";

我们可以设置源和收听者之间的最大距离 (maxDistance) — 如果源距离此点更远,则音量将不再减小。这可能很有用,因为你可能会发现你想要模拟距离,但是音量会下降,而实际上并不是你想要的。默认情况下,它是 10,000(无单位的相对值)。我们可以像这样保持它:

js
const maxDistance = 10000;

还有一个参考距离 (refDistance),由距离模型使用。我们也可以将其保持为默认值1

js
const refDistance = 1;

然后就是 roll-off 因子 (rolloffFactor) — 描述随着 panner 远离收听者,音量减小的速度有多快。默认值为 1;让我们使其大一些以放大我们的动作。

js
const rollOff = 10;

现在我们可以开始设置我们 boombox 的位置和方向。这与我们如何设置收听者很像。这些也是我们在使用界面上的控件时要改变的参数。

js
const positionX = posX;const positionY = posY;const positionZ = posZ;const orientationX = 0.0;const orientationY = 0.0;const orientationZ = -1.0;

注意 z 方向的负值 - 这会将 boombox 设置为面向我们。正值会使声源背离我们。让我们使用相关的构造函数来创建我们的 panner 节点,并传入我们在上面设置的所有参数:

js
const panner = new PannerNode(audioCtx, {  panningModel: pannerModel,  distanceModel: distanceModel,  positionX: positionX,  positionY: positionY,  positionZ: positionZ,  orientationX: orientationX,  orientationY: orientationY,  orientationZ: orientationZ,  refDistance: refDistance,  maxDistance: maxDistance,  rolloffFactor: rollOff,  coneInnerAngle: innerCone,  coneOuterAngle: outerCone,  coneOuterGain: outerGain,});

移动 boombox

现在我们将在我们的“房间”中四处移动 boombox。我们已经设置了一些控件来执行此操作。我们可以左右移动,上下移动,来回移动;我们也可以旋转它。声音方向来自前面的 boombox 的扬声器,因此当我们旋转它时,我们可以改变声音的方向 - 即当音箱旋转 180 度并背向我们时,使其向后投射。我们需要为界面设置一些东西。首先,我们将获得我们想要移动的元素的引用,然后存储我们在设置CSS transforms 来实际执行移动时将要更改的值的引用。最后,我们将设置一些边界值,以便我们的 boombox 在任何方向上都不会走得太远:

js
const moveControls = document  .querySelector("#move-controls")  .querySelectorAll("button");const boombox = document.querySelector(".boombox-body");// the values for our css transformslet transform = {  xAxis: 0,  yAxis: 0,  zAxis: 0.8,  rotateX: 0,  rotateY: 0,};// set our boundsconst topBound = -posY;const bottomBound = posY;const rightBound = posX;const leftBound = -posX;const innerBound = 0.1;const outerBound = 1.5;

让我们创建一个函数,将我们想要移动的方向作为参数,并且修改 CSS 变换及更新我们的 panner 节点的位置和方向属性值以适当地更改声音。首先让我们看看左,右,上,下值,因为这些非常简单。我们将沿着这些轴移动 boombox,并更新合适的位置。

js
function moveBoombox(direction) {  switch (direction) {    case "left":      if (transform.xAxis > leftBound) {        transform.xAxis -= 5;        panner.positionX.value -= 0.1;      }      break;    case "up":      if (transform.yAxis > topBound) {        transform.yAxis -= 5;        panner.positionY.value -= 0.3;      }      break;    case "right":      if (transform.xAxis < rightBound) {        transform.xAxis += 5;        panner.positionX.value += 0.1;      }      break;    case "down":      if (transform.yAxis < bottomBound) {        transform.yAxis += 5;        panner.positionY.value += 0.3;      }      break;  }}

移入和移出也是类似的故事:

js
case 'back':    if (transform.zAxis > innerBound) {        transform.zAxis -= 0.01;        panner.positionZ.value += 40;    }break;case 'forward':    if (transform.zAxis < outerBound) {        transform.zAxis += 0.01;        panner.positionZ.value -= 40;    }break;

然而,我们的旋转值稍为复杂,因为我们需要在周围移动声音。我们不仅需要更新两个轴值(例如,如果围绕 x 轴旋转对象,则更新该对象的 y 和 z 坐标),还需要为此进行更多的数学运算。旋转是一个圆圈,我们需要Math.sinMath.cos 来帮助我们绘制这个圆圈。让我们设置一个旋转速率,我们将会将它转换为弧度范围的值,以便稍后在Math.sinMath.cos 中使用,当我们在旋转我们的 boombox,需要计算出新的坐标时:

js
// set up rotation constantsconst rotationRate = 60; // bigger number equals slower sound rotationconst q = Math.PI / rotationRate; //rotation increment in radians

我们也可以使用它来计算旋转度,这将有助于我们即将必须创建的 CSS 变换(注意我们需要用于 CSS 变换的 x 和 y 轴):

js
// get degrees for cssconst degreesX = (q * 180) / Math.PI;const degreesY = (q * 180) / Math.PI;

我们以左旋转为例看一看。我们需要更改 panner x 方向和 z 方向的坐标,以围绕 y 轴移动进行左旋转:

js
case 'rotate-left':  transform.rotateY -= degreesY;  // 'left' is rotation about y-axis with negative angle increment  z = panner.orientationZ.value*Math.cos(q) - panner.orientationX.value*Math.sin(q);  x = panner.orientationZ.value*Math.sin(q) + panner.orientationX.value*Math.cos(q);  y = panner.orientationY.value;  panner.orientationX.value = x;  panner.orientationY.value = y;  panner.orientationZ.value = z;break;

这有点令人困惑,但我们正在做的是使用 sin 和 cos 来帮助我们计算出旋转 boombox 所需的圆周运动的坐标。我们可以为所有轴做到这一点。只需要选择正确的方向进行更新,以及我们是想要正增量还是负增量。

js
case 'rotate-right':  transform.rotateY += degreesY;  // 'right' is rotation about y-axis with positive angle increment  z = panner.orientationZ.value*Math.cos(-q) - panner.orientationX.value*Math.sin(-q);  x = panner.orientationZ.value*Math.sin(-q) + panner.orientationX.value*Math.cos(-q);  y = panner.orientationY.value;  panner.orientationX.value = x;  panner.orientationY.value = y;  panner.orientationZ.value = z;break;case 'rotate-up':  transform.rotateX += degreesX;  // 'up' is rotation about x-axis with negative angle increment  z = panner.orientationZ.value*Math.cos(-q) - panner.orientationY.value*Math.sin(-q);  y = panner.orientationZ.value*Math.sin(-q) + panner.orientationY.value*Math.cos(-q);  x = panner.orientationX.value;  panner.orientationX.value = x;  panner.orientationY.value = y;  panner.orientationZ.value = z;break;case 'rotate-down':  transform.rotateX -= degreesX;  // 'down' is rotation about x-axis with positive angle increment  z = panner.orientationZ.value*Math.cos(q) - panner.orientationY.value*Math.sin(q);  y = panner.orientationZ.value*Math.sin(q) + panner.orientationY.value*Math.cos(q);  x = panner.orientationX.value;  panner.orientationX.value = x;  panner.orientationY.value = y;  panner.orientationZ.value = z;break;

最后一件事 - 我们需要更新 CSS 并保留鼠标事件最后一步的引用。这是最终的moveBoombox 函数。

js
function moveBoombox(direction, prevMove) {  switch (direction) {    case "left":      if (transform.xAxis > leftBound) {        transform.xAxis -= 5;        panner.positionX.value -= 0.1;      }      break;    case "up":      if (transform.yAxis > topBound) {        transform.yAxis -= 5;        panner.positionY.value -= 0.3;      }      break;    case "right":      if (transform.xAxis < rightBound) {        transform.xAxis += 5;        panner.positionX.value += 0.1;      }      break;    case "down":      if (transform.yAxis < bottomBound) {        transform.yAxis += 5;        panner.positionY.value += 0.3;      }      break;    case "back":      if (transform.zAxis > innerBound) {        transform.zAxis -= 0.01;        panner.positionZ.value += 40;      }      break;    case "forward":      if (transform.zAxis < outerBound) {        transform.zAxis += 0.01;        panner.positionZ.value -= 40;      }      break;    case "rotate-left":      transform.rotateY -= degreesY;      // 'left' is rotation about y-axis with negative angle increment      z =        panner.orientationZ.value * Math.cos(q) -        panner.orientationX.value * Math.sin(q);      x =        panner.orientationZ.value * Math.sin(q) +        panner.orientationX.value * Math.cos(q);      y = panner.orientationY.value;      panner.orientationX.value = x;      panner.orientationY.value = y;      panner.orientationZ.value = z;      break;    case "rotate-right":      transform.rotateY += degreesY;      // 'right' is rotation about y-axis with positive angle increment      z =        panner.orientationZ.value * Math.cos(-q) -        panner.orientationX.value * Math.sin(-q);      x =        panner.orientationZ.value * Math.sin(-q) +        panner.orientationX.value * Math.cos(-q);      y = panner.orientationY.value;      panner.orientationX.value = x;      panner.orientationY.value = y;      panner.orientationZ.value = z;      break;    case "rotate-up":      transform.rotateX += degreesX;      // 'up' is rotation about x-axis with negative angle increment      z =        panner.orientationZ.value * Math.cos(-q) -        panner.orientationY.value * Math.sin(-q);      y =        panner.orientationZ.value * Math.sin(-q) +        panner.orientationY.value * Math.cos(-q);      x = panner.orientationX.value;      panner.orientationX.value = x;      panner.orientationY.value = y;      panner.orientationZ.value = z;      break;    case "rotate-down":      transform.rotateX -= degreesX;      // 'down' is rotation about x-axis with positive angle increment      z =        panner.orientationZ.value * Math.cos(q) -        panner.orientationY.value * Math.sin(q);      y =        panner.orientationZ.value * Math.sin(q) +        panner.orientationY.value * Math.cos(q);      x = panner.orientationX.value;      panner.orientationX.value = x;      panner.orientationY.value = y;      panner.orientationZ.value = z;      break;  }  boombox.style.transform =    "translateX(" +    transform.xAxis +    "px) translateY(" +    transform.yAxis +    "px) scale(" +    transform.zAxis +    ") rotateY(" +    transform.rotateY +    "deg) rotateX(" +    transform.rotateX +    "deg)";  const move = prevMove || {};  move.frameId = requestAnimationFrame(() => moveBoombox(direction, move));  return move;}

连接我们的控件

连接控制按钮相对简单 - 现在我们可以在控件上监听鼠标事件并运行此方法,并在释放鼠标时停止它:

js
// for each of our controls, move the boombox and change the position valuesmoveControls.forEach(function (el) {  let moving;  el.addEventListener(    "mousedown",    function () {      let direction = this.dataset.control;      if (moving && moving.frameId) {        window.cancelAnimationFrame(moving.frameId);      }      moving = moveBoombox(direction);    },    false,  );  window.addEventListener(    "mouseup",    function () {      if (moving && moving.frameId) {        window.cancelAnimationFrame(moving.frameId);      }    },    false,  );});

概述

希望本文能让你深入了解 Web Audio 空间化的工作原理,以及每个PannerNode 属性的作用(其中有很多属性)。这些值有时难以操作,根据你的使用情况,可能需要一些时间才能使它们正确。

备注:音频空间化在不同浏览器中的听起来略有不同。panner 节点在底层做了一些非常复杂的数学运算;这里有许多测试,因此你可以跟踪不同平台上此节点的内部工作状态。

再次,你可以在这里查看最终的演示,同时最终的源代码在这里。还有一个Codepen 的演示

如果你正在使用 3D 游戏和/或 WebXR,最好利用 3D 库来创建此类功能,而不是尝试从最初的规则完成所有这些操作。我们在本文中提出了自己的想法,让你了解它是如何工作的,但是通过利用别人在你之前所做的工作,你将节省大量时间。

Help improve MDN

Learn how to contribute

This page was last modified on byMDN contributors.


[8]ページ先頭

©2009-2025 Movatter.jp