视觉负责告诉用户"界面上有什么",声音负责告诉用户"它在哪儿、离我多远"。在 2D 界面里声音只是提示音,播完就完了;到了空间场景,声音可以承担方位信息——一个提示从右后方传来,比在屏幕中间弹一行文字更快建立空间关系。空间音频(Audio Spatialization)就是把这套"听声辨位"的能力交给系统。
方位感是怎么被听出来的
人耳判断声源方向,靠的是几组物理线索。理解了它们,才能知道要调什么、能调什么。
- ITD(Interaural Time Difference):声音到达左右耳的时间差。正前方的声音同时到达,偏左的声音先到左耳。
- ILD(Interaural Level Difference):两耳之间的响度差。高频声被头部遮挡,远离声源的一侧更轻。
- HRTF(Head-Related Transfer Function):头部、耳廓对声音的滤波效应,让"正前方"和"正上方"听起来不同。双耳渲染(binaural rendering)就是把 HRTF 卷积进音频。
- 距离衰减:声源越远,响度越低、高频衰减越多、混响比例越高。
- 头部追踪:用户转头时,声场保持在世界坐标系里不动,声音相对双耳的位置随之变化。
系统把这些线索封装成一次渲染。应用侧要做的是:判断设备是否具备渲染能力、提供合适的音源、以及决定声音对象在空间里的位置。
AudioKit 暴露了什么
空间音频能力查询和状态订阅从 API version 18 开始支持。核心入口是AudioSpatializationManager,从AudioManager取到。
import{audio}from'@kit.AudioKit';letaudioManager=audio.getAudioManager();// 拿到空间音频管理器letaudioSpatializationManager=audioManager.getSpatializationManager();几件事可以做:
| 能力 | 接口 | 用途 |
|---|---|---|
| 判断设备能否渲染 | AudioDeviceDescriptor.spatializationSupported | 决定是否走空间化方案 |
| 查询开关状态 | isSpatializationEnabledForCurrentDevice() | 当前设备空间音频是否开启 |
| 订阅状态变化 | on('spatializationEnabledChangeForCurrentDevice') | 用户中途切换开关时实时响应 |
| 取消订阅 | off('spatializationEnabledChangeForCurrentDevice') | 页面销毁时解绑,避免泄漏 |
要拿设备描述符,通过路由管理器的getDevicesSync取当前输出设备:
import{audio}from'@kit.AudioKit';letroutingManager=audioManager.getRoutingManager();letdescriptors=routingManager.getDevicesSync(audio.DeviceFlag.OUTPUT_DEVICES_FLAG);// 逐个检查是否具备空间音频渲染能力for(constdeviceofdescriptors){console.info(`device:${device.name}, spatializationSupported:${device.spatializationSupported}`);}状态订阅的写法:
import{audio}from'@kit.AudioKit';// 用户在系统里开关空间音频时,应用收到回调,UI 可以同步更新audioSpatializationManager.on('spatializationEnabledChangeForCurrentDevice',(enabled:boolean)=>{console.info(`spatialization enabled:${enabled}`);// 这里可以切换界面上"空间音频"开关的显示状态});// 页面销毁时务必解绑audioSpatializationManager.off('spatializationEnabledChangeForCurrentDevice');这里要分清两个概念:开关状态只是开关,实际是否生效还取决于当前设备是否支持渲染。isSpatializationEnabledForCurrentDevice()返回true,但如果输出设备不支持空间音频,声音听起来仍然是普通立体声。
音源决定了天花板
空间音频的沉浸程度很大一部分由音源格式决定。普通立体声只有左右两个声道,系统能做的空间化有限。Audio Vivid(由 UWA 与 AVS 联合制定的音频编解码标准)携带音频对象的元数据,能把人声、乐器作为独立的声音对象,重新定义它们的位置、移动轨迹、远近。
| 维度 | 普通立体声 | Audio Vivid 空间音频 |
|---|---|---|
| 声道 | 左右两声道 | 多声道 / 声音对象 |
| 位置信息 | 无,靠混音预设 | 每个对象有独立坐标与轨迹 |
| 高度感 | 无 | 支持上方声场 |
| 移动表现 | 只能整体平移 | 对象可独立移动、远近变化 |
| 沉浸感 | 平面 | 全方位萦绕 |
应用能否解码播放 Audio Vivid,走的是另一套播放链路(using-ohaudio-for-playback里有"播放 Audio Vivid 格式音源"的说明)。如果音源本身是普通 PCM,用AudioRenderer也能播,只是空间化收益来自设备的双耳渲染,不来自元数据。
用声音定位界面元素
把方位当成 UI 属性来看,很多 2D 里说不清的关系会变得直接。
- 方位一致原则:声音从哪来,对应的 UI 元素就在哪。右下角弹出的通知,提示音就从右下方传来。用户不需要重新建立映射。
- 距离即层级:需要立刻处理的提示"近",背景性的状态"远"。远的表现是响度低、混响多、方位模糊。
- 音量即重要度:重要提示提高响度,但要注意和"距离"配合——又近又响是紧急,又远又响会让人困惑。
- 单次、可辨认:空间提示是给用户定位用的,不是背景音乐。用短促、有辨识度的音色,控制在一次以内。
代码:空间化播放与状态联动
一个可套用的组合:先查能力,再决定是否开启空间化播报,并订阅系统开关变化。播放用AudioRenderer播放 PCM 数据。
import{audio}from'@kit.AudioKit';import{BusinessError}from'@kit.BasicServicesKit';letaudioManager=audio.getAudioManager();letspatialManager=audioManager.getSpatializationManager();letrenderer:audio.AudioRenderer|undefined=undefined;// 音频参数:48k 采样率与多数输出设备一致,能减少重采样开销letstreamInfo:audio.AudioStreamInfo={samplingRate:audio.AudioSamplingRate.SAMPLE_RATE_48000,channels:audio.AudioChannel.CHANNEL_2,sampleFormat:audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE,encodingType:audio.AudioEncodingType.ENCODING_TYPE_RAW};letrendererInfo:audio.AudioRendererInfo={usage:audio.StreamUsage.STREAM_USAGE_NAVIGATION,// 导航类播报,不打断用户音乐,只压低音量rendererFlags:0};// 判断当前是否具备空间化播放的条件functioncanSpatialize():boolean{letrouting=audioManager.getRoutingManager();letdevices=routing.getDevicesSync(audio.DeviceFlag.OUTPUT_DEVICES_FLAG);constsupported=devices.some((d)=>d.spatializationSupported);returnsupported&&spatialManager.isSpatializationEnabledForCurrentDevice();}asyncfunctionprepareRenderer():Promise<void>{returnnewPromise((resolve,reject)=>{audio.createAudioRenderer({streamInfo,rendererInfo},(err,instance)=>{if(err){console.error(`createAudioRenderer failed:${err.code},${err.message}`);reject(err);return;}renderer=instance;// 写好 writeData 回调(从 PCM 文件读取数据),再启动resolve();});});}asyncfunctionstartAnnouncement():Promise<void>{if(!canSpatialize()){// 不支持时降级:仍可播报,只是不保证方位感console.info('spatialization unavailable, fallback to stereo announcement');}if(renderer===undefined){awaitprepareRenderer();}renderer?.start((err:BusinessError)=>{if(err){console.error(`start failed:${err.code},${err.message}`);}});}// 页面卸载时释放,避免占用音频流资源asyncfunctionreleaseRenderer():Promise<void>{if(renderer!==undefined){renderer.release((err:BusinessError)=>{if(err){console.error(`release failed:${err.code},${err.message}`);}});renderer=undefined;}}注意StreamUsage的选择。播报类场景用STREAM_USAGE_NAVIGATION或提示类 usage 会压低正在播放的音乐,而用STREAM_USAGE_MUSIC会把音乐直接打断——这是很多人踩过的坑。
案例:空间化语音提示
无障碍场景里,视障用户依赖语音。传统做法是所有提示都从"正前方"播报,方向信息全靠语言描述(“屏幕下方有一个按钮”)。用空间音频,可以把方位直接变成听得出来的线索。
设计映射:
- 屏幕左侧的可操作元素,提示音从左侧来;
- 弹窗类内容从正前方、近距离播报;
- 后台状态更新从上方、远距离播报,不抢当前焦点;
- 用户转头(头部追踪)时声场锁定在世界坐标,转过头声音会偏到另一侧。
这套映射把"方位"从文字描述里解放出来,代价是要求输出设备支持空间音频——单扬声器设备上会退化成单声道,所以语言描述不能完全省掉,两者要并存。
几条小小经验
先用
spatializationSupported判断,再决定要不要主打空间化卖点。设备不支持时,投入在方位上的设计成本收不回来。方位和视觉要对齐。声音从左边来、按钮在右边,用户会本能地往错误方向看。
提示音保持短、少、可辨认。空间音频擅长定位,不擅长长时间承载信息。
StreamUsage按真实场景选。播报打断音乐是最容易招差评的细节。状态订阅记得在
aboutToDisappear里off,否则页面销毁后回调还在触发。把
isSpatializationEnabledForCurrentDevice()当成"正在空间化"。它只表示开关状态,设备不支持时依旧不生效。在未停用音频流的情况下反复创建
AudioRenderer,音频流数量受限,创建会失败。用完及时release()。把长音频当提示音播,用户会以为在播放媒体,且难以和后台音乐区分。
只在带耳机的场景验证。外放时双耳渲染的前提消失,空间感会明显减弱,需要单独定义外放下的提示策略。