06.Android音频焦点

音频音频焦点 (audio focus)

前言

Android 是多任务系统,Audio 系统是竞争资源。在 Android 系统中允许多个应用同时播放音频,例如,我们在播放音乐的时候,点开了一个视频,如果音乐和视频声音混合在一起,这样就会让人不爽;如果,我们在听音乐的时候,收到信息,我们又希望能听到信息的提示音,可以让音乐的声音先降低,在结束音结束后再恢复音量。为了管理音频焦点,Android 引入音频焦点 (audio focus) 这一特性,旨在保证同一时段内只有一个应用可以维持音频聚焦。

音频焦点的良好协作性,主要依赖于应用程序是否遵循音频焦点指南,操作系统没有强制执行音频焦点的规范来约束应用程序,如果应用选择在失去音频焦点后继续大声播放音频,会带来不良的用户体验,可能直接导致应户卸载应用,但这是无法阻止的行为,只能靠开发者自我约束。

常见的音频焦点用例

当你的应用需要播放声音的时候,应该先请求音频聚焦,在获得音频焦点后再播放声音。

用户在使用你的应用播放音频 1 时,打开另一个应用并尝试播放该应用相关的音频 2

  1. 你的应用不处理音频焦点的情况下
    您的音频 1 和另一个应用的音频 2 会重叠播放,用户无法正常听到来自任何应用的音频,这样的用户体验很不友好。
  2. 您的应用处理了音频焦点的情况下
    在另一个应用需要播放音频时,它会请求音频焦点常驻,即音频永久聚焦。一旦系统授权,它便会开始播放音频,这时候您的应用需要响应音频焦点的丢失通知,停止播放。这样用户就只会听到另一个应用的音频。

同样的道理,假如过了五分钟,您的应用需要播放音频,您同样需要申请音频焦点,一旦获得系统授权,我们就可以开始播放音频,其它应用响应音频焦点丢失通知,停止播放。

当您播放音频时候,正好手机来电,需要播放响铃

  1. 您的应用不处理音频焦点的情况下
    手机响铃后,用户会听到铃声和您的手机音频叠加在一起播放。如果用户选择直接挂断电话,您的音频会保持播放。如果用户选择接通电话,他会听到通话声音和您的应用音频叠加在一起播放,挂断通话后您的应用音频会保持播放。无论如何,您的应用音频将全程保持播放状态。这带来的通话体验极差。
  2. 您的应用处理了音频焦点的情况下
    当手机响铃(您还未接通电话),您的应用应该选择相应的回避(这是系统应用的要求)措施来响应短暂的音频焦点丢失。回避的措施可以是把应用的音量降低到百分之二十,也可以是直接暂停播放(如果您的应用是播客类,语音类应用)。

[你的 App 是音乐类] 当后台运行的导航程序正在播报转向语音的时候,你的应用正在播放音乐

  1. 您的应用不处理音频焦点的情况下
    导航语音和音乐混在一起播放将会使用户分心。
  2. 您的应用处理了音频焦点的情况下
    当导航开始播报语音的时候,您的应用需要响应音频焦点丢失,选择回避模式,降低声音。

这里所说的回避模式,没有约束规定,建议您做到把音量调节到百分之二十。有一些特殊的情况,如果应用是有声读物,播客或口语类应用,建议暂停声音播放。

当语音播报完,导航应用会释放掉音频焦点,您的应用可以再次获得音频聚焦,然后恢复到原有音量播放(选择降低音量的回避模式时),或者恢复播放(选择暂停的回避模式时)。

[你的 App 是游戏] 用户在打电话的时候启动游戏(游戏播放音频)

  1. 您的应用不处理音频焦点的情况下
    通话声音和游戏声音的重叠播放同样会让用户的体验非常糟糕。
  2. 您的应用处理了音频焦点的情况下
    在 Android O 中,有一个应对诸如本用例的音频焦点的功能,叫做延迟音频聚焦

假如当用户在通话中打开游戏,他们想玩游戏,不想听到游戏声音。但是当他们通话结束的时候他们想听到游戏声音(通话应用暂时持有音频焦点)。如果您的应用支持延迟音频聚焦,会发生如下情况:

目前低于 Android O 的版本是不支持延迟音频聚焦这个功能的,所以本用例在其它版本下,应用并不会延迟获得音频焦点。

[后台生成语音的 App] 导航应用或其它能生成音频通知的应用程序

如果您正在开发一款能够在短时间内以突发的方式生成音频的应用程序,类似的应用程序功能如:生成通知声音,提醒声音或一次又一次地在后台生成口语播放的应用程序。

假设您的应用正在后台运行,并且即将生成一些音频。 用户正在收听音乐或播客,而您的应用正好在短时间内生成音频:

在您的应用程序生成音频之前,它应该请求短暂的音频焦点。只有当它被授予焦点时,才能播放音频。优秀的应用程序应该遵守音频焦点的短暂丢失选择降低音量,如果抢占音频焦点的应用程序是播客应用程序,则您可以考虑暂停,直到重新获得音频焦点以恢复播放为止。未能正确请求音频焦点将导致用户同时听到音乐(或播客)和您的应用音频。

[录音 App] 录音应用程序或语音识别应用程序

如果您正在开发一款需要在一段时间内录制音频的应用程序,在这段时间内系统或其他应用程序不应该发出任何声音(通知或其他媒体播放)。

您的应用请求获得的音频焦点,如果是来自于系统授权的,那么便可以安心地开始录制,因为系统了解并确保手机在此期间可能生成或存在的其它音频不会干扰到您的录制。在此期间,来自于其它应用的音频焦点申请都会被系统拒绝。当录制完成记得释放音频焦点,以便系统授权其它应用正常播放声音。

音频焦点处理

处理音频焦点一些规则

  1. 在开始播放之前,调用 requestAudioFocus() 方法,并检查返回值是否是 AUDIOFOCUS_REQUEST_GRANTED,若成功获取,则开始播放。
  2. 当 App 失去音频焦点时,根据失去的焦点类型,应该暂停播放,或者将音量调低 (Android8.0,AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK 自动降低音量)。
  3. 当播放结束时,释放音频焦点。

请求音频焦点

只是发出请求,并非直接获取。为了申请到音频聚焦,您必须向系统描述好您的意图。介绍四个常见音频焦点类型:

请求音频焦点类型

AudioManager.AUDIOFOCUS_GAIN 请求长时间音频聚焦

请求的这类音频焦点持续时间是未知的,通常用来表示长时间获取焦点。例如:播放音乐、多媒体播放或者播客等应用。

AudioManager.AUDIOFOCUS_GAIN_TRANSIENT 临时需要音频焦点

请求的音频焦点持续时间比较短,通常用来播放导航路线的声音,或者播放通知声音。例如:电话、QQ、微信等通话应用。

AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK 临时需要音频焦点

这个也是表明请求的音频焦点持续时间比较短,但是在这段时间内,它希望其他应用以较低音量继续播放。例如:闹铃,导航 (我们在使用导航的时候可以听音乐,当出现导航语音的时候,音乐音量会降低以便我们能听清楚导航的语音,当导航语音播放完毕后,音乐恢复音量,继续播放) 等应用。

AudioManager.UDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE  临时需要音频焦点,不需要其他 App 获取焦点

这个也是表明音请求的音频焦点持续时间比较短,但是在这段时间内,不希望任何应用 (包括系统应用) 来做任何与音频相关的事情,就算是降低音量播放音乐也是不被希望的。例如当我们进行录音或者语音识别的时候,我们不希望其他的声音出现干扰。

请求音频焦点代码

Android 8.0(API 级别 26) 以及更高的版本

从 Android 8.0(API 级别 26)开始,当您调用 requestAudioFocus() 时,必须提供 AudioFocusRequest 参数。要释放音频焦点,请调用 abandonAudioFocusRequest() 方法,该方法也接受 AudioFocusRequest 作为参数。在请求和放弃焦点时,应使用相同的 AudioFocusRequest 实例。

要创建 AudioFocusRequest,请使用 AudioFocusRequest.Builder。由于焦点请求始终必须指定请求的类型,因此此类型会包含在构建器的构造函数中。使用构建器的方法来设置请求的其他字段。

FocusGain 字段为必需字段;所有其他字段均为可选字段:

方法 作用
setFocusGain(int focusGain) 每个请求中都必须包含此字段。此字段的值与 Android 8.0 之前的 requestAudioFocus() 调用中所使用的 durationHint 值相同:AUDIOFOCUS_GAIN
AUDIOFOCUS_GAIN_TRANSIENT
AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE
setAudioAttributes(AudioAttributes) 这个方法是用来描述 app 的使用情况。这方法需要传入一个 AudioAttributes 对象,这个对象也是使用 Builder 模式来构造,例如使用 AudioAttributes.Builder.setUsage() 来描述使用这个音频来干什么,我们可以传入一个 AudioAttributes.USAGE_MEDIA
来表明用这个音频来作为媒体文件来播放,也可以传入一个 AudioAttributes.USAGE_ALARM
来表明用这个来作为闹铃。
setWillPauseWhenDucked(boolean pauseOnDuck) 当其他应用使用 AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK 请求焦点时,持有焦点的应用通常不会收到 onAudioFocusChange() 回调,因为系统可以自行降低音量。如果您需要暂停播放而不是降低音量,请调用 setWillPauseWhenDucked(true),然后创建并设置 OnAudioFocusChangeListener,具体如自动降低音量中所述。
setAcceptsDelayedFocusGain(boolean acceptsDelayedFocusGain) 当焦点被其他应用锁定时,对音频焦点的请求可能会失败。此方法可实现延迟获取焦点,即在焦点可用时异步获取焦点。请注意,要使 " 延迟获取焦点 " 起作用,您还必须在音频请求中指定 AudioManager.OnAudioFocusChangeListener,因为您的应用必须收到回调才能知道自己获取了焦点。
setOnAudioFocusChangeListener(OnAudioFocusChangeListener) 只有当您在请求中还指定了 willPauseWhenDucked(true) 或 setAcceptsDelayedFocusGain(true) 时,才需要 OnAudioFocusChangeListener。有两个方法可以设置监听器:一个带处理程序参数,一个不带。处理程序是运行监听器的线程。如果您未指定处理程序,则会使用与主 Looper 关联的处理程序。

示例代码:

AudioManager mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
AudioAttributes mAudioAttributes =
       new AudioAttributes.Builder()
               .setUsage(AudioAttributes.USAGE_MEDIA)
               .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
               .build();
AudioFocusRequest mAudioFocusRequest =
       new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
               .setAudioAttributes(mAudioAttributes)
               .setAcceptsDelayedFocusGain(true)
               .setOnAudioFocusChangeListener(...) // Need to implement listener
               .build();
int focusRequest = mAudioManager.requestAudioFocus(mAudioFocusRequest);
switch (focusRequest) {
   case AudioManager.AUDIOFOCUS_REQUEST_FAILED:
       // 不允许播放
   case AudioManager.AUDIOFOCUS_REQUEST_GRANTED:
       // 开始播放
}
延迟获取焦点

在 Android 8.0 之前,当我们请求音频焦点的时候,只会返回两种结果,要么请求成功 (AUDIOFOCUS_REQUEST_GRANTED),要么请求失败 (AUDIOFOCUS_REQUEST_FAILED)。

而从 Android 8.0 开始,还有一种结果,延迟成功请求 (AUDIOFOCUS_REQUEST_DELAYED),这个也是成功的请求,但是这个请求具有延迟性。例如当我们处于通话状态的时候,我们很显然不希望任何 app 来获取到音频焦点来做些事,例如播放音乐。

然而只有设置过 AudioFocusRequest.Builder.setAcceptsDelayedFocusGain(true) 才能获取到这种结果。获取到了音频焦点呢,当然还需要设置 AudioManager.OnAudioFocusChangeListener 这个音频焦点变化的监听器,通过回调确认何时获取到了音频焦点。

public void requestPlayback() {
    int audioFocus = mAudioManager.requestAudioFocus(mAudioFocusRequest);
    switch (audioFocus) {
        case AudioManager.AUDIOFOCUS_REQUEST_FAILED:
            ...
        case AudioManager.AUDIOFOCUS_REQUEST_GRANTED:
            ...
        case AudioManager.AUDIOFOCUS_REQUEST_DELAYED:
            mAudioFocusPlaybackDelayed = true;
    }
}
// 在您 OnAudioFocusChangeListener 的实现,您需要检查 mAudioFocusPlaybackDelayed 这个变量,当您响应 AUDIOFOCUS_GAIN 音频聚焦的时候, 如下所示:
private void onAudioFocusChange(int focusChange) {
   switch (focusChange) {
       case AudioManager.AUDIOFOCUS_GAIN:
           logToUI("Audio Focus: Gained");
           if (mAudioFocusPlaybackDelayed || mAudioFocusResumeOnFocusGained) {
               mAudioFocusPlaybackDelayed = false;
               mAudioFocusResumeOnFocusGained = false;
               start();
           }
           break;
       case AudioManager.AUDIOFOCUS_LOSS:
           mAudioFocusResumeOnFocusGained = false;
           mAudioFocusPlaybackDelayed = false;
           stop();
           break;
       case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
           mAudioFocusResumeOnFocusGained = true;
           mAudioFocusPlaybackDelayed = false;
           pause();
           break;
       case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
           pause();
           break;
   }
}
Android 8.0(API 级别 26) 之前

Android O 以下,不需要用到 AudioFocusRequest

// AudioManager
public int requestAudioFocus(OnAudioFocusChangeListener l, int streamType, int durationHint)
// Android将系统的声音分为以下几类常见的(未写全):
STREAM_ALARM:警告声
STREAM_MUSCI:音乐声,例如music等
STREAM_RING:铃声
STREAM_SYSTEM:系统声音 ,例如低电提示音,锁屏音等
STREAM_VOCIE_CALL:电话声音

示例代码:

AudioManager mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
int focusRequest = mAudioManager.requestAudioFocus(
       OnAudioFocusChangeListener, // Need to implement listener
       AudioManager.STREAM_MUSIC,
       AudioManager.AUDIOFOCUS_GAIN);
switch (focusRequest) {
   case AudioManager.AUDIOFOCUS_REQUEST_FAILED:
       // don't start playback
   case AudioManager.AUDIOFOCUS_REQUEST_GRANTED:
       // actually start playback
}

AudioManager.requestAudioFocus() 请求焦点结果值及延迟聚焦

AudioManager.AUDIOFOCUS_REQUEST_GRANTED 表明请求焦点成功

请求 audio focus change 成功

AudioManager.AUDIOFOCUS_REQUEST_FAILED 表明请求焦点失败

请求 audio focus change 失败

"Note: The system will not grant audio focus (AUDIOFOCUS_REQUEST_FAILED) if there is a phone call currently in process and the application will not receive AUDIOFOCUS_GAIN after the call ends."

AudioManager.AUDIOFOCUS_REQUEST_DELAYED 延迟聚焦

参考:AudioFocusRequest.Builder.setAcceptsDelayedFocusGain(boolean)

响应音频焦点的状态改变

一旦获得音频聚焦,您的应用要马上做出响应,因为它的状态可能在任何时间发生改变(丢失或重新获取),您可以实现 OnAudioFocusChangeListener 的来响应状态改变。

private final class AudioFocusHelper
        implements AudioManager.OnAudioFocusChangeListener {
private void abandonAudioFocus() {
        mAudioManager.abandonAudioFocus(this);
    }
@Override
    public void onAudioFocusChange(int focusChange) {
        switch (focusChange) {
            case AudioManager.AUDIOFOCUS_GAIN:
                if (mPlayOnAudioFocus && !isPlaying()) {
                    play();
                } else if (isPlaying()) {
                    setVolume(MEDIA_VOLUME_DEFAULT);
                }
                mPlayOnAudioFocus = false;
                break;
            case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
                setVolume(MEDIA_VOLUME_DUCK);
                break;
            case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
                if (isPlaying()) {
                    mPlayOnAudioFocus = true;
                    pause();
                }
                break;
            case AudioManager.AUDIOFOCUS_LOSS:
                mAudioManager.abandonAudioFocus(this);
                mPlayOnAudioFocus = false;
                stop();
                break;
        }
    }
}

应对焦点丢失

选择在 OnAudioFocusChangeListener 中暂停还是降低音量,取决于您应用的交互方式。在 Android O 上,会自动的帮您降低音量,所以您可以忽略 OnAudioFocusChangeListener 接口的 AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK 事件。在 Android O 以下的版本,您需要自己用代码实现。

自动降低音量

在 Android 8.0 之前,如果请求焦点使用了 AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK 参数,它表明希望拥有了音频焦点的其他应用降低音量来使用音频,然而并不是所有的应用都会这样做 (有可能开发者忘记了优化),因为这并不是系统强制的。 从 Android8.0 开始,这个降低音量的工作,就是系统默认行为了,可以说是一个良心的优化。

在 Android8.0 及以上,如果不希望系统自动降低音量,而是想自己控制,这个可以通过 AudioFocusRequest.Builder.setWillPauseWhenDucked(true) 方法取消系统的默认行为,然后通过监听音频焦点变化,来自己处理。

释放音频焦点

播放完音频,记得使用 AudioManager.abandonAudioFocus(…) 来释放掉音频焦点

public final void pause() {
   if (!mPlayOnAudioFocus) {
       mAudioFocusHelper.abandonAudioFocus();
   }
  onPause();
}

工具类

https://wrichikbasu.github.io/AudioFocusController/

public final class AudioFocusController implements AudioManager.OnAudioFocusChangeListener {

    private final Context context;
    private final boolean pauseWhenDucked;
    private final boolean pauseWhenNoisy;
    private final int streamType;
    private final int durationHint;

    private final OnAudioFocusChangeListener listener;
    private final AudioManager audioManager;
    private final AudioFocusRequest audioFocusRequest;

    private boolean focusAbandoned;
    private boolean volumeDucked;

    private final BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            if (intent.getAction().equals(AudioManager.ACTION_AUDIO_BECOMING_NOISY)) { // wired headset is unplugged
                listener.onPause();
                abandonFocus();
            }
        }
    };

    //-----------------------------------------------------------------------------------------------------------

    public interface OnAudioFocusChangeListener {

        /**
         * Duck the volume.
         * <p>
         * Will be called if and only if {@link Builder#setPauseWhenAudioIsNoisy(boolean)} is passed
         * {@code true}.
         * </p>
         */
        default void onDecreaseVolume() {
        }

        /**
         * Revive the volume to what it was before ducking.
         * <p>
         * Will be called if and only if {@link Builder#setPauseWhenAudioIsNoisy(boolean)} is passed
         * {@code true}.
         * </p>
         */
        default void onIncreaseVolume() {
        }

        /**
         * Pause the playback.
         */
        void onPause();

        /**
         * Resume/start the playback.
         */
        void onResume();

        /**
         * request focus failed.
         *
         * Note: The system will not grant audio focus (AUDIOFOCUS_REQUEST_FAILED) if there is a phone call currently in process and the application will not receive AUDIOFOCUS_GAIN after the call ends.
         */
        default void onRequestFocusFailed() {

        }

    }

    //------------------------------------------------------------------------------------------------------------

    /**
     * Builder class for {@link AudioFocusController} class objects.
     */
    public static final class Builder {

        private final Context context;
        private int usage;
        private int contentType;
        private boolean acceptsDelayedFocus;
        private boolean pauseWhenDucked;
        private OnAudioFocusChangeListener listener;
        private int stream;
        private int durationHint;
        private boolean pauseOnAudioNoisy;

        //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

        /**
         * @param context The {@link Context} that is asking for audio focus.
         */
        @SuppressLint("InlinedApi")
        public Builder(@NonNull Context context) {

            this.context = context;

            acceptsDelayedFocus = true;
            pauseWhenDucked = false;
            pauseOnAudioNoisy = false;

            listener = null;

            usage = AudioAttributes.USAGE_UNKNOWN; // Android21
            durationHint = AudioManager.AUDIOFOCUS_GAIN;
            contentType = AudioAttributes.CONTENT_TYPE_UNKNOWN; // Android21
            stream = AudioManager.USE_DEFAULT_STREAM_TYPE;
        }

        //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

        /**
         * Sets the attribute describing what is the intended use of the audio signal.
         *
         * @param usage one of {@link AudioAttributes#USAGE_UNKNOWN}, {@link
         *              AudioAttributes#USAGE_MEDIA}, {@link AudioAttributes#USAGE_VOICE_COMMUNICATION},
         *              {@link AudioAttributes#USAGE_VOICE_COMMUNICATION_SIGNALLING}, {@link
         *              AudioAttributes#USAGE_ALARM}, {@link AudioAttributes#USAGE_NOTIFICATION},
         *              {@link AudioAttributes#USAGE_NOTIFICATION_RINGTONE}, {@link
         *              AudioAttributes#USAGE_NOTIFICATION_COMMUNICATION_REQUEST}, {@link
         *              AudioAttributes#USAGE_NOTIFICATION_COMMUNICATION_INSTANT}, {@link
         *              AudioAttributes#USAGE_NOTIFICATION_COMMUNICATION_DELAYED}, {@link
         *              AudioAttributes#USAGE_NOTIFICATION_EVENT}, {@link AudioAttributes#USAGE_ASSISTANT},
         *              {@link AudioAttributes#USAGE_ASSISTANCE_ACCESSIBILITY}, {@link
         *              AudioAttributes#USAGE_ASSISTANCE_NAVIGATION_GUIDANCE}, {@link
         *              AudioAttributes#USAGE_ASSISTANCE_SONIFICATION}, {@link
         *              AudioAttributes#USAGE_GAME}.
         * @return The same Builder instance.
         */
        @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
        public Builder setUsage(int usage) {
            this.usage = usage;
            return this;
        }

        //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

        /**
         * Sets the attribute describing the content type of the audio signal, such as speech, or
         * music.
         *
         * @param contentType the content type values, one of {@link AudioAttributes#CONTENT_TYPE_MOVIE},
         *                    {@link AudioAttributes#CONTENT_TYPE_MUSIC}, {@link
         *                    AudioAttributes#CONTENT_TYPE_SONIFICATION}, {@link
         *                    AudioAttributes#CONTENT_TYPE_SPEECH}, {@link AudioAttributes#CONTENT_TYPE_UNKNOWN}.
         * @return the same Builder instance.
         */
        @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
        public Builder setContentType(int contentType) {
            this.contentType = contentType;
            return this;
        }

        //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

        /**
         * Sets whether the app will accept delayed focus gain. Default is {@code true}.
         *
         * @param acceptsDelayedFocus Whether the app accepts delayed focus gain.
         * @return The same Builder instance.
         */
        public Builder setAcceptsDelayedFocus(boolean acceptsDelayedFocus) {
            this.acceptsDelayedFocus = acceptsDelayedFocus;
            return this;
        }

        //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

        /**
         * Sets whether the audio will be paused instead of ducking when {@link
         * AudioManager#AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK} is received. Default is {@code false}.
         *
         * @param pauseWhenDucked Whether the audio will be paused instead of ducking.
         * @return The same Builder instance.
         */
        public Builder setPauseWhenDucked(boolean pauseWhenDucked) {
            this.pauseWhenDucked = pauseWhenDucked;
            return this;
        }

        //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

        /**
         * Sets the {@link OnAudioFocusChangeListener} that will receive callbacks.
         *
         * @param listener The {@link OnAudioFocusChangeListener} implementation that will receive
         *                 callbacks.
         * @return The same Builder instance.
         */
        public Builder setAudioFocusChangeListener(@NonNull OnAudioFocusChangeListener listener) {
            this.listener = listener;
            return this;
        }

        //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

        /**
         * Sets the audio stream for devices lower than Android Oreo.
         *
         * @param stream The stream that will be used for playing the audio. Should be one of {@link
         *               AudioManager#STREAM_ACCESSIBILITY}, {@link AudioManager#STREAM_ALARM},
         *               {@link AudioManager#STREAM_DTMF}, {@link AudioManager#STREAM_MUSIC}, {@link
         *               AudioManager#STREAM_NOTIFICATION}, {@link AudioManager#STREAM_RING}, {@link
         *               AudioManager#STREAM_SYSTEM} or {@link AudioManager#STREAM_VOICE_CALL}.
         * @return The same Builder instance.
         */
        public Builder setStream(int stream) {
            this.stream = stream;
            return this;
        }

        //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

        /**
         * Sets the duration for which the audio will be played.
         *
         * @param durationHint The duration hint, one of {@link AudioManager#AUDIOFOCUS_GAIN},
         *                     {@link AudioManager#AUDIOFOCUS_GAIN_TRANSIENT}, {@link
         *                     AudioManager#AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE} or {@link
         *                     AudioManager#AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK}.
         * @return The same Builder instance.
         */
        public Builder setDurationHint(int durationHint) {
            this.durationHint = durationHint;
            return this;
        }

        //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

        /**
         * Sets whether playback will be paused when audio becomes noisy. Default is {@code false}.
         * <p>
         * If this function is passed {@code true}, a context-registered broadcast receiver is
         * registered for {@link AudioManager#ACTION_AUDIO_BECOMING_NOISY}. When this broadcast is
         * received, {@link OnAudioFocusChangeListener#onPause()} will be called, and focus will be
         * abandoned.
         * </p>
         *
         * @param value Whether playback will be paused when audio becomes noisy.
         * @return The same Builder instance.
         */
        public Builder setPauseWhenAudioIsNoisy(boolean value) {
            this.pauseOnAudioNoisy = value;
            return this;
        }

        //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

        /**
         * Builds a new {@link AudioFocusController} instance combining all the information gathered
         * by this {@code Builder}'s configuration methods.
         * <p>
         * Throws {@link IllegalStateException} when the listener has not been set.
         * </p>
         *
         * @return the {@link AudioFocusController} instance qualified by all the properties set on
         * this {@code Builder}.
         */
        @NonNull
        public AudioFocusController build() {
            if (listener == null) {
                throw new IllegalStateException("Listener cannot be null.");
            }
            return new AudioFocusController(this);
        }

    }

    //------------------------------------------------------------------------------------------------------------

    private AudioFocusController(Builder builder) {

        context = builder.context;
        boolean acceptsDelayedFocus = builder.acceptsDelayedFocus;
        pauseWhenDucked = builder.pauseWhenDucked;
        pauseWhenNoisy = builder.pauseOnAudioNoisy;
        listener = builder.listener;
        int usage = builder.usage;
        int contentType = builder.contentType;
        streamType = builder.stream;
        durationHint = builder.durationHint;

        focusAbandoned = false;
        volumeDucked = false;

        audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            AudioAttributes attributes = new AudioAttributes.Builder()
                    .setUsage(usage)
                    .setContentType(contentType)
                    .build(); // >=Android21
            audioFocusRequest = new AudioFocusRequest.Builder(durationHint)
                    .setAudioAttributes(attributes)
                    .setWillPauseWhenDucked(pauseWhenDucked)
                    .setAcceptsDelayedFocusGain(acceptsDelayedFocus)
                    .setOnAudioFocusChangeListener(this)
                    .build(); // >=Android26
        } else {
            audioFocusRequest = null;
        }

    }

    //------------------------------------------------------------------------------------------------------------

    /**
     * Requests audio focus from the system.
     * <p>
     * This function should be called every time you want to start/resume playback. If the system
     * grants focus, you will get a call in {@link OnAudioFocusChangeListener#onResume()}.
     * </p>
     * <p>
     * If the system issues delayed focus, or rejects the request, then no callback will be issued.
     * However, once the system grants full focus after delayed focus has been issued, the {@link
     * OnAudioFocusChangeListener#onResume()} method will be called.
     * </p>
     */
    public void requestFocus() {

        int status;

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            status = audioManager.requestAudioFocus(audioFocusRequest);
        } else {
            status = audioManager.requestAudioFocus(this, streamType, durationHint);
        }

        if (status == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
            listener.onResume();
            registerReceiver();
            focusAbandoned = false;
            if (volumeDucked) {
                listener.onIncreaseVolume();
                volumeDucked = false;
            }
        } else if (status == AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
            listener.onRequestFocusFailed();
        }

    }

    //------------------------------------------------------------------------------------------------------------

    /**
     * Abandons audio focus.
     * <p>
     * Call this method every time you stop/pause playback. This will free audio focus for other
     * apps.
     * </p>
     */
    public void abandonFocus() {

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            audioManager.abandonAudioFocusRequest(audioFocusRequest);
        } else {
            audioManager.abandonAudioFocus(this);
        }

        focusAbandoned = true;
        unregisterReceiver();
    }

    //------------------------------------------------------------------------------------------------------------

    @Override
    public void onAudioFocusChange(int focusChange) {
        switch (focusChange) {
            case AudioManager.AUDIOFOCUS_LOSS:
                listener.onPause();
                abandonFocus();
                unregisterReceiver();
                break;

            case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
                listener.onPause();
                unregisterReceiver();
                break;

            case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
                if (pauseWhenDucked) {
                    listener.onPause();
                    unregisterReceiver();
                } else {
                    listener.onDecreaseVolume();
                    volumeDucked = true;
                }
                break;

            case AudioManager.AUDIOFOCUS_GAIN:
                if (volumeDucked) {
                    volumeDucked = false;
                    listener.onIncreaseVolume();
                } else {
                    listener.onResume();
                    registerReceiver();
                }
                break;

            default:
                break;
        }
    }

    //------------------------------------------------------------------------------------------------------

    /**
     * Unregisters the broadcast receiver for {@link AudioManager#ACTION_AUDIO_BECOMING_NOISY}.
     */
    private void unregisterReceiver() {
        if (pauseWhenNoisy) {
            try {
                context.unregisterReceiver(broadcastReceiver);
            } catch (Exception ignored) {
            }
        }
    }

    //------------------------------------------------------------------------------------------------------

    /**
     * Registers the broadcast receiver for {@link AudioManager#ACTION_AUDIO_BECOMING_NOISY}.
     */
    private void registerReceiver() {
        if (pauseWhenNoisy) {
            IntentFilter intentFilter = new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY);
            try {
                context.registerReceiver(broadcastReceiver, intentFilter);
            } catch (Exception ignored) {
            }
        }
    }
}

使用:

// 创建实例
audioFocusController = new AudioFocusController.Builder(context) // Context must be passed
        .setAudioFocusChangeListener(this) // Pass the listener instance created above
        .setAcceptsDelayedFocus(true) // Indicate whether you will accept delayed focus
        .setPauseWhenAudioIsNoisy(false) // Indicate whether you want to be paused when audio becomes noisy
        .setPauseWhenDucked(false) // Indicate whether you want to be paused instead of ducking
        .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) // Set the content type
        .setDurationHint(AudioManager.AUDIOFOCUS_GAIN) // Set the duration hint
        .setUsage(AudioAttributes.USAGE_MEDIA) // Set the usage
        .setStream(AudioManager.STREAM_MUSIC) // Set the stream
        .build();

Ref

Android 音量控制

调整当前视频音量

需要当 MediaPlayer 准备好了,调用才有用;需要在 onPrepared(mp: MediaPlayer?) 中回调设置才有效;setVolume 的取值范围是 0f~1f,这样设置,并不会影响手机本身按钮控制的音量大小

MediaPlayer?.setVolume(volume, volume)

VideoView

videoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
            @Override
        public void onPrepared(MediaPlayer mp) {
            mediaPlayer = mp;//赋值得到MediaPlayer引用
            mp.setVolume(0f, 0f);
        }
    });
    
//控制按钮
btn_voice.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            if (mediaPlayer != null) {
                if (!slience) {
                    mediaPlayer.setVolume(0f, 0f);
                } else {
                    mediaPlayer.setVolume(1, 1);
                }
                slience = !slience;
                btn_voice.setText(slience ? "静音" : "有声");
            }
        }
    });

静音播放

MediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
MediaPlayer.setVolume(0, 0);

当我们接听电话返回后,会发现静音失效,原本静音播放的视频有声音了!解决这个问题的办法是设置:

mMediaPlayer.setAudioStreamType(AudioManager.STREAM_ALARM);使用AudioManager.STREAM_ALARM这种音频模式来实现静音播放!

调整系统音量

监听系统媒体音量变化

public class VolumeChangeObserver {

    private static final String ACTION_VOLUME_CHANGED = "android.media.VOLUME_CHANGED_ACTION";
    private static final String EXTRA_VOLUME_STREAM_TYPE = "android.media.EXTRA_VOLUME_STREAM_TYPE";

    private Context mContext;
    private OnVolumeChangeListener mOnVolumeChangeListener;
    private VolumeReceiver mVolumeReceiver;
    private AudioManager mAudioManager;

    public static VolumeChangeObserver get(Context context) {
        return new VolumeChangeObserver(context);
    }

    private VolumeChangeObserver(Context context) {
        mContext = context;
        mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
    }

    public int getCurrentVolume() {
        return mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
    }

    public VolumeChangeObserver registerVolumeReceiver() {
        IntentFilter intentFilter = new IntentFilter();
        intentFilter.addAction(ACTION_VOLUME_CHANGED);
        mVolumeReceiver = new VolumeReceiver(this);
        mContext.registerReceiver(mVolumeReceiver, intentFilter);
        return this;
    }

    public void unregisterVolumeReceiver() {
        if (mVolumeReceiver != null) mContext.unregisterReceiver(mVolumeReceiver);
        mOnVolumeChangeListener = null;
    }

    public VolumeChangeObserver setOnVolumeChangeListener(OnVolumeChangeListener listener) {
        this.mOnVolumeChangeListener = listener;
        return this;
    }

    public interface OnVolumeChangeListener {
        void onVolumeChange(int volume);
    }

    private static class VolumeReceiver extends BroadcastReceiver {
        private WeakReference<VolumeChangeObserver> mObserver;

        VolumeReceiver(VolumeChangeObserver observer) {
            mObserver = new WeakReference<>(observer);
        }

        @Override
        public void onReceive(Context context, Intent intent) {
            if (mObserver == null) return;
            if (mObserver.get().mOnVolumeChangeListener == null) return;
            if (isReceiveVolumeChange(intent)) {
                OnVolumeChangeListener listener = mObserver.get().mOnVolumeChangeListener;
                listener.onVolumeChange(mObserver.get().getCurrentVolume());
            }
        }

        private boolean isReceiveVolumeChange(Intent intent) {
            return intent.getAction() != null
                    && intent.getAction().equals(ACTION_VOLUME_CHANGED)
                    && intent.getIntExtra(EXTRA_VOLUME_STREAM_TYPE, -1) == AudioManager.STREAM_MUSIC;
        }
    }
}

AudioManager 控制音量

Android 扫描 SD 卡音乐

注意点

  1. 需要读 SD 卡权限
  2. query 时 projection 不要写具体的列名,因为不能的 ROM 可能有的列没有
  3. 设置过滤条件
  4. 弊端是依赖于 Android 系统媒体库,有时新增音乐后没有通知系统扫描,就无法获得该音乐的信息,不够灵活。

系统媒体扫描

当 android 的系统启动的时候,系统会自动扫描 sdcard 内的多媒体文件,并把获得的信息保存在一个系统数据库中,以后在其他程序中如果想要访问多媒体文件的信息,其实就是在这个数据库中进行的,而不是直接去 sdcard 中取,理解了这一点以后,问题也随着而来:如果我在开机状态下在 sdcard 内增加、删除一些多媒体文件,系统会不会自动扫描一次呢?答案是否定的,也就是说,当你改变 sdcard 内的多媒体文件时,保存多媒体信息的系统数据库文件是不会动态更新的。
android 系统开始扫描 sdcard 以及扫描完毕时都会发送一个系统广播来表示当前扫描的状态,这样我们就可以很方便通过判断当前的扫描状态加一些自己的逻辑操作

android 操作 sdcard 中的多媒体文件(二)——音乐列表的更
https://blog.csdn.net/MaximusKiang/article/details/31474477

系统媒体库更新

Android 系统每次开机的时候都会过一遍文件,然后根据文件的媒体类型做分类,主要就是视频、音频、图片、文件、安装包、压缩包等等类型的分类,例如视频、图片、音频等信息,手机在使用途中增删改的媒体,在媒体库中基本上是不会自动刷新的。

很多的软件都是在媒体库读取的,如果你的 app 生成的一些媒体,需要在别的地方用到,结果别的软件没有找到,这个就比较尴尬了,所以需要比较及时的更新相关的媒体信息。

媒体库何时更新?

在系统开机或 sdcard 卡被加载时,系统会自动扫描 sdcard,将扫描到的如音频、图片等媒体文件保存到媒体数据库中,通过 Android 提供的相应的 ContentProvider,我们可以获取这些媒体资源;但如果我们在开机状态下,在 sdcard 内增加或删除一些媒体文件时,系统并不会自动扫描,因此媒体库不会更新(除非自行向媒体数据库中添加或删除)

如果让媒体库更新?

扫描 SD 卡指定类型文件

FileScanner 是一个扫描 Android /storage/emulated/0/目录中指定格式的文件。扫描结果会保存在 FileScanner 数据库中

https://github.com/HayDar-Android/FileScanner

code

/**
 * 
<pre>
 *     author: yangchong
 *     blog  : https://github.com/yangchong211
 *     time  : 2018/01/22
 *     desc  : 本地音乐扫描工具类
 *     revise: 参考链接:https://www.jianshu.com/p/498c9d06c193
 *                      https://blog.csdn.net/chay_chan/article/details/76984665
 * </pre>
*/
public class FileMusicScanManager {

    private static FileMusicScanManager mInstance;

    private static final long AUDIO_FILTER_SIZE = 5_1024; // 5K
    private static final long AUDIO_FILTER_TIME = 1_000; // 1s

    private static final Object mLock = new Object();

    public static FileMusicScanManager getInstance() {
        if (mInstance == null) {
            synchronized (mLock) {
                if (mInstance == null) {
                    mInstance = new FileMusicScanManager();
                }
            }
        }
        return mInstance;
    }

    public Observable<List<AudioBean>> scanMusicObservable(Context context) {
        return Observable.create(new ObservableOnSubscribe<List<AudioBean>>() {
            @Override
            public void subscribe(ObservableEmitter<List<AudioBean>> emitter) throws Exception {
                List<AudioBean> musicDatas = scanMusic(context);
                if (ListUtils.isEmpty(musicDatas)) {
                    if (!emitter.isDisposed()) {
                        emitter.onError(new QbException("没有扫描到音乐"));
                    }
                    return;
                } else {
                    if (!emitter.isDisposed()) {
                        emitter.onNext(musicDatas);
                    }
                    if (!emitter.isDisposed()) {
                        emitter.onComplete();
                    }
                }
            }
        });
    }

    public int delete(Context context, String path) {
        int rowsDel = context.getContentResolver().delete(
                MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, MediaStore.Audio.AudioColumns.DATA + " = ?", new String[]{path});
        return rowsDel;
    }

    /**
     * ----------------------------------扫描歌曲------------------------------------------------
     **/

    // 本地音乐过滤条件
    private static final String SELECTION = MediaStore.Audio.AudioColumns.SIZE + " >= ? AND " +
            MediaStore.Audio.AudioColumns.DURATION + " >= ?";

    /**
     * 扫描歌曲
     */
    @NonNull
    public List<AudioBean> scanMusic(Context context) {
        List<AudioBean> musicList = new ArrayList<>();
        Cursor cursor = context.getContentResolver().query(
                MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
//                new String[]{
//                        BaseColumns._ID,
//                        MediaStore.Audio.AudioColumns.IS_MUSIC,
//                        MediaStore.Audio.AudioColumns.TITLE,
//                        MediaStore.Audio.AudioColumns.ARTIST,
//                        MediaStore.Audio.AudioColumns.ALBUM,
//                        MediaStore.Audio.AudioColumns.ALBUM_ID,
//                        MediaStore.Audio.AudioColumns.DATA,
//                        MediaStore.Audio.AudioColumns.DISPLAY_NAME,
//                        MediaStore.Audio.AudioColumns.SIZE,
//                        MediaStore.Audio.AudioColumns.DURATION
//                },
                null, // 不为null的话,MIUI10.2查询不到数据
                SELECTION,
                new String[]{String.valueOf(AUDIO_FILTER_SIZE), String.valueOf(AUDIO_FILTER_TIME)},
                MediaStore.Audio.Media.DEFAULT_SORT_ORDER);

        if (cursor == null) {
            return musicList;
        }

        int i = 0;
        while (cursor.moveToNext()) {
            // 是否为音乐,魅族手机上始终为0
            int isMusic = cursor.getInt(cursor.getColumnIndex(MediaStore.Audio.AudioColumns.IS_MUSIC));
            if (!isFly() && isMusic == 0) {
                continue;
            }

            long id = cursor.getLong(cursor.getColumnIndex(BaseColumns._ID));
            String title = cursor.getString((cursor.getColumnIndex(MediaStore.Audio.AudioColumns.TITLE)));
            String artist = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.AudioColumns.ARTIST));
            String album = cursor.getString((cursor.getColumnIndex(MediaStore.Audio.AudioColumns.ALBUM)));
            long albumId = cursor.getLong(cursor.getColumnIndex(MediaStore.Audio.AudioColumns.ALBUM_ID));
            long duration = cursor.getLong(cursor.getColumnIndex(MediaStore.Audio.Media.DURATION));
            String path = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.AudioColumns.DATA));
            String fileName = cursor.getString((cursor.getColumnIndex(MediaStore.Audio.AudioColumns.DISPLAY_NAME)));
            long fileSize = cursor.getLong(cursor.getColumnIndex(MediaStore.Audio.Media.SIZE));

            AudioBean music = new AudioBean();
            music.setId(String.valueOf(id));
            music.setType(AudioBean.Type.LOCAL);
            music.setTitle(title);
            music.setArtist(artist);
            music.setAlbum(album);
            music.setAlbumId(albumId);
            music.setDuration(duration);
            music.setPath(path);
            music.setFileName(fileName);
            music.setFileSize(fileSize);
            if (++i <= 20) {
                // 只加载前20首的缩略图
//                CoverLoader.getInstance().loadThumbnail(music);
            }
            musicList.add(music);
        }
        cursor.close();
        return musicList;
    }
    
    private boolean isFly() {
        String flyFlag = getSystemProperty("ro.build.display.id");
        return !TextUtils.isEmpty(flyFlag) && flyFlag.toLowerCase().contains("fly");
    }

    private String getSystemProperty(String key) {
        try {
            @SuppressLint("PrivateApi")
            Class<?> classType = Class.forName("android.os.SystemProperties");
            Method getMethod = classType.getDeclaredMethod("get", String.class);
            return (String) getMethod.invoke(classType, key);
        } catch (Throwable th) {
            th.printStackTrace();
        }
        return null;
    }

}

Ref