音频焦点使用及原理
音频焦点使用及原理
本博客代码基于Android 10源码
为什么会有音频焦点这一概念?
在Android音频领域中,应用层所有的App播放音频,最终都是走到音频回播线程PlaybackThread中,如果多个App都走到同一个PlaybackThread中去,就会出现混音情况,Android本身对混音也有很好的支持,但是也会造成某些重要音频资源播放时,用户听不太清晰,这个时候就引入音频焦点这一概念!
所谓的音频焦点,可以理解为一个播放权限的东西,App获得了音频焦点,你就可以播放你的音频内容,当你失去了音频焦点,你就得暂停、停止或降低你播放的音频;在Android 10上验证了,以上这些工作就是App自己要遵守、完成的工作,App自己监听音频焦点状态,得到不同的音频焦点状态,执行对应的操作。
Android音频焦点基本用法
在Android 10版本上音频焦点申请,主要分为三个步骤:
- 组装音频焦点申请请求
AudioAttributes.Builder attributes = new AudioAttributes.Builder();
attributes.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC).setUsage(AudioAttributes.USAGE_MEDIA);
//App应用申请的音频焦点类型
AudioFocusRequest.Builder request = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN);
request.setAudioAttributes(attributes.build())
.setOnAudioFocusChangeListener{
public void onAudioFocusChange(int i) {
//音频焦点状态回调
}
}
以上new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)这句,表明App申请焦点类型,有以下:
焦点类型 | 焦点解释 |
---|---|
AUDIOFOCUS_GAIN | 长时间占用音频焦点,如音频播放这种,失去焦点停止播放 |
AUDIOFOCUS_GAIN_TRANSIENT | 短时获取焦点,失去焦点暂停播放,比如语音、电话 |
AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK | 短时获取焦点,失去焦点降低音量,如导航 |
AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE | 短暂占有焦点,但希望失去焦点者不要有声音播放,比如电话 |
- 音频焦点申请
/** @gainFlag
* AUDIOFOCUS_REQUEST_DELAYED = 2;
* AUDIOFOCUS_REQUEST_FAILED = 0;
* AUDIOFOCUS_REQUEST_GRANTED = 1;
*/
int gainFlag = mAudioManager.requestAudioFocus(request.build());
如上请求后获取的返回值gainFlag取值1就是请求通过,可以播放音频了;0就是被拒绝了不允许播放,2就是延迟获取申请的结果
- 音频状态回调
在第一个步骤中,设置的setOnAudioFocusChangeListener焦点回调,音频焦点就是通过这个回调返回的,如下代码:
public void onAudioFocusChange(int i) {
Log.i(TAG, "onAudioFocusChange " + i);
switch (i){
//永久的失去音频焦点
case AudioManager.AUDIOFOCUS_LOSS:
Log.d(TAG, "AUDIOFOCUS_LOSS");
mMediaPlayer.stop();
mAudioManager.abandonAudioFocusRequest(request.build());
break;
//短暂失去焦点,并可能会恢复
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
Log.d(TAG, "AUDIOFOCUS_LOSS_TRANSIENT");
mMediaPlayer.pause();
break;
//短暂性丢失焦点并作降音处理
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
Log.d(TAG, "AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK");
mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, 3, 0);
break;
//当其他应用申请焦点之后又释放焦点会触发此回调
case AudioManager.AUDIOFOCUS_GAIN:
Log.d(TAG, "AUDIOFOCUS_GAIN");
if(isMediaPrepared) {
mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, 10, 0);
mMediaPlayer.start();
}
break;
}
}
具体如上代码,返回值如上,并有注释!
音频焦点原理framework分析
这里主要分析上节的2和3步骤,申请焦点与焦点回调两个过程,在framework中是如何运作的?
申请音频焦点
流程图如下:
如上图,请求过程相对简单,依次经历AudioManager、AudioService和MediaFocusControl三个类中,主要的工作是在红圈1处AudioManager和红圈3处MediaFocusControl中执行的;
AudioManager中做的事情
public int requestAudioFocus(@NonNull AudioFocusRequest afr, @Nullable AudioPolicy ap) {
.....
registerAudioFocusRequest(afr);
final IAudioService service = getService();
final int status;
int sdk;
try {
sdk = getContext().getApplicationInfo().targetSdkVersion;
} catch (NullPointerException e) {
// some tests don't have a Context
sdk = Build.VERSION.SDK_INT;
}
final String clientId = getIdForAudioFocusListener(afr.getOnAudioFocusChangeListener());
final BlockingFocusResultReceiver focusReceiver;
synchronized (mFocusRequestsLock) {
try {
// TODO status contains result and generation counter for ext policy
status = service.requestAudioFocus(afr.getAudioAttributes(),
afr.getFocusGain(), mICallBack,
mAudioFocusDispatcher,
clientId,
getContext().getOpPackageName() /* package name */, afr.getFlags(),
ap != null ? ap.cb() : null,
sdk);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
if (status != AudioManager.AUDIOFOCUS_REQUEST_WAITING_FOR_EXT_POLICY) {
// default path with no external focus policy
return status;
}
.....
}
}
主要完成的三件事:
- 将此次的请求AudioFocusRequest注册进去,调用registerAudioFocusRequest,其内部就是将请求push到一个map结构中去
- getIdForAudioFocusListener从第一个步骤中map的对应key,也就是clientId
- 调用audioService的requestAudioFocus方法,并将重要参数如clientId和mAudioFocusDispatcher传递过去
上述第一步的register方法如下:
public void registerAudioFocusRequest(@NonNull AudioFocusRequest afr) {
final Handler h = afr.getOnAudioFocusChangeListenerHandler();
final FocusRequestInfo fri = new FocusRequestInfo(afr, (h == null) ? null :
new ServiceEventHandlerDelegate(h).getHandler());
final String key = getIdForAudioFocusListener(afr.getOnAudioFocusChangeListener());
//focus回调集合
mAudioFocusIdListenerMap.put(key, fri);
}
mAudioFocusIdListenerMap也就是一个map集合
上述第三步的mAudioFocusDispatcher是啥?
private final IAudioFocusDispatcher mAudioFocusDispatcher = new IAudioFocusDispatcher.Stub() {
@Override
public void dispatchAudioFocusChange(int focusChange, String id) {
final FocusRequestInfo fri = findFocusRequestInfo(id);
if (fri != null) {
final OnAudioFocusChangeListener listener =
fri.mRequest.getOnAudioFocusChangeListener();
if (listener != null) {
final Handler h = (fri.mHandler == null) ?
mServiceEventHandlerDelegate.getHandler() : fri.mHandler;
final Message m = h.obtainMessage(
MSSG_FOCUS_CHANGE/*what*/, focusChange/*arg1*/, 0/*arg2 ignored*/,
id/*obj*/);
h.sendMessage(m);
}
}
}
@Override
public void dispatchFocusResultFromExtPolicy(int requestResult, String clientId) {
.......
}
};
实质就是一个aidl的远端回调接口,因为它要和AudioService测进行binder通信,那肯定得用aidl接口
MediaFocusControl中做的事情
protected int requestAudioFocus(@NonNull AudioAttributes aa, int focusChangeHint, IBinder cb,
IAudioFocusDispatcher fd, @NonNull String clientId, @NonNull String callingPackageName,
int flags, int sdk, boolean forceDuck) {
synchronized(mAudioFocusLock) {
//MAX_STACK_SIZE 100
if (mFocusStack.size() > MAX_STACK_SIZE) {
return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
}
//为此次焦点申请在service端创建对应的实体类
final FocusRequester nfr = new FocusRequester(aa, focusChangeHint, flags, fd, cb,
clientId, afdh, callingPackageName, Binder.getCallingUid(), this, sdk);
if (focusGrantDelayed) {
// focusGrantDelayed being true implies we can't reassign focus right now
// which implies the focus stack is not empty.
final int requestResult = pushBelowLockedFocusOwners(nfr);
if (requestResult != AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
notifyExtPolicyFocusGrant_syncAf(nfr.toAudioFocusInfo(), requestResult);
}
return requestResult;
} else {
// propagate the focus change through the stack
if (!mFocusStack.empty()) {
propagateFocusLossFromGain_syncAf(focusChangeHint, nfr, forceDuck);
}
// 加入到栈中
mFocusStack.push(nfr);
nfr.handleFocusGainFromRequest(AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
}
}
......
}
主要完成的工作:
- 检测栈成员mFocusStack是否已经装满,装满就返回请求失败;
- 为此次客户端的焦点请求创建对应的实体类FocusRequester,其中它的参数clientId和cb,就是客户端App的信息和远端aidl回调
- 最后,在将FocusRequester压栈;
这个mFocusStack很重要,是一个栈结构,先进后出,在栈顶的FocusRequester就会获得音频焦点
最后,经过上述的流程后,客户端AudioManager与服务端AudioService就建立好了返回的引用链路,也就是音频焦点回调链路,如下图:
音频焦点回调
虽然流程图是焦点回调,但还是包含了音频焦点申请部分,为什么呢?
因为实际上就是通过这种流程触发的,假如我们第一个音乐App申请焦点成功后,在播放音乐music,此时它的FocusRequestor位于FocusStack栈顶,此时若有电话接入,电话App应用会申请音频焦点,电话App会位于FocusStack的栈顶,而music的app在电话App的下面,就会触发对于音乐App失去焦点的回调,当然还有其他焦点触发变化的情况,此处解释就是上述流程图的红圈1处!
在红圈2处的handleFocusXXX,所有的音频焦点获得gain、失去loss等,替换XXX字符串的方面名,然后通过aidl的回调接口IAudioFocusDispatcher回调到应用端App,应用端收到后根据焦点状态情况,对音频进行播放、暂停、降低音量等操作。
IAudioFocusDispatcher的回调方法dispatchAudioFocusChange去看看前面章节AudioMananger的aidl接口即可!
var code = “8e648381-599f-4d4c-bf1d-2f5d57018f33”
小结
上面只是分析了数据逻辑如何调用跳转,但是当多个申请者进行焦点申请和放弃时,内部的逻辑如何?各个申请者获得的焦点状态又是什么样呢?
首先看FocusRequester.java中的focusLossForGainRequest函数:
/** gainRequest新拥有这申请的焦点类型,会影响上一个焦点拥有者失去焦点类型
* For a given audio focus gain request, return the audio focus loss type that will result
* from it, taking into account any previous focus loss.
* @param gainRequest
* @return the audio focus loss type that matches the gain request
*/
private int focusLossForGainRequest(int gainRequest) {
switch(gainRequest) {
//新申请者申请长焦点,则老焦点拥有者会失去焦点
case AudioManager.AUDIOFOCUS_GAIN:
switch(mFocusLossReceived) {
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
case AudioManager.AUDIOFOCUS_LOSS:
case AudioManager.AUDIOFOCUS_NONE:
return AudioManager.AUDIOFOCUS_LOSS;
}
//新申请者申请短焦点,则老焦点拥有者会短暂失去焦点;如果已经失去焦点了,直接失去焦点
case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE:
case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT:
switch(mFocusLossReceived) {
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
case AudioManager.AUDIOFOCUS_NONE:
return AudioManager.AUDIOFOCUS_LOSS_TRANSIENT;
case AudioManager.AUDIOFOCUS_LOSS:
return AudioManager.AUDIOFOCUS_LOSS;
}
//新申请短降低音量型焦点,则老焦点根据之前的焦点丢失状态返回
case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK:
switch(mFocusLossReceived) {
case AudioManager.AUDIOFOCUS_NONE:
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
return AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK;//返回降音处理策略,收到此类型丢失焦点,音频应该降音处理
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
return AudioManager.AUDIOFOCUS_LOSS_TRANSIENT;
case AudioManager.AUDIOFOCUS_LOSS:
return AudioManager.AUDIOFOCUS_LOSS;
}
default:
Log.e(TAG, "focusLossForGainRequest() for invalid focus request "+ gainRequest);
return AudioManager.AUDIOFOCUS_NONE;
}
}
这段代码的精髓就是申请者的申请焦点类型gainRequest,对栈中其它老的申请者的丢失焦点类型是什么?也就是返回值就是老的获得的丢失焦点类型;根据这段代码我总结了以下笔记:
图片的焦点类型字符没写全,但大致也能看懂是申请是什么类型
- 1、2、3分别是A、B和C申请焦点,分别在栈中的位置以及其丢失焦点类型
- 3步骤C申请AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK时,会根据MediaFocusControl的ENFORCE_DUCKING 来决定谁负责降音,true由framework进行降音,false将会以AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK类型返回给B,由B自己负责降音
/**
* set to true so the framework enforces ducking itself, without communicating to apps
* that they lost focus for most use cases.
*/
static final boolean ENFORCE_DUCKING = true;
- 4步骤是再次申请焦点时,
1)如果申请者A再次申请,已经存在栈中但又不在栈顶,则先移除栈中的A,在从新push到栈顶
2)如果申请者C再次申请,已经存在且在栈顶,并且申请的焦点类型与之前相同就直接返回给C获得焦点;反之申请焦点类型不同,则会移除栈顶C,然后在重新push到栈顶 - 4步骤释放abandon焦点;何时释放焦点呢?
1)焦点使用者播放完音频资源后主动释放焦点
2)焦点使用者收到了LOSS_GAIN永久丢失焦点类型,主动释放焦点
释放规则如上图,自行查看
framework如何做到降音
当MediaFocusControl的ENFORCE_DUCKING,系统如何做降音呢?
在上面方法handleFocusLoss中这段代码:
if (frWinner.mCallingUid != this.mCallingUid) {
//如果申请者类型是DUCK类型,这里forceDuck为true,所以会走到最后的else
if (!forceDuck && ((mGrantFlags
& AudioManager.AUDIOFOCUS_FLAG_PAUSES_ON_DUCKABLE_LOSS) != 0)) {
// the focus loser declared it would pause instead of duck, let it
// handle it (the framework doesn't pause for apps)
handled = false;
Log.v(TAG, "not ducking uid " + this.mCallingUid + " - flags");
} else if (!forceDuck && (MediaFocusControl.ENFORCE_DUCKING_FOR_NEW &&
this.getSdkTarget() <= MediaFocusControl.DUCKING_IN_APP_SDK_LEVEL))
{
// legacy behavior, apps used to be notified when they should be ducking
handled = false;
Log.v(TAG, "not ducking uid " + this.mCallingUid + " - old SDK");
} else {
//系统降音duckPlayers,frWinner是新申请者,this是老的,会采取降音
handled = mFocusController.duckPlayers(frWinner, this, forceDuck); //modify by xieyonghui
}
duckPlayers就是降音方法,以下是方法调用流程图:
记住调用链最前面的参数duckPlayers(winner,losser,foceDuck)winner正常播放,losser要降音处理,forceDuck强制降音标记
在PlaybackActivityMonitor类中的duckPlayers方法中,会从集合mPlayers中遍历找到losser,如下:
final Iterator<AudioPlaybackConfiguration> apcIterator = mPlayers.values().iterator();
while (apcIterator.hasNext()) {
if (!winner.hasSameUid(apc.getClientUid())
&& loser.hasSameUid(apc.getClientUid())
&& apc.getPlayerState() == AudioPlaybackConfiguration.PLAYER_STATE_STARTED)
{
apcsToDuck.add(apc);
}
mDuckingManager.duckUid(loser.getClientUid(), apcsToDuck);
}
这里问题的关键mPlayers中怎么会有losser呢?看下面的图就知道了:
播放的几个类AudioTrack、MediaPlayer和SoundPool都继承PlayerBase,通过上面的调用链最终都会将播放的客户端添加到mPlayer里面去;
回到上面降音流程,在最后的addDuck中:
private static final VolumeShaper.Configuration DUCK_VSHAPE =
new VolumeShaper.Configuration.Builder()
.setId(VOLUME_SHAPER_SYSTEM_DUCK_ID)
.setCurve(new float[] { 0.f, 1.f } /* times */,
new float[] { 1.f, 0.2f } /* volumes */)
.setOptionFlags(VolumeShaper.Configuration.OPTION_FLAG_CLOCK_TIME)
.setDuration(MediaFocusControl.getFocusRampTimeMs(
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK,
new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_NOTIFICATION)
.build()))
.build();
void addDuck(@NonNull AudioPlaybackConfiguration apc, boolean skipRamp){
.......
apc.getPlayerProxy().applyVolumeShaper(
DUCK_VSHAPE,
skipRamp ? PLAY_SKIP_RAMP : PLAY_CREATE_IF_NEEDED);
.......
}
最终就是通过音频播放客户端采用volumeShaper来进行降音,volumeShaper不是本文重点,网上有很多相关使用案例,可自行查阅,到这里降音就分析完了
规则之外的电话
有一种特殊情况,当比如电话焦点使用者占用焦点时,它的优先级最高,此时其它拥有者申请时不允许申请,在MediaFocusControl中的requestAudioFocus函数中:
protected int requestAudioFocus(@NonNull AudioAttributes aa, int focusChangeHint, IBinder cb,
IAudioFocusDispatcher fd, @NonNull String clientId, @NonNull String callingPackageName,
int flags, int sdk, boolean forceDuck) {
.....
//如果当前栈顶拥有者是电话,返回false,取反条件就成立
f (!canReassignAudioFocus()) {
//并且申请的焦点不允许延迟回调,也就是是说电话通话结束后回调;这里一般的申请者flags都没有申请延迟获得焦点,所以就返回AUDIOFOCUS_REQUEST_FAILED
if ((flags & AudioManager.AUDIOFOCUS_FLAG_DELAY_OK) == 0) {
Log.i(TAG, "-----cowin003-----requestAudioFocus-----callingPackageName:" +
callingPackageName + "----AudioAttributes:" + aa.toString() + "-----801-----AUDIOFOCUS_REQUEST_FAILED");
return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
} else {
// request has AUDIOFOCUS_FLAG_DELAY_OK: focus can't be
// granted right now, so the requester will be inserted in the focus stack
// to receive focus later
focusGrantDelayed = true;
}
}
......
}
private boolean canReassignAudioFocus() {
// 当前栈顶peek焦点拥有者和电话通话进程的id一样,就返回false
if (!mFocusStack.isEmpty() && isLockedFocusOwner(mFocusStack.peek())) {
return false;
}
return true;
}
private boolean isLockedFocusOwner(FocusRequester fr) {
//和电话通话进程的id一样
return (fr.hasSameClient(AudioSystem.IN_VOICE_COMM_FOCUS_ID) || fr.isLockedFocusOwner());
}
小结:
看上面代码就知道,主要就是对比栈顶焦点拥有者是不是电话应用,通过clientid对比判断电话应用是否在通话中,所以我们在适配修改适配电话应用,可以在此块进行修改,比如车机上有多个通话应用,我们把这些应用写道一个集合中去,当有焦点申请来临时,peek栈顶申请者去集合中找,找到了就说通话应用存在,不允许新的焦点申请