音频焦点使用及原理

news2024/11/20 6:28:02

音频焦点使用及原理

本博客代码基于Android 10源码



为什么会有音频焦点这一概念?

在Android音频领域中,应用层所有的App播放音频,最终都是走到音频回播线程PlaybackThread中,如果多个App都走到同一个PlaybackThread中去,就会出现混音情况,Android本身对混音也有很好的支持,但是也会造成某些重要音频资源播放时,用户听不太清晰,这个时候就引入音频焦点这一概念!

所谓的音频焦点,可以理解为一个播放权限的东西,App获得了音频焦点,你就可以播放你的音频内容,当你失去了音频焦点,你就得暂停、停止或降低你播放的音频;在Android 10上验证了,以上这些工作就是App自己要遵守、完成的工作,App自己监听音频焦点状态,得到不同的音频焦点状态,执行对应的操作。



Android音频焦点基本用法

在Android 10版本上音频焦点申请,主要分为三个步骤:

  1. 组装音频焦点申请请求
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短暂占有焦点,但希望失去焦点者不要有声音播放,比如电话
  1. 音频焦点申请
/** @gainFlag
    * AUDIOFOCUS_REQUEST_DELAYED = 2;
    * AUDIOFOCUS_REQUEST_FAILED = 0;
    * AUDIOFOCUS_REQUEST_GRANTED = 1;
    */
int gainFlag = mAudioManager.requestAudioFocus(request.build());

如上请求后获取的返回值gainFlag取值1就是请求通过,可以播放音频了;0就是被拒绝了不允许播放,2就是延迟获取申请的结果

  1. 音频状态回调
    在第一个步骤中,设置的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;
        }
    .....
    }
}

主要完成的三件事:

  1. 将此次的请求AudioFocusRequest注册进去,调用registerAudioFocusRequest,其内部就是将请求push到一个map结构中去
  2. getIdForAudioFocusListener从第一个步骤中map的对应key,也就是clientId
  3. 调用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);
        }
    }
    ......
}

主要完成的工作:

  1. 检测栈成员mFocusStack是否已经装满,装满就返回请求失败;
  2. 为此次客户端的焦点请求创建对应的实体类FocusRequester,其中它的参数clientId和cb,就是客户端App的信息和远端aidl回调
  3. 最后,在将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接口即可!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/495614.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

chat错误代码1020是什么原因导致的-解决chat错误代码1020

ChatGPT拒绝访问1020是什么原因 ChatGPT拒绝访问1020可能是因为服务器故障、网络连接问题或者人工智能模型正在进行训练或调整等原因所致。一般来说,ChatGPT会在进行维护、升级或训练时暂停服务,这可能导致用户在访问ChatGPT时遇到拒绝访问的情况。在该…

2023年淮阴工学院五年一贯制专转本财务管理基础考试大纲

2023年淮阴工学院五年一贯制专转本财务管理基础考试大纲 一、考核对象 本课程的考核对象为五年一贯制高职专转本财务管理专业入学考试普通在校生考生。 二、考核方式 本课程考核采用闭卷笔试的方式。 三、命题依据及原则 1、命题依据 本课程考核命题教材为靳磊编著&…

2023年会展服务研究报告

第一章 行业概况 会展行业是指一系列与会议、展览、展示相关的服务和经济活动的总称,是加强企业间交流、促进合作和推动经济发展的重要手段。该行业涉及广泛,包括会议和展览的组织、场地租赁和设计、活动策划和执行、展品运输和咨询服务等各个环节。随着…

每个人都可以用来提高生产力的「项目管理策略」

与以往相比,越来越多的公司为员工提供混合办公(办公室工作远程办公)和远程办公的选择,这实际是出于对效率的强需求。 尽管员工的工作时间增加了,但办公室等工作场所的工作效率正在以几十年来最快的速度下降。专家们列…

一起了解可以用手机操作显示的BOD水质检测仪

什么叫BOD(生化需氧量): BOD是指表示水中有机化合物等需氧物质含量的一个综合指标。当水中所含有机物与空气接触时,由于需氧微生物的作用而分解,使之无机化或气体化时所需消耗的氧量。 生化需氧量,以毫克/升…

震惊,为了学会泛型类竟做这种事?!

上一节,我们基本学会了Java泛型类的用法。 传送门:彻底弄懂Java的泛型 - 泛型类 这一节,我们转变一下风格,具体是什么风格呢,你马上就懂了。 宝子们,欢迎大家来到我们的泛型直播间,这一讲呢&a…

通过gulp+rollup从零到一搭建前端组件库

核心要点 通过 webpack 搭建运行环境通过 gulp 执行编译流程通过 rollup 编译组件代码编写 组件测试 组件打包 编译组件上传 npm 1、通过 webpack 搭建运行环境 这里主要是创建一个可以运行的测试的组件的环境,全局安装vue-cli脚手架,并初始化项目 v…

代码提交格式化工具---prettier的使用与配置

为什么要格式化代码 我们在合作写项目的时候,因为每个人的代码书写习惯以及编辑器的配置都不相同,这样就可能导致我们写的代码的格式都完全不一样,有的代码格式看起来也比较难以阅读,那么prettierd就由此产生了。 Prettier是一个…

题集-链表分割

链表分割_牛客题霸_牛客网 (nowcoder.com) 链表去做的话,可以分为带哨兵位和不带哨兵位两种,但是不带哨兵位会更麻烦一些,更容易出现空指针问题。 这里两种思路都是:将小于x的结点尾插到less链表中,将大于x的结点尾插到…

数据结构之队列,实现队列的增删改查

目录 一、队列的定义 二、队列的实现 1.使用链表来实现队列 2.实现队列的接口 初始化队列 void QueueInit(Queue *pq) 队尾入队列 void QueuePush(Queue *pq,QDataType data) 队头出队列 void QueuePop(Queue *pq) 获取队列头部元素 QDataType QueueFront(Queue *pq) …

uni-app获取手机号-获取用户地理位置-根据位置获取经纬度跳转高德

一.获取手机号 1.获取手机号首先要先登录拿到code,用code去获取session_key 2.获取 code需要知道自己的AppID(小程序ID)和AppSecret(小程序密钥) 3.解密后得到手机号 登录微信公众平台拿到自己的AppID(小程序ID)和AppSecret(小程序密钥) 微信公众平台 获取sessio…

【Java EE 初阶】如何保证线程安全二

目录 1.线程不安全是什么? 2.线程不安全的成因 3.解决线程不安全之一Synchronized关键字(监视器锁) 1.Synchronized使用方法 2.锁对象是什么? ​3.锁对象的练习 4.Synchronized的特性 1.互斥性 2.刷新内存 3.可重入 5.总…

事务及分布式事务解决方案

基础概念 1.1.事务 事务可以看做是一次大的活动,它由不同的小活动组成,这些活动要么全部成功,要么全部失败。 1.2.本地事务 在计算机系统中,更多的是通过关系型数据库来控制事务,利用数据库本身的事务特性来实现&a…

SAP 从入门到放弃系列之批次追溯功能

首先执行MB57,建立批次追溯关系,并存储在CHVW表。根据情况选择要追溯的期间,在过账日期范围内填写。 不勾选‘基于清单显示’,为ALV显示结果 勾选‘基于清单显示’,为清单显示结果 执行MB56,查询批次追溯 可以设置显示…

【.NET AI Books 前言】Azure OpenAI Service 入门

本书是为 .NET 开发者而写的,让 .NET 开发者能快速掌握 Azure OpenAI Service 的使用技巧。 ChatGPT 的到来意味着我们已经置身于 AI 引起的全新变革中,作为开发者你可能将面临几种改变: GPT 模型到来后,如何去架构好企业解决方案…

Dex-Net 2.0<论文>

题目:Deep Learning to Plan Robust Grasps with Synthetic Point Clouds and Analytic Grasp 引言 传统抓取方法的局限性 缺乏泛化能力需要大量计算资源和手工标注数据【前两种依赖物体形状、材料、质量等先验知识通常要对物体建模姿态评估运动学分析】只能处理…

【C语言】深入理解注释

文章目录 一. 预处理阶段对注释的处理二. 注释使用时的注意事项1. C风格的注释无法嵌套使用2. 基本注释注意事项3. 注释导致的二义性 四. 关于注释的一个使用建议 一. 预处理阶段对注释的处理 我们知道一个源文件要变成可执行程序的话,首先要经过预处理&#xff0c…

Vtk7.1.1+PCL1.12.0安装

错误可参考:Ubuntu20.04 编译 pcl1.8可能出现的问题 安装参考1:ubuntu20.04下安装pcl_ubuntu安装pcl_Yuannau_jk的博客-CSDN博客 安装参考2:Ubuntu20.04 安装pcl详细教程_ubuntu20.04安装pcl_LYiiiiiii的博客-CSDN博客 安装参考3&#xff1a…

涨知识!你不知道的中国手机号码的编码和划分规则

引言 在当今信息化的时代,移动电话号码已经成为人们日常生活中必不可少的联系方式。中国作为世界上拥有庞大人口数量的国家之一,移动电话号码的编码和划分显得尤为重要。 中国的移动电话号码分为三大运营商,每个运营商又有自己的号码段&…

Spring Security OAuth2.0(三)-----基于Redis存储和JDBC存储

问题 令牌往哪里存? 客户端信息入库 第三方应用优化 1.令牌往哪里存? 在我们配置授权码模式的时候,有两个东西当时存在了内存中: InMemoryAuthorizationCodeServices 这个表授权码存在内存中。InMemoryTokenStore 表示生成的令…