Android 处理音频焦点,解决音乐播放冲突的问题

1. 音频焦点是什么

在Android中,两个或多个 Android 应用可以同时将音频播放到同一输出流,系统会将所有音频混合在一起。
但是多数情况下,这对于用户来说是感到困惑的。为了避免多个应用的多个音频一起播放,Android 引入了“音频焦点”的概念,这样能够实现一次只有一个应用获得音频焦点。

在启动逻辑声音流之前,应用应使用将用于其逻辑声音流的同一个音频属性来请求获得音频焦点。虽然我们建议发送此类焦点请求,但系统不会强制要求发送。有些应用可能会明确跳过发送请求的步骤,以实现特定行为(例如,在通话期间故意播放声音)。为此,应将焦点视为间接控制播放和消除播放冲突的一种方式。

2. 音频焦点在不同Android版本的表现

2.1 在 Android 12 之前

Android 12API 31)之前,音频焦点不由系统管理。因此,虽然我们鼓励应用开发者遵守音频焦点准则,但如果应用在搭载 Android 11API 30)或更低版本的设备上丢失音频焦点后仍继续大声播放,系统将无法阻止此类播放。但是,此应用行为会导致糟糕的用户体验,并且常常会导致用户卸载出现异常的应用。

2.2 在 Android 12 及之后

音频焦点最终由系统管理和控制,而不是由应用开发者直接控制。
当其他应用请求音频焦点时,系统会强制使某个应用的音频播放淡出。收到来电时,系统会将音频播放静音。

2.3 交互矩阵

下表罗列了传入焦点请求的 CarAudioContext(列)与现有焦点持有者的上下文(行)之间的焦点交互。每个单元格表示两种上下文的预期交互类型,其中:

  • R 代表拒绝交互
  • E 代表独占交互
  • C 代表并发交互
    在这里插入图片描述

这部分具体详见 音频焦点 | Android Open Source Project (google.cn)

3. 如何实现音频焦点

Android8.0之前可以通过requestAudioFocus()来请求音频焦点。
Android8.0之后引入了新的音频焦点的API,目前Android8.0以前的机型已经很少了,可以不考虑老API
而是直接使用新的音频焦点的API,所以我们这里直接来介绍新的音频焦点API

3.1 请求音频焦点

官方推荐我们先请求音频焦点后,再进行播放音频等操作。

mAudioAttributes = AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA).setContentType(AudioAttributes.CONTENT_TYPE_MUSIC).build()
mFocusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN).setAudioAttributes(mAudioAttributes!!).setWillPauseWhenDucked(false).setOnAudioFocusChangeListener(this).build()
val result : Int = mAudioManager.requestAudioFocus(mFocusRequest!!)
if(result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED){//执行播放音频等自己的操作
}

这里有一个关注点

  • AudioAttributes : 描述的是应用的用例
    • setUsage : 设置描述音频信号的预期用途的属性。
    • setContentType:设置描述音频信号的内容类型的属性。
  • AudioFocusRequest :
    • 构造方法传参 : 它的值和 Android 8.0 之前的 requestAudioFocus() 中使用的 durationHint值相同:AUDIOFOCUS_GAIN、AUDIOFOCUS_GAIN_TRANSIENT、AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK 或 AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE
    • setWillPauseWhenDucked : 当其他应用使用 AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK 请求获得焦点时,获得焦点的应用通常不会收到 onAudioFocusChange() 回调,因为系统可以自行降低音量。如果需要暂停播放而不是降低音量,需要调用 setWillPauseWhenDucked(true) 并创建并设置 OnAudioFocusChangeListener,如自动降低音量中所述。
    • setOnAudioFocusChangeListener : 音频焦点回调监听

3.2 释放音频焦点

释放音频焦点后,就不会回调OnAudioFocusChangeListener了。
一般可以在完成音频播放、销毁当前页面的时候,来释放音频焦点。

mAudioManager.abandonAudioFocusRequest(mFocusRequest)   

4. 封装代码

我们可以对上述代码进行封装

4.1 音频焦点封装

class AudioFocusManager(context: Context) : OnAudioFocusChangeListener {private val mAudioManager: AudioManagerprivate var mFocusRequest: AudioFocusRequest? = nullprivate var mAudioAttributes: AudioAttributes? = nullprivate var mAudioFocusChangeListener: OnAudioFocusChangeListener? = nullinit {mAudioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager}data class RequestParams(var usage: Int,var contentType: Int,var focusGain: Int,/**  默认为false,如果设为false,onAudioFocusChange应该返回-3时,不会回调onAudioFocusChange,*  而是系统自己已经处理了,系统会自动将音量调低,不用再由我们代码中额外处理*  如果设为true,则需要自己在onAudioFocusChange里单独处理-3的逻辑*/var willPauseWhenDucked: Boolean = false) {companion object {/** AudioManager.AUDIOFOCUS_LOSS : 永久失去焦点,应该停止音乐的播放* AudioManager.AUDIOFOCUS_LOSS_TRANSIENT : 短暂失去焦点,应该暂停音乐的播放* AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK : 短暂失去焦点,应该降低音量继续播放*///播放音乐场景 : 其他应用OnAudioFocusChangeListener会回调AudioManager.AUDIOFOCUS_LOSSval REQUEST_MUSIC = RequestParams(AudioAttributes.USAGE_MEDIA,AudioAttributes.CONTENT_TYPE_MUSIC,AudioManager.AUDIOFOCUS_GAIN)//语音识别场景 : 其他应用OnAudioFocusChangeListener会回调AudioManager.AUDIOFOCUS_LOSS_TRANSIENTval REQUEST_SPEAK = RequestParams(AudioAttributes.USAGE_VOICE_COMMUNICATION,AudioAttributes.CONTENT_TYPE_SPEECH,AudioManager.AUDIOFOCUS_GAIN_TRANSIENT)//导航场景 : 其他应用OnAudioFocusChangeListener会回调AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCKval REQUEST_NAVIGATION = RequestParams(AudioAttributes.USAGE_NOTIFICATION,AudioAttributes.CONTENT_TYPE_SONIFICATION,AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK)}}/*** Request audio focus.*/fun requestFocus(requestParams: RequestParams): Int {/*if (mAudioAttributes == null) {}if (mFocusRequest == null) {}*/mAudioAttributes = AudioAttributes.Builder().setUsage(requestParams.usage).setContentType(requestParams.contentType).build()mFocusRequest = AudioFocusRequest.Builder(requestParams.focusGain).setAudioAttributes(mAudioAttributes!!)// 默认为false,如果设为false,onAudioFocusChange应该返回-3时,不会回调onAudioFocusChange,// 而是系统自己已经处理了,系统会自动将音量调低,不用再由我们代码中额外处理// 如果设为true,则需要自己在onAudioFocusChange里单独处理-3的逻辑.setWillPauseWhenDucked(requestParams.willPauseWhenDucked).setOnAudioFocusChangeListener(this).build()return mAudioManager.requestAudioFocus(mFocusRequest!!)}override fun onAudioFocusChange(focusChange: Int) {if (mAudioFocusChangeListener != null) {mAudioFocusChangeListener!!.onAudioFocusChange(focusChange)}}/*** Release audio focus.*/fun releaseAudioFocus() {if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {if (mFocusRequest != null) {mAudioManager.abandonAudioFocusRequest(mFocusRequest!!)mFocusRequest = null}} else {mAudioManager.abandonAudioFocus(this)}}/*** Same as AudioManager.OnAudioFocusChangeListener.*/interface OnAudioFocusChangeListener {fun onAudioFocusChange(focusChange: Int)}fun setOnAudioFocusChangeListener(listener: OnAudioFocusChangeListener?) {mAudioFocusChangeListener = listener}
}

4.2 音乐播放封装

这里附带上音乐播放的封装

class MusicManger {private var mediaPlayer: MediaPlayer? = nullfun play(context: Context, loop: Boolean, musicResId: Int) {if (mediaPlayer == null) {mediaPlayer = MediaPlayer.create(context, musicResId)mediaPlayer?.isLooping = loopmediaPlayer?.start()} else {mediaPlayer?.reset()mediaPlayer?.isLooping = loopval afd = context.resources.openRawResourceFd(musicResId)mediaPlayer?.setDataSource(afd)mediaPlayer?.prepare()mediaPlayer?.start()}}fun play() {if (mediaPlayer?.isPlaying == false){mediaPlayer?.start()}}fun pause() {if (mediaPlayer?.isPlaying == true) {mediaPlayer?.pause()}}fun stop() {mediaPlayer?.stop()}
}

4.3 进行调用

当创建的时候

val audioFocusFlowManager = AudioFocusManager()override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)binding = ActivityMainBinding.inflate(layoutInflater)setContentView(binding.root)audioFocusManager = AudioFocusManager(this)audioFocusManager.setOnAudioFocusChangeListener(object :AudioFocusManager.OnAudioFocusChangeListener {override fun onAudioFocusChange(focusChange: Int) {Log.i(TAG, "onAudioFocusChange:$focusChange")if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT) { //-2//短暂失去焦点,应该暂停音乐的播放Log.i(TAG, "onAudioFocusChange:短暂失去焦点,应该暂停音乐的播放")musicManger.pause()} else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) { //-3//短暂失去焦点,应该降低音量继续播放Log.i(TAG, "onAudioFocusChange:短暂失去焦点,应该降低音量继续播放,或者根据自己的业务逻辑,可以停止播放")//musicManger.pause()//由于设置了.setWillPauseWhenDucked(false),系统自己处理了,这个分支不会被回调//如果想要被回调,那么需设置.setWillPauseWhenDucked(true)} else if (focusChange == AudioManager.AUDIOFOCUS_LOSS) { //-1//永久失去焦点,应该停止音乐的播放musicManger.stop()Log.i(TAG, "onAudioFocusChange:永久失去焦点,应该停止音乐的播放")} else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) { //1Log.i(TAG, "onAudioFocusChange:重新获得焦点")musicManger.play()}}})
}

请求焦点,这里我已经封装了常用的三个场景

  • REQUEST_MUSIC : 播放音乐场景 : 请求焦点后,其他应用OnAudioFocusChangeListener会回调AudioManager.AUDIOFOCUS_LOSS
  • REQUEST_SPEAK : 语音识别场景 : 请求焦点后,其他应用OnAudioFocusChangeListener会回调AudioManager.AUDIOFOCUS_LOSS_TRANSIENT
  • REQUEST_NAVIGATION : 导航场景 : 请求焦点后,其他应用OnAudioFocusChangeListener会回调AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK
//请求焦点
val requestParams = AudioFocusManager.RequestParams.REQUEST_MUSIC
/*默认为false,如果设为false,onAudioFocusChange应该返回-3时,不会回调onAudioFocusChange,
而是系统自己已经处理了,系统会自动将音量调低,不用再由我们代码中额外处理
如果设为true,则需要自己在onAudioFocusChange里单独处理-3的逻辑*/
requestParams.willPauseWhenDucked = false
val result = audioFocusManager.requestFocus(requestParams)
if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {Toast.makeText(this, "获取了焦点", Toast.LENGTH_SHORT).show()musicManger.play(this@MainActivity, true, R.raw.music1)
}

当销毁的时候

override fun onDestroy() {super.onDestroy()//暂停播放音乐musicManger.stop()//释放音频焦点audioFocusManager.releaseAudioFocus()
}

5. 其他

参考文章

音频焦点
管理音频焦点
Android音频焦点处理

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

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

相关文章

【百度智能体】零代码创建你的 AI 宠物助手

前言 今天给大家介绍一下百度的 AI 产品 – 百度智能体,在文心智能体平台你可以0代码就可以创建出属于自己的 AI 机器人,几乎可以选择任何你想要的领域或者行业机器人,进行无代码打造自己的对话助手,本文将介绍文心智能体&#x…

jmeter之MD5加密接口请求教程

前言: 有时候在项目中,需要使用MD5加密的方法才可以登录,或者在某一个接口中遇到 登录获取token后才可以进行关联,下面介绍下遇到的常见使用 一、第一种方法:使用jmeter自带的函数助手digest 选择工具,选择…

高效的大型语言模型适应方法:提升基础性的解决方案

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗?订阅我们的简报,深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同,从行业内部的深度分析和实用指南中受益。不要错过这个机会,成为AI领…

表查询基础【mysql】【表内容 增,删,改,查询】

博客主页:花果山~程序猿-CSDN博客 文章分栏:Linux_花果山~程序猿的博客-CSDN博客MySQL之旅_花果山~程序猿的博客-CSDN博客Linux_花果山~程序猿的博客-CSDN博客 关注我一起学习,一起进步,一起探索编程的无限可能吧!让我…

Redis(十三) 事务

文章目录 前言事务的特性Redis事务的执行原理Redis中使用事务WATCH UNWATCH实现乐观锁 前言 前面我们学习 MySQL 的时候,肯定也学习了事务。事务是什么?给大家举个例子:假如我给朋友微信转账,我给他转了 100 块钱,当我…

Golang | Leetcode Golang题解之第114题二叉树展开为链表

题目: 题解: func flatten(root *TreeNode) {curr : rootfor curr ! nil {if curr.Left ! nil {next : curr.Leftpredecessor : nextfor predecessor.Right ! nil {predecessor predecessor.Right}predecessor.Right curr.Rightcurr.Left, curr.Righ…

Vue3实战笔记(46)—Vue 3高效开发定制化Dashboard的权威手册

文章目录 前言Dashboard开发总结 前言 后台管理系统中的Dashboard是一种图形化的信息显示工具,通常用于提供一个特定领域或系统的概况。它可以帮助用户监控和分析数据,快速获取重要信息。可以帮助用户监控业务状况、分析数据、获取关键信息和管理资源。…

list的模拟实现(一)

嗨喽大家好,时隔许久阿鑫又给大家带来了新的博客,list的模拟实现(一),下面让我们开始今天的学习吧! list的模拟实现(一) 1.list splice接口的使用 2.list尾插的实现 3.list的迭代…

Python编程的黑暗魔法:模块与包的神秘力量!

哈喽,我是阿佑,今天给大家讲讲模块与包~ 文章目录 1. 引言1.1 模块化编程的意义1.2 Python中模块与包的概念概述 2. 背景介绍2.1 Python模块系统模块的定义与作用Python标准库简介 2.2 包的结构与目的包的定义与目录结构包在项目组织中的重要性 3. 创建与…

【C语言】strstr函数的使用和模拟

前言 今天给大家带来一个字符串函数,strstr()的使用介绍和模拟实现。 模拟实现这个函数,可以帮助我们更深刻地理解这个函数的功能和提高解决字符串相关问题的能力,有兴趣的话就请往下看吧。 strstr函数介绍 函数功能: strstr函…

Three.js 研究:3、创建一个高科技圆环

打开Alpha混合 修改环形颜色,更改发光的颜色,更改发光的强度为2 更改世界环境灯光

如何编辑 PDF 中的文本?4个有效的编辑PDF方法

PDF 文件可以轻松打开和查看,但修改要复杂得多 - 尤其是在 PDF 中的文本编辑方面。 知道如何离线编辑 PDF 中的文本对于任何需要快速更改而无需在线加载文档或担心安全问题的人来说都非常有益。它使用户能够更好地控制他们的文档,并有更广泛的字体和图形…

着急联系媒体投稿发表文章有什么好方法?

作为一名曾经的信息宣传员,我深知在紧张的宣传节点上,急于将精心撰写的文章推向更广阔的读者群体,那种紧迫感和焦虑几乎成了常态。记得那段时间,为了能让稿件得到及时有效的曝光,我不得不亲自踏上了一场寻找媒体联系方式的“马拉松”。那时,我手头的资源有限,仅有的几个联系方式…

JRT1.7发布

JRT1.7连仪器在线演示视频 JRT1.5实现质控主体、1.6基本完成质控;本次版本推进到1.7,1.7集菜单权限、登录、打印导出客户端、初始化、质控、Linux客户端、仪器连接和监控体系各种功能大全,上十年写系统用到的都全了。 这次直接挑战检验最难…

就业信息|基于SprinBoot+vue的就业信息管理系统(源码+数据库+文档)

就业信息管理系统 目录 基于SprinBootvue的就业信息管理系统 一、前言 二、系统设计 三、系统功能设计 1前台功能模块 2后台功能模块 4.2.1管理员功能 4.2.2学生功能 4.2.3企业功能 4.2.4导师功能 四、数据库设计 五、核心代码 六、论文参考 七、最新计算机毕设…

MySql基础(一)--最详细基础入门,看完就懂啦(辛苦整理,想要宝宝的赞和关注嘻嘻)

前言 希望你向太阳一样,有起有落,不失光彩~ 一、数据库概述 1. 什么是数据库 数据库就是存储数据的仓库,其本质是一个文件系统,数据按照特定的格式将数据存储起来,用户可以对数据库中的数据进行增加,修改&…

大白话DC3算法

DC3算法是什么 DC3算法(也称为Skew算法)是一种高效的构建后缀数组的算法,全称为Difference Cover Modulo 3算法。 该算法于2002年被提出,论文参考: https://www.cs.cmu.edu/~guyb/paralg/papers/KarkkainenSanders0…

Vue3:封装Table 表格组件

组件官网 elementPlus : 点击跳转 封装组件 创建新的组件文件: Table.vue <!-- PropTableS &#xff1a; 父组件传递过来的数据 (对象)PropTableS.tables : 父组件传递的对象中 存放表格每行显示的数据PropTableS.keyS &#xff1a; 父组件传递过来的对象&#xff0c;里…

docker搭建私有仓库并推送本地镜像

1、私仓搭建 docker pull registry#拉取镜像 docker images#查看镜像 mkdir -p /czx/myregistry 创建挂载目录 运行私有库registry (相当于本地有个是有docker hub) docker run -d -p 5000:5000 -v /czx/myregistry/:/tmp/registry --restartalways --privilegedtrue regist…

STM32 USART的字符编码(发送器的实现逻辑)

目录 概述 1 字符编码 1.1 USART 字符说明 1.2 字长编程 2 发送器 2.1 字符发送 2.2 可配置的停止位 2.3 配置停止位方法 3 单字节通信 4 中断字符 5 空闲字符 概述 本文主要讲述STM32 USART的发送端功能实现的原理&#xff0c;包括字节编码长度&#xff0c;发送器…