空间音频
ᯅ Apple Vision Pro 发售,沉溺式体会再上一台阶,是时分为你的 visionOS App 和游戏适配一下空间音频作用了。
空间音频(Spatial Audio)是一种音频技能,经过增加高档编码和信号处理技能,模拟和再现三维空间中的声响方位和环境,供给愈加沉溺和传神的听觉体会。它使听众能够感受到声响的方向、间隔和运动,从而创造出愈加实在和身临其境的听觉环境。
空间音频广泛运用于各种领域,包括音乐制造、电影、游戏和虚拟现实(VR)等。它供给了愈加沉溺和传神的听觉体会,运用户能够更好地感受到环境音效、音乐和声响作用的立体和实在感。
有 AirPods 的同学或许已经体会过相应的作用,特别是开启了空间音频后,左右回头时会有,音源是固定在实在空间的感受。
经过阅览本文,咱们将会:
- 具体了解苹果的空间音频开发结构 PHASE
- 运用 SceneKit 来完成一个空间方位的可视化功用
- 完成一个空间音频的 Demo,感受音频在你耳边盘绕的感觉
PHASE
PHASE 便是一个苹果供给的用来开发空间音频的结构。
先看官方文档,来个大致了解:
运用PHASE(物理音频空间化引擎)为您的游戏和运用供给杂乱而动态的音频体会。经过PHASE,您能够实时操控声响层并调整音频参数。在开发运用程序时,与运用程序的视觉场景的动态集成使音频能够主动响应逻辑和视觉变化。该结构支持各种音频硬件,使您的运用程序能够在各渠道和耳机、扬声器等输出设备上供给共同的空间音频体会。
PHASE将声响与视觉作用相结合,并经过以下方法最大限度地减少运用的音频保护:
- 接受场景几许信息,并下降被阻挡的发声场景目标的音量。例如,当玩家躲在墙后时,PHASE会下降飞来的火球的音量。
- 供给依据运用程序运转时状况播映的杂乱声响事情。例如,当玩家走在草地上或许沙砾上时,应当宣布不一样的声响
- 增加从形状宣布的音效。当您向PHASE供给场景目标的形状时,声响的音量会依据玩家与形状的间隔和方向进行调整。例如,给一条河增加体积音源,这样就不需求经过多个点音源来模拟一条河宣布的水流声响。
- 增加混响和守时音频反射,以创立环境作用并模拟室内场景。例如,在大房间或许小房间时,声响的混响和反射是不同的。
如何做?
咱们先不着急一头扎直接扎进文档堆里,先来还原声响原本的样子,设想一下:你头戴 Apple Vision Pro,坐在沙发上,想声临其境的听一支乐队为你演奏。假如咱们尝试建模这样一个场景,并且整理出来有哪些因素会影响到你赏识音乐的。
- 收听者:你所在的方位和朝向都会影响到收听的作用
- 音源:放置的方位、朝向,音源形状等
- 或许存在的遮挡:音源与收听者之前是否有遮挡?遮挡特的材料影响,是墙?仍是木门?
- 房间的大小会影响混响
这是一个比较简略的场景,PHASE 经过构建一个虚拟房间, 设置一个虚拟的听众, 依照实在的方位来放置乐器音源,假如你佩戴了 Airpods 来收听这场音乐会,当你左右回头的时分,你会感觉乐器的音源是放在那个固定的方位,假如咱们实时的调整音源方位的变化,你会感受到声响在运动,这便是空间音乐。
当然还有更杂乱的影响因素,后边再讲,下面介绍一个官方文档内容
概念
下面几个便是咱们上面说到实在场景中几个目标的抽象,比较好理解。
PHASEListener
一个用于界说在场景中对用户最易听到的方位的中心参阅点。
PHASESource
一个从场景中的3D方位和方向播映音频的目标,由 3D transform 来定位和定向。可所以一个发声点或区域,能够有形状或许体积。经过运用PHASEShape来表明 3D 体积。
PHASEOccluder
一个具有形状和方位的目标,阻止音频达到听众的方位。声响经过个方位时,会被下降音量。在咱们说的例子中并未运用,但在游戏中较为常用。同样经过运用PHASEShape来表明 3D 体积。
Sound Event Nodes
Sound Event 代表一个逻辑层次结构,它界说结构在运转时播映声响的内容、时刻和方法。简略来说便是,我的经过 Sound Event 来告诉引擎,放什么和怎样放,所以主要分红两种节点:Audio-Providing Nodes、Control Nodes。
其间 Audio-Providing Nodes 由 PHASESamplerNodeDefinition 来界说完整音频、由PHASEPushStreamNodeDefinition 来界说音频流,一起也能够设置播映形式、除掉选项、响度校对。
Control Nodes 有这几种:PHASESwitchNodeDefinition、PHASERandomNodeDefinition、PHASEBlendNodeDefinition、PHASEContainerNodeDefinition,如下:
Sound Event Tree
这个能够借用官方 Session 里的一张图来解释:一个 Sound Event 的逻辑层次结构,从图中就能够看到上面说到过的一切 Node。
Mixer
Mixer 是来决定音频分层和作用的,有三个类型
-
PHASEChannelMixerDefinition:这个是直接将声响输出,不考虑作用,比如:菜单的点击音效。
-
PHASEAmbientMixerDefinition:界说在 3D 空间中的特定方向输出声响的音频分层。
-
PHASESpatialMixerDefinition:界说在 3D 场景中,跟据环境特征来决定音频作用。
这是要点说一下 PHASESpatialMixerDefinition,PHASESpatialMixerDefinition 有三种方法会影响到声响作用,分别是:Sound Reflections and Resonance(声响的反射和共识)、Distance Modeling(间隔建模,声响近大远小)、Sound Directivity(声响指向性,想象一个扬声器,它正对着你的时分声响大)。
PHASEEngine
是时分来了解引擎了,PHASEEngine 是用来办理音频资源、操控播映和装备环境作用的。依照官方文档将基功用分为:
- Creating an Engine:创立引擎,能够装备更新规矩。
- Registering Audio Resources:加载和卸载音频资源的目标,完整音频需求提早注册、Sound Event 也需求提早注册。
- Accessing Scene Hierarchy:拜访场景层次结构,像上面说到的 Listener、Source、Occluder 的方位联系。
- Defining Environmental Effects:决定声响的共识环境、传播的物理原料、创立 3D 声响体会的形式。
- Controlling and Inspecting Playback State:操控和查看播映状况,这是播映器都有的,如:暂停、播映等。
- Managing Groups of Sounds:办理声响组的,有一些声响会运用相同装备,会界说出声响组。
- Accessing In-Flight Audio:在各种运转时环境下播映的声响的集合。
- Measuring Units:时刻和间隔的单位转换,统一标准运用。
Demo
咱们将做一个经过摇杆来操控的飞机,飞机能够在你的周围飞行,运用 PHASE 来播映音频,这样就能够感受一下飞机在你耳边盘绕飞行的空间音频体会。
准备工作
由于涉及到 3D 方位一起还有小飞机,所以咱们能够基于 SceneKit 初始工程修改就能够。PHASE 不挑渠道,非常独立,能够基于 visionOS、iOS、iPadOS 等,也不挑与之合作的技能栈,能够合作 UIKit、SwiftUI、RealityKit、SceneKit 等一起开发。
- 创立 SceneKit 游戏工程
- 强制横屏,并运转程序,就会发现有一个飞机在旋转,咱们将运用这个飞来完成咱们的 Demo。
- 咱们对工程做简略修改,在 GameViewController 中将摄像头视角调整为从正上方往下看,并移除旋转相关代码。
// create and add a camera to the scene
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
scene.rootNode.addChildNode(cameraNode)
// place the camera
cameraNode.position = SCNVector3(x: 0, y: 0, z: 15)
// 将以上代码修改为
// create and add a camera to the scene
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.camera?.usesOrthographicProjection = true
cameraNode.camera?.orthographicScale = 20
scene.rootNode.addChildNode(cameraNode)
// place the camera
cameraNode.position = SCNVector3(x: 0, y: 15, z: 0)
cameraNode.eulerAngles = SCNVector3(-Float.pi / 2, 0, 0)
// 注释此行代码
// animate the 3d object
// ship.runAction(SCNAction.repeatForever(SCNAction.rotateBy(x: 0, y: 2, z: 0, duration: 1)))
// 将 allowsCameraControl 修改为 false
// allows the user to manipulate the camera
scnView.allowsCameraControl = false
完成后作用如下:
- 运用 SPM 增加一个开源的摇杆控件 github.com/michael94el… , 并在页面上增加控件
// 由于控件是 SwiftUI 控件,咱们桥接一下
import SwiftUI
import SwiftUIJoystick
public struct Joystick: View {
@ObservedObject public var joystickMonitor: JoystickMonitor
private let dragDiameter: CGFloat
private let shape: JoystickShape
public init(monitor: JoystickMonitor, width: CGFloat, shape: JoystickShape = .circle) {
self.joystickMonitor = monitor
self.dragDiameter = width
self.shape = shape
}
public var body: some View {
VStack{
JoystickBuilder(
monitor: self.joystickMonitor,
width: self.dragDiameter,
shape: .circle,
background: {
// Example Background
Circle().fill(Color.blue.opacity(0.9))
.frame(width: dragDiameter, height: dragDiameter)
},
foreground: {
// Example Thumb
Circle().fill(Color.black)
.frame(width: 20, height: 20)
},
locksInPlace: true)
}
}
}
增加 GameViewController 中增加如下代码
@ObservedObject var monitor = JoystickMonitor()
咱们在 viewDidLoad 里增加摇杆
let joyStick = Joystick(monitor: monitor, width: 150, shape: .circle)
let hostingController = UIHostingController(rootView: joyStick)
view.addSubview(hostingController.view)
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
hostingController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -70),
hostingController.view.centerXAnchor.constraint(equalTo: view.centerXAnchor),
hostingController.view.widthAnchor.constraint(equalToConstant: 150),
hostingController.view.heightAnchor.constraint(equalToConstant: 150)
])
hostingController.didMove(toParent: self)
运转代码:
- 完成摇杆来操作飞机
// 作法有点粗糙,但这不是要点
cancel = monitor.objectWillChange.sink { _ in
let x = self.monitor.xyPoint.x / 10
let z = self.monitor.xyPoint.y / 10
// 创立旋转矩阵
// 核算从 p1 到 p2 的方向向量
let p1 = ship.position
let p2 = SCNVector3(x, 0, z)
let directionVector = SCNVector3(p2.x - p1.x, p2.y - p1.y, p2.z - p1.z)
// 核算旋转角度和轴
let angle = atan2(directionVector.x, directionVector.z)
let rotationAxis = SCNVector3(0, 1, 0)
let rotationMatrix = SCNMatrix4MakeRotation(Float(angle), rotationAxis.x, rotationAxis.y, rotationAxis.z)
// 创立平移矩阵
let translationMatrix = SCNMatrix4MakeTranslation(Float(x), 0, Float(z))
ship.transform = SCNMatrix4Mult(rotationMatrix, translationMatrix)
}
- 假如不想处理这块代码能够下载:初始化工程
给小飞机配个空间音频
-
装备
PHASEEngine
和 环境
import PHASE
class GameViewController : UIViewController {
// PHASE
var engine: PHASEEngine!
var listener: PHASEListener!
var source: PHASESource!
override func viewDidLoad() {
super.viewDidLoad()
// 装备 PHASE 相关
configEngine()
// other code
}
func configEngine() {
self.phaseEngine = PhaseEngine(updateMode: .automatic)
self.engine.defaultReverbPreset = .largeRoom
try? self.engine.start()
}
}
-
创立一个能听到声响的目标,并设置方位
PHASEListener
是运用中听到声响的目标;它是用户体会空间音频的中心方位和方向。该结构依据声源相关于听众的独特方位和方历来调整声源的音量。例如,在间隔听者较远的当地播映的声响会安静地播映,而间隔听者较近的声响会播映得更大声
func configEngine() {
// *****
// Listener
self.listener = PHASEListener(engine: self.engine)
// 设置收听者的方位
self.listener.transform = matrix_identity_float4x4
// 增加到场景中
try? self.engine.rootObject.addChild(listener)
}
-
创立声源和设置声源方位
PHASESource
是运用中宣布声响的目标;后边咱们将经过设置 transform 来界说声源在场景中的方位变换。
func configEngine() {
// *****
// 设置播映源 Source
// 播映源形状
let mesh = MDLMesh.newIcosahedron(withRadius: 0.0142, inwardNormals: false, allocator:nil)
let shape = PHASEShape(engine: engine, mesh: mesh)
let source = PHASESource(engine: engine, shapes: [shape])
// 设置播映源方位,
source.transform = matrix_identity_float4x4
// 增加到场景中
try? engine.rootObject.addChild(source)
}
-
描述输出管道
作为音频播映装备的最终步骤之一,运用指定特定的目标或混音器,用于组合相关的音频信号以传输到输出设备。关于空间音频,运用会创立一个空间混合器PHASESpatialMixerDefinition
,除了directPathTransmission
之外,空间混音器还能够在 flags 参数中加入 earlyReflections 或 lateReverb 装备,从而为输出增加环境层,例如反射或混响。
// 输出管道
let pipeline = PHASESpatialPipeline(flags: [.directPathTransmission, .lateReverb])!
pipeline.entries[PHASESpatialCategory.lateReverb]!.sendLevel = 0.1
-
装备依据间隔调理音量
PHASE 经过观察您在空间混音器上界说的间隔模型来衰减源和听众之间间隔上的声响。当声源宣布声响时,空间混音器会依据与听众的间隔来调整其音量。声源间隔听者越远,音量衰减得越多,声响相关于听者来说就越安静。假如没有装备相关间隔模型,则会以稳定的音频播映声响。
// 装备依据间隔调理音量
// PHASEGeometricSpreadingDistanceModelParameters 模拟随间隔的声响损失的模型
let distanceModelParameters = PHASEGeometricSpreadingDistanceModelParameters()
// 咱们在操控了飞机在 15 米为半径的圆内飞行
// 此处设置超越 16 米,超越 16 米声响渐隐
distanceModelParameters.fadeOutParameters = PHASEDistanceModelFadeOutParameters(cullDistance: 16)
distanceModelParameters.rolloffFactor = 0.3
let spatialMixerDefinition = PHASESpatialMixerDefinition(spatialPipeline: pipeline)
spatialMixerDefinition.distanceModelParameters = distanceModelParameters
-
生成声响事情
前面是让 PHASE 了解场景和装备,接下来咱们将运用PHASESoundEvent
来播映和操控声响,这儿就能够跟据自己的事务需求经过PHASESoundEvent
来定制声响播映。
首要音频资源需求注册至财物表中,我已经在工程中增加了飞机的音源:plane.mp3
。
// 注册声响资源
let soundURL = Bundle.main.url(forResource: "plane", withExtension: "mp3")!
let soundAsset = try! self.engine.assetRegistry.registerSoundAsset(
url: soundURL,
identifier: "planeAsset",
assetType: .resident,
channelLayout: nil,
normalizationMode: .dynamic)
// 创立采样器节点
let samplerNodeDefinition = PHASESamplerNodeDefinition(
soundAssetIdentifier: soundAsset.identifier,
mixerDefinition: spatialMixerDefinition
)
samplerNodeDefinition.playbackMode = .looping
samplerNodeDefinition.setCalibrationMode(calibrationMode: .relativeSpl, level: 0)
samplerNodeDefinition.cullOption = .sleepWakeAtRealtimeOffset
// 向引擎注册声响事情节点的财物来供给有关声响事情事情信息
let planeSoundEventAsset = try! engine.assetRegistry.registerSoundEventAsset(
rootNode: samplerNodeDefinition,
identifier: soundAsset.identifier + "_SoundEventAsset"
)
// 经过为每个空间混音器装备 PHASEMixerParameters 目标来界说播映音频的声源以及收听音频的听者
let mixerParameters = PHASEMixerParameters()
mixerParameters.addSpatialMixerParameters(
identifier: spatialMixerDefinition.identifier,
source: source,
listener: listener
)
// 要播映声响,请为每个节点生成一个 PHASESoundEvent 实例,并在声响事情上调用 start(completion:)
// 经过 PHASESoundEvent 初始化器 mixerParameters 参数传递空间混合器参数,将源与声响事情相关起来
let planeSoundEvent = try! PHASESoundEvent(
engine: engine,
assetIdentifier: planeSoundEventAsset.identifier,
mixerParameters: mixerParameters
)
planeSoundEvent.start()
-
运转一下
你将会听到声响正常播映了,但操控了摇杆并不能操控声响,是由于咱们还没有把声源的方位和飞机的方位相关
-
相关声源与飞机的方位,并试玩
cancel = monitor.objectWillChange.sink { _ in
// *****
// 创立平移矩阵
let translationMatrix = SCNMatrix4MakeTranslation(Float(x), 0, Float(z))
ship.transform = SCNMatrix4Mult(rotationMatrix, translationMatrix)
// 相关音源 与 飞机的方位
self.source.transform = simd_float4x4(translationMatrix)
}
总结
本文介绍了 PHASE 的结构和运用方法,并完成了一个 Demo代码在这. 假如要做进一步深度运用的话,仍是需求再研讨一下,比如混音的一些参数装备等, Event Tree 也值得研讨研讨。