오디오 기능

Android TV 기기에는 TV 스피커, HDMI 연결 홈 시네마, 블루투스 헤드폰 등 여러 오디오 출력이 동시에 연결될 수 있습니다. 이러한 오디오 출력 장치는 인코딩 (Dolby Digital+, DTS, PCM), 샘플링 레이트, 채널과 같은 다양한 오디오 기능을 지원할 수 있습니다. 예를 들어 HDMI 연결 TV는 다양한 인코딩을 지원하는 반면 연결된 블루투스 헤드폰은 일반적으로 PCM만 지원합니다.

사용 가능한 오디오 기기 목록과 라우팅된 오디오 기기는 HDMI 기기를 핫플러그인하거나, 블루투스 헤드폰을 연결하거나 분리하거나, 사용자가 오디오 설정을 변경해도 변경될 수 있습니다. 앱이 미디어를 재생하는 중에도 오디오 출력 기능이 변경될 수 있으므로 앱은 이러한 변경사항에 적절하게 적응하고 새롭게 라우팅된 오디오 기기 및 기능에서 재생을 계속해야 합니다. 잘못된 오디오 형식을 출력하면 오류가 발생하거나 소리가 재생되지 않을 수 있습니다.

앱은 여러 인코딩으로 동일한 콘텐츠를 출력하여 오디오 기기 기능에 따라 사용자에게 최상의 오디오 환경을 제공할 수 있습니다. 예를 들어 TV에서 지원하는 경우 Dolby Digital 인코딩 오디오 스트림이 재생되고, 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
}

자바

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

자바

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에서 오디오 기기 변경사항이 감지되면 앱은 업데이트된 오디오 기능을 확인하고 필요한 경우 다른 AudioFormatAudioTrack를 다시 만들어야 합니다. 이제 더 높은 품질의 인코딩이 지원되거나 이전에 사용한 인코딩이 더 이상 지원되지 않는 경우 이렇게 하세요.

샘플 코드

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