3D音频
在前面系列中,我们了解如何定位追踪用户(实际是定位用户的移动设备)的位置与方向,然后通过摄像机的投影矩阵将虚拟物体投影到用户移动设备屏幕。如果用户移动了,则通过VIO 和 IMU更新用户的位置与方向信息,更新投影矩阵,这样就可以把虚拟物体固定在空间的某点上(这个点就是锚点),从而达到以假乱真的视觉体验。
3D音效处理的目的是让用户进一步相信AR 应用虚拟生成的数字世界是真实的,营造沉浸的AR体验。事实上,3D音效在电影、电视、电子游戏中被广泛应用,但在AR 场景中,3D声音的处理有其特别之处,类似于电影采用的技术并不能很好地解决 AR中3D音效的问题。
在电影院中,观众的位置是固定的,因此可以通过在影院的四周都加装上音响设备,通过设计不同位置音响设备上声音的大小和延迟,就能给观众营造逼真的3D 声音效果。经过大量的研究与努力,人们根据人耳的结构与声音的传播特性开发出了很多技术,可以只用两个音响或者耳机就能模拟出3D音效,这种技术叫双耳声(Binaural Sound),它的技术原理如图11-2所示。
在图11-2中,从声源发出来的声音会直接传播到左耳和右耳,但因为左耳离声源近,所以声音会先到达左耳再到达右耳,由于在传播过程中的衰减,左耳听到的声音要比右耳大,这是直接的声音信号,大脑会接收到两只耳朵传过来的信号。同时,从声源发出的声音也会被周围的物体反射,这些反射与直接信号相比有一定的延迟并且音量更小,这些是间接的声音信号。大脑会采集到直接信号与所有的间接信号并比较从左耳与右耳采集的信号,经过分析计算,从而达到定位声源的效果。在了解大脑的工作模式后,就可以通过算法控制两个音响或者耳机的音量与延迟来达到模拟3D声源的效果,让大脑产生出虚拟的3D声场效果。
3D声场原理
3D声场,也称为三维音频、虚拟3D音频、双耳音频等,它是根据人耳对声音信号的感知特性,使用信号处理的方法对到达两耳的声音信号进行模拟,以重建复杂的空间声场。
通俗地说就是把耳朵以外的世界看作一个系统,对任意一个声音源,在耳膜处接收到信号后,三维声场重建就是把两个耳朵接收到的声音尽可能准确地模拟出来,让人产生听到三维音频的感觉。
如前所述,当人耳在接收到声源发出的声音时,人的耳廓、耳道、头盖骨、肩部等对声波的折射、绕射和衍射及鼓膜接收到的信息会被大脑所接收,大脑通过经验对声音的方位进行判断。与大脑工作原理类似,在计算机中通过信号处理的数学方法,构建头部相关传输函数(HeadRelated Transfer Functions, HRTF),根据多组的滤波器计算人耳接收到的声源的“位置信息”。
目前3D声场重建技术口冬比校成戴们不时加送之上术都假设用户是静止的(或者说与用户位置无关),而在 AR应用中,情况却有很大不同,AR应用的用户是随时移动的,这意味着用户周围的3D声音也需要调整,这一特殊情况导致目前的3D声场重建技术在AR应用时失效。
RealityKit 中的 3D 音效
ARKit 通过世界跟踪功能定位声源位置,然后根据用户与声源的相对位置和方向自动混音,将3D音频技术带人 AR中。在 AR场景中放置一个声源,当用户接近或远离时,声音音量大小会自动增加或减弱,当用户围绕声源旋转时,声音也会呈现沉浸式的3D效果。在 RealityKit 中,使用3D音效的典型代码如代码如下所示,稍后我们将对代码进行详细解析。
//
// Audio3DView.swift
// ARKitDeamo
//
// Created by zhaoquan du on 2024/3/22.
//
import SwiftUI
import ARKit
import RealityKit
import Combine
struct Audio3DView: View {
var body: some View {
Audio3DViewContainer().navigationTitle("3D音频").edgesIgnoringSafeArea(.all)
}
}
struct Audio3DViewContainer:UIViewRepresentable {
func makeUIView(context: Context) -> some ARView {
let arView = ARView(frame: .zero)
let config = ARWorldTrackingConfiguration()
config.planeDetection = .horizontal
//createPlane(arView: arView)
arView.session.run(config)
arView.createAudioPlane()
return arView
}
func updateUIView(_ uiView: UIViewType, context: Context) {
}
static var audioEvent : Cancellable!
func createPlane(arView:ARView){
let planAnchor = AnchorEntity(plane: .horizontal)
let boxMesh = MeshResource.generateBox(size: 0.2)
let boxMaterial = SimpleMaterial(color: .red, isMetallic: true)
let boxEntity = ModelEntity(mesh: boxMesh, materials: [boxMaterial])
guard let audio = try? AudioFileResource.load(named: "fox.mp3",in: .main, inputMode: .spatial,loadingStrategy: .preload,shouldLoop: false) else {
return
}
let audioControler = boxEntity.prepareAudio(audio)
audioControler.play()
boxEntity.generateCollisionShapes(recursive: false)
planAnchor.addChild(boxEntity)
arView.scene.addAnchor(planAnchor)
arView.installGestures(for: boxEntity)
Audio3DViewContainer.audioEvent = arView.scene.subscribe(to: AudioEvents.PlaybackCompleted.self) { event in
print("音频播放完毕")
}
}
}
var audioEvent : Cancellable!
extension ARView{
func createAudioPlane(){
do{
let planeAnchor = AnchorEntity(plane:.horizontal)
let boxMesh = MeshResource.generateBox(size: 0.2)
let boxMaterial = SimpleMaterial(color:.red,isMetallic: true)
let boxEntity = ModelEntity(mesh:boxMesh,materials:[boxMaterial])
let audio = try AudioFileResource.load(named:"fox.mp3",in:.main,inputMode: .spatial,loadingStrategy: .preload,shouldLoop: false)
boxEntity.playAudio(audio)
let audioController = boxEntity.prepareAudio(audio)
audioController.play()
boxEntity.generateCollisionShapes(recursive: false)
planeAnchor.addChild(boxEntity)
self.scene.addAnchor(planeAnchor)
self.installGestures(.all,for:boxEntity)
audioEvent = self.scene.subscribe(
to: AudioEvents.PlaybackCompleted.self
){ event in
print("音频播放完毕")
}
}
catch{
print("Error Loading audio file")
}
}
}
#Preview {
Audio3DView()
}
编译运行 AR 应用,使用耳机(注意耳机上的左右耳塞勿戴反,一般会标有I.和 R字样)或者双通道音响体验3D音效,在检测到的平面放置虛拟立方体对象后,移动手机或者旋转手机朝向,体验在 AR场景中声源定位的效果。
从代码清单 11-3中我们可以看出,在 RealityKit 中使用3D音频分为3步:
(1) 加载音频。
(2)设置音频播放参数。
(3)将音频放置到 AR 场景中并播放。
下面我们针对这3个步骤进行详细学习。在 RealityKit 中使用音频,必须将音频加载为 AudioResource(或者其子类 AudioFileResource)类型对象才能正确播放,通常使用 AudioFileResource 类将音频从文件系统或者 URL 加载到内存中,该类有4个方法,可以同步/异步从文件/URL 中加载音频,如表11-2所示。
表 11-2 AudioFileResource 类加载音频方法
方法 | 描述 |
load (named: String, in: Bundle?,inputMode: AudioResource. InputMode,loadingStrategy: AudioFileResource. LoadingStrategy,shouldLoop: Bool)—> AudioFileResource | 同步从程序 Bundle 中加载音频 |
loadAsync(named: String,in: Bundle?, inputMode: AudioResource. InputMode,loadingStrategy: AudioFileResource. LoadingStrategy,shouldLoop: Bool)—> LoadRequest < AudioFileResource > | 异步从程序 Bundle 中加载音频 |
load ( contentsOf: URL, withName: String?, inputMode: AudioResource. InputMode,loadingStrategy: AudioFileResource. LoadingStrategy,shouldLoop: Bool) -> AudioFileResource | 同步从 URL 中加载音频 |
load Async(contentsOf: URL, withName: String?, inputMode: AudioResource. InputMode,loadingStrategy: AudioFileResource. LoadingStrategy,shouldLoop: Bool) -> LoadRequest < AudioFileResource > | 异步从 URL 中加载音频 |
在 RealityKit 中加载音频与加载模型一样,每一种同步加载方法都有对应的异步加载方法。加载方法中的参数因加载方法不同而不同,基本的参数及其意义如表11-3所示。
表11-3 加载音频方法中各参数的意义
描述 | 方法 |
named | 从 Bundle 中加载时文件路径与名称 |
contentsOf | 从 URL 中加载时的 URL 地址 |
withName | 从 URL 中加载时的音频名称 |
in | 从程序 Bundle 中加载时音频所在 Bundle 名称 |
shouldLoop | 布尔值,是否循环播放 |
loadingStrategy | AudioFileResource. LoadingStrategy 枚举类型,指定加载音频时的策略,共有两个枚举值:preload (预加载音频,在使用之前将音频加载到内存中)、stream(流媒体编码,边加载边播放)。通常在使用时,preload 适合短小、内存占用少,播放频度高的音频,而 stream适合较长、播放频度低的音频 |
inputMode | AudioResource. InputMode 枚举类型,指定3D音频类型,共有3个枚举值:nonSpatial(不使用3D音效)、spatial(使用空间音效)、ambient(环境音效,声音不会随距离发生音量变化,但声音可以反映方向变化,如用户围绕声源转动时,音效会发生变化) |
在加载音频完成后,可以通过实体对象的 prepareAudio(_:)方法获取一个 AudioPlaybackController 类型的音频控制器,利用该控制器可以使用其 play()、pause()、stop()方法控制音频的播放,可以通过isPlaying 属性获取音频的播放状态,还可以设置音频的播放增益(gain)、速度(speed)、混音(reverb SendL.evel),及衰減(fade()方法)和音频播放完后的回调(completionHandler),可以满足音频使用的各类个性化需求。
通常在 AR中使用3D,音频,需要将音频绑定到实体对象(Entity)上,当实体对象放置在场景中时,实体对象所在的空间位置即为声源位置,RealityKit 会根据用户设备所在空间位置与声源位置进行3D音效模拟,营造沉浸式的声场效果。
利用 AudioPlaybackController 类可以很方便地控制音频的播放,而且可以重复地进行暂停、播放等操作,但如果只需要一次性地播放,也可以不使用该类,而直接使用boxEntity. playAudio(audio),这种方法更简洁,当音频播放完后即结束,特别适合3D物体音效模拟,如子弹击中柽物时的音频播放。
在使用 AudioPlaybackController 类控制音频播放时,可以通过其 completionHandler 属性设置音频播放完后的回调函数。除此之外,也可以通过订阅 AudioEvents 事件进行后续处理,目前,音频只有一个AudioEvents. PlaybackCompleted 事件,即音频播放完毕事件。
在订阅 AudioEvents事件时有两点需要注意,一是保存事件订阅的引用,不然无法捕获事件,具体可参阅第2章相关内容:三是只有当 shouldLoop 设置为 false(即不循环播放)时,才会触发 AudioEvents.PlaybackCompleted 事件。