原生音频插件 SDK
音频性能分析器

空间音响 SDK

概述

空间音响 SDK 是原生音频插件 SDK 的扩展,可用于更改音频从音频源传输到周围空间的方式。内置的音频源平移功能可视为一种简单的空间化形式,因为它获取音频源,并基于音频监听器 (AudioListener) 和音频源 (AudioSource) 之间的距离和角度来调节左耳和右耳影响力的增益。在水平面上为玩家提供了简洁的方向提示。

背景

随着虚拟现实和增强现实系统的出现,空间化方法作为满足玩家沉浸感的关键组成部分显得尤为重要。耳朵和大脑可以非常明显地察觉到左耳和右耳接收声音的细微延迟。此外,无意中可以分辨出高频平衡的变化,从而判断物体是在前面还是后面,甚至是上面还是下面。也可以根据每只耳朵听到的声音的不同来判断物体是否被遮挡,或者根据声音的反射推断出我们所处房间的结构。换言之:声音对于日常生活指引非常重要,可能平时没有过于关注!

就计算能力而言,声音遮挡是难以解决的问题。光在全局光照中的运动是一瞬间,声音的移动速度非常慢。因此,计算声音在房间中实际移动的轨迹(作为波)无法通过计算来实现。同理,在空间化的处理过程中,有着这方面的难题。

一些解决方案仅解决 HRTF 问题。HRTF 表示头部相关传输函数 (Head-Related Transfer Function),在图形学中可以类比为球形的谐波:即,两只耳朵听到的声音方向会影响滤波,包括耳朵之间的微弱延迟以及耳罩、头部本身和肩部构成的定向滤波。相对于传统平移解决方案,添加 HRTF 滤波已经极大地改善方向感(典型且有名的示例就是虚拟理发店的双耳录音)。但是直接 HRTF 在某种程度上受到限制,因为它只涉及音频的直线路径,而不涉及在空间中的传输情况。

遮挡是在此基础上更进一步的问题,它可以在墙壁上间接反射声音。若要再次与图形世界进行粗略类比,可将其与镜面反射进行比较;在某种意义上,音频源和听者位置都决定了结果,当然每个反射的定向声波都会以不同的 HRTF 抵达每只耳朵,并根据声波行进的路径长度而产生不同延迟。

在房间内反射多数情况类似全局光照解决方案的漫反射部分,因为声音被发散到房间后在多个墙壁上反射,最后作为重叠声波的效果抵达耳朵,每个声波都有不同的方向和相对于音频源的延迟效果。

SDK 和实现示例

对应于如此之多的难题,存在各种不同的音频空间化解决方案。在 Unity 中支持这些的最佳方法是创建一个开放式接口,也就是空间音响 SDK,这是基于原生音频插件 SDK 的扩展,允许将 Unity 中的标准平移器替换为更高级的平移器,并提供有关计算所需的音频源和监听器的重要元数据的访问权。

此处提供了空间音响的一个实现示例。这个示例故意做得很简单,只支持直接 HRTF,需要针对生产用途进行优化。插件随附一个简单的混响,仅用于展示如何将音频数据从空间音响插件路由到混响插件。HRTF 滤波基于 KEMAR 数据集,此数据集是由麻省理工学院媒体实验室的 Bill Gardner 在虚拟头部上进行的一组耳脉冲响应记录。这些脉冲响应通过快速傅立叶变换完成快速卷积与输入信号的卷积。位置元数据仅用于选取正确的脉冲响应集,因为数据集是由头部向下 40 度到向上 90 度仰角的循环脉冲响应组成。

初始化

Unity 中的空间音响效果和混音器效果之间的主要区别在于:空间音响效果位于产生音频数据流的音频源解码器之后,使得每个音频源具有自己的效果实例来处理该音频源产生的音频。这与混音器插件不同,混音器插件会处理连接到混音器组的各种音频源产生的音频混合。要使插件能够像这样工作,必须在效果的描述位字段中设置一个标志:

definition.flags |= UnityAudioEffectDefinitionFlags_IsSpatializer;

在初始化时设置此标志会在插件扫描阶段通知 Unity 这是一个空间音响效果,因此,在创建此插件的实例时会为 UnityAudioEffectState 结构的 spatializerdata 成员分配 UnityAudioSpatializerData 结构。

为了能够在项目中使用空间音响效果,首先需要在音频项目设置中选择空间音响效果:

空间音响插件选择器
空间音响插件选择器

在 AudioSource 上,Spatialize 复选框可以启用空间音响。此外,还可在脚本中通过 AudioSource.spatialize 属性来控制此选项。在具有大量音频的游戏中,合理做法是仅对附近的声音启用空间音响,而对远处的声音使用传统方案。

AudioSource 上的 Spatializer 复选框
AudioSource 上的 Spatializer 复选框

空间音响效果元数据

与混音器中对混合声音运行的其他效果不同,空间音响效果是在音频源解码音频数据后直接应用。因此,空间音响效果的每个实例都有一个自身的 UnityAudioSpatializerData 实例,该实例与音频源的主要数据相关联。

struct UnityAudioSpatializerData
{
    float listenermatrix[16]; // 此矩阵用于将音频源位置变换为监听器的本地空间
    float sourcematrix[16];   // 变换音频源的矩阵
    float spatialblend;       // 距离控制的空间混合
    float reverbzonemix;      // 音频源上的混响区混音级别参数(和曲线)
    float spread;             // 音频源的传播参数(0 到 360 度)
    float stereopan;          // 音频源的立体声平移参数(-1:全左,1:全右)
                              // 空间音响插件可以重写距离衰减
                              // 以便影响语音优先级(将此回调保留为 NULL 可使用
                              // 内置音频源衰减曲线)
    UnityAudioEffect_DistanceAttenuationCallback distanceattenuationcallback;
};

该结构包含用于监听器和音频源的完整 4x4 变换矩阵。监听器矩阵已经被反转,因此可轻松将两个矩阵相乘以获得相对方向矢量。监听器矩阵始终是正交的,因此反转计算开销很低。此外,该结构还包含与音频源属性(Spatial Blend、Reverb Zone Mix、Spread 和 Stereo Pan)相对应的字段。空间音响负责正确地实现这些字段,因为当空间音响处于活动状态时,Unity 的音频系统仅以立体声信号形式提供原始音频源(即使音频源为单声道或多声道,这种情况下使用上混或下混)。

矩阵协议

sourcematrix 字段包含与音频源相关联的变换矩阵的普通副本。对于游戏对象上未旋转的普通音频源,这只是一个转换矩阵,在元素 12、13 和 14位置处编码。 listenermatrix 字段包含与音频监听器相关联的变换矩阵的逆矩阵。这样可以非常方便地确定从监听器到音频源的方向矢量,如下所示:

float dir_x = L[0] * S[12] + L[4] * S[13] + L[ 8] * S[14] + L[12];
float dir_y = L[1] * S[12] + L[5] * S[13] + L[ 9] * S[14] + L[13];
float dir_z = L[2] * S[12] + L[6] * S[13] + L[10] * S[14] + L[14];

其中 L 表示 listenermatrix 字段,S 表示 sourcematrix 字段。如果一个 listenermatrix 未旋转并且具有均匀缩放值 1(摄像机矩阵绝不能缩放),请注意 (L[12], L[13], L[14]) 中的位置实际上是您在 Unity 的检视面板中看到的值的反数值。这是因为 listenermatrix 是摄像机变换矩阵的逆矩阵。如果摄像机也已旋转,我们将无法仅通过求反直接从矩阵读取位置,而必须首先撤消旋转的效果。幸运的是,很容易对此类变换-旋转-缩放矩阵进行求逆,具体方法如此处所述,所以我们需要做的是转置 L 的左上角 3x3 旋转矩阵,并按如下所示来计算位置:

float listenerpos_x = -(L[0] * L[12] + L[ 1] * L[13] + L[ 2] * L[14]);
float listenerpos_y = -(L[4] * L[12] + L[ 5] * L[13] + L[ 6] * L[14]);
float listenerpos_z = -(L[8] * L[12] + L[ 9] * L[13] + L[10] * L[14]);

衰减曲线和可听度

仍然由 Unity 音频系统处理的唯一事情是距离衰减,距离衰减在声音进入空间化阶段之前应用于声音,并且必须这样做才能让音频系统知道音频源的近似可听度,这可以用于基于重要性的声音动态虚拟化,从而匹配用户定义的 Max Real Voices 限制。由于这是一个“先有鸡还是先有蛋”问题,所以这个信息不是从实际信号水平测量中检索得出,而是对应于我们从距离控制衰减曲线、Volume 属性和混音器应用的衰减中读取的值的组合结果。但是可以用您自己的曲线来覆盖衰减曲线,或者使用通过音频源的曲线计算出的值作为修改基础。为此,UnityAudioSpatializerData 结构中提供了一个可以实现的回调:

typedef UNITY_AUDIODSP_RESULT (UNITY_AUDIODSP_CALLBACK* UnityAudioEffect_DistanceAttenuationCallback)(
    UnityAudioEffectState* state,
    float distanceIn,
    float attenuationIn,
    float* attenuationOut);

一个简单的自定义对数曲线可以按如下方式实现:

UNITY_AUDIODSP_RESULT UNITY_AUDIODSP_CALLBACK SimpleLogAttenuation(
    UnityAudioEffectState* state,
    float distanceIn,
    float attenuationIn,
    float* attenuationOut)
{
    const float rollOffScale = 1.0f; // 类似于音频项目设置
    *attenuationOut = 1.0f / max(1.0f, rollOffScale * distanceIn);
    return UNITY_AUDIODSP_OK;
}

脚本 API

作为本地端的补充,音频源中还有两个新方法,允许从空间音响效果中设置和获取参数。这两个新方法的名称为 SetSpatializerFloat/GetSpatializerFloat,它们的工作方式类似于通用原生音频插件接口中使用的 SetFloatParameter/GetFloatParameter。主要区别在于 SetSpatializerFloat/GetSpatializerFloat 接受要设置/读取的参数并进行索引,而 SetSpatializerFloat/GetSpatializerFloat 按名称引用这些参数。

此外,布尔值属性 AudioSource.spatializer 链接到音频源检视面板中的复选框,并根据音频项目设置中的选择来控制空间音响效果的实例化和取消分配。如果空间音响效果的实例化会导致开销巨大(在内存分配、预计算等方面),可考虑让 Unity 插件接口绑定关系保持轻量级,并动态分配效果到资源池中,使激活/停用不会导致丢帧。

示例插件的已知限制

由于使用了快速卷积算法,因此快速移动会导致一些拉链瑕疵,这些瑕疵可以通过使用重叠保存卷积或交叉淡化缓冲来消除。此外,代码不支持将头部倾斜到侧面,但应该很容易修复此问题。 KEMAR 数据集是本演示中唯一使用的数据集。IRCAM 提供了几个通过人类受试者获取的数据集

原生音频插件 SDK
音频性能分析器