WWDC Quick Look 💓 By SwiftGGTeam
Enhance your spatial computing app with RealityKit audio

Enhance your spatial computing app with RealityKit audio

观看原视频

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)与独立音频源

想让引擎声音有方向感——面对你时明亮,背对你时低沉——需要设置 SpatialAudioComponentdirectivity 属性(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)换算为对数分贝值,更符合人耳感知
  • 直接修改 SpatialAudioComponentgain 属性,因为它遵守 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 中更新

核心启发

  1. 给每个音源创建独立的 Entity 子节点:视觉实体的位置和朝向不一定适合声音传播。把音频源作为子节点挂载,可以独立控制声音的方向和位置,比如让引擎声从喷口而不是引擎中心发出。怎么开始:为需要方向控制的音源新建 Entity(),设置 orientationSpatialAudioComponent,再 addChild 到视觉实体上。

  2. 用 AudioFileGroupResource 消除碰撞音的机械感:同一组碰撞音提供多个微妙变体,每次播放随机选取。增益由碰撞速度的对数分贝值驱动,慢碰撞安静、快碰撞有力。怎么开始:为每种材质配对制作 3~5 个变体文件,构建 AudioFileGroupResource,在 CollisionEvents.Began 回调中用速度计算增益。

  3. 大文件用 .stream 加载,短文件用 shouldLoop + shouldRandomizeStartTime:一段 10 秒的循环文件比一段 5 分钟的完整音频节省大量内存,两个引擎从不同位置开始播放又能避免同相叠加。怎么开始:把所有持续背景音做成短循环文件,shouldLoop: true + shouldRandomizeStartTime: true;背景音乐等大文件用 loadingStrategy: .stream

  4. 用 AudioMixGroupsComponent 把音量控制权交给用户:把音效、音乐、语音分成不同 Mix Group,用户可以根据偏好独立调节。怎么开始:在 AudioFileResource.Configuration 中设置 mixGroupName,创建一个专门的 audioMixerEntity 持有 AudioMixGroupsComponent,通过 UI 滑块更新各组的 gain


关联 Session

评论

GitHub Issues · utterances