[Twoja aplikacja – odtwarzacz audio] AudioFocus. Część 2

W serii „Twoja aplikacja” będę pokazywał, w jaki sposób stworzyć aplikację kompletną wraz z najważniejszymi komponentami. Taka aplikacja będzie posiadać wszystkie podstawowe rzeczy, która powinna mieć. Seria będzie podzielona na części, a każda część będzie zawierać poszczególne zagadnienie.

W tej serii stworzymy aplikację do odtwarzania muzyki wraz z najważniejszymi komponentami.

  1. Część 1: MediaSession i MediaController
  2. Cześć 2: AudioFocus

W poprzedniej części przedstawiłem Ci MediaSsesionMediaController. W tym artykule skupimy się na AudioFocus.

1. Czym jest AudioFocus?

Każdy telefon wydaje różne dźwięki i to czasem jednocześnie. Gdy słuchasz muzyki i dostaniesz powiadomienie o przyjściu wiadomości to aplikacja do odtwarzania muzyki zmniejsza głośność lub wstrzymuje dźwięk na chwilę. Jest to uzasadnione, ponieważ dzięki temu użytkownik jest wstanie szybko zorientować się co dokładnie zdarzyło się. Gdyby wszystkie dźwięki były o tej samej głośności to byś się pogubił. Ciężko by było rozmawiać przez telefon i słuchać muzyki jednocześnie.
W tym celu właśnie powstało AudioFocus w usłudze systemu AudioManager. AudioFocus to prawo do odtwarzania dźwięku i jest przyznawania tylko jednej aplikacji na raz. Najprościej mówiąc, focus stosujemy, gdy aplikacja musi odtworzyć dźwięk. Powinna poprosić o fokus dźwiękowy. Po otrzymaniu zgody może odtwarzać audio. Jednak po uzyskaniu fokusu możesz nie być w stanie go zachować na stałe, ponieważ inna aplikacja może zażądać fokusu,  Jeśli tak się stanie, Twoja aplikacja powinna przerwać odtwarzanie lub obniżyć jego głośność, aby ułatwić użytkownikom usłyszenie nowego źródła dźwięku. Gdy druga aplikacja zwolni focus Twoja aplikacja odzyska focusa i może kontynuować odtwarzanie dźwięku.

2. Kod.

W klasie PlayerService w metodzie onCreate() dodaj:

audioFocus = new AudioFocus(this, mediaSessionHelper);
mediaSessionHelper.setAudioFocus(audioFocus);

Następnie stwórz klasę AudioFocus. I dodaj do niej poniższy kod:

private final Context context;
    private MediaSessionHelper mediaSessionHelper;
    private AudioManager audioManager;
    private AudioFocusRequest audioFocusRequest;
    public AudioFocus(Context ctx, MediaSessionHelper msc) {
        this.context = ctx;
        this.mediaSessionHelper = msc;
        initAudiFocus();
    }
    private void initAudiFocus() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            AudioAttributes audioAttributes = new AudioAttributes.Builder()
                    .setUsage(AudioAttributes.USAGE_MEDIA)
                    .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
                    .build();
            audioFocusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
                    .setOnAudioFocusChangeListener(this)
                    .setAcceptsDelayedFocusGain(false)
                    .setWillPauseWhenDucked(true)
                    .setAudioAttributes(audioAttributes)
                    .build();
        }
        audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
    }
    public int getRequestAudioFocus() {
        int audioFocusResult;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            audioFocusResult = audioManager.requestAudioFocus(audioFocusRequest);
        } else {
            audioFocusResult = audioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
        }
        return audioFocusResult;
    }
    public int getAbandonAudioFocus() {
        int audioFocusResult;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            audioFocusResult = audioManager.abandonAudioFocusRequest(audioFocusRequest);
        } else {
            audioFocusResult = audioManager.abandonAudioFocus(this);
        }
        return audioFocusResult;
    }
    @Override
    public void onAudioFocusChange(int focusChange) {
        switch (focusChange) {
            case AudioManager.AUDIOFOCUS_GAIN:
                mediaSessionHelper.onPlay();
                break;
            case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
                mediaSessionHelper.onPause();
                break;
            default:
                mediaSessionHelper.onPause();
                break;
        }
    }

W konstruktorze ustawiamy Context oraz klasę MediaSession, którą w poprzednim wpisie utworzyliśmy. Będzie ona nam potrzebna do zarządzania stanem odtwarzania. Widać to ładnie w nadpisanej metodzie onAudioFocusChange(). Metoda ta służy do reagowania na poszczególne stany focusa. I tak na przykład, jeśli dostaniemy krótkie powiadomienie, system zatrzyma utwór i za chwilę go wznowi. Oczywiście tutaj możemy zrobić to w inny sposób, na przykład zmniejszając głośność, a następnie zwiększając za pomocą setvolume() – MediaPlayer.
Przyznawanie focusa dźwiękowego jest uzależnione od uruchomionej wersji Androida:

  • Począwszy od Androida 2.2 lub nowszy, aplikacja wywołuje metodę requestAudioFocus()   przyznanie focusa i abandonAudioFocus() – zwolnienie focusa. Aplikacje musi także zarejestrować metodę AudioManager.OnAudioFocusChangeListener().
  • Aplikacje z poziomem interfejsu API 26 lub nowszym powinny używać metody requestAudioFocus(), która przyjmuje argument AudioFocusRequest. Reszta bez zmian.

3. AudioFocusRequest.

W Androidzie O i późniejszej wersji musisz utworzyć obiekt AudioFocusRequest, używając a buildera. W tym obiekcie będziesz musiał określić, jak długo Twoja aplikacja będzie korzystać z fokusa. Pole FocusGain jest wymagane; wszystkie pozostałe pola są opcjonalne. Możemy wyróżnić różne typy żądań skupienia:

    • AUDIOFOCUS_GAIN – Twoja aplikacja jest teraz jedynym źródłem dźwięku, którego słucha użytkownik. Czas odtwarzania dźwięku jest nieznany i prawdopodobnie jest bardzo długi. Zastosuj to wzmocnienia do odtwarzania muzyki, gry lub odtwarzacza wideo.

 

    • AUDIOFOCUS_GAIN_TRANSIENT ~ do krótkich odtwarzań dźwiękowych lub wiadomo kiedy dźwięk zostanie wyłączony. Przykładowym zastosowaniem może być budzik. Wiadomo, że alarm zostanie przerwany, ma początek i koniec. Gdy użytkownik słuchał muzyki i alarm został uruchomiony to użytkownik liczy, że po zakończeniu alarmu, muzyka się wznowi automatycznie.

 

    • AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK – ten typ żądania skupienia jest podobny do AUDIOFOCUS_GAIN_TRANSIENT. Różnica polega na tym, że zamiast wstrzymywać muzykę, powinna grać ciszej. Na przykład podczas odtwarzania wskazówek dojazdu. Muzyka jest na tyle cicha, aby użytkownik zrozumiał wskazówki. Typowe tłumienie to współczynnik 0.2f lub -14dB.

 

  • AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE – tymczasowe żądanie, które oczekuje, że urządzenie nie odtwarza żadnych dźwięków. Jest to zwykle używane dla nagrywania dźwięków lub rozpoznawania mowy i nie chcesz, aby w tym czasie były odtwarzane przez system powiadomienia.

W systemie Android 8, gdy inna aplikacja wymaga AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK, system może automatycznie ściszyć muzykę zamiast ją wstrzymywać. W tym celu skorzystaj z setWillPauseWhenDucked(false). A jeżeli chcesz to wyłączyć to ustaw setWillPauseWhenDucked(true) oraz utworzyć OnAudioFocusChangeList. W ten sposób informujesz system, że ma korzystać z callbacka.
Czasami system nie może udzielić prośby o fokus dźwiękowy, ponieważ fokus jest „zablokowany” przez inną aplikację, na przykład podczas rozmowy telefonicznej. W takim przypadku requestAudioFocus() zwraca AUDIOFOCUS_REQUEST_FAILED. W takim przypadku Twoja aplikacja nie powinna odtwarzać dźwięku, ponieważ nie uzyskała ostrości. Metoda setAcceptsDelayedFocusGain(true) pozwala Twojej aplikacji obsłużyć asynchronicznie prośbę o ustawienie ostrości. Kiedy stan, który zablokował fokus dźwiękowy już nie istnieje, na przykład po zakończeniu połączenia telefonicznego, system udziela oczekującej prośby o skupienie i wywołuje onAudioFocusChange() do powiadomienia aplikacji.

4. Atrybuty Audio.

W przypadku aplikacji z Androidem 5.0 i nowszych wersji, aplikacje dźwiękowe powinny korzystać z AudioAttributes. Zastępuje metodę setMode() w AudioManager. Ustawiając atrybut setUsage() informujemy system do czego „służy” aplikacja. Czy to jest strumień dźwiękowy o charakterze muzycznym, alarmowym, a może jest to po prostu gra. Dzięki tym informacją platforma jest w stanie lepiej wyostrzyć dźwięk.
Kolejnym atrybutem, który jest opcjonalny to kategoria treści na przykład CONTENT_TYPE_MOVIE dla usługi przesyłania strumieniowego filmów lub CONTENT_TYPE_MUSIC aplikacji do odtwarzania muzyki lub aplikacji, które odtwarzają mowę – CONTENT_TYPE_SPEECH.
Jeszcze mamy atrybut flagi (nie wymagane) określa, w jaki sposób framework audio stosuje efekty do odtwarzania audio. FLAG_AUDIBILITY_ENFORCED – żąda, aby system zapewniał słyszalność, HW_AV_SYNC – system wybiera strumień wyjściowy, który obsługuje sprzętową synchronizację AV.
Korzystania z atrybutów nie jest wymagane, mogą obsługiwać starsze typy strumieni. W takich przypadkach platforma zachowuje kompatybilność wsteczną ze starszymi urządzeniami i wersjami systemu Android, automatycznie tłumacząc starsze typy strumieni audio na atrybuty audio. Gdzie wstawiać te atrybuty? Atrybuty ustawiamy np.: w MediaPlayer.setAudioAttributes() lub tak jak w powyższym kodzie. Zalecane jest użycie tego samego AudioAttributes dla żądania, co atrybuty używane do odtwarzania dźwięku / multimediów, np.: MediaPlayer.

5. Obsługa zdarzeń AudioFocus w aplikacji.

Jeżeli już mamy skonfigurowane powyższe elementy to najwyższy czas dostać uprawnienia. Do uzyskiwania focusa korzystamy z requestAudioFocus(). Jeżeli zwalniamy focus to używamy abandonAudioFocusRequest() lub abandonAudioFocus() w zależności od wersji systemu Android. W klasie MediaSessionHelper poprawiamy nasze metody onPlay()onStop() na coś takiego:

@Override
    public void onPlay() {
        int audioFocusResult = mAudioFocus.getRequestAudioFocus();
        if (audioFocusResult == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
            player.playMedia();
            mediaSession.setActive(true);
            mediaSession.setPlaybackState(
                    stateBuilder.setState(PlaybackStateCompat.STATE_PLAYING,
                            PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN, 1)
                            .build());
            updateMetadataFromTrack();
        }
    }
    @Override
    public void onStop() {
        int audioFocusResult = mAudioFocus.getAbandonAudioFocus();
        if (audioFocusResult == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
            player.stopMedia();
            mediaSession.setActive(false);
            mediaSession.setPlaybackState(
                    stateBuilder.setState(PlaybackStateCompat.STATE_STOPPED,
                            PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN, 1)
                            .build());
            updateMetadataFromTrack();
        }
    }

6. Reagowanie na odłączenie słuchawek.

Jeżeli zastosujesz powyższy kod w swojej aplikacji na pewno będzie działać, ale jest jeszcze jedna rzecz, na którą musisz zwrócić uwagę. Gdy użytkownik słucha muzyki na słuchawkach i odłączy słuchawki to nadal muzyka będzie grała. Nie jest to dobre zachowanie, ponieważ telefon może nagle głośno grać i zostanie to źle odebrane przez otoczenia lub użytkownika. Jeżeli chcesz w swojej aplikacji uchronić się przed tym błędem, rozszerz swoją klasę AudioFocus o BroadcastReceiver i dodaj następujący kod:

@Override
public void onReceive(Context context, Intent intent) {
    if (AudioManager.ACTION_AUDIO_BECOMING_NOISY.equals(intent.getAction())) {
        mediaSessionHelper.onPause();
    }
}

Klasę MediaSessionHelper uzupełnij o następujący kod:

private IntentFilter intentFilter = new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY);
@Override
public void onPlay() {
	context.registerReceiver(mAudioFocus, intentFilter);
...
}
@Override
public void onStop() {
	context.unregisterReceiver(mAudioFocus);
...
}

7. Podsumowanie

AudioFocus to koncepcja wprowadzona w API 8. Służy ona do wyostrzenia dźwięków w taki sposób, że użytkownik może skupić się tylko na jednym strumieniu audio np.: słuchając muzyki lub podcastu. W niektórych przypadkach może być odtwarzanych wiele strumieni audio w tym samym czasie, ale jest tylko jeden, który użytkownik naprawdę słucha, podczas gdy drugi gra w tle. Przykładem tego są wskazówki dojazdu, gdy muzyka odtwarzana jest z mniejszą głośnością.
Aby zapewnić dobrą obsługę dźwięku w systemie Android, musisz uważać, aby aplikacja współgrała z systemem i innymi aplikacjami, które odtwarzają multimedia. Zazwyczaj fokus jest zawsze zapewniony. Wyjątkiem jest tylko aktywna rozmowa telefoniczna. Technicznie, nikt nie zmusza nas do korzystania z focusa, ale czy na pewno chcesz denerwować użytkownika?

Co dalej?

  • Polub stronę MYENV na Facebooku oraz śledź mnie na Twitterze
  • Zachęcam do komentowania i pisania propozycji tematów, o których chcesz przeczytać
  • Poleć ten wpis za pomocą poniższych przycisków. Będę Ci za to bardzo wdzięczny 🙂
  • Życzę Ci miłego dnia i miłego kodowania 🙂