Bootstrap

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: AudioManager
    private var mFocusRequest: AudioFocusRequest? = null
    private var mAudioAttributes: AudioAttributes? = null
    private var mAudioFocusChangeListener: OnAudioFocusChangeListener? = null

    init {
        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_LOSS
            val REQUEST_MUSIC = RequestParams(
                AudioAttributes.USAGE_MEDIA,
                AudioAttributes.CONTENT_TYPE_MUSIC,
                AudioManager.AUDIOFOCUS_GAIN
            )

            //语音识别场景 : 其他应用OnAudioFocusChangeListener会回调AudioManager.AUDIOFOCUS_LOSS_TRANSIENT
            val REQUEST_SPEAK = RequestParams(
                AudioAttributes.USAGE_VOICE_COMMUNICATION,
                AudioAttributes.CONTENT_TYPE_SPEECH,
                AudioManager.AUDIOFOCUS_GAIN_TRANSIENT
            )

            //导航场景 : 其他应用OnAudioFocusChangeListener会回调AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK
            val 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? = null

    fun play(context: Context, loop: Boolean, musicResId: Int) {
        if (mediaPlayer == null) {
            mediaPlayer = MediaPlayer.create(context, musicResId)
            mediaPlayer?.isLooping = loop
            mediaPlayer?.start()
        } else {
            mediaPlayer?.reset()
            mediaPlayer?.isLooping = loop
            val 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) { //1
                Log.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音频焦点处理

;