Highlight
PHASE 框架将几何信息引入音频引擎,支持体积声源、遮挡物和材质属性,让声音根据场景几何体自动传播和衰减,无需手动混合多个点声源。
核心内容
游戏音频的长期痛点
传统游戏开发中,音频子系统与物理、动画、视觉特效等子系统分离运行。音频通常通过第三方中间件制作,不感知游戏模拟状态。声音设计师需要预先烘焙音频资产、手工调音,让声音效果匹配游戏画面。一旦视觉场景发生变化,音频系统需要重新制作才能保持一致性。(01:00)
空间音频方面,传统做法是在场景中放置多个点声源,通过射线追踪等技术确定滤波和混合比例,手工混合各个声源。当场景变化时,这个调音过程需要重新来过。(02:40)
PHASE 如何改变游戏音频
PHASE(Physical Audio Spatialization Engine)是全新的空间音频框架,将音频系统与其他子系统紧密连接。(01:36)
体积声源。PHASE 允许将声源定义为几何形状(如球体、立方体、网格),而不是单个点。音频引擎会自动管理声音从体积内的传播,开发者无需手动混合多个点声源。(02:27)
几何遮挡。遮挡物(如墙壁、障碍物)也可以定义为几何形状,并可指定声学材质属性(如木头、金属、玻璃)。PHASE 会自动计算声音如何被遮挡物衰减和滤波。(02:38)
设备一致性。PHASE 在所有支持的 Apple 设备上提供一致的空间音频体验,无需针对不同平台手动调音。(02:07)
详细内容
PHASE 核心概念
PHASE 框架围绕以下几个核心概念构建:
- Engine:音频引擎的核心对象,管理整个音频会话
- Sound Asset:音频文件资源,可注册到引擎中
- Sound Event:可播放的声音事件,由 Sound Event Asset 定义
- Source:声源,可以定义为体积形状
- Listener:听者,代表玩家或用户的耳朵位置和朝向
- Occluder:遮挡物,阻挡声音传播的几何体
- Material:声学材质,定义遮挡物的吸声特性
- Spatial Pipeline:空间化处理管线,包含直接路径传输和混响
创建 PHASE Engine
创建 PHASE Engine 并注册音频资源:(18:31)
import PHASE
// 创建自动更新模式的引擎
let engine = PHASEEngine(updateMode: .automatic)
// 获取应用 Bundle 中的音频文件 URL
let audioFileUrl = Bundle.main.url(forResource: "DrumLoop_24_48_Mono",
withExtension: "wav")!
// 注册音频文件资源
// 命名为 "drums",加载到常驻内存,应用动态归一化准备播放
let soundAsset = try engine.assetRegistry.registerSoundAsset(
url: audioFileUrl,
identifier: "drums",
assetType: .resident,
channelLayout: nil,
normalizationMode: .dynamic
)
关键点:
updateMode: .automatic表示引擎自动管理更新,适合大多数场景.resident类型将音频加载到常驻内存,适合频繁播放的声音normalizationMode: .dynamic应用动态归一化,确保播放时音量一致identifier是资源的唯一标识符,后续通过它引用
创建 Sound Event Asset
Sound Event Asset 定义声音的处理流程,从采样器到混音器:(20:47)
// 从单声道布局标签创建声道布局
let channelLayout = AVAudioChannelLayout(layoutTag: kAudioChannelLayoutTag_Mono)!
// 从声道布局创建声道混音器
let channelMixerDefinition = PHASEChannelMixerDefinition(channelLayout: channelLayout)
// 创建采样器节点,连接到下方的声道混音器
let samplerNodeDefinition = PHASESamplerNodeDefinition(
soundAssetIdentifier: "drums",
mixerDefinition: channelMixerDefinition
)
// 设置采样器的播放模式为循环
samplerNodeDefinition.playbackMode = .looping
// 设置校准模式为相对声压级,电平为 0 dB
samplerNodeDefinition.setCalibrationMode(.relativeSpl, level: 0)
// 注册声音事件资源,命名为 "drumEvent"
let soundEventAsset = try engine.assetRegistry.registerSoundEventAsset(
rootNode: samplerNodeDefinition,
identifier: "drumEvent"
)
关键点:
PHASEChannelMixerDefinition定义音频如何混合到输出声道PHASESamplerNodeDefinition是播放音频文件的节点.looping让音频循环播放,适合背景音乐或环境音.relativeSpl校准模式表示相对声压级,0 dB 为参考电平rootNode是节点树的根,可以构建复杂的音频处理图
启动和停止播放
创建 Sound Event 并启动引擎:(22:21)
// 从 Sound Event Asset 创建 Sound Event
let soundEvent = try PHASESoundEvent(engine: engine,
assetIdentifier: "drumEvent")
// 启动引擎
// 这会内部启动音频 IO 线程
try engine.start()
// 启动声音事件
try soundEvent.start()
清理资源:(23:05)
// 停止并使声音事件无效
soundEvent.stopAndInvalidate()
// 停止引擎
// 这会内部停止音频 IO 线程
engine.stop()
// 注销声音事件资源
engine.assetRegistry.unregisterAsset(identifier: "drumEvent", completionHandler: nil)
// 注销音频文件
engine.assetRegistry.unregisterAsset(identifier: "drums", completionHandler: nil)
// 销毁引擎
engine = nil
关键点:
start()启动音频 IO 线程,开始音频处理stop()停止音频 IO 线程stopAndInvalidate()停止播放并释放事件资源- 清理时按相反顺序注销资源
空间化管线配置
创建空间化管线实现空间音频效果:(25:14)
// 创建空间化管线
let spatialPipelineOptions: PHASESpatialPipeline.Options = [
.directPathTransmission,
.lateReverb
]
let spatialPipeline = PHASESpatialPipeline(options: spatialPipelineOptions)!
spatialPipeline.entries[PHASESpatialCategory.lateReverb]!.sendLevel = 0.1
engine.defaultReverbPreset = .mediumRoom
// 创建空间混音器
let spatialMixerDefinition = PHASESpatialMixerDefinition(spatialPipeline: spatialPipeline)
// 设置距离模型参数
let distanceModelParameters = PHASEGeometricSpreadingDistanceModelParameters()
distanceModelParameters.fadeOutParameters = PHASEDistanceModelFadeOutParameters(
cullDistance: 10.0
)
distanceModelParameters.rolloffFactor = 0.25
spatialMixerDefinition.distanceModelParameters = distanceModelParameters
// 创建采样器节点,连接到空间混音器
let samplerNodeDefinition = PHASESamplerNodeDefinition(
soundAssetIdentifier: "drums",
mixerDefinition: spatialMixerDefinition
)
// 设置循环播放
samplerNodeDefinition.playbackMode = .looping
// 设置校准电平为 12 dB
samplerNodeDefinition.setCalibrationMode(.relativeSpl, level: 12)
// 设置剔除选项为休眠
samplerNodeDefinition.cullOption = .sleepWakeAtRealtimeOffset
// 注册声音事件
let soundEventAsset = try engine.assetRegistry.registerSoundEventAsset(
rootNode: samplerNodeDefinition,
identifier: "drumEvent"
)
关键点:
.directPathTransmission启用直接路径传输.lateReverb启用后期混响sendLevel控制发送到混响的信号电平cullDistance是剔除距离,超过此距离声音停止播放rolloffFactor控制距离衰减的斜率.sleepWakeAtRealtimeOffset让远处的声源休眠,接近时自动唤醒
设置听者和声源
创建听者:(27:05)
// 创建听者
let listener = PHASEListener(engine: engine)
// 设置听者的变换矩阵为原点,无旋转
listener.transform = matrix_identity_float4x4
// 将听者附加到引擎场景图的根对象
// 这会在模拟中激活听者
try engine.rootObject.addChild(listener)
创建体积声源:(27:46)
import ModelIO
// 创建二十面体网格
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])
// 将声源移动到听者前方 2 米,并旋转回朝向听者
var sourceTransform: simd_float4x4
sourceTransform.columns.0 = simd_make_float4(-1.0, 0.0, 0.0, 0.0)
sourceTransform.columns.1 = simd_make_float4(0.0, 1.0, 0.0, 0.0)
sourceTransform.columns.2 = simd_make_float4(0.0, 0.0, -1.0, 0.0)
sourceTransform.columns.3 = simd_make_float4(0.0, 0.0, 2.0, 1.0)
source.transform = sourceTransform
// 将声源附加到引擎的场景图
try engine.rootObject.addChild(source)
关键点:
PHASEListener代表听者的位置和朝向PHASESource是声源,可以用一个或多个PHASEShape定义其体积transform是 4x4 变换矩阵,包含位置、旋转和缩放- 声源和听者都需要添加到场景图才能参与空间化计算
设置遮挡物
创建遮挡物和材质:(29:15)
// 创建盒子网格
let boxMesh = MDLMesh.newBox(
withDimensions: simd_make_float3(0.6096, 0.3048, 0.1016),
segments: simd_uint3(repeating: 6),
geometryType: .triangles,
inwardNormals: false,
allocator: nil
)
// 从网格创建形状
let boxShape = PHASEShape(engine: engine, mesh: boxMesh)
// 创建材质
// 这里我们使用纸板预设
let material = PHASEMaterial(engine: engine, preset: .cardboard)
// 在形状上设置材质
boxShape.elements[0].material = material
// 从形状创建遮挡物
let occluder = PHASEOccluder(engine: engine, shapes: [boxShape])
// 将遮挡物移动到听者前方 1 米
// 这将遮挡物放在声源和听者中间
var occluderTransform: simd_float4x4
occluderTransform.columns.0 = simd_make_float4(-1.0, 0.0, 0.0, 0.0)
occluderTransform.columns.1 = simd_make_float4(0.0, 1.0, 0.0, 0.0)
occluderTransform.columns.2 = simd_make_float4(0.0, 0.0, -1.0, 0.0)
occluderTransform.columns.3 = simd_make_float4(0.0, 0.0, 1.0, 1.0)
occluder.transform = occluderTransform
// 将遮挡物附加到引擎的场景图
try engine.rootObject.addChild(occluder)
关键点:
PHASEOccluder是阻挡声音传播的几何体PHASEMaterial定义遮挡物的声学特性- 预设材质包括
.cardboard、.brick、.concrete、.glass、.metal、.wood等 - 材质影响声音被遮挡时的衰减和频谱特性
启动空间化声音事件
将声源和听者与空间混音器关联:(30:33)
// 将声源和听者与空间混音器关联
let mixerParameters = PHASEMixerParameters()
mixerParameters.addSpatialMixerParameters(
identifier: spatialMixerDefinition.identifier,
source: source,
listener: listener
)
// 创建声音事件
let soundEvent = try PHASESoundEvent(
engine: engine,
assetIdentifier: "drumEvent",
mixerParameters: mixerParameters
)
关键点:
PHASEMixerParameters定义混音器参数addSpatialMixerParameters将声源和听者与空间混音器关联- 声音事件会根据声源和听者的相对位置计算空间化效果
高级示例:脚步声系统
演示构建一个复杂的脚步声系统,支持随机化、地形切换、湿度和衣物摩擦声。(31:28)
示例 1:单声脚步声
let footstep_wood_sampler_1 = PHASESamplerNodeDefinition(
soundAssetIdentifier: "footstep_wood_clip_1",
mixerDefinition: channelMixerDefinition
)
示例 2:随机化脚步声
// 创建两个采样器节点
let footstep_wood_sampler_1 = PHASESamplerNodeDefinition(
soundAssetIdentifier: "footstep_wood_clip_1",
mixerDefinition: channelMixerDefinition
)
let footstep_wood_sampler_2 = PHASESamplerNodeDefinition(
soundAssetIdentifier: "footstep_wood_clip_2",
mixerDefinition: channelMixerDefinition
)
// 创建随机节点
// 权重越高,被选中的概率越大
let footstep_wood_random = PHASERandomNodeDefinition()
footstep_wood_random.addSubtree(footstep_wood_sampler_1, weight: 2)
footstep_wood_random.addSubtree(footstep_wood_sampler_2, weight: 1)
关键点:
PHASERandomNodeDefinition随机选择子节点播放weight参数控制选中概率- 随机化避免重复感,提升真实感
示例 3:地形切换
// 创建碎石脚步声随机节点
let footstep_gravel_sampler_1 = PHASESamplerNodeDefinition(
soundAssetIdentifier: "footstep_gravel_clip_1",
mixerDefinition: channelMixerDefinition
)
let footstep_gravel_sampler_2 = PHASESamplerNodeDefinition(
soundAssetIdentifier: "footstep_gravel_clip_2",
mixerDefinition: channelMixerDefinition
)
let footstep_gravel_random = PHASERandomNodeDefinition()
footstep_gravel_random.addSubtree(footstep_gravel_sampler_1, weight: 2)
footstep_gravel_random.addSubtree(footstep_gravel_sampler_2, weight: 1)
// 创建地形字符串元参数
let terrain = PHASEStringMetaParameterDefinition(value: "creaky_wood")
// 创建地形切换节点
let terrain_switch = PHASESwitchNodeDefinition(
switchMetaParameterDefinition: terrain
)
terrain_switch.addSubtree(footstep_wood_random, switchValue: "creaky_wood")
terrain_switch.addSubtree(footstep_gravel_random, switchValue: "soft_gravel")
关键点:
PHASEStringMetaParameterDefinition定义字符串元参数PHASESwitchNodeDefinition根据元参数值选择播放哪个子树- 可在运行时通过修改元参数切换地形声音
示例 4:湿度混合
// 创建水声随机节点
let splash_sampler_1 = PHASESamplerNodeDefinition(
soundAssetIdentifier: "splash_clip_1",
mixerDefinition: channelMixerDefinition
)
let splash_sampler_2 = PHASESamplerNodeDefinition(
soundAssetIdentifier: "splash_clip_2",
mixerDefinition: channelMixerDefinition
)
let splash_random = PHASERandomNodeDefinition()
splash_random.addSubtree(splash_sampler_1, weight: 9)
splash_random.addSubtree(splash_sampler_2, weight: 7)
// 创建湿度数字元参数
let wetness = PHASENumberMetaParameterDefinition(
value: 0.5,
minimum: 0,
maximum: 1
)
// 创建湿度混合节点
let wetness_blend = PHASEBlendNodeDefinition(
blendMetaParameterDefinition: wetness
)
wetness_blend.addRangeForInputValues(
belowValue: 1,
fullGainAtValue: 0,
fadeCurveType: .linear,
subtree: terrain_switch
)
wetness_blend.addRangeForInputValues(
aboveValue: 0,
fullGainAtValue: 1,
fadeCurveType: .linear,
subTree: splash_random
)
关键点:
PHASENumberMetaParameterDefinition定义数字元参数PHASEBlendNodeDefinition根据元参数值混合多个子节点- 湿度从 0(干燥)到 1(完全湿润),混合干燥脚步声和水声
示例 5:添加衣物摩擦声
// 创建衣物摩擦声随机节点
let noisy_clothing_sampler_1 = PHASESamplerNodeDefinition(
soundAssetIdentifier: "gortex_clip_1",
mixerDefinition: channelMixerDefinition
)
let noisy_clothing_sampler_2 = PHASESamplerNodeDefinition(
soundAssetIdentifier: "gortex_clip_2",
mixerDefinition: channelMixerDefinition
)
let noisy_clothing_random = PHASERandomNodeDefinition()
noisy_clothing_random.addSubtree(noisy_clothing_sampler_1, weight: 3)
noisy_clothing_random.addSubtree(noisy_clothing_sampler_2, weight: 5)
// 创建容器节点
let actor_container = PHASEContainerNodeDefinition()
actor_container.addSubtree(wetness_blend)
actor_container.addSubtree(noisy_clothing_random)
关键点:
PHASEContainerNodeDefinition容器节点,可以同时播放多个子节点- 地形脚步声和衣物摩擦声会同时播放
- 构建了完整的声音系统,支持随机化、地形切换、湿度和衣物声
核心启发
-
做什么:为游戏构建动态脚步声系统
- 为什么值得做:PHASE 的随机节点、切换节点和混合节点可以构建复杂的动态声音系统,根据地形、湿度等条件自动调整
- 怎么开始:创建多个声音采样器,用
PHASERandomNodeDefinition随机化,用PHASESwitchNodeDefinition切换地形,用PHASEBlendNodeDefinition混合不同条件下的声音
-
做什么:实现真实的遮挡和混响效果
- 为什么值得做:PHASE 支持几何遮挡和材质属性,声音会被墙壁阻挡并根据材质产生不同的衰减特性,提升沉浸感
- 怎么开始:用
PHASEOccluder定义场景中的遮挡物,为每个遮挡物指定PHASEMaterial,设置引擎的混响预设为.mediumRoom或.largeHall
-
做什么:构建体积声源实现环境音效
- 为什么值得做:传统点声源需要手动混合多个点来模拟大区域的声音,PHASE 的体积声源自动管理声音从几何体内的传播
- 怎么开始:用
MDLMesh创建球体或立方体网格,用PHASEShape包装,传给PHASESource作为体积声源
-
做什么:实现距离感知的音频剔除
- 为什么值得做:远处的声音不需要播放,剔除可以节省 CPU 资源,提升性能
- 怎么开始:设置
PHASEGeometricSpreadingDistanceModelParameters的cullDistance,设置采样器的cullOption为.sleepWakeAtRealtimeOffset
-
做什么:创建场景化的空间音频体验
- 为什么值得做:PHASE 将声源、听者和遮挡物统一在场景图中,移动听者或声源时自动计算空间化效果
- 怎么开始:创建
PHASEListener和多个PHASESource,添加到引擎的场景图,在游戏循环中更新它们的transform属性
关联 Session
- Explore game audio with AVAudioEngine — 学习使用 AVAudioEngine 构建游戏音频系统
- Advances in Audio Codec — 音频编解码器的新进展
- What’s new in Camera Capture — 相机捕获的新功能
- Explore Metal performance improvements — Metal 性能优化
评论
GitHub Issues · utterances