【共创稿事节】HarmonyOS 7空间音频:用声音构建方位感与沉浸感
视觉负责告诉用户"界面上有什么",声音负责告诉用户"它在哪儿、离我多远"。在 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';
let audioManager = audio.getAudioManager();
// 拿到空间音频管理器
let audioSpatializationManager = audioManager.getSpatializationManager();
几件事可以做:
| 能力 | 接口 | 用途 |
|---|---|---|
| 判断设备能否渲染 | AudioDeviceDescriptor.spatializationSupported | 决定是否走空间化方案 |
| 查询开关状态 | isSpatializationEnabledForCurrentDevice() | 当前设备空间音频是否开启 |
| 订阅状态变化 | on('spatializationEnabledChangeForCurrentDevice') | 用户中途切换开关时实时响应 |
| 取消订阅 | off('spatializationEnabledChangeForCurrentDevice') | 页面销毁时解绑,避免泄漏 |
要拿设备描述符,通过路由管理器的 getDevicesSync 取当前输出设备:
import { audio } from '@kit.AudioKit';
let routingManager = audioManager.getRoutingManager();
let descriptors = routingManager.getDevicesSync(audio.DeviceFlag.OUTPUT_DEVICES_FLAG);
// 逐个检查是否具备空间音频渲染能力
for (const device of descriptors) {
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';
let audioManager = audio.getAudioManager();
let spatialManager = audioManager.getSpatializationManager();
let renderer: audio.AudioRenderer | undefined = undefined;
// 音频参数:48k 采样率与多数输出设备一致,能减少重采样开销
let streamInfo: 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
};
let rendererInfo: audio.AudioRendererInfo = {
usage: audio.StreamUsage.STREAM_USAGE_NAVIGATION, // 导航类播报,不打断用户音乐,只压低音量
rendererFlags: 0
};
// 判断当前是否具备空间化播放的条件
function canSpatialize(): boolean {
let routing = audioManager.getRoutingManager();
let devices = routing.getDevicesSync(audio.DeviceFlag.OUTPUT_DEVICES_FLAG);
const supported = devices.some((d) => d.spatializationSupported);
return supported && spatialManager.isSpatializationEnabledForCurrentDevice();
}
async function prepareRenderer(): Promise<void> {
return new Promise((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();
});
});
}
async function startAnnouncement(): Promise<void> {
if (!canSpatialize()) {
// 不支持时降级:仍可播报,只是不保证方位感
console.info('spatialization unavailable, fallback to stereo announcement');
}
if (renderer === undefined) {
await prepareRenderer();
}
renderer?.start((err: BusinessError) => {
if (err) {
console.error(`start failed: ${err.code}, ${err.message}`);
}
});
}
// 页面卸载时释放,避免占用音频流资源
async function releaseRenderer(): 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()。 -
把长音频当提示音播,用户会以为在播放媒体,且难以和后台音乐区分。
-
只在带耳机的场景验证。外放时双耳渲染的前提消失,空间感会明显减弱,需要单独定义外放下的提示策略。
更多推荐


所有评论(0)