一、背景

在社交元宇宙、大逃杀等类型的游戏场景下,用户在通过简单语音交流外,结合场景也需要一些立体声效果来让用户感知游戏角色周围其他用户的存在及其对应的距离和方位,提高语音互动的趣味性。

为了满足上述需求 ZEGO Express Web SDK 从  v2.10.0(Native 为 v2.11.0)开始加入范围语音功能模块,为游戏提供语音服务。

当前范围语音功能模块主要包括如下功能:

  • 范围语音:房间内的收听者对音频的接收距离有范围限制,若发声者与自己的距离超过该范围,则无法听到声音。
  • 3D 音效:听者接收的声音根据发声者相对于听者的距离和方位模拟现实中声音的立体声效果。
  • 小队语音:玩家可以选择加入小队,并支持在房间内自由切换“全世界”模式和“仅小队”模式。

其中对于 Web 3D 音效这部分功能的实现,我们是基于浏览器提供的 Web Audio API 对音频进行处理。这里小编也通过使用 Web Audio API 做了一个简单的环绕音的 demo 页面。

demo 在线体验地址:https://keen_wang.gitee.io/demo/music3d,页面如下图,点击“开始播放”按钮开始播放音乐,再点击“开闭空间化”进行开启或关闭 3D 音效,打开 3D 音效后就可以听到空间环绕声效果。(在体验 3D 音效时需要使用左右声道分开的耳机或音响设备)

下文将介绍如何使用 Web Audio API 来做这个环绕音 demo。

二、Web Audio API 简介

Web Audio API 用于操作声音,很多时候用于替代 <audio> 标签来播放一段音频,除此之外,还有音频处理的功能,比如音量调节、音频混合、音频空间化等。

Web Audio API 使用户可以在音频上下文( AudioContext )中进行音频操作,具有模块化路由的特点。

下面是最简单的一个路由图,表示音频源通过效果处理后输出到音频目的地,图中的 inputs、Effects、Destination 三个模块分别对应为音频节点( AudioNode )的输入源节点、处理节点、输出节点。

下面我们将介绍 Web Audio API 的简单使用步骤:

1、创建 AudioContext 音频上下文实例

// 创建音频上下文
const AudioContext = window.AudioContext || window.webkitAudioContext;
const audioCtx = new AudioContext();

AudioContext 为音频处理提供一个上下文环境,相当于一个中央控制器,用于控制着音频路由图中的各个音频节点。

2、在音频上下文里创建输入源节点和处理节点。

// 创建输入结点,解码 audio 标签的音频源
const audioEl = document.querySelector('audio');
const sourceNode = audioCtx.createMediaElementSource(audioEl);
// 创建用于控制音频振幅的处理结点 GainNode
const gainNode = audioCtx.createGain(); 

3、将输入源节点连到处理节点。

输入源节点通过 connect 方法将音频数据传输给处理节点。 

sourceNode.connect(gainNode);

4、将处理节点连接到选定的输出节点进行效果输出。

处理节点通过 connect 方法将处理完的音频数据传输给输出节点进行效果输出。 

这里的输出节点 audioCtx.destination 为当前使用的扬声器。

gainNode.connect(audioCtx.destination);

5、修改处理节点的属性以修改输出效果。

// 设置静音处理
gainNode.gain.setValueAtTime(0, audioCtx.currentTime);

了解完 Web Audio API 的使用特点,接下来介绍如何进行音频空间化处理。

三、实现 3D 音效

音频空间化的实现主要是通过 PannerNode 和 AudioListener 结合使用来处理声音效果。这两个类的实例对象进行设置空间方位信息后动态处理音频源并输出到左右声道。

  • AudioListener 对象代表三维空间中的听者(用户),通过 AudioContext.listener 属性获取对应的实例对象;

  • PannerNode 对象指的是三维空间中的声音源,通过 new 的方式或者 AudioContext.createPanner() 创建得到。

下面将介绍如何设置 AudioListener 和 PannerNode 的属性来改变 3D 音效效果。

1、设置 AudioListener 

AudioListener 对象表示听者,这里可以定义听者在空间中的位置和他(她)们面向的方向,PannerNode 可以计算出声音相对于收听者位置的位置。

对于听者位置信息,AudioListener 提供了三个位置属性:positionX 、positionY 、positionZ ,它分别代表听者当前位置的 xyz 坐标,这里坐标系使用的是右手笛卡尔坐标系,x 轴和 z 轴在水平方向、y 轴在垂直方向。

// 为 listener 设置 position
const listener = audioCtx.listener;
listener.positionX = camera.position.x;
listener.positionY = camera.position.y;
listener.positionZ = camera.position.z; 

(听者朝向向量的图示说明)

对于听者的朝向可通过 AudioListener 的forwardXforwardY 、forwardZ 这三个属性设置听者的正面朝向向量,默认值是 (0,0,-1) 。通过 AudioListener 的 upXupYupZ 这三个属性设置听者的头顶朝向方向向量,默认值是 (0,1,0) ,即垂直朝上的方向。通过这两个朝向向量的设置,即可确定听者左右耳的位置来生成立体声效果。

2、设置 PannerNode 

PannerNode 是一个处理节点,提供了 3D 空间音频能力,PannerNode 通过相对于 AudioContext 的 AudioListener 的位置和朝向信息对声音进行空间化处理。

PannerNode 有以下几个常用属性:

  • panningModel:音频空间化算法模型,默认值是 “equalpower”,即等幂平移算法,建议设置更为只能的 “HRTF” 。
  • positionX/positionY/positionZ:声源位置坐标。
  • orientationX/orientationY/orientationZ:声源朝向向量。
  • coneInnerAngle:锥形角度,单位为度,默认是 360。
  • rolloffFactor:声音随距离的衰减速度,默认值为 1。
  • distanceModel:声音衰减算法模型,默认值是 “inverse”,即相反距离模型。

3、环绕音 Demo

了解了这些 Web Audio API,就可以开始实现一个音频空间化效果了。下面是一个播放环绕声歌曲的 demo 代码,模拟空间中一个音源在听者周围环绕。通过在播放过程中动态修改 PannerNode 的定位信息来生成环绕效果。

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Web Audio</title>
</head>

<body>
  <audio loop autoplay crossorigin="anonymous"
    src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/858/outfoxing.mp3"></audio>
  <button onclick="startPlay()">开始播放</button>
  <button onclick="spatialize()">开闭空间化</button>
  <span>音效状态:</span><span id="status">关闭</span>
  <script>
    // 音源初始位置信息
    const audioPosition = [0, 0, 1]
    // 创建音频上下文
    const AudioContext = window.AudioContext || window.webkitAudioContext;
    const audioCtx = new AudioContext();
    // 设置 AudioListener
    const listener = audioCtx.listener;
    listener.positionX.value = 0;
    listener.positionY.value = 0;
    listener.positionZ.value = 0;
    listener.forwardX.value = 0;
    listener.forwardY.value = 0;
    listener.forwardZ.value = -1;

    // 创建输入结点,解码 audio 标签的音频源;创建处理结点,处理音频
    const audioEl = document.querySelector('audio');
    const sourceNode = audioCtx.createMediaElementSource(audioEl);
    // 创建和设置 PannerNode
    const pannerNode = new PannerNode(audioCtx, {
      panningModel: "HRTF",  // 音频空间化算法模型
      distanceModel: "linear",  // 远离时的音量衰减算法
      rolloffFactor: 1,  // 衰减速度
      coneInnerAngle: 360, // 声音 360 度扩散
      positionX: audioPosition[0],
      positionY: audioPosition[1],
      positionZ: audioPosition[2],
      maxDistance: 10000,
    });
    // 将输入节点直接连接到输出节点
    sourceNode.connect(audioCtx.destination);

    // 设置音源自动分别沿 xyz 三个轴来回移动效果,形成环绕效果
    function autoMove(axis, interval, step = 100, maxDistance = 1000) {
      let isAdd = true
      const positionAxisMap = ["positionX", "positionY", "positionZ"]
      setInterval(() => {
        if (isAdd && audioPosition[axis] >= maxDistance) {
          isAdd = false;
        } else if (!isAdd && audioPosition[axis] <= -maxDistance) {
          isAdd = true;
        }
        if (isAdd) {
          audioPosition[axis] += step;
        } else {
          audioPosition[axis] -= step;
        }
        pannerNode[positionAxisMap[axis]].value = audioPosition[axis]
        console.log('audioPosition', audioPosition);
      }, interval)

    }
    // 沿 x 轴在 -1000 到 1000 之间来回移动
    autoMove(0, 100, 100, 1000)
    // 沿 z 轴在 -1000 到 1000 之间来回移动
    autoMove(2, 200, 100, 1000)
    // 沿 y 轴在 -100 到 100 之间来回移动
    autoMove(1, 400, 10, 100)

    // 开始播放音乐
    function startPlay() {
      audioCtx.resume();
      // 设置静音播放。
      audioEl.play();
    }

    // 开关 3D 音效
    let isSpatialized = false
    function spatialize() {
      isSpatialized = !isSpatialized
      document.querySelector("#status").innerText = isSpatialized ? "开启" : "关闭"
      if (isSpatialized) {
        sourceNode.disconnect();
        sourceNode.connect(pannerNode);
        // 将处理节点连接到 destination 输出节点进行效果输出。
        pannerNode.connect(audioCtx.destination);
      } else {
        sourceNode.disconnect();
        sourceNode.connect(audioCtx.destination);
      }
    }
  </script>
</body>

</html>

四、结语

本文主要讲解了对于 Web Audio API 的基本使用及使用 AudioListener 和 PannerNode 实现环绕声效果。 

Web Audio API 除了进行 3D 音效外还有很多强大的音频处理能力,可以查看 MDN 上的文档了解 Web Audio API 更多能力,链接:https://developer.mozilla.org/zh-CN/docs/Web/API/Web_Audio_API#%E5%AE%9A%E4%B9%89%E9%9F%B3%E6%95%88 。

如果想要了解更多关于 ZEGO Express SDK 范围语音功能模块,可以查看 ZEGO 官网介绍文档,链接: https://doc-zh.zego.im/article/12045 。

也可以打开我们的范围语音 Demo 进行体验:https://zegoim.github.io/express-demo-web/src/Examples/Others/RangeAudio/index.html

Logo

致力于链接即构和开发者,提供实时互动和元宇宙领域的前沿洞察、技术分享和丰富的开发者活动,共建实时互动世界。

更多推荐