오디오 기능

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
}

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 객체 목록을 반환합니다. 선호 AudioFormat 항목의 목록은 일치하는 지원되는 AudioProfile를 찾을 때까지 반복됩니다. 이 AudioProfilebestAudioProfile로 저장됩니다. 최적의 샘플링 레이트와 채널 마스크는 bestAudioProfile에서 결정됩니다. 마지막으로 적절한 AudioFormat 인스턴스를 만듭니다.

오디오 트랙 만들기

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

오디오 기기 변경사항 가로채기

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

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

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