android学习笔记----关于音频焦点Audio Focus

2023-05-06 19:27:32 浏览数 (3)

为了便于理解,我们以android的8.0以前的版本为例,8.0以后有一定改动,但是基本思路一样。

关于管理音频焦点(8.0以前和更高版本)的官方文档:https://developer.android.google.cn/guide/topics/media-apps/audio-focus

Demo1地址:https://github.com/liuchenyang0515/MiniCase(该Demo包含了ListView、自定义Adapter、音频释放问题、音焦处理问题)

Demo2地址:https://github.com/liuchenyang0515/MiniCase_2(功能上和上一个相比没变,就是将activity重构成了fragment,加上viewpager)

在应用中处理 Audio Focus 将是非常重要的,我们先从 AudioManager 请求 Audio Focus,我们发现我们需要调用requestAudioFocus 方法,该方法需要以下三个输入:一个 onAudioFocusChangeListener 对象streamType 和 a durationHint,最后两个输入 streamType 和durationHint,它们是整型类型。 第一个问题:当我们调用 requestAudioFocus 方法时 我们应该传入什么?作为 streamType 它是我们的第二个参数,指的是我们要播放的音频的类型,是歌曲还是铃声?

第二个问题:我们应该传入什么作为 durationHint(即第三个参数,指的是Audio Focus所需的时长)? 是需要很短的时间还是很长时间?

提示!↓↓↓↓↓↓↓↓

在 AudioManager 类的参考文档中,滚动到这个公开常量部分,这些都是可以传入 requestAudioFocus 方法的可能 streamType,左列给出了每个 streamType 的说明,当我们向 AudioManager 传入这个常量值时,它就会知道我们指的是这种特定的 streamType。

那么 durationHint 呢?我们转到 requestAudioFocus 方法,方法说明部分列出了durationHint 的不同有效选项,例如 AUDIOFOCUS_GAIN_TRANSIENT,AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK,AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE,AUDIOFOCUS_GAIN,每个都提供了解释说明

requestAudioFocus 文档地址: https://developer.android.google.cn/reference/android/media/AudioManager?utm_source=udacity&utm_medium=course&utm_campaign=android_basics#requestAudioFocus(android.media.AudioManager.OnAudioFocusChangeListener, int, int)

当我们请求 Audio Focus 时,我们必须指定 streamTypeAndroid 为你提供了这些选项供你选择,这些内容位于 AudioManager 参考部分的public常量部分。假如我们的是个简短的音频文件,不是闹钟、DTMF 音调(用于拨打电话号码),不是音乐或通知,也不是系统声音或语音电话,查看过后,在所有这些选项中STREAM_MUSIC 最合适,当你开发的应用会播放音频,例如音乐或播客播放器时,你可能就会用到这个 streamType。

对于第一个问题,我们希望使用的 streamType 是AudioManager.STREAM_MUSIC。

第二个问题,现在来看看第三个参数 durationHint,对我们来说最合适的应该是AUDIOFOCUS_GAIN_TRANSIENT。

我们来讲讲几个参数的区别,放在这里是否合适,假设我们仅仅需要播放单词或者句子的声音。

AUDIOFOCUS_GAIN_TRANSIENT表示我们要请求 Audio Focus并使用很短的时间,因为我们的音频文件非常短,只播放几秒钟,所以很合适。例如如果有首歌正在播放,用户想要听听某个单词的发音,我们不希望在播放单词发音的同时还播放歌曲,即使该歌曲的音量变低了。我们希望播放我们的音频时,我们想要暂时完全让所有其他内容保持静音(系统提示音除外),因此我们使用 AudioManager.AUDIO_GAIN_TRANSIENT。设置这个选项系统提示音是可以将其打断的。

其他 Audio Focus 状态对我们来说都不太合适,例如我们播放的不是一首长的歌曲或视频,所以不需要 AUDIOFOCUS_GAIN。AUDIOFOCUS_GAIN是用于未知持续时间的焦点请求,可能会很久,例如播放歌曲或视频。

不需要 AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE,即也用于很短的音频焦点请求,当我们播放我们的音频时,系统声音(例如通知)不会播放,我们不希望用户错过这些重要提示音,常用于语音备忘录录或语音识别等用例,因为语音识别是不希望其他声音干扰的,因此不会使用这个选项。

AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK看起来合适,但是使用它的话会存在背景噪音,为什么呢? 现在我们来看看 ducking 的意思,它是用来表示短暂的Audio Focus 请求,预计持续短暂的时间,可以接受在降低输出级别后(声音降低)让其他音频应用继续播放,即回避,例如在播放其他内容时降低级别。意思是比如我们在播放单词或句子的声音,现在来了一个通知或者短信,我们的播放声音降低了,系统提示音(音频焦点竞争的获胜者)正常播放来引起我们的注意,看起来就像我们播放的声音回避了系统提示音。虽然自动回避是音乐和视频播放应用程序可以接受的行为,但在播放语音内容(例如在有声书应用程序中)时却没有用。在这种情况下,应用程序应该暂停。

来看看AudioManager的一个内部接口OnAudioFocusStateChangeListener,这是一个焦点状态改变的监听器,每当音频焦点改变时(由于另一个应用或设备,我们获得或失去音频焦点),该监听器被触发。

我们来看看这个接口中的方法 public abstract void onAudioFocusChange (int focusChange) 在监听器上调用,以通知它此侦听器的音频焦点已更改。 focusChange值表示焦点是否已获得,焦点是否丢失,以及该丢失是否是短暂的,或者新的焦点持有者是否会持续一段未知的时间。当失去焦点时,监听者可以使用焦点变化信息来决定失去焦点时采用的行为。例如,音乐播放器可以选择降低其音乐流(回避)的音量以用于瞬时焦点损失,否则暂停。

也就是说,可以根据这些状态来设置我们想要的操作,是继续播放?重头播放?还是停止了释放资源等。 来看看可能的 Audio Focus 状态。

对于AUDIOFOCUS_GAIN,它用于表示持续时间未知的音频焦点或音频焦点请求。说明部分应该是在之前失去 Audio Focus 后又获得 Audio Focus 了,当应用进入该状态时,我们应该执行什么操作?我们应该继续播放音频文件。

对于AUDIOFOCUS_LOSS,用于表示未知持续时间的音频焦点丢失。说明部分应该是永久失去 Audio Focus,对这里的例子来说,采取的操作应该是停止 MediaPlayer 并释放资源。

对于AUDIOFOCUS_LOSS_TRANSIENT,用于指示音频焦点的瞬时丢失。即暂时失去了 Audio Focus,对于我们这里的例子,意味着我们暂停音频文件,并且准备下次从头播放。

对于AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK,用于指示音频焦点的瞬时丢失,如果竞争音频焦点的失败者想要继续播放(也称为“回避”),则可以降低其输出音量,因为新的焦点拥有者不需要其他人保持沉默。也是意味着暂时失去 Audio Focus,在适用时,我们可以回避或降低音量。对于这里的例子,我们可以暂停音频文件,并且下次从头播放。因为单词发音的每个部分都很重要,用户需要听到,而不是为了省事继续播放,比如正在播放单词的时候,到了一条短信,如果单词回避短信声音,单词声音小,短信声音大,那么体验很不好。如果我们的处理是暂停,下次继续从这里播放,比如banana(不拿了),发音:不~(暂停),发音:拿了。那将会很糟糕。所以本例会采用pause()和seekTo(0)处理。

总结:

当应用程序获得音频焦点时,它必须能够在另一个应用程序请求自己的音频焦点时释放它。发生这种情况时,您的应用程序会在应用程序调用requestAudioFocus()时,接收指定AudioFocusChangeListener中对onAudioFocusChange()方法的调用。

暂时失去焦点

如果焦点变化是瞬态的(AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCKAUDIOFOCUS_LOSS_TRANSIENT),您的应用程序应该回避(如果您不依赖于自动回避)或暂停播放,否则保持相同的状态。 在瞬间失去音频焦点期间,您应该继续监视音频焦点的变化,并准备在重新获得焦点时恢复正常播放。当别的应用程序放弃焦点时(调用abandonAudioFocus),您会收到回调(AUDIOFOCUS_GAIN)。此时,您可以将音量恢复到正常水平或重新开始播放。

永久失去焦点

如果音频焦点丢失是永久性的(AUDIOFOCUS_LOSS),另一个应用程序正在播放音频。您的应用应立即暂停播放(或者释放资源),因为它不会收到AUDIOFOCUS_GAIN回调。要重新开始播放,用户必须采取明确的操作,例如在通知或应用UI中按播放传输控件。

所以,当 Audio Focus 状态发生变化时,我们应该调节我的音频播放行为,以便恰当地处理音频干扰。

首先,请求 Audio Focus 第二步,创建 AudioManager.OnAudioFocusChangeListener 的实例,并实施回调方法 第三步,当 Audio Focus 状态发生变化时,调整播放行为 最后,当 Audio Focus 不再需要时,释放 Audio Focus

首先,我们想要请求 Audio Focus,意味着我们需要 AudioManager 对象实例。

我将为 AudioManager创建一个全局变量并在生命周期 Activity 中初始化一次,接着在 onCreate 方法中,通过调用getSystemService 来初始化 AudioManager,并传入 AUDIO_SERVICE 常量。 获得 AudioManager 对象后,我们可以对其调用 requestAudioFocused 方法,应该在哪请求 Audio Focus 呢?当某项内容被点击后,我希望使用 AudioManager 来请求 Audio Focus,然后再设置 MediaPlayer 来播放声音。所以在ListView的点击监听事件里面操作。

部分代码如下:

代码语言:javascript复制
public class NumbersActivity extends AppCompatActivity {
    /**
     * Handles playback of all the sound files
     */
    private MediaPlayer mMediaPlayer;
    private static final String TAG = "NumbersActivity";
    /**
     * Handles audio focus when playing a sound file
     */
    private AudioManager mAudioManager;

    /**
     * This listener gets triggered when the {@link MediaPlayer} has completed
     * playing the audio file.
     */
    private MediaPlayer.OnCompletionListener mCompletionListener = new MediaPlayer.OnCompletionListener() {
        @Override
        public void onCompletion(MediaPlayer mediaPlayer) {
            // Now that the sound file has finished playing, release the media player resources.
            releaseMediaPlayer();
        }
    };

    /**
     * This listener gets triggered whenever the audio focus changes
     * (i.e., we gain or lose audio focus because of another app or device).
     * 每当音频焦点改变时(由于另一个应用或设备,我们获得或失去音频焦点),该触发器被触发。
     */
    private AudioManager.OnAudioFocusChangeListener mOnAudioFocusChangeListener = new AudioManager.OnAudioFocusChangeListener() {
        @Override
        public void onAudioFocusChange(int focusChange) {
            if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT ||
                    focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
                // The AUDIOFOCUS_LOSS_TRANSIENT case means that we've lost audio focus for a
                // short amount of time. The AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK case means that
                // our app is allowed to continue playing sound but at a lower volume. We'll treat
                // both cases the same way because our app is playing short sound files.
                // Pause playback and reset player to the start of the file. That way, we can
                // play the word from the beginning when we resume playback.
                // AUDIOFOCUS_LOSS_TRANSIENT表示我们在短时间内丢失了音频焦点。
                // AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK情况意味着我们的应用程序可以继续播放声音,但音量较低。
                // 我们会以同样的方式对待这两种情况,因为我们的应用程序正在播放简短的声音文件。
                // 暂停播放并将播放器重置到文件的开头。这样,我们可以在恢复播放时从头开始播放单词。
                mMediaPlayer.pause();
                mMediaPlayer.seekTo(0);
            } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
                // The AUDIOFOCUS_GAIN case means we have regained focus and can resume playback.
                // AUDIOFOCUS_GAIN表示我们已重新获得焦点并可以恢复播放。
                mMediaPlayer.start();
            } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS) {
                // The AUDIOFOCUS_LOSS case means we've lost audio focus and
                // Stop playback and clean up resources
                // AUDIOFOCUS_LOSS表示我们失去了音频焦点,停止播放和清理资源
                releaseMediaPlayer();
            }
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.word_list);
        // Create and setup the {@link AudioManager} to request audio focus
        mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
        // Create a list of words
        final ArrayList<Word> words = new ArrayList<Word>();
        words.add(new Word("one", "lutti", R.drawable.number_one, R.raw.number_one));
        words.add(new Word("two", "otiiko", R.drawable.number_two, R.raw.number_two));
        words.add(new Word("three", "tolookosu", R.drawable.number_three, R.raw.number_three));
        words.add(new Word("four", "oyyisa", R.drawable.number_four, R.raw.number_four));
        words.add(new Word("five", "massokka", R.drawable.number_five, R.raw.number_five));
        words.add(new Word("six", "temmokka", R.drawable.number_six, R.raw.number_six));
        words.add(new Word("seven", "kenekaku", R.drawable.number_seven, R.raw.number_seven));
        words.add(new Word("eight", "kawinta", R.drawable.number_eight, R.raw.number_eight));
        words.add(new Word("nine", "wo’e", R.drawable.number_nine, R.raw.number_nine));
        words.add(new Word("ten", "na’aacha", R.drawable.number_ten, R.raw.number_ten));

        // Create an {@link WordAdapter}, whose data source is a list of {@link Word}s. The
        // adapter knows how to create list items for each item in the list.
        final WordAdapter adapter = new WordAdapter(this, words, R.color.category_numbers);

        // Find the {@link ListView} object in the view hierarchy of the {@link Activity}.
        // There should be a {@link ListView} with the view ID called list, which is declared in the
        // word_list.xml layout file.
        ListView listView = (ListView) findViewById(R.id.list);

        // Make the {@link ListView} use the {@link WordAdapter} we created above, so that the
        // {@link ListView} will display list items for each {@link Word} in the list.
        listView.setAdapter(adapter);
        listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> adapterView, View view, int position, long l) {
// Release the media player if it currently exists because we are about to
                // play a different sound file
                releaseMediaPlayer();
                // Get the {@link Word} object at the given position the user clicked on
                Word word = words.get(position);


                // Request audio focus so in order to play the audio file. The app needs to play a
                // short audio file, so we will request audio focus with a short amount of time
                // with AUDIOFOCUS_GAIN_TRANSIENT.
                // 请求音频焦点,以便播放音频文件。该应用程序需要播放短音频文件,
                // 因此我们将使用AUDIOFOCUS_GAIN_TRANSIENT在短时间内请求音频焦点。
                int result = mAudioManager.requestAudioFocus(mOnAudioFocusChangeListener,
                        AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);

                if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
                    // We have audio focus now.
                    // Create and setup the {@link MediaPlayer} for the audio resource associated
                    // with the current word
                    mMediaPlayer = MediaPlayer.create(NumbersActivity.this, word.getAudioResourceId());

                    // Start the audio file
                    mMediaPlayer.start();

                    // Setup a listener on the media player, so that we can stop and release the
                    // media player once the sound has finished playing.
                    mMediaPlayer.setOnCompletionListener(mCompletionListener);
                }
            }
        });
    }

    @Override
    protected void onStop() {
        super.onStop();
        // When the activity is stopped, release the media player resources because we won't
        // be playing any more sounds.
        releaseMediaPlayer();
    }


    /**
     * Clean up the media player by releasing its resources.
     */
    private void releaseMediaPlayer() {
        // If the media player is not null, then it may be currently playing a sound.
        if (mMediaPlayer != null) {
            // Regardless of the current state of the media player, release its resources
            // because we no longer need it.
            mMediaPlayer.release();

            // Set the media player back to null. For our code, we've decided that
            // setting the media player to null is an easy way to tell that the media player
            // is not configured to play an audio file at the moment.
            mMediaPlayer = null;
            // Regardless of whether or not we were granted audio focus, abandon it. This also
            // unregisters the AudioFocusChangeListener so we don't get anymore callbacks.
            // 无论我们是否获得音频焦点,都放弃它。
            // 这也取消注册AudioFocusChangeListener,我们不再回调。
            // abandonAudioFocus放弃音频焦点。导致前一个焦点所有者(如果有)获得焦点。
            mAudioManager.abandonAudioFocus(mOnAudioFocusChangeListener);
        }
    }
}

一般情况,我们使用的音频焦点监听器的模版如下,可与修改逻辑,无论哪种方式,只要正确使用音频焦点即可!

代码语言:javascript复制
AudioManager.OnAudioFocusChangeListener afChangeListener = 
    new AudioManager.OnAudioFocusChangeListener() {
  public void onAudioFocusChange(int focusChange) {
    if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT) {
      // Pause playback because your Audio Focus was
      // temporarily stolen, but will be back soon.
      // i.e. for a phone call
    } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS) {
      // Stop playback, because you lost the Audio Focus.
      // i.e. the user started some other playback app
      // Remember to unregister your controls/buttons here.
      // And release the kra — Audio Focus!
      // You’re done.
      am.abandonAudioFocus(afChangeListener);
    } else if (focusChange ==
        AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
      // Lower the volume, because something else is also
      // playing audio over you.
      // i.e. for notifications or navigation directions
      // Depending on your audio playback, you may prefer to
      // pause playback here instead. You do you.
    } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
      // Resume playback, because you hold the Audio Focus
      // again!
      // i.e. the phone call ended or the nav directions
      // are finished
      // If you implement ducking and lower the volume, be
      // sure to return it to normal here, as well.
    }
  }
};

0 人点赞