Android音频之车载音频焦点

 

Android音频之车载音频焦点

在 Android 的车载音频系统中,音频焦点(Audio Focus) 是一个核心概念,用于协调多个音频源之间的播放冲突,确保用户能够体验到最优的音频效果。

在启动逻辑声音流之前,应用会使用与逻辑声音流相同的音频属性来请求音频焦点。应用必须尊重焦点损失,以便在汽车用例中按预期运行。

虽然我们建议发送焦点请求,但系统不会强制要求发送。因此,请将焦点视为间接控制和避免播放期间发生冲突的手段,而不是主要的音频控制机制。车辆不应依赖焦点系统来操作音频子系统。

官方网站 https://source.android.com/docs/automotive/audio/audio-focus


什么是音频焦点?

音频焦点是 Android 提供的一种机制,用于在多个应用或音频流之间协调音频播放的优先级。音频焦点的核心思想是:在同一时间只有一个音频流能够占据焦点,并享有播放的权利,而其他音频流则需要根据优先级调整行为(如暂停、降低音量等)。

在车载环境中,音频焦点尤为重要,因为音频设备种类繁多,例如:

  • 导航系统
  • 语音助手
  • 多媒体播放器(音乐、视频)
  • 电话通话

焦点交互

为了支持 AAOS,系统会根据请求的 CarAudioContext 和当前焦点持有者的 CarAudioContext 之间的预定义交互来处理音频焦点请求。交互分为三种类型:

  • 拒绝
  • 独占
  • 并发

拒绝

在拒绝交互中,传入的请求一律会遭到拒绝。例如,在通话过程中尝试播放音乐。在这种情况下,如果拨号器为某个通话持有音频焦点,而第二个应用请求获得焦点以播放音乐,则音乐应用发出的请求会收到 AUDIOFOCUS_REQUEST_FAILED 响应。由于焦点请求遭拒,因此系统不会向当前焦点持有者分派任何焦点丢失事件。

独占

这是 Android 中最常用的交互模型。

在独占交互中,一次只允许一个应用持有焦点。因此,在传入的焦点请求被授予焦点的同时,现有的焦点持有者会失去焦点。由于两个应用都播放媒体,因此仅允许一个应用持有焦点。结果就是,新启动的应用发出的焦点请求将返回 AUDIOFOCUS_REQUEST_GRANTED;而当前播放音乐的应用将收到焦点更改事件,该事件中的丢失状态与所发请求的类型相对应。

并发

并发交互是 AAOS 独有的。在这种交互模式下,请求音频焦点的车载应用可与其他应用同时持有焦点。例如,在播放音乐过程中,音乐应用持有音频焦点,而导航应用请求获得焦点已播放导航,导航应用发出的请求会收到AUDIOFOCUS_REQUEST_GRANTED响应;因为音乐应用的焦点没有发生变化,音乐应用不会收到焦点更改事件。

在实际应用时,导航应用请求获得焦点时音乐媒体应用持有焦点,不仅两个应用并行播放,音乐媒体应用还要降低音量。为了满足这类需求,在并发交互类型下增加了两种新交互类型,请求者降音和持有者降音。如下图:

int INTERACTION_REJECT = 0; // Focus not granted
int INTERACTION_EXCLUSIVE = 1; // Focus granted, others loose focus
int INTERACTION_CONCURRENT = 2; // Focus granted, others keep focus, do not duck any volume
int INTERACTION_CONCURRENT_DUCK_REQUESTER = 3; // Focus granted, others keep focus, requester duck volume
int INTERACTION_CONCURRENT_DUCK_HOLDER = 4; // Focus granted, others keep focus, holder duck volume

音频交互矩阵

下表显示了由 CarAudioService 定义的交互矩阵。每行都代表当前焦点持有者的 CarAudioContext,每列都代表传入请求的 CarAudioContext。

例如,如果导航应用请求获得焦点时音乐媒体应用持有焦点,此矩阵会指示这两种交互可以并行播放(假设满足并发交互的其他条件)。

由于并发交互的缘故,可能会存在多个焦点持有者。在这种情况下,系统会将传入的焦点请求与当前的各个焦点持有者进行比较,然后决定应用哪种交互。此时,最保守的交互会胜出。先是拒绝交互,然后是独占交互,最后是并发交互。

交互矩阵


音频焦点的使用

Android 系统使用 AudioManager使用音频焦点。车载音频焦点的使用如下:

1. AudioFocusRequest

AudioFocusRequest 是申请音频焦点的关键类。开发者需要构建一个 AudioFocusRequest 对象来申请音频焦点,指定以下参数:

  • 音频属性(AudioAttributes):描述音频流的用途(如音乐、语音、导航)。
  • 焦点类型(AudioFocusType):定义当前音频的优先级(如获取或放弃焦点)。
  • 回调(AudioFocusListener):用于接收音频焦点状态变化的通知。

代码示例:

AudioAttributes audioAttributes = new AudioAttributes.Builder()
        .setUsage(AudioAttributes.USAGE_MEDIA)
        .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
        .build();

AudioFocusRequest audioFocusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
        .setAudioAttributes(audioAttributes)
        .setOnAudioFocusChangeListener(audioFocusChangeListener)
        .build();

AudioManager mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
int result = mAudioManager.requestAudioFocus(audioFocusRequest);

if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
    // 成功获取音频焦点,可以播放音频
}

2. AudioFocusListener

通过回调 AudioFocusListener,应用可以监听音频焦点的变化,并根据状态调整音频播放行为。常见的音频焦点状态包括:

  • AUDIOFOCUS_GAIN:当前应用获得音频焦点。
  • AUDIOFOCUS_GAIN_TRANSIENT: 当前应用暂时获得音频焦点
  • AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK: 当前应用短暂获得音频焦点,可能会降音
  • AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE: 当前应用短暂获得音频焦点,而且是独占
  • AUDIOFOCUS_LOSS:当前应用永久失去音频焦点。
  • AUDIOFOCUS_LOSS_TRANSIENT:当前应用暂时失去音频焦点。
  • AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:当前应用失去焦点,但可以继续播放音频,需降低音量。

回调示例:

AudioManager.OnAudioFocusChangeListener audioFocusChangeListener = focusChange -> {
    switch (focusChange) {
        case AudioManager.AUDIOFOCUS_GAIN:
        case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT:
        case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK:
        case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE:
            // 恢复播放
            break;
        case AudioManager.AUDIOFOCUS_LOSS:
            // 停止播放并释放音频焦点资源
            break;
        case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
            // 暂停播放
            break;
        case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
            // 降低音量
            break;
    }
};

音频焦点的实现

车载音频架构如下图:

车载音频架构

AudioServiceCarAudioService 负责管理音频焦点。在AOSP中AudioService负责处理音频焦点,在AAOS出来之后,为了方便车载音频,在AudioService中加入了external-focus-policy,从而使CarAudioService也能处理音频焦点。

注册流程

// packages/services/Car/service/src/com/android/car/audio/CarAudioService.java
--> init()
----> setupDynamicRoutingLocked()
------> loadCarAudioZonesLocked()
------> mFocusHandler = new CarZonesAudioFocus(mAudioManager,mContext.getPackageManager(),mCarAudioZones,mCarAudioSettings, ENABLE_DELAYED_AUDIO_FOCUS);
// packages/services/Car/service/src/com/android/car/audio/CarZonesAudioFocus.java
--------> CarZonesAudioFocus(...) //extends AudioPolicy.AudioPolicyFocusListener
--------> for (int i = 0; i < carAudioZones.size(); i++)
// packages/services/Car/service/src/com/android/car/audio/AudioFocus.java
--------> CarAudioFocus zoneFocusListener =new CarAudioFocus();//extends AudioPolicy.AudioPolicyFocusListener
--------> mFocusZones.put(audioZoneId, zoneFocusListener);
// frameworks/base/media/java/android/media/audiopolicy/AudioPolicy.java
------> Builder builder.setAudioPolicyFocusListener(mFocusHandler);
--------> setAudioPolicyFocusListener(AudioPolicyFocusListener l)
--------> mFocusListener = l;
------> builder.setIsAudioFocusPolicy(true);
--------> setIsAudioFocusPolicy(boolean isFocusPolicy)
--------> mIsFocusPolicy = isFocusPolicy;
------> mAudioPolicy = builder.build()
------> mFocusHandler.setOwningPolicy(this, mAudioPolicy);
------> mAudioManager.registerAudioPolicy(mAudioPolicy)
// frameworks/base/media/java/android/media/AudioManager.java
--------> registerAudioPolicyStatic()
--------> IAudioService service = getService()
--------> service.registerAudioPolicy()
// frameworks/base/services/core/java/com/android/server/audio/AudioService.java
--------> registerAudioPolicy()
--------> AudioPolicyProxy app = new AudioPolicyProxy()
----------> AudioPolicyProxy(AudioPolicyConfig , IAudioPolicyCallback ,boolean , boolean , boolean ,boolean , IMediaProjection )
// frameworks/base/services/core/java/com/android/server/audio/MediaFocusControl.java
----------> mMediaFocusControl.setFocusPolicy(mPolicyCallback, mIsTestFocusPolicy)
------------> mFocusPolicy = policy;

使用流程

// frameworks/base/media/java/android/media/AudioManager.java
requestAudioFocus(AudioFocusRequest focusRequest)
--> requestAudioFocus(AudioFocusRequest afr,AudioPolicy ap)
----> registerAudioFocusRequest(afr);
------> mAudioFocusIdListenerMap.put(key, fri);
----> IAudioService service = getService()
----> BlockingFocusResultReceiver focusReceiver;
----> service.requestAudioFocus(afr.getAudioAttributes(),afr.getFocusGain(), mICallBack,mAudioFocusDispatcher,clientId,getContext().getOpPackageName(), afr.getFlags(),ap != null ? ap.cb() : null,sdk);
// frameworks/base/services/core/java/com/android/server/audio/AudioService.java
------> requestAudioFocus(AudioAttributes aa, int durationHint, IBinder cb,IAudioFocusDispatcher fd, String clientId, String callingPackageName, int flags,IAudioPolicyCallback pcb, int sdk)
// frameworks/base/services/core/java/com/android/server/audio/MediaFocusControl.java
-------> int requestAudioFocus(AudioAttributes aa, int focusChangeHint, IBinder cb,IAudioFocusDispatcher fd,String clientId, String callingPackageName,int flags, int sdk, boolean forceDuck);
---------> afiForExtPolicy = new AudioFocusInfo(aa, Binder.getCallingUid(),clientId, callingPackageName, focusChangeHint, 0,flags, sdk)
---------> notifyExtFocusPolicyFocusRequest_syncAf(afiForExtPolicy, fd, cb)
-----------> mFocusOwnersForFocusPolicy.put(afi.getClientId(),new FocusRequester(afi, fd, cb, hdlr, this));
-----------> mFocusPolicy.notifyAudioFocusRequest(afi, AudioManager.AUDIOFOCUS_REQUEST_GRANTED)
// frameworks/base/media/java/android/media/audiopolicy/AudioPolicy.java
-------------> sendMsg(MSG_FOCUS_REQUEST, afi, requestResult);
-------------> EventHandler.handleMessage(Message msg);
-------------> case MSG_FOCUS_REQUEST:
-------------> mFocusListener.onAudioFocusRequest((AudioFocusInfo) msg.obj, msg.arg1);
// packages/services/Car/service/src/com/android/car/audio/CarZonesAudioFocus.java
---------------> public void onAudioFocusRequest(AudioFocusInfo afi, int requestResult)
---------------> CarAudioFocus focus = getFocusForAudioFocusInfo(afi);
---------------> focus.onAudioFocusRequest(afi, requestResult);
// packages/services/Car/service/src/com/android/car/audio/CarAudioFocus.java
---------------> public void onAudioFocusRequest(AudioFocusInfo afi, int requestResult);
---------------> response = evaluateFocusRequestLocked(afi);
-----------------> private int evaluateFocusRequestLocked(AudioFocusInfo afi);
-----------------> int requestedContext = CarAudioContext.getContextForUsage(afi.getAttributes().getSystemUsage());
-----------------> for (FocusEntry entry : mFocusHolders.values())
-----------------> int interactionResult = mFocusInteraction.evaluateRequest(...);
-----------------> for (FocusEntry entry : mFocusLosers.values())
-----------------> int interactionResult = mFocusInteraction.evaluateRequest(...);
-----------------> ...
---------------> mAudioManager.setFocusRequestResult(afi, response, policy);
-----------------> public void setFocusRequestResult(@NonNull AudioFocusInfo afi,int requestResult,AudioPolicy ap)
-----------------> IAudioService service = getService();
-----------------> service.setFocusRequestResultFromExtPolicy(afi, requestResult, ap.cb());
-------------------> public void setFocusRequestResultFromExtPolicy(AudioFocusInfo afi, int requestResult,IAudioPolicyCallback pcb);
-------------------> mMediaFocusControl.setFocusRequestResultFromExtPolicy(afi, requestResult);
---------------------> void setFocusRequestResultFromExtPolicy(AudioFocusInfo afi, int requestResult)
---------------------> FocusRequester fr = mFocusOwnersForFocusPolicy.get(afi.getClientId());
---------------------> fr.dispatchFocusResultFromExtPolicy(requestResult);
// frameworks/base/services/core/java/com/android/server/audio/FocusRequester.java
-----------------------> void dispatchFocusResultFromExtPolicy(int requestResult)
-----------------------> IAudioFocusDispatcher fd = mFocusDispatcher;
-----------------------> fd.dispatchFocusResultFromExtPolicy(requestResult, mClientId);
// frameworks/base/media/java/android/media/AudioManager.java | IAudioFocusDispatcher mAudioFocusDispatcher
-------------------------> public void dispatchFocusResultFromExtPolicy(int requestResult, String clientId)
-------------------------> BlockingFocusResultReceiver focusReceiver = mFocusRequestsAwaitingResult.remove(clientId);
-------------------------> focusReceiver.notifyResult(requestResult);
---------------------------> void notifyResult(int requestResult);
---------------------------> mResultReceived = true;
---------------------------> mFocusRequestResult = requestResult;
---------------------------> mLock.safeNotify();
----> focusReceiver = new BlockingFocusResultReceiver(clientId);
----> mFocusRequestsAwaitingResult.put(clientId, focusReceiver);
----> focusReceiver.waitForResult(EXT_FOCUS_POLICY_TIMEOUT_MS);
------> mLock.safeWait(timeOutMs);
----> return focusReceiver.requestResult();
------> return mFocusRequestResult;