오디오 기능

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

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

앱은 동일한 콘텐츠를 여러 인코딩으로 출력하여 오디오 기기 기능에 따라 사용자에게 최상의 오디오 환경을 제공할 수 있습니다. 예를 들어 TV에서 지원하는 경우 Dolby Digital로 인코딩된 오디오 스트림이 재생되는 반면, Dolby Digital이 지원되지 않을 때 더 광범위하게 지원되는 PCM 오디오 스트림이 선택됩니다. 오디오 스트림을 PCM으로 변환하는 데 사용되는 내장 Android 디코더 목록은 지원되는 미디어 형식에서 확인할 수 있습니다.

재생 시 스트리밍 앱은 출력 오디오 기기에서 지원하는 최상의 AudioFormatAudioTrack를 생성해야 합니다.

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

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

지원되는 인코딩 가져오기

getAudioProfiles()(API 수준 31 이상) 또는 getEncodings()(API 수준 23 이상)을 사용하여 기본 오디오 기기에서 사용 가능한 오디오 형식을 확인합니다.

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

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

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

예상 오디오 경로

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 인스턴스가 생성됩니다.

오디오 트랙 만들기

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

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

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

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