지연 시간은 신호가 시스템을 이동하는 데 걸리는 시간입니다. 오디오 앱과 관련된 일반적인 지연 시간 유형은 다음과 같습니다.
- 오디오 출력 지연 시간은 오디오 샘플이 앱에 의해 생성된 후부터 헤드폰 잭 또는 내장 스피커를 통해 재생될 때까지의 시간입니다.
- 오디오 입력 지연 시간은 마이크와 같은 기기의 오디오 입력 장치로 오디오 신호를 수신한 후부터 동일한 오디오 데이터를 앱에서 사용할 수 있을 때까지의 시간입니다.
왕복 지연 시간은 입력 지연 시간, 앱 처리 시간, 출력 지연 시간을 합한 것입니다.
- 터치 지연 시간은 사용자가 화면을 터치한 후부터 앱에서 터치 이벤트를 수신할 때까지의 시간입니다.
- 준비 지연 시간은 데이터가 처음 버퍼 큐에 추가되었을 때 오디오 파이프라인을 시작하는 데 걸리는 시간입니다.
이 페이지에서는 입력 및 출력 지연 시간이 짧은 오디오 앱을 개발하는 방법과 준비 지연 시간을 최소화하는 방법을 설명합니다.
지연 시간 측정
오디오 입력 및 출력 지연 시간을 따로 측정하려는 경우 간단한 테스트 회로와 오실로스코프를 사용하면 측정할 수는 있지만, 첫 번째 샘플을 오디오 경로로 보낸 시간을 정확하게 알아야 하므로 측정이 매우 어렵습니다. 왕복 오디오 지연 시간을 알고 있으면 대략적인 측정 방법(오디오 입력(및 출력) 지연 시간은 신호 처리를 하지 않는 경로를 통한 왕복 오디오 지연 시간의 절반에 해당)을 사용할 수 있습니다.
왕복 오디오 지연 시간은 기기 모델과 Android 빌드에 따라 큰 차이가 있습니다. Nexus 기기의 왕복 지연 시간을 개괄적으로 알아보려면 게시된 측정 방법을 읽어보세요.
왕복 오디오 지연 시간을 측정하려면 오디오 신호를 생성하여 그 신호를 수신하고, 이 신호를 전송한 뒤부터 수신하기까지의 시간을 측정하는 앱을 만들면 됩니다.
신호 처리를 최소화한 오디오 경로에서 지연 시간이 가장 짧기 때문에 오디오 루프백 동글을 사용하여 헤드셋 커넥터에서 테스트를 실행해도 됩니다.
지연 시간 최소화를 위한 권장사항
오디오 성능 검증
Android 호환성 정의 문서(CDD)에는 호환되는 Android 기기의 하드웨어 및 소프트웨어 요구사항이 나와 있습니다. 전체 호환성 프로그램에 관한 자세한 내용은 Android 호환성을, 실제 CDD 문서는 CDD를 참조하세요.
왕복 지연 시간은 뮤지션의 경우 대개 10ms가 필요하지만 CDD에는 20ms 이하로 지정되어 있습니다. 20ms를 사용하는 중요한 사용 사례가 있기 때문입니다.
현재 런타임 시 Android 기기의 모든 경로에서 오디오 지연 시간을 확인하는 API는 없습니다. 하지만 다음 하드웨어 기능 플래그를 사용하면 기기에서 지연 시간을 보장하고 있는지 확인할 수 있습니다.
android.hardware.audio.low_latency
는 연속적인 출력 지연 시간이 45ms 이하임을 나타냅니다.android.hardware.audio.pro
는 연속적인 왕복 지연 시간이 20ms 이하임을 나타냅니다.
위 플래그의 보고 기준은 CDD의 5.6 오디오 지연 시간 및 5.10 프로 오디오 섹션에 정의되어 있습니다.
자바에서 이러한 기능을 확인하는 방법은 다음과 같습니다.
Kotlin
val hasLowLatencyFeature: Boolean = packageManager.hasSystemFeature(PackageManager.FEATURE_AUDIO_LOW_LATENCY) val hasProFeature: Boolean = packageManager.hasSystemFeature(PackageManager.FEATURE_AUDIO_PRO)
자바
boolean hasLowLatencyFeature = getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUDIO_LOW_LATENCY); boolean hasProFeature = getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUDIO_PRO);
오디오 기능 간의 관계에서 android.hardware.audio.low_latency
기능은 android.hardware.audio.pro
의 전제조건입니다. 기기는 android.hardware.audio.low_latency
를 구현하고 android.hardware.audio.pro
는 구현하지 않을 수 있지만, 그 반대로는 불가능합니다.
오디오 성능 가정하지 않기
지연 시간 문제를 방지하려면 다음 사항을 염두에 두세요.
- 휴대기기에 사용되는 스피커와 마이크의 음향 효과가 일반적으로 좋을 것으로 가정하지 마세요. 스피커와 마이크의 크기가 작아 보통 음향 효과가 좋지 않으므로 음질을 개선하기 위해 신호 처리가 추가됩니다. 이 신호 처리로 인해 지연 시간이 발생합니다.
- 입력 및 출력 콜백이 동기화된다고 가정하지 마세요. 동시 입출력을 위해 각각에 별도의 버퍼 큐 완료 핸들러가 사용됩니다. 입력과 출력에서 동일한 샘플링 레이트를 사용하더라도 이러한 콜백의 상대적 순서 또는 오디오 시계의 동기화는 보장되지 않습니다. 애플리케이션에서 적절한 버퍼 동기화를 통해 데이터를 버퍼링해야 합니다.
- 실제 샘플링 레이트가 정격 샘플링 레이트와 정확히 일치한다고 가정하지 마세요. 예를 들어 정격 샘플링 레이트가 48,000Hz인 경우 오디오 시계는 운영체제
CLOCK_MONOTONIC
보다 약간 빠른 것이 정상입니다. 오디오 시계와 시스템 시계가 서로 다른 크리스털을 사용할 수 있기 때문입니다. - 특히 엔드포인트가 서로 다른 경로에 있는 경우 실제 재생 샘플링 레이트가 실제 캡처 샘플링 레이트와 정확히 일치한다고 가정하지 마세요. 예를 들어 기기에 있는 마이크에서 48,000Hz의 정격 샘플링 레이트로 캡처하여 USB 오디오에서 동일한 샘플링 레이트로 재생하는 경우 실제 샘플링 레이트는 서로 약간 다를 수 있습니다.
독립 오디오 시계로 인해 비동기 샘플링 레이트를 변환해야 할 수도 있습니다. 오디오 품질에 좋진 않지만 비동기 샘플링 레이트를 간단하게 변환하는 방법은 0 교차점에 가까운 샘플을 필요에 따라 복제하거나 삭제하는 것입니다. 보다 정교한 변환도 가능합니다.
입력 지연 시간 최소화
이 섹션에서는 내장 마이크 또는 외부 헤드셋 마이크로 녹음할 때 오디오 입력 지연 시간을 단축할 수 있는 권장사항을 제시합니다.
- 앱에서 입력을 모니터링하는 경우 첫 실행 시 헤드폰에 가장 적합이라고 화면에 표시하는 등의 방법을 통해 사용자에게 헤드셋을 사용하도록 권장하세요. 헤드셋을 사용한다고 해서 가장 짧은 지연 시간이 보장되는 것은 아닙니다. 오디오 경로에서 원치 않는 신호 처리를 삭제하려면 녹음할 때
VOICE_RECOGNITION
프리셋을 사용하는 등 추가 단계를 진행해야 할 수도 있습니다. - getProperty(String)에서 PROPERTY_OUTPUT_SAMPLE_RATE를 대상으로 보고한 정격 샘플링 레이트, 44,100Hz 및 48,000Hz를 처리할 수 있도록 대비하세요. 다른 샘플링 레이트도 가능하지만 매우 드뭅니다.
- getProperty(String)에서 PROPERTY_OUTPUT_FRAMES_PER_BUFFER를 대상으로 보고한 버퍼 크기를 처리할 수 있도록 대비하세요. 일반적인 버퍼 크기는 96, 128, 160, 192, 240, 256 또는 512개 프레임이지만 다른 값도 가능합니다.
출력 지연 시간 최소화
오디오 플레이어 생성 시 최적 샘플링 레이트 사용
가장 짧은 지연 시간을 실현하려면 기기의 최적 샘플링 레이트 및 버퍼 크기와 일치하는 오디오 데이터를 지정해야 합니다. 자세한 내용은 지연 시간 단축을 위한 디자인을 참조하세요.
자바에서는 다음 코드 예에서와 같이 AudioManager에서 최적의 샘플링 레이트를 얻을 수 있습니다.
Kotlin
val am = getSystemService(Context.AUDIO_SERVICE) as AudioManager val sampleRateStr: String? = am.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE) var sampleRate: Int = sampleRateStr?.let { str -> Integer.parseInt(str).takeUnless { it == 0 } } ?: 44100 // Use a default value if property not found
자바
AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE); String sampleRateStr = am.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE); int sampleRate = Integer.parseInt(sampleRateStr); if (sampleRate == 0) sampleRate = 44100; // Use a default value if property not found
최적의 샘플링 레이트를 알면 플레이어를 생성할 때 이를 적용할 수 있습니다. 다음 예에서는 OpenSL ES를 사용합니다.
// create buffer queue audio player void Java_com_example_audio_generatetone_MainActivity_createBufferQueueAudioPlayer (JNIEnv* env, jclass clazz, jint sampleRate, jint framesPerBuffer) { ... // specify the audio source format SLDataFormat_PCM format_pcm; format_pcm.numChannels = 2; format_pcm.samplesPerSec = (SLuint32) sampleRate * 1000; ... }
참고: samplesPerSec
은 채널당 샘플링 레이트(단위: mHz)를 나타냅니다(1Hz = 1000mHz).
최적의 버퍼 사이즈를 사용하여 오디오 데이터를 대기열에 추가
최적의 샘플링 레이트와 유사한 방법으로 AudioManager API를 사용하여 최적의 버퍼 크기를 구할 수 있습니다.
Kotlin
val am = getSystemService(Context.AUDIO_SERVICE) as AudioManager val framesPerBuffer: String? = am.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER) var framesPerBufferInt: Int = framesPerBuffer?.let { str -> Integer.parseInt(str).takeUnless { it == 0 } } ?: 256 // Use default
자바
AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE); String framesPerBuffer = am.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER); int framesPerBufferInt = Integer.parseInt(framesPerBuffer); if (framesPerBufferInt == 0) framesPerBufferInt = 256; // Use default
PROPERTY_OUTPUT_FRAMES_PER_BUFFER
속성은 하드웨어 추상화 계층(HAL) 버퍼에서 보유할 수 있는 오디오 프레임 수를 나타냅니다. 오디오 버퍼에 정확히 이 오디오 프레임 수의 배수가 포함되도록 구성해야 합니다. 올바른 수의 오디오 프레임을 사용하면 콜백이 일정한 간격으로 실행되어 잡음이 줄어듭니다.
HAL 버퍼 크기는 기기와 Android 빌드에 따라 달라지므로 하드코딩 값을 사용하기보다는 API를 사용하여 버퍼 크기를 확인하시기 바랍니다.
신호 처리와 관련된 출력 인터페이스 추가하지 않기
빠른 믹서에서는 다음 인터페이스만 지원됩니다.
- SL_IID_ANDROIDSIMPLEBUFFERQUEUE
- SL_IID_VOLUME
- SL_IID_MUTESOLO
다음 인터페이스는 신호 처리를 동반하고 빠른 트랙 요청이 거부되게 하므로 허용되지 않습니다.
- SL_IID_BASSBOOST
- SL_IID_EFFECTSEND
- SL_IID_ENVIRONMENTALREVERB
- SL_IID_EQUALIZER
- SL_IID_PLAYBACKRATE
- SL_IID_PRESETREVERB
- SL_IID_VIRTUALIZER
- SL_IID_ANDROIDEFFECT
- SL_IID_ANDROIDEFFECTSEND
플레이어를 생성할 때는 다음 예에서와 같이 빠른 인터페이스만 추가해야 합니다.
const SLInterfaceID interface_ids[2] = { SL_IID_ANDROIDSIMPLEBUFFERQUEUE, SL_IID_VOLUME };
지연 시간이 짧은 트랙 사용 여부 확인
다음 단계에 따라 지연 시간이 짧은 트랙을 성공적으로 가져왔는지 확인합니다.
- 앱을 시작한 후 다음 명령어를 실행합니다.
- 앱의 프로세스 ID를 기록해 둡니다.
- 이제 앱에서 일부 오디오를 재생합니다. 터미널에서 약 3초 동안 다음 명령어를 실행할 수 있습니다.
- 프로세스 ID를 스캔합니다. Name 열에 F가 있으면 프로세스 ID가 지연 시간이 짧은 트랙에 있는 것입니다(F는 빠른 트랙을 나타냄).
adb shell ps | grep your_app_name
adb shell dumpsys media.audio_flinger
준비 지연 시간 최소화
오디오 데이터를 큐에 처음 추가하면 기기 오디오 회로에서 준비하는 데 짧긴 하지만 어느 정도 시간이 걸립니다. 이러한 준비 지연 시간을 최소화하려면 다음 코드 예에서와 같이 무음이 포함된 오디오 데이터의 버퍼를 큐에 넣으면 됩니다.
#define CHANNELS 1 static short* silenceBuffer; int numSamples = frames * CHANNELS; silenceBuffer = malloc(sizeof(*silenceBuffer) * numSamples); for (i = 0; i<numSamples; i++) { silenceBuffer[i] = 0; }
오디오가 생성되어야 하는 시점에 실제 오디오 데이터가 포함된 버퍼를 큐에 넣도록 전환할 수 있습니다.
참고: 지속적으로 오디오를 출력하면 상당한 전력이 소비됩니다. onPause() 메서드의 출력을 중지하세요. 또한 일정 기간 동안 사용자 활동이 없으면 무음 출력을 일시 중지하는 것도 고려할 만합니다.
추가 샘플 코드
오디오 지연 시간을 보여주는 샘플 앱을 다운로드하려면 NDK 샘플을 참조하세요.