Funkcje audio

Android TV obsługuje wiele równoczesnych wyjść audio: głośniki telewizora, kino domowe HDMI, Bluetooth i inne. Te urządzenia obsługują różne kodowania (np. Dolby Digital+, DTS, PCM), częstotliwości próbkowania i kanały. Telewizory podłączone przez HDMI obsługują wiele formatów, ale Bluetooth obsługuje tylko PCM.

Dostępne urządzenia audio i routing mogą się zmieniać podczas odtwarzania: użytkownicy mogą podłączać HDMI, łączyć się przez Bluetooth lub modyfikować ustawienia. Aplikacje muszą się odpowiednio dostosowywać, aby zapewnić ciągłe odtwarzanie przy użyciu możliwości nowego urządzenia. Nieobsługiwane formaty mogą powodować błędy lub ciszę.

Oferuj wiele kodowań, aby zapewnić najlepsze wrażenia w zależności od możliwości urządzenia. Na przykład jeśli jest obsługiwany format Dolby Digital, jest on używany. W przeciwnym razie aplikacja wraca do PCM. Zobacz Obsługiwane formaty multimediów, aby dowiedzieć się więcej o dekoderach Androida, które przekształcają strumienie w PCM.

Podczas odtwarzania aplikacja do odtwarzania strumieniowego powinna utworzyć element AudioTrack z najlepszym AudioFormat obsługiwanym przez urządzenie audio.

Tworzenie ścieżki w odpowiednim formacie

Aplikacje powinny utworzyć element AudioTrack, rozpocząć jego odtwarzanie i wywołać getRoutedDevice(), aby określić domyślne urządzenie audio, z którego ma być odtwarzany dźwięk. Może to być na przykład bezpieczna, krótka, cicha ścieżka zakodowana w PCM, która służy tylko do określenia urządzenia, do którego jest kierowany dźwięk, i jego możliwości audio.

Pobieranie obsługiwanych kodowań

Aby określić formaty audio dostępne na domyślnym urządzeniu audio, użyj funkcji getAudioProfiles() (poziom interfejsu API 31 lub nowszy) albo getEncodings() (poziom interfejsu API 23 lub nowszy).

Sprawdzanie obsługiwanych profili i formatów audio

Aby sprawdzić obsługiwane kombinacje formatu, liczby kanałów i częstotliwości próbkowania, użyj funkcji AudioProfile (poziom interfejsu API 31 lub nowszy) albo isDirectPlaybackSupported() (poziom interfejsu API 29 lub nowszy).

Niektóre urządzenia z Androidem mogą obsługiwać kodowania, które nie są obsługiwane przez urządzenie wyjściowe audio. Te dodatkowe formaty powinny być wykrywane za pomocą funkcji isDirectPlaybackSupported(). W takich przypadkach dane audio są ponownie kodowane do formatu obsługiwanego przez urządzenie wyjściowe audio. Użyj funkcji isDirectPlaybackSupported(), aby prawidłowo sprawdzić obsługę wybranego formatu, nawet jeśli nie ma go na liście zwracanej przez funkcję getEncodings().

Przewidywany routing audio

W Androidzie 13 (poziom interfejsu API 33) wprowadzono przewidywane routingi audio. Możesz przewidzieć obsługę atrybutów audio urządzenia i przygotować ścieżki dla aktywnego urządzenia audio. Aby sprawdzić, czy getDirectPlaybackSupport() jest obsługiwane na obecnie używanym urządzeniu audio w przypadku danego formatu i atrybutów, możesz użyć funkcji bezpośrednie odtwarzanie:

Kotlin

val format = AudioFormat.Builder()
    .setEncoding(AudioFormat.ENCODING_E_AC3)
    .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1)
    .setSampleRate(48000)
    .build()
val attributes = AudioAttributes.Builder()
    .setUsage(AudioAttributes.USAGE_MEDIA)
    .build()

if (AudioManager.getDirectPlaybackSupport(format, attributes) !=
    AudioManager.DIRECT_PLAYBACK_NOT_SUPPORTED
) {
    // The format and attributes are supported for direct playback
    // on the currently active routed audio path
} else {
    // The format and attributes are NOT supported for direct playback
    // on the currently active routed audio path
}

Java

AudioFormat format = new AudioFormat.Builder()
        .setEncoding(AudioFormat.ENCODING_E_AC3)
        .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1)
        .setSampleRate(48000)
        .build();
AudioAttributes attributes = new AudioAttributes.Builder()
        .setUsage(AudioAttributes.USAGE_MEDIA)
        .build();

if (AudioManager.getDirectPlaybackSupport(format, attributes) !=
        AudioManager.DIRECT_PLAYBACK_NOT_SUPPORTED) {
    // The format and attributes are supported for direct playback
    // on the currently active routed audio path
} else {
    // The format and attributes are NOT supported for direct playback
    // on the currently active routed audio path
}

Możesz też sprawdzić, które profile są obsługiwane w przypadku bezpośredniego odtwarzania multimediów na obecnie używanym urządzeniu audio. Nie obejmuje to profili, które nie są obsługiwane lub które np. byłyby transkodowane przez framework Androida:

Kotlin

private fun findBestAudioFormat(audioAttributes: AudioAttributes): AudioFormat {
    val preferredFormats = listOf(
        AudioFormat.ENCODING_E_AC3,
        AudioFormat.ENCODING_AC3,
        AudioFormat.ENCODING_PCM_16BIT,
        AudioFormat.ENCODING_DEFAULT
    )
    val audioProfiles = audioManager.getDirectProfilesForAttributes(audioAttributes)
    val bestAudioProfile = preferredFormats.firstNotNullOf { format ->
        audioProfiles.firstOrNull { it.format == format }
    }
    val sampleRate = findBestSampleRate(bestAudioProfile)
    val channelMask = findBestChannelMask(bestAudioProfile)
    return AudioFormat.Builder()
        .setEncoding(bestAudioProfile.format)
        .setSampleRate(sampleRate)
        .setChannelMask(channelMask)
        .build()
}

Java

private AudioFormat findBestAudioFormat(AudioAttributes audioAttributes) {
    Stream<Integer> preferredFormats = Stream.<Integer>builder()
            .add(AudioFormat.ENCODING_E_AC3)
            .add(AudioFormat.ENCODING_AC3)
            .add(AudioFormat.ENCODING_PCM_16BIT)
            .add(AudioFormat.ENCODING_DEFAULT)
            .build();
    Stream<AudioProfile> audioProfiles =
            audioManager.getDirectProfilesForAttributes(audioAttributes).stream();
    AudioProfile bestAudioProfile = (AudioProfile) preferredFormats.map(format ->
            audioProfiles.filter(profile -> profile.getFormat() == format)
                    .findFirst()
                    .orElseThrow(NoSuchElementException::new)
    );
    Integer sampleRate = findBestSampleRate(bestAudioProfile);
    Integer channelMask = findBestChannelMask(bestAudioProfile);
    return new AudioFormat.Builder()
            .setEncoding(bestAudioProfile.getFormat())
            .setSampleRate(sampleRate)
            .setChannelMask(channelMask)
            .build();
}

W tym przykładzie preferredFormats to lista instancji AudioFormat. Jest ona uporządkowana w taki sposób, że na początku listy znajduje się najbardziej preferowany format, a na końcu – najmniej preferowany. getDirectProfilesForAttributes() zwraca listę obsługiwanych AudioProfile obiektów dla obecnie używanego urządzenia audio z podanymi AudioAttributes. Lista preferowanych elementów AudioFormat jest iterowana do momentu znalezienia pasującego obsługiwanego elementu AudioProfile. Ten element AudioProfile jest przechowywany jako bestAudioProfile. Optymalne częstotliwości próbkowania i maski kanałów są określane na podstawie elementu bestAudioProfile. Na koniec tworzona jest odpowiednia AudioFormat instancja.

Tworzenie ścieżki audio

Aplikacje powinny używać tych informacji do utworzenia elementu AudioTrack dla formatu AudioFormat o najwyższej jakości obsługiwanego przez domyślne urządzenie audio (i dostępnego w przypadku wybranej treści).

Przechwytywanie zmian urządzenia audio

Aby przechwytywać zmiany urządzenia audio i na nie reagować, aplikacje powinny:

  • W przypadku poziomów interfejsu API 24 lub nowszych dodać element OnRoutingChangedListener, aby monitorować zmiany urządzenia audio (HDMI, Bluetooth itp.).
  • W przypadku poziomu interfejsu API 23 zarejestrować element AudioDeviceCallback, aby otrzymywać informacje o zmianach na liście dostępnych urządzeń audio.
  • W przypadku poziomów interfejsu API 21 i 22 monitorować zdarzenia podłączenia HDMI i używać dodatkowych danych z transmisji.
  • Zarejestrować też element BroadcastReceiver, aby monitorować zmiany stanu elementu BluetoothDevice w przypadku urządzeń z poziomem interfejsu API niższym niż 23, ponieważ element AudioDeviceCallback nie jest jeszcze obsługiwany.

Gdy w przypadku elementu AudioTrack zostanie wykryta zmiana urządzenia audio, aplikacja powinna sprawdzić zaktualizowane możliwości audio i w razie potrzeby utworzyć ponownie element AudioTrack z innym formatem AudioFormat. Zrób to, jeśli jest teraz obsługiwane kodowanie o wyższej jakości lub jeśli wcześniej używane kodowanie nie jest już obsługiwane.

Przykładowy kod

Kotlin

// audioPlayer is a wrapper around an AudioTrack
// which calls a callback for an AudioTrack write error
audioPlayer.addAudioTrackWriteErrorListener {
    // error code can be checked here,
    // in case of write error try to recreate the audio track
    restartAudioTrack(findDefaultAudioDeviceInfo())
}

audioPlayer.audioTrack.addOnRoutingChangedListener({ audioRouting ->
    audioRouting?.routedDevice?.let { audioDeviceInfo ->
        // use the updated audio routed device to determine
        // what audio format should be used
        if (needsAudioFormatChange(audioDeviceInfo)) {
            restartAudioTrack(audioDeviceInfo)
        }
    }
}, handler)

Java

// audioPlayer is a wrapper around an AudioTrack
// which calls a callback for an AudioTrack write error
audioPlayer.addAudioTrackWriteErrorListener(new AudioTrackPlayer.AudioTrackWriteError() {
    @Override
    public void audioTrackWriteError(int errorCode) {
        // error code can be checked here,
        // in case of write error try to recreate the audio track
        restartAudioTrack(findDefaultAudioDeviceInfo());
    }
});

audioPlayer.getAudioTrack().addOnRoutingChangedListener(new AudioRouting.OnRoutingChangedListener() {
    @Override
    public void onRoutingChanged(AudioRouting audioRouting) {
        if (audioRouting != null && audioRouting.getRoutedDevice() != null) {
            AudioDeviceInfo audioDeviceInfo = audioRouting.getRoutedDevice();
            // use the updated audio routed device to determine
            // what audio format should be used
            if (needsAudioFormatChange(audioDeviceInfo)) {
                restartAudioTrack(audioDeviceInfo);
            }
        }
    }
}, handler);