車機環境下的音頻使用場景,相較於原始 Android 的音頻使用場景,存在這些特殊性: + **使用專門的 aDSP 晶元進行音效處理;** + **需要播放/控制原始 Android 預設之外的音源(AudioUsage);** + **音源間交互行為更加複雜(AudioFocus);** + ... ...
註意:本文基於 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 中的音量調節。
<resources>
<bool name="config_useFixedVolume">true</bool>
......
</resources>
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
屬性。 如下:
<devicePort tagName="Speaker" role="sink" type="AUDIO_DEVICE_OUT_SPEAKER" address="">
<profile name="" format="AUDIO_FORMAT_PCM_16_BIT"
samplingRates="48000" channelMasks="AUDIO_CHANNEL_OUT_STEREO"/>
<gains>
<gain name="gain_1" mode="AUDIO_GAIN_MODE_JOINT"
minValueMB="-8400"
maxValueMB="4000"
defaultValueMB="0"
stepValueMB="100"
useForVolume="true"/>
</gains>
</devicePort>
因為 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。 如下:
<resources>
<bool name="audioUseDynamicRouting">true</bool>
......
</resources>
否則,代碼會回滾為使用 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 環境下的三種音量調節方式,及底層代碼邏輯。