오디오 기능

Android TV는 여러 동시 오디오 출력(TV 스피커, HDMI 홈 시네마, 블루투스 등)을 지원합니다. 이러한 기기는 다양한 인코딩 (예: Dolby Digital+, DTS, PCM), 샘플링 레이트, 채널을 지원합니다. HDMI로 연결된 TV는 여러 형식을 지원하지만 블루투스는 PCM만 지원합니다.

재생 중에 사용 가능한 오디오 기기 및 라우팅이 변경될 수 있습니다. 사용자가 HDMI를 핫플러그하거나 블루투스를 연결하거나 설정을 수정할 수 있습니다. 앱은 새 기기의 기능을 사용하여 지속적인 재생을 보장하도록 적절하게 조정해야 합니다. 지원되지 않는 형식은 오류 또는 무음을 유발할 수 있습니다.

기기 기능에 따라 최상의 환경을 제공하는 여러 인코딩을 제공합니다. 예를 들어 Dolby Digital이 지원되는 경우 사용되고, 그렇지 않으면 앱이 PCM으로 대체됩니다. 스트림을 PCM으로 변환하는 Android 디코더는 지원되는 미디어 형식을 참고하세요.

재생 시 스트리밍 앱은 출력 오디오 기기에서 지원하는 최상의 AudioFormat으로 AudioTrack을 만들어야 합니다.

올바른 형식으로 트랙 만들기

앱은 AudioTrack을 만들고 재생을 시작한 후 getRoutedDevice()를 호출하여 사운드를 재생할 기본 오디오 기기를 결정해야 합니다. 예를 들어 라우팅된 기기 및 오디오 기능을 결정하는 데만 사용되는 안전한 짧은 무음 PCM 인코딩 트랙일 수 있습니다.

지원되는 인코딩 가져오기

getAudioProfiles() (API 수준 31 이상) 또는 getEncodings() (API 수준 23 이상)를 사용하여 기본 오디오 기기에서 사용할 수 있는 오디오 형식을 결정합니다.

지원되는 오디오 프로필 및 형식 확인

AudioProfile (API 수준 31 이상) 또는 isDirectPlaybackSupported() (API 수준 29 이상)를 사용하여 지원되는 형식, 채널 수, 샘플링 레이트의 조합을 확인합니다.

일부 Android 기기는 출력 오디오 기기에서 지원하는 인코딩 외에 다른 인코딩을 지원할 수 있습니다. 이러한 추가 형식은 isDirectPlaybackSupported()를 통해 감지해야 합니다. 이러한 경우 오디오 데이터는 출력 오디오 기기에서 지원하는 형식으로 다시 인코딩됩니다. isDirectPlaybackSupported()를 사용하여 선택한 형식이 getEncodings()에서 반환된 목록에 없더라도 지원을 올바르게 확인합니다.

예상 오디오 라우트

Android 13 (API 수준 33)에서는 예상 오디오 라우트가 도입되었습니다. 기기 오디오 속성 지원을 예상하고 활성 오디오 기기의 트랙을 준비할 수 있습니다. getDirectPlaybackSupport()를 사용하여 현재 라우팅된 오디오 기기에서 지정된 형식 및 속성에 직접 재생이 지원되는지 확인할 수 있습니다.

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
}

또는 현재 라우팅된 오디오 기기를 통해 직접 미디어 재생에 지원되는 프로필을 쿼리할 수 있습니다. 여기에는 지원되지 않거나 예를 들어 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();
}

이 예에서 preferredFormatsAudioFormat 인스턴스 목록입니다. 목록에서 가장 선호하는 항목이 먼저 나오고 가장 선호하지 않는 항목이 마지막에 나오도록 정렬됩니다. getDirectProfilesForAttributes()는 제공된 AudioAttributes가 있는 현재 라우팅된 오디오 기기에 지원되는 AudioProfile 객체 목록을 반환합니다. 일치하는 지원되는 AudioProfile이 발견될 때까지 선호하는 AudioFormat 항목 목록이 반복됩니다. 이 AudioProfilebestAudioProfile로 저장됩니다. 최적의 샘플링 레이트와 채널 마스크는 bestAudioProfile에서 결정됩니다. 마지막으로 적절한 AudioFormat 인스턴스가 생성됩니다.

오디오 트랙 만들기

앱은 이 정보를 사용하여 기본 오디오 기기에서 지원하고 선택한 콘텐츠에 사용할 수 있는 최고 품질의 AudioFormatAudioTrack을 만들어야 합니다.

오디오 기기 변경 가로채기

오디오 기기 변경을 가로채고 이에 반응하려면 앱은 다음을 실행해야 합니다.

  • API 수준이 24 이상인 경우 OnRoutingChangedListener를 추가하여 오디오 기기 변경 (HDMI, 블루투스 등)을 모니터링합니다.
  • API 수준 23의 경우 AudioDeviceCallback을 등록하여 사용 가능한 오디오 기기 목록의 변경사항 을 수신합니다.
  • API 수준 21 및 22의 경우 HDMI 플러그 이벤트를 모니터링하고 브로드캐스트의 추가 데이터를 사용합니다.
  • AudioDeviceCallback이 아직 지원되지 않으므로 API 23 미만의 기기의 BluetoothDevice 상태 변경을 모니터링하는 BroadcastReceiver도 등록합니다.

AudioTrack의 오디오 기기 변경이 감지되면 앱은 업데이트된 오디오 기능을 확인하고 필요한 경우 다른 AudioFormat으로 AudioTrack을 다시 만들어야 합니다. 이제 더 높은 품질의 인코딩이 지원되거나 이전에 사용한 인코딩이 더 이상 지원되지 않는 경우 이 작업을 실행합니다.

샘플 코드

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)

자바

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