注意:本文基于 Android 12/S 进行分析
Qidi 2023.07.20 (MarkDown & EnterpriseArchitect & Haroopad)
0. 车机环境下音量调节的特殊性
车机环境下的音频使用场景,相较于原始 Android 的音频使用场景,存在这些特殊性:
- 使用专门的 aDSP 芯片进行音效处理;
- 需要播放/控制原始 Android 预设之外的音源(AudioUsage);
- 音源间交互行为更加复杂(AudioFocus);
- 需要响应更复杂的电源模式变化。
其中第一、二点会直接影响用户从 APP 层调节音量的方式,以及 AudioHAL 的实现。
0.1 在 aDSP 芯片中进行音效处理
众所周知,Android 在 AudioFlinger::MixerThread
里已经实现了一套调节音量的逻辑,这势必对 aDSP 中的音量调节效果造成影响。为了使送入 aDSP 的音频信号完整,就要禁用这部分音量调节功能。
在 Android 框架代码中,可以将 frameworks/base/core/res/res/values/config.xml
中的 config_useFixedVolume
属性通过 overlay 的方式(参考 前文中的操作)设置为 true,来禁用 AudioFlinger 中的音量调节。
true
......
AudioService.java
在各音量函数入口会检查该属性值,从而跳过设置音量到 AudioFlinger::MixerThread
的逻辑。相应代码片段如下:
public class AudioService ... {
......
public AudioService(Context context, AudioSystemAdapter audioSystem,
SystemServerAdapter systemServer) {
......
mUseFixedVolume = mContext.getResources().getBoolean(
com.android.internal.R.bool.config_useFixedVolume);
......
}
private void setStreamVolume(int streamType, int index, int flags, String callingPackage,
String caller, int uid, boolean hasModifyAudioSettings) {
......
if (mUseFixedVolume) {
return;
}
......
}
......
}
0.2 调节 Android 预设之外的音源音量
APP 要播放声音和控制音量,通常需要指定 AudioUsage
。但在车机系统上,很多音源在原始 Android 框架里是没有对应的 AudioUsage
的,比如 ECall、Chime,这样的音源一般称之为“外部音源”。 对于这些 Android 预设之外的音源,APP 自然无法通过 AudioManager.setStreamVolume()
等 API 在 AudioFlinger::MixerThread
调节音量,所以我们需要想办法把音量调节请求发送到 AudioHAL 进行处理,或由 AudioHAL 再转发给 aDSP 进行处理。
这就需要 AudioHAL 实现 IDevice::setAudioPortConfig()
接口。Android 12 在 hardware/interfaces/audio/7.0/IDevice.hal
中对该接口的描述如下:
/**
* Set audio port configuration.
*
* @param config audio port configuration.
* @return retval operation completion status.
*/
setAudioPortConfig(AudioPortConfig config) generates (Result retval);
1. 通过 AudioManager 调节音量
1.1 混音音源
“混音音源” 指数据要经过 MixerThread
的音源。对于这些音源,为了让 APP 能使用 AudioManager
的 API 将音量调节命令发送到 aDSP 中,根据上一节说明,我们需要将 config.xml
中的 config_useFixedVolume
属性值配置为 false。
此外,通过阅读 Android 框架代码,发现还需要在 audio_policy_configuration.xml
中给 devicePort
的 gain
节点加上 useForVolume
属性。 如下:
因为 SwAudioOutputDescriptor::setVolume()
函数中会判断这个属性值。只有当 useForVolume
属性值为 true 时,才会调用 AudioFlinger::setAudioPortConfig()
。相应代码片段如下:
bool SwAudioOutputDescriptor::setVolume(float volumeDb,
VolumeSource vs, const StreamTypeVector &streamTypes,
const DeviceTypeSet& deviceTypes,
uint32_t delayMs,
bool force)
{
......
for (const auto& devicePort : devices()) {
if (isSingleDeviceType(deviceTypes, devicePort->type()) &&
devicePort->hasGainController(true) && isActive(vs)) {
......
audio_port_config config = {};
devicePort->toAudioPortConfig(&config);
config.config_mask = AUDIO_PORT_CONFIG_GAIN;
config.gain.values[0] = gainValueMb;
return mClientInterface->setAudioPortConfig(&config, 0) == NO_ERROR;
}
}
......
}
如此修改后,对 “混音音源” 调节音量的命令,就会同时发送给 MixerThread
和 AudioHAL。 时序图如下:
AudioHAL 可以直接进行音量调节处理,或者将命令转发给 aDSP 进行处理。
1.2 非混音音源
“非混音音源” 指数据不经过 MixerThread
,而是送到 DirectOutputThread
、OffloadThread
、MmapThread
的音源。
为了拉起这些线程,我们(AudioHAL 开发人员)需要在 audio_policy_configuration.xml
里给对应的 mixPort
分别配置下列 flags
属性:
- AUDIO_OUTPUT_FLAG_DIRECT
- AUDIO_OUTPUT_FLAG_COMPRESS_OFFLOAD
- AUDIO_OUTPUT_FLAG_MMAP_NOIRQ
并且由于 AudioPolicyManager
使用 “优先比对 flags 是否匹配” 的策略来选择播放线程,所以 APP 开发人员创建 AudioTrack
时,也要进行以下操作,才能保证数据不被写到 MixerThread
线程上:
- 设置音频格式为
non-linear PCM
格式之一,比如ENCODING_MP3
、ENCODING_AAC_LC
、ENCODING_IEC61937
等(框架代码会据此自动添加AUDIO_OUTPUT_FLAG_COMPRESS_OFFLOAD
标记位。代码片段如下);
status_t AudioTrack::set(...)
{
......
// force direct flag if format is not linear PCM
// or offload was requested
if ((flags & AUDIO_OUTPUT_FLAG_COMPRESS_OFFLOAD)
|| !audio_is_linear_pcm(format)) {
ALOGV( (flags & AUDIO_OUTPUT_FLAG_COMPRESS_OFFLOAD)
? "%s(): Offload request, forcing to Direct Output"
: "%s(): Not linear PCM, forcing to Direct Output",
__func__);
flags = (audio_output_flags_t)
// FIXME why can't we allow direct AND fast?
((flags | AUDIO_OUTPUT_FLAG_DIRECT) & ~AUDIO_OUTPUT_FLAG_FAST);
}
......
}
- 或者,设置数据传输模式为
MODE_STREAM
,并通过AudioTrack.Builder.setOffloadedPlayback(true)
显式设置播放模式为 offload(框架代码据此会自动添加AUDIO_OUTPUT_FLAG_COMPRESS_OFFLOAD
标记位。代码片段如下);
static jint android_media_AudioTrack_setup(...)
{
......
switch (memoryMode) {
case MODE_STREAM:
status = lpTrack->set(......,
offload ? AUDIO_OUTPUT_FLAG_COMPRESS_OFFLOAD
: AUDIO_OUTPUT_FLAG_NONE,
......
);
break;
......
}
......
}
- 或者,在其使用的
AudioAttributes
变量里设置AUDIO_OUTPUT_FLAG_HW_AV_SYNC
标记位(框架代码据此会自动添加AUDIO_OUTPUT_DIRECT
标记位。代码片段如下)。
static inline void audio_flags_to_audio_output_flags(
const audio_flags_mask_t audio_flags,
audio_output_flags_t *flags)
{
if ((audio_flags & AUDIO_FLAG_HW_AV_SYNC) != 0) {
*flags = (audio_output_flags_t)(*flags |
AUDIO_OUTPUT_FLAG_HW_AV_SYNC | AUDIO_OUTPUT_FLAG_DIRECT);
}
if ((audio_flags & AUDIO_FLAG_LOW_LATENCY) != 0) {
*flags = (audio_output_flags_t)(*flags | AUDIO_OUTPUT_FLAG_FAST);
}
// check deep buffer after flags have been modified above
if (*flags == AUDIO_OUTPUT_FLAG_NONE && (audio_flags & AUDIO_FLAG_DEEP_BUFFER) != 0) {
*flags = AUDIO_OUTPUT_FLAG_DEEP_BUFFER;
}
}
因为 “非混音音源” 数据不参与 AudioMixer
混音,所以理论上来说,在非车机环境上调节这些音源音量的代码,可以不加修改地直接在车机环境上使用。 APP 通过 AudioManager
API 调节这些音源的音量,对 aDSP 接收到的数据没有副作用。
通过 AudioManager
API 调节 “非混音音源” 的音量,其 Java 层的处理逻辑与调节 “混音音源” 音量的逻辑相同,故可参考上个时序图;其 Native 层的处理逻辑与通过 AudioTrack
API 调节音量的处理逻辑相同,故可参考下一节的时序图。此处省略时序图绘制。
2. 通过 AudioTrack 调节音量
除了 AudioManager
,当 APP 直接使用 AudioTrack
播放声音时,也可以通过 AudioTrack.setVolume()
来调节音量。
基本步骤有两个。第一步,新的音量值通过 AudioTrackClientProxy
以 audio_track_cblk_t
结构体的形式被存储到共享内存里;第二步,在 PlaybackThread
、DirectOutputThread
等线程的 threadLoop()
函数中,通过 AudioTrackServerProxy
读取 audio_track_cblk_t
中的音量值。根据线程类型不同,音量值会被送给 AudioMixer
进行混音,或者通过 StreamOut::setVolume()
发给 AudioHAL。
时序图如下:
PS: 不知道大家是怎么理解 cblk 这个字串的含义的。虽然没有官方说明,但我认为它应该是 Control Block 的意思。
3. 通过 CarAudioManager 调节音量
车机 Android 上还有个特有的组件可用于音量调节,就是 CarAudioManager
。APP 通过 CarAudioManager.setGroupVolume()
接口可以设置指定音量组的音量。底层实现这个功能的接口仍然是 IDevice::setAudioPortConfig()
。
要使用 CarAudioManager
API 调节音量,必须将 packages/services/Car/service/res/values/config.xml
中的 audioUseDynamicRouting
属性通过 overlay 方式设置为 true。 如下:
true
......
否则,代码会回滚为使用 AudioManager.setStreamVolume()
进行调节。相应代码如下:
public class CarAudioService extends ICarAudio.Stub implements CarServiceBase {
......
public CarAudioService(Context context) {
......
mUseDynamicRouting = mContext.getResources().getBoolean(R.bool.audioUseDynamicRouting);
......
}
......
@Override
public void setGroupVolume(int zoneId, int groupId, int index, int flags) {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
callbackGroupVolumeChange(zoneId, groupId, flags);
// For legacy stream type based volume control
if (!mUseDynamicRouting) {
mAudioManager.setStreamVolume(
CarAudioDynamicRouting.STREAM_TYPES[groupId], index, flags);
return;
}
synchronized (mImplLock) {
CarVolumeGroup group = getCarVolumeGroupLocked(zoneId, groupId);
group.setCurrentGainIndex(index);
}
}
......
}
时序图如下(AudioSystem 之后会经过 AudioFlinger 调用到 AudioHAL 实现的 IDevice::setAudioPortConfig()
,此图略去):
以上就是车机 Android 环境下的三种音量调节方式,及底层代码逻辑。