Recursos de áudio

Os dispositivos Android TV podem ter várias saídas de áudio conectadas ao mesmo tempo: alto-falantes da TV, home cinema conectado por HDMI, fones de ouvido Bluetooth etc. Esses dispositivos de saída de áudio podem oferecer suporte a diferentes recursos de áudio, como codificações (Dolby Digital+, DTS e PCM), taxa de amostragem e canais. Por exemplo, as TVs conectadas por HDMI são compatíveis com vários encodings, enquanto os fones de ouvido Bluetooth conectados geralmente são compatíveis apenas com PCM.

A lista de dispositivos de áudio disponíveis e o dispositivo de áudio roteado também podem mudar ao conectar ou desconectar dispositivos HDMI, fones de ouvido Bluetooth ou quando o usuário muda as configurações de áudio. Como os recursos de saída de áudio podem mudar mesmo quando os apps estão reproduzindo mídia, eles precisam se adaptar a essas mudanças e continuar a reprodução no novo dispositivo de áudio roteado e nos recursos dele. A saída no formato de áudio errado pode resultar em erros ou em nenhum som.

Os apps podem gerar o mesmo conteúdo em várias codificações para oferecer a melhor experiência de áudio ao usuário, dependendo dos recursos do dispositivo de áudio. Por exemplo, uma transmissão de áudio codificada em Dolby Digital é reproduzida se a TV for compatível. Caso contrário, uma transmissão de áudio PCM mais amplamente compatível é escolhida. A lista de decodificadores integrados do Android usados para transformar um stream de áudio em PCM pode ser encontrada em Formatos de mídia compatíveis.

No momento da reprodução, o app de streaming precisa criar um AudioTrack com o melhor AudioFormat compatível com o dispositivo de áudio de saída.

Criar uma faixa com o formato certo

Os apps precisam criar um AudioTrack, começar a tocar e chamar getRoutedDevice() para determinar o dispositivo de áudio padrão em que o som será reproduzido. Por exemplo, uma faixa PCM codificada com um silêncio curto e seguro usada apenas para determinar o dispositivo roteado e os recursos de áudio dele.

Receber codificações compatíveis

Use getAudioProfiles() (nível 31 da API e mais recentes) ou getEncodings() (nível 23 da API e mais recentes) para determinar os formatos de áudio disponíveis no dispositivo de áudio padrão.

Verificar formatos e perfis de áudio compatíveis

Use AudioProfile (nível 31 da API e mais recentes) ou isDirectPlaybackSupported() (nível 29 da API e mais recentes) para verificar as combinações compatíveis de formato, contagem de canais e taxa de amostragem.

Alguns dispositivos Android são capazes de oferecer suporte a codificações além daquelas compatíveis com o dispositivo de áudio de saída. Esses formatos extras precisam ser detectados por isDirectPlaybackSupported(). Nesses casos, os dados de áudio são recodificados para um formato compatível com o dispositivo de saída de áudio. Use isDirectPlaybackSupported() para verificar corretamente o suporte ao formato desejado, mesmo que ele não esteja presente na lista retornada por getEncodings().

Rota de áudio antecipada

O Android 13 (nível 33 da API) introduziu rotas de áudio antecipadas. É possível antecipar a compatibilidade com atributos de áudio do dispositivo e preparar faixas para o dispositivo de áudio ativo. Use getDirectPlaybackSupport() para verificar se a reprodução direta é compatível com o dispositivo de áudio conectado no momento para um determinado formato e atributos:

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
}

Como alternativa, é possível consultar quais perfis são compatíveis com a reprodução direta de mídia pelo dispositivo de áudio roteado no momento. Isso exclui perfis sem suporte ou que seriam, por exemplo, transcodificados pelo framework do Android:

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();
}

Neste exemplo, preferredFormats é uma lista de instâncias AudioFormat. Ela é ordenada com a mais preferida primeiro e a menos preferida por último. getDirectProfilesForAttributes() retorna uma lista de objetos AudioProfile compatíveis para o dispositivo de áudio roteado no momento com o AudioAttributes fornecido. A lista de itens AudioFormat preferidos é iterada até que um AudioProfile compatível correspondente seja encontrado. Esse AudioProfile é armazenado como bestAudioProfile. As taxas de amostragem e as máscaras de canal ideais são determinadas com base em bestAudioProfile. Por fim, uma instância AudioFormat adequada é criada.

Criar faixa de áudio

Os apps precisam usar essas informações para criar um AudioTrack para o AudioFormat de mais alta qualidade compatível com o dispositivo de áudio padrão (e disponível para o conteúdo selecionado).

Interceptar mudanças no dispositivo de áudio

Para interceptar e reagir a mudanças no dispositivo de áudio, os apps precisam:

  • Para níveis de API iguais ou maiores que 24, adicione um OnRoutingChangedListener para monitorar mudanças no dispositivo de áudio (HDMI, Bluetooth etc.).
  • Para o nível 23 da API, registre um AudioDeviceCallback para receber mudanças na lista de dispositivos de áudio disponíveis.
  • Para os níveis de API 21 e 22, monitore os eventos de plugue HDMI e use os dados extras das transmissões.
  • Registre também um BroadcastReceiver para monitorar mudanças de estado do BluetoothDevice em dispositivos anteriores à API 23, já que o AudioDeviceCallback ainda não é compatível.

Quando uma mudança no dispositivo de áudio é detectada para o AudioTrack, o app precisa verificar os recursos de áudio atualizados e, se necessário, recriar o AudioTrack com um AudioFormat diferente. Faça isso se uma codificação de qualidade superior for compatível ou se a codificação usada anteriormente não for mais compatível.

Exemplo de código

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);