OpenSL ES 프로그래밍 노트

이 섹션의 노트는 OpenSL ES 1.0.1 사양을 보완하는 내용입니다.

객체 및 인터페이스 초기화

신입 개발자들이 OpenSL ES 프로그래밍 모델을 어렵게 생각하는 것은 객체와 인터페이스의 구분 및 초기화 시퀀스라는 두 가지 측면 때문입니다.

간단하게 설명하면 OpenSL ES 객체는 연결된 인터페이스를 통해서만 볼 수 있다는 점을 제외하면 자바 및 C++ 같은 프로그래밍 언어의 객체 개념과 유사합니다. OpenSL ES에는 모든 객체의 초기 인터페이스인 SLObjectItf가 포함되어 있습니다. 객체 자체의 핸들은 없고 객체의 SLObjectItf 인터페이스에 관한 핸들만 있습니다.

SLObjectItf를 반환하는 OpenSL ES 객체를 먼저 생성한 후에 실현합니다. 이는 객체를 먼저 생성(메모리 부족이나 잘못된 매개변수 외의 이유로 실패할 확률 없음)한 다음 초기화를 완료(리소스 부족으로 실패할 수 있음)하는 일반적인 프로그래밍 패턴과 유사합니다. 실현 단계에서는 필요한 경우 추가 리소스를 할당할 논리적 공간을 구현에 제공합니다.

객체를 생성하는 API에서 애플리케이션은 나중에 가져올 인터페이스의 배열을 지정합니다. 이 배열은 인터페이스를 자동으로 가져오는 게 아니라 나중에 가져오겠다고 명시한 것에 불과합니다. 인터페이스는 암시적 인터페이스명시적 인터페이스로 구분됩니다. 명시적 인터페이스는 나중에 가져올 수 있도록 배열에 나열해야 합니다. 암시적 인터페이스는 객체 생성 배열에 나열할 필요는 없지만 나열해도 무관합니다. OpenSL ES에는 동적 인터페이스라고 하는 인터페이스 종류가 하나 더 있습니다. 동적 인터페이스는 객체 생성 배열에 지정할 필요가 없으며 객체를 생성한 후에 추가할 수 있습니다. Android 구현은 이러한 복잡성을 해소하기 위해 편리한 기능을 제공합니다. 이 기능은 객체 생성 시 동적 인터페이스에 설명되어 있습니다.

객체를 생성 및 실현한 후 애플리케이션은 초기 SLObjectItf에서 GetInterface를 사용하여 필요한 각 기능에 맞는 인터페이스를 가져옵니다.

마지막으로 객체는 인터페이스를 통해서만 사용할 수 있으며 일부 객체의 경우에는 추가 설정이 필요합니다. 특히 URI 데이터 소스가 포함된 오디오 플레이어에서 연결 오류를 감지하려면 추가 준비 작업이 필요합니다. 자세한 내용은 오디오 플레이어 미리 가져오기 섹션을 참조하세요.

애플리케이션에서 객체 작업을 완료한 후에는 이 객체를 명시적으로 삭제해야 합니다. 아래 삭제 섹션을 참조하세요.

오디오 플레이어 미리 가져오기

URI 데이터 소스가 포함된 오디오 플레이어의 경우 Object::Realize는 리소스를 할당하지만 데이터 소스에 연결(준비)하거나 데이터 미리 가져오기를 시작하지는 않습니다. 이 작업은 플레이어 상태가 SL_PLAYSTATE_PAUSED 또는 SL_PLAYSTATE_PLAYING으로 설정되어야 실행됩니다.

이 시퀀스에는 비교적 늦게까지 알 수 없는 정보도 있습니다. 특히 처음에 Player::GetDurationSL_TIME_UNKNOWN을 반환하고 MuteSolo::GetChannelCount는 채널 수를 0으로 반환하거나 오류 결과인 SL_RESULT_PRECONDITIONS_VIOLATED를 반환합니다. 속성 정보를 파악한 후 API는 적합한 값을 반환합니다.

처음에 알 수 없는 기타 속성에는 샘플링 레이트, 콘텐츠 헤더 검사에 기반한 실제 미디어 콘텐츠 유형 등이 있습니다(애플리케이션 지정 MIME 유형 및 컨테이너 유형과는 반대). 이러한 속성은 준비/미리 가져오기 도중에 늦게 파악되기도 하지만 API에서는 이들 속성을 검색하지 않습니다.

미리 가져오기 상태 인터페이스는 모든 정보를 이용할 수 있는 경우 또는 애플리케이션에서 주기적인 폴링이 가능한 경우를 확인하는 데 유용합니다. 스트리밍 MP3의 시간과 같은 일부 정보는 끝까지 알지 못할 수도 있습니다.

미리 가져오기 상태 인터페이스는 오류를 감지하는 데도 매우 유용합니다. 콜백을 등록하고 최소한 SL_PREFETCHEVENT_FILLLEVELCHANGESL_PREFETCHEVENT_STATUSCHANGE 이벤트를 사용 설정합니다. 두 이벤트가 동시에 전달되어 PrefetchStatus::GetFillLevel이 0레벨을, PrefetchStatus::GetPrefetchStatusSL_PREFETCHSTATUS_UNDERFLOW를 보고하면 데이터 소스에 복구할 수 없는 오류가 있음을 나타냅니다. 이는 로컬 파일 이름이 없거나 네트워크 URI가 유효하지 않기 때문에 데이터 소스에 연결할 수 없다는 의미이기도 합니다.

OpenSL ES의 다음 버전에서는 데이터 소스의 오류 처리를 위한 명시적 지원이 추가될 예정입니다. 하지만 향후 바이너리 호환성을 위해 복구할 수 없는 오류를 보고하는 현재 메서드는 계속 지원합니다.

요약하면 권장 코드 시퀀스는 다음과 같습니다.

  1. Engine::CreateAudioPlayer
  2. Object:Realize
  3. SL_IID_PREFETCHSTATUS Object::GetInterface
  4. PrefetchStatus::SetCallbackEventsMask
  5. PrefetchStatus::SetFillUpdatePeriod
  6. PrefetchStatus::RegisterCallback
  7. SL_IID_PLAY Object::GetInterface
  8. Play::SetPlayState - SL_PLAYSTATE_PAUSED 또는 SL_PLAYSTATE_PLAYING

참고: 준비 및 미리 가져오기가 실행되는 동안 주기적 상태 업데이트와 함께 콜백이 호출됩니다.

삭제

애플리케이션을 종료할 때 모든 객체를 삭제하세요. 종속 객체가 있는 객체를 삭제하는 것은 안전하지 않으므로 객체는 생성 순서와 반대로 삭제해야 합니다. 예를 들어 오디오 플레이어 및 레코더, 출력 믹스, 엔진 순서로 삭제합니다.

OpenSL ES는 인터페이스의 자동 가비지 컬렉션 또는 참조 계산을 지원하지 않습니다. Object::Destroy를 호출하면 연결된 객체에서 파생된 현존하는 인터페이스가 모두 정의되지 않은 상태로 바뀝니다.

Android OpenSL ES 구현은 그와 같은 인터페이스가 잘못 사용되어도 이를 감지하지 못합니다. 객체가 삭제된 후에도 그러한 인터페이스를 계속 사용하면 애플리케이션이 비정상 종료될 수도 있고 예기치 못한 방식으로 작동할 수도 있습니다.

객체 삭제 시퀀스에서 기본 객체 인터페이스와 연결된 모든 인터페이스를 명시적으로 NULL로 설정하는 것이 좋습니다. 이렇게 하면 오래된 인터페이스 핸들을 실수로 사용하는 일을 방지할 수 있습니다.

스테레오 패닝

Volume::EnableStereoPosition을 사용하여 모노 소스의 스테레오 패닝을 사용 설정하면 총 음력 레벨이 3-dB 감소합니다. 소스가 특정 채널에서 다른 채널로 패닝될 때 음력 레벨을 일정하게 유지하기 위해서는 이렇게 해야 합니다. 따라서 필요한 경우에만 스테레오 포지셔닝을 사용 설정해야 합니다. 자세한 내용은 오디오 패닝에 관한 위키백과 문서를 참조하세요.

콜백 및 스레드

콜백 핸들러는 일반적으로 구현이 이벤트를 감지할 때 동시에 호출됩니다. 이 지점은 애플리케이션에서 비동기이기 때문에 비차단 동기화 메커니즘을 사용하여 애플리케이션과 콜백 핸들러가 공유하는 모든 변수 액세스를 제어해야 합니다. 예시 코드의 버퍼 큐와 같은 경우에는 편의를 위해 이러한 동기화를 생략하거나 차단 동기화를 사용했습니다. 하지만 모든 프로덕션 코드에는 적절한 비차단 동기화를 사용하는 것이 중요합니다.

콜백 핸들러는 Android 런타임에 연결되지 않은 내부의 비 애플리케이션 스레드에서 호출되기 때문에 JNI를 사용할 수 없습니다. 이러한 내부 스레드는 OpenSL ES 구현의 무결성을 유지하는 데 매우 중요하기 때문에 콜백 핸들러도 작업을 과도하게 차단하거나 실행해서는 안 됩니다.

콜백 핸들러가 JNI를 사용하거나 콜백에 맞지 않는 작업을 실행해야 하는 경우 대신 핸들러는 다른 스레드가 처리할 이벤트를 게시해야 합니다. 허용 가능한 콜백 워크로드에는 다음 출력 버퍼를 렌더링하고 큐에 추가(AudioPlayer의 경우), 방금 채워진 입력 버퍼를 처리하고 비어 있는 다음 버퍼를 큐에 추가(AudioRecorder의 경우), 단순 API(예: 대부분의 Get 그룹) 등이 있습니다. 워크로드에 관한 내용은 아래 성능 섹션을 참조하세요.

반대 경우는 안전합니다. 즉, JNI를 시작한 Android 애플리케이션 스레드는 이를 차단하는 API를 비롯해 OpenSL ES API를 직접 호출할 수 있습니다. 하지만 메인 스레드에서 호출을 차단하면 ANR(애플리케이션 응답 없음) 오류가 발생할 수 있으므로 차단하지 않는 것이 좋습니다.

콜백 핸들러를 호출하는 스레드는 대체로 구현에 따라 판단됩니다. 이러한 유연성이 주어지는 이유는 특히 멀티코어 기기에서 향후 최적화가 가능하도록 하기 위해서입니다.

콜백 핸들러가 실행되는 스레드에서 여러 호출의 ID가 모두 동일하다고 보장할 수는 없습니다. 그러므로 호출 전체의 일관성 유지를 위해 pthread_self()가 반환하는 pthread_t 또는 gettid()가 반환하는 pid_t에만 의존하지 마세요. 같은 이유로 콜백의 pthread_setspecific()pthread_getspecific() 같은 스레드 로컬 저장소(TLS) API도 사용하지 않는 것이 좋습니다.

이 구현은 동일한 객체에 관해 동일한 종류의 동시 콜백이 발생하지 않도록 보장합니다. 하지만 여러 스레드에서 동일한 객체에 관해 서로 다른 종류의 동시 콜백이 발생할 수는 있습니다.

성능

OpenSL ES는 네이티브 C API이기 때문에 OpenSL ES를 호출하는 비 런타임 애플리케이션 스레드에는 가비지 컬렉션 일시중지와 같은 런타임 관련 오버헤드가 없습니다. OpenSL ES를 사용한다고 해도 아래에 설명된 한 가지 예외를 제외하면 성능 면에서 추가적인 이점은 없습니다. 특히 OpenSL ES를 사용해도 오디오 지연 시간이 단축되고 플랫폼에서 일반적으로 제공하는 것보다 예약 우선순위가 높아지는 등의 기능 향상이 보장되지는 않습니다. 반면 Android 플랫폼과 특정 기기 구현이 꾸준히 발전함에 따라 OpenSL ES 애플리케이션은 향후 시스템 성능 개선으로 인한 혜택을 누릴 수 있을 것으로 예상됩니다.

이와 같은 발전된 사항 중 하나가 단축된 오디오 출력 지연 시간 지원입니다. 이는 단축된 출력 지연 시간에 관한 기본 개념을 Android 4.1(API 수준 16)에 처음 도입한 이후 Android 4.2(API 수준 17)에서 계속해서 발전해 나간 결과입니다. 이러한 개선사항은 android.hardware.audio.low_latency 기능을 필요로 하는 기기 구현을 위한 OpenSL ES를 통해 이용할 수 있습니다. 기기가 이 기능을 요구하지는 않지만 Android 2.3(API 수준 9) 이상을 지원하는 경우 OpenSL ES API를 사용할 수 있더라도 출력 지연 시간이 길 수 있습니다. 애플리케이션이 기기의 네이티브 출력 구성과 호환되는 버퍼 크기와 샘플링 레이트를 요청한 경우에만 짧은 출력 지연 시간 경로가 사용됩니다. 이 매개변수는 기기에 따라 다르기 때문에 아래 설명된 방법으로 가져와야 합니다.

Android 4.2(API 수준 17)부터 애플리케이션은 기기의 기본 출력 스트림에 관한 플랫폼 네이티브 또는 최적 출력 샘플링 레이트와 버퍼 크기를 쿼리할 수 있습니다. 쿼리 기능을 앞서 언급한 기능 테스트와 함께 사용하면 지원을 요구하는 기기에서 앱이 짧은 출력 지연 시간에 적절한 설정을 자체적으로 구성할 수 있습니다.

Android 4.2(API 수준 17) 이하에서는 짧은 지연 시간에 버퍼 수가 두 개 이상이어야 합니다. Android 4.3(API 수준 18)부터는 짧은 지연 시간에 버퍼 수가 한 개면 충분합니다.

모든 출력 이펙트용 OpenSL ES 인터페이스는 짧은 지연 시간 경로를 제외합니다.

권장 시퀀스는 다음과 같습니다.

  1. API 수준 9 이상에서 OpenSL ES를 사용하는지 확인합니다.
  2. 다음과 같은 코드를 사용하여 android.hardware.audio.low_latency 기능을 확인합니다.

    Kotlin

    import android.content.pm.PackageManager
    ...
    val pm: PackageManager = context.packageManager
    val claimsFeature: Boolean = pm.hasSystemFeature(PackageManager.FEATURE_AUDIO_LOW_LATENCY)
    

    자바

    import android.content.pm.PackageManager;
    ...
    PackageManager pm = getContext().getPackageManager();
    boolean claimsFeature = pm.hasSystemFeature(PackageManager.FEATURE_AUDIO_LOW_LATENCY);
    
  3. API 수준 17 이상에서 android.media.AudioManager.getProperty()를 사용하는지 확인합니다.
  4. 다음과 같은 코드를 사용하여 이 기기의 기본 출력 스트림에 관한 네이티브 또는 최적 출력 샘플링 레이트와 버퍼 크기를 가져옵니다.

    Kotlin

    import android.media.AudioManager
    ...
    val am = getSystemService(Context.AUDIO_SERVICE) as AudioManager
    val sampleRate: String = am.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE)
    val framesPerBuffer: String = am.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER)
    

    자바

    import android.media.AudioManager;
    ...
    AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
    String sampleRate = am.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE);
    String framesPerBuffer = am.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER);
    
    sampleRateframesPerBuffer문자열입니다. 먼저 null인지 확인한 다음 Integer.parseInt()를 사용하여 int 유형으로 변환합니다.
  5. 이제 OpenSL ES를 사용하여 PCM 버퍼 큐 데이터 로케이터가 있는 AudioPlayer를 생성합니다.

참고: 오디오 버퍼 크기 테스트 앱을 사용하여 오디오 기기의 OpenSL ES 오디오 애플리케이션에 관한 네이티브 버퍼 크기 및 샘플링 레이트를 확인할 수 있습니다. 또한 GitHub에서 오디오 버퍼 크기 샘플도 볼 수 있습니다.

지연 시간이 짧은 오디오 플레이어의 수는 제한적입니다. 애플리케이션에 많은 오디오 소스가 필요하다면 애플리케이션 수준의 오디오 믹싱을 고려해 보세요. 오디오 플레이어는 다른 앱과 공유하는 전역 리소스이므로 활동이 일시중지되면 오디오 플레이어를 삭제해야 합니다.

오디오 결함을 방지하려면 버퍼 큐 콜백 핸들러가 예측 가능한 단시간 내에 실행되어야 합니다. 이는 일반적으로 뮤텍스, 조건 또는 I/O 작업에 무제한 차단이 없다는 의미입니다. 대신 try locks, 제한 시간이 있는 lock 및 wait와 비차단 알고리즘을 고려해 보세요.

다음 버퍼를 렌더링하는 데 필요한 계산(AudioPlayer의 경우) 또는 이전 버퍼를 사용하는데 필요한 계산(AudioRecord의 경우)에는 각 콜백에 소요되는 시간이 대략 같은 수준이어야 합니다. 실행 소요 시간이 비확정적이거나 계산에 버스트가 많은 알고리즘은 피하세요. 지정된 콜백에 걸리는 CPU 시간이 평균 시간보다 훨씬 길면 콜백 연산이 버스트됩니다. 요약하면 핸들러의 CPU 실행 시간은 0에 가깝게 분산되어 있고 핸들러는 무제한 시간을 차단하지 않는 것이 가장 좋습니다.

지연 시간이 짧은 오디오는 다음 출력 장치에서만 이용할 수 있습니다.

일부 기기에서는 스피커 보정 및 보호를 위한 디지털 신호 처리로 인해 스피커 지연 시간이 다른 경로보다 깁니다.

Android 5.0(API 수준 21)부터 일부 기기에서 지연 시간이 짧은 오디오 입력이 지원됩니다. 이 기능을 활용하려면 먼저 위에서 설명한 대로 짧은 지연 시간 출력을 사용할 수 있는지 확인해야 합니다. 짧은 지연 시간 출력을 사용할 수 있어야만 짧은 지연 시간 입력 기능을 사용할 수 있습니다. 그런 다음 출력과 동일한 샘플링 레이트 및 버퍼 크기를 사용하여 AudioRecorder를 생성합니다. 입력 이펙트용 OpenSL ES 인터페이스는 짧은 지연 시간 경로를 제외합니다. 짧은 지연 시간에는 반드시 레코드 프리셋 SL_ANDROID_RECORDING_PRESET_VOICE_RECOGNITION을 사용해야 합니다. 이 프리셋은 입력 경로의 지연 시간을 늘릴 수 있는 기기별 디지털 신호 처리를 사용 중지합니다. 레코드 프리셋에 관한 자세한 내용은 위의 Android 구성 인터페이스 섹션을 참조하세요.

동시 입출력을 위해 각각에 별도의 버퍼 큐 완료 핸들러가 사용됩니다. 입력과 출력에서 동일한 샘플링 레이트를 사용하더라도 이러한 콜백의 상대적 순서 또는 오디오 시계의 동기화는 보장되지 않습니다. 애플리케이션에서 적절한 버퍼 동기화를 통해 데이터를 버퍼링해야 합니다.

독립 오디오 시계로 인해 비동기 샘플링 레이트를 변환해야 할 수도 있습니다. 오디오 품질에 좋진 않지만 비동기 샘플링 레이트를 간단하게 변환하는 방법은 0 교차점에 가까운 샘플을 필요에 따라 복제하거나 삭제하는 것입니다. 보다 정교한 변환도 가능합니다.

성능 모드

OpenSL ES는 Android 7.1(API 수준 25)부터 오디오 경로에 성능 모드를 지정할 방법을 도입했습니다. 옵션은 다음과 같습니다.

  • SL_ANDROID_PERFORMANCE_NONE: 특정한 성능 요구사항이 없습니다. 하드웨어 및 소프트웨어 이펙트를 허용합니다.
  • SL_ANDROID_PERFORMANCE_LATENCY: 지연 시간에 우선순위를 부여합니다. 하드웨어 또는 소프트웨어 이펙트가 없습니다. 이는 기본값입니다.
  • SL_ANDROID_PERFORMANCE_LATENCY_EFFECTS: 하드웨어 및 소프트웨어 이펙트를 허용하면서도 지연 시간에 우선순위를 부여합니다.
  • SL_ANDROID_PERFORMANCE_POWER_SAVING: 절전에 우선순위를 부여합니다. 하드웨어 및 소프트웨어 이펙트를 허용합니다.

참고: 짧은 지연 시간 경로가 필요하지 않고 기기의 내장 오디오 이펙트를 활용하려면(예를 들어 동영상 재생용 음향 품질 개선을 위해) 성능 모드는 명시적으로 SL_ANDROID_PERFORMANCE_NONE으로 설정해야 합니다.

성능 모드를 설정하려면 아래 표시된 것처럼 Android 구성 인터페이스를 사용하여 SetConfiguration을 호출해야 합니다.

  // Obtain the Android configuration interface using a previously configured SLObjectItf.
  SLAndroidConfigurationItf configItf = nullptr;
  (*objItf)->GetInterface(objItf, SL_IID_ANDROIDCONFIGURATION, &configItf);

  // Set the performance mode.
  SLuint32 performanceMode = SL_ANDROID_PERFORMANCE_NONE;
    result = (*configItf)->SetConfiguration(configItf, SL_ANDROID_KEY_PERFORMANCE_MODE,
                                                     &performanceMode, sizeof(performanceMode));

보안 및 권한

누가 어떤 작업을 할 수 있는지와 관련해서 Android의 보안 기능은 프로세스 수준에서 작동합니다. 자바 프로그래밍 언어 코드는 네이티브 코드 이상의 작업을 할 수 없으며 네이티브 코드도 자바 프로그래밍 언어 코드 이상의 작업을 할 수 없습니다. 두 코드 간 차이점은 이용할 수 있는 API의 종류뿐입니다.

OpenSL ES를 사용하는 애플리케이션은 유사한 비 네이티브 API에 필요한 권한을 요청해야 합니다. 예를 들면 애플리케이션이 오디오를 레코딩하는 경우에는 android.permission.RECORD_AUDIO 권한이 필요합니다. 오디오 이펙트를 사용하는 애플리케이션에는 android.permission.MODIFY_AUDIO_SETTINGS가, 네트워크 URI 리소스를 재생하는 애플리케이션에는 android.permission.NETWORK가 필요합니다. 자세한 내용은 시스템 권한 사용을 참조하세요.

플랫폼 버전과 구현에 따라 미디어 콘텐츠 파서와 소프트웨어 코덱은 OpenSL ES를 호출하는 Android 애플리케이션의 컨텍스트 내에서 실행될 수 있습니다(하드웨어 코덱은 추상화되지만 기기의 영향을 받습니다). 파서와 코덱의 취약점을 이용하는 잘못된 형식의 콘텐츠는 알려진 공격 벡터입니다. 신뢰할 수 있는 소스의 미디어만 재생하거나, 신뢰할 수 없는 소스의 미디어를 처리하는 코드는 상대적으로 샌드박스 환경에서 실행되도록 애플리케이션을 분할하는 것이 좋습니다. 예를 들면 신뢰할 수 없는 소스의 미디어를 별도의 프로세스에서 처리할 수 있습니다. 두 프로세스가 계속 동일한 UID에서 실행된다고 해도 이렇게 별도로 처리하면 공격하기가 더 어려워집니다.