Highlight
这场 Session 以一个太空飞船游戏为例,逐步展示了 RealityKit 音频 API 的全部能力。飞船的声音设计采用了多层结构——每个引擎有蒸汽尾迹、排气管和涡轮三层声音,分别用文件播放和实时生成两种方式实现。
核心内容
做空间计算应用时,最常见的问题是:3D 对象看起来很有体积感,但声音却是扁平的。一个飞船从你头顶飞过,音量没有随距离变化,方向感也没有——体验立刻出戏。RealityKit 的音频 API 从根本上解决这个问题:所有通过 Entity.playAudio 播放的声音默认就是空间化的,自带六自由度渲染、基于物理的距离衰减,以及根据你真实环境实时模拟的混响。
这场 Session 用一个太空飞船游戏逐步展示了整个音频工具链。飞船有两个引擎,每个引擎有三层声音:蒸汽尾迹用循环文件播放,排气管音量跟随油门变化,涡轮用 AU 音频单元实时生成。碰撞声音根据材质配对选择不同的 AudioFileGroupResource,增益由碰撞速度决定。进入沉浸式环境后,ReverbComponent 切换混响预设。背景音乐用四声道布局配合 AmbientAudioComponent 渲染。最后,AudioMixGroupsComponent 让用户自由调节各类声音的音量比例。
从最简单的两行代码播放,到方向性控制、实时音频生成、材质驱动的碰撞声、混响预设、环境音乐、混音分组——这条路径覆盖了空间计算音频的几乎所有场景。
详细内容
空间音频文件播放
最基础的用法只有两行代码(03:11):
func playVaporTrailAudio(from engine: Entity) async throws {
let resource = try await AudioFileResource(named: "VaporTrail")
engine.playAudio(resource)
}
AudioFileResource(named:)从 Bundle 加载音频文件engine.playAudio(resource)在引擎实体上播放,自动获得空间渲染
播放即空间化,不需要额外配置。但有两个细节值得注意:音频文件应该用单声道(空间化前会强制下混为单声道,提前用单声道文件可以避免下混伪影);短文件配合循环和随机起点,可以让多个相同音源听起来各不相同(04:02):
let resource = try await AudioFileResource(
named: "VaporTrail",
configuration: AudioFileResource.Configuration(
shouldLoop: true,
shouldRandomizeStartTime: true
)
)
let controller: AudioPlaybackController = engine.playAudio(resource)
controller.gain = -.infinity
controller.fade(to: .zero, duration: 1)
shouldLoop: true让短文件无缝循环shouldRandomizeStartTime: true让两个引擎从文件的不同位置开始播放controller.gain = -.infinity从静音开始,避免循环文件突然爆音controller.fade(to: .zero, duration: 1)在一秒内淡入到标称音量
方向性(Directivity)与独立音频源
想让引擎声音有方向感——面对你时明亮,背对你时低沉——需要设置 SpatialAudioComponent 的 directivity 属性(04:02):
let audioSource = Entity()
audioSource.orientation = .init(angle: .pi, axis: [0, 1, 0])
audioSource.components.set(
SpatialAudioComponent(directivity: .beam(focus: 0.25))
)
engine.addChild(audioSource)
let controller = audioSource.playAudio(resource)
- 创建独立的
Entity作为音频源,设为引擎的子节点——这样位置跟随引擎,但方向可以独立控制 .beam(focus: 0.25)设置波束方向性,focus 范围 0(全向)到 1(窄束),0.25 是轻微的方向感- 旋转 audioSource 让声音从引擎喷口方向发出
油门驱动增益
排气管音量跟随油门线性变化(07:10):
func updateAudio(for exhaust: Entity, throttle: Float) {
let gain = decibels(amplitude: throttle)
exhaust.components[SpatialAudioComponent.self]?.gain = Audio.Decibel(gain)
}
func decibels(amplitude: Float) -> Float { 20 * log10(amplitude) }
- 线性油门值(0~1)换算为对数分贝值,更符合人耳感知
- 直接修改
SpatialAudioComponent的gain属性,因为它遵守 Component 协议,可以在自定义 System 中每帧更新
实时音频生成(AudioGeneratorController)
涡轮声音由自定义 AU 音频单元实时生成(08:17):
var turbineController: AudioGeneratorController?
func playTurbineAudio(from engine: Entity) {
let audioUnit = try await AudioUnitTurbine.instantiate()
let configuration = AudioGeneratorConfiguration(layoutTag: kAudioChannelLayoutTag_Mono)
let format = AVAudioFormat(
standardFormatWithSampleRate: Double(AudioGeneratorConfiguration.sampleRate),
channelLayout: .init(layoutTag: configuration.layoutTag)!
)
try audioUnit.outputBusses[0].setFormat(format)
try audioUnit.allocateRenderResources()
let renderBlock = audioUnit.internalRenderBlock
turbineController = try engine.playAudio(configuration: configuration) {
isSilence, timestamp, frameCount, outputData in
var renderFlags = AudioUnitRenderActionFlags()
return renderBlock(&renderFlags, timestamp, frameCount, 0, outputData, nil, nil)
}
}
AudioUnitTurbine是自定义 AU,内部用多个振荡器根据油门值生成声音AudioGeneratorConfiguration配置单声道输出engine.playAudio(configuration:callback:)的回调中直接写音频缓冲区- 返回的
AudioGeneratorController必须被持有,否则音频停止
距离衰减自定义
想让飞船只在靠近时才能听到舱内音乐,需要加大距离衰减(11:28):
func configureDistanceAttenuation(for spaceshipHifi: Entity) {
spaceshipHifi.components.set(
SpatialAudioComponent(
gain: -18,
distanceAttenuation: .rolloff(factor: 4)
)
)
}
.rolloff(factor: 1)是自然衰减默认值;factor 为 4 表示衰减速度是自然的四倍gain: -18额外降低音量,确保只有极近距离才能听到
材质驱动的碰撞声
碰撞声音根据两个物体的材质配对选择(15:04):
func handleCollisionBegan(_ collision: CollisionEvents.Began) {
guard
let audioMaterials = audioMaterials(for: collision),
let resource: AudioFileGroupResource = collisionAudio[audioMaterials]
else {
return
}
let controller = collision.entityA.playAudio(resource)
controller.gain = relativeLoudness(for: collision)
}
AudioFileGroupResource包含多个变体,每次播放随机选择一个,避免重复感relativeLoudness(for:)根据碰撞速度计算增益- 材质通过自定义
AudioMaterialComponent标注在实体上(rock、plastic、metal 等) - 真实物体的碰撞通过 ARKit 场景重建网格的面分类映射到材质枚举
混响预设
进入虚拟环境时设置混响(17:18):
func prepareStudioEnvironment() async throws {
let studio = try await Entity(named: "Studio", in: studioBundle)
studio.components.set(
ReverbComponent(reverb: .preset(.veryLargeRoom))
)
rootEntity.addChild(studio)
}
ReverbComponent放在实体层级中即可生效,同一时刻只有一个混响激活- 渐进式沉浸模式下,真实声学与预设混响按沉浸程度混合
- 全沉浸模式下,空间音源完全使用预设混响
环境音乐与混音分组
四声道布局音乐用 AmbientAudioComponent 渲染(20:05):
let resource = try await AudioFileResource(
named: "JoyRideMusic",
configuration: .init(
loadingStrategy: .stream,
shouldLoop: true
)
)
entity.components.set(AmbientAudioComponent())
entity.playAudio(resource)
loadingStrategy: .stream流式加载,适合大文件,减少内存占用AmbientAudioComponent三自由度渲染:响应头部和音源旋转,但不响应位移- 四声道文件必须将声道布局写入文件,RealityKit 才能从正确角度渲染各声道
混音分组让用户独立控制各类声音(21:57):
let resource = try await AudioFileResource(
named: "JoyRideMusic",
configuration: .init(
loadingStrategy: .stream,
shouldLoop: true,
mixGroupName: "Music"
)
)
var audioMixerEntity = Entity()
func updateMixGroup(named mixGroupName: String, to level: Audio.Decibel) {
var mixGroup = AudioMixGroup(name: mixGroupName)
mixGroup.gain = level
let component = AudioMixGroupsComponent(mixGroups: [mixGroup])
audioMixerEntity.components.set(component)
}
mixGroupName: "Music"把该资源归入 Music 分组AudioMixGroupsComponent遵守 Component 协议,可以在 UI 交互或自定义 System 中更新
核心启发
-
给每个音源创建独立的 Entity 子节点:视觉实体的位置和朝向不一定适合声音传播。把音频源作为子节点挂载,可以独立控制声音的方向和位置,比如让引擎声从喷口而不是引擎中心发出。怎么开始:为需要方向控制的音源新建
Entity(),设置orientation和SpatialAudioComponent,再addChild到视觉实体上。 -
用 AudioFileGroupResource 消除碰撞音的机械感:同一组碰撞音提供多个微妙变体,每次播放随机选取。增益由碰撞速度的对数分贝值驱动,慢碰撞安静、快碰撞有力。怎么开始:为每种材质配对制作 3~5 个变体文件,构建
AudioFileGroupResource,在CollisionEvents.Began回调中用速度计算增益。 -
大文件用 .stream 加载,短文件用 shouldLoop + shouldRandomizeStartTime:一段 10 秒的循环文件比一段 5 分钟的完整音频节省大量内存,两个引擎从不同位置开始播放又能避免同相叠加。怎么开始:把所有持续背景音做成短循环文件,
shouldLoop: true+shouldRandomizeStartTime: true;背景音乐等大文件用loadingStrategy: .stream。 -
用 AudioMixGroupsComponent 把音量控制权交给用户:把音效、音乐、语音分成不同 Mix Group,用户可以根据偏好独立调节。怎么开始:在
AudioFileResource.Configuration中设置mixGroupName,创建一个专门的audioMixerEntity持有AudioMixGroupsComponent,通过 UI 滑块更新各组的gain。
关联 Session
- Discover RealityKit APIs for iOS, macOS, and visionOS — 本 Session 示例游戏的完整构建过程,涵盖物理力效果、关节和 Portal 穿越等
- Build a spatial drawing app with RealityKit — 另一个 RealityKit 实战案例,用空间绘画应用演示交互设计
- Compose interactive 3D content in Reality Composer Pro — 在 Reality Composer Pro 中编排 3D 内容,可以与音频工作流配合
- Enhance the immersion of media viewing in custom environments — 混响预设在 Destination Video 示例中的具体用法
评论
GitHub Issues · utterances