Notas de programación de OpenSL ES

Las notas de esta sección complementan la especificación de OpenSL ES 1.0.1.

Inicialización de objetos y de la interfaz

Dos aspectos del modelo de programación OpenSL ES que pueden no ser tan conocidos para los nuevos desarrolladores son la distinción entre interfaces y objetos, y la secuencia de inicialización.

En pocas palabras, un objeto OpenSL ES es similar al concepto de objeto en los lenguajes de programación como Java y C++, con la excepción de que un objeto OpenSL ES solo puede visualizarse a través de sus interfaces asociadas, como la interfaz inicial para todos los objetos, llamada SLObjectItf. No hay controladores para un objeto, solo un controlador para la interfaz SLObjectItf del objeto.

Primero debe crearse un objeto OpenSL ES, que muestra un SLObjectItf, y luego debe materializarse. Esto es similar al patrón de programación común que consiste en primero construir un objeto (que nunca debe fallar, excepto por falta de memoria o parámetros no válidos) y, luego, completar la inicialización (que puede fallar por falta de recursos). El paso de materialización otorga a la implementación una ubicación lógica para asignar recursos adicionales, si fuera necesario.

Como parte de la API para crear un objeto, una aplicación especifica un arreglo de interfaces deseadas que prevé adquirir más adelante. Ten en cuenta que ese arreglo no adquiere las interfaces automáticamente; solo indica una intención de adquirirlas en el futuro. Las interfaces se diferencian como implícitas o explícitas. Una interfaz explícita se debe indicar en el arreglo si se prevé adquirirla más adelante. No es necesario indicar una interfaz implícita en el arreglo de creación de objetos, aunque hacerlo no generará inconvenientes. OpenSL ES tiene una clase más de interfaz llamada dinámica, que no es necesario especificar en el arreglo de creación de objetos y se puede agregar más adelante, después de crear un objeto. La implementación de Android proporciona una función práctica para evitar esta complejidad que se describe en Interfaces dinámicas en la creación de objetos.

Después de crear y materializar el objeto, la aplicación debe adquirir interfaces para cada función que necesite usando GetInterface en el SLObjectItf inicial.

Por último, el objeto estará disponible para usarse a través de sus interfaces, aunque debes tener en cuenta que algunos objetos requieren configuración adicional. En particular, un reproductor de audio con fuente de datos de URI requiere un poco más de preparación para poder detectar errores de conexión. Consulta la sección Precarga del reproductor de audio para obtener más información.

Una vez que tu aplicación finalice con el objeto, debes destruirlo explícitamente; consulta la sección Destrucción a continuación.

Carga previa del reproductor de audio

Para un reproductor de audio con fuente de datos de URI, Object::Realize asigna recursos, pero no se conecta a la fuente de datos (preparación) ni comienza la precarga de datos. Esto ocurre una vez que el estado del reproductor se establece en SL_PLAYSTATE_PAUSED o SL_PLAYSTATE_PLAYING.

Es posible que parte de la información no se conozca hasta mucho más adelante en esta secuencia. Inicialmente, Player::GetDuration devuelve SL_TIME_UNKNOWN y MuteSolo::GetChannelCount muestra correctamente un recuento de canales de cero o el resultado del error SL_RESULT_PRECONDITIONS_VIOLATED. Estas APIs muestran los valores correspondientes una vez que se conocen.

Entre otras propiedades que se desconocen inicialmente se incluyen la tasa de muestreo y el tipo de contenido multimedia real en función del análisis del encabezado del contenido (contrariamente al tipo MIME y de contenedor especificados por la aplicación). Estas propiedades también se determinan más adelante durante la preparación y precarga, pero no hay APIs para recuperarlas.

La interfaz con estado de precarga es útil para detectar cuándo está disponible toda la información, o tu aplicación puede sondear periódicamente. Ten en cuenta que parte de la información, como la duración de una transmisión en MP3, podría no conocerse nunca.

La interfaz con estado de precarga también es útil para detectar errores. Registra una devolución de llamada y habilita, al menos, los eventos SL_PREFETCHEVENT_FILLLEVELCHANGE y SL_PREFETCHEVENT_STATUSCHANGE. Si ambos eventos se entregan simultáneamente, PrefetchStatus::GetFillLevel informa un nivel de cero y PrefetchStatus::GetPrefetchStatus informa SL_PREFETCHSTATUS_UNDERFLOW; esto indica un error no recuperable en la fuente de datos. Esto incluye la incapacidad para establecer conexión con la fuente de datos debido a que el nombre del archivo local no existe o el URI de la red no es válido.

Se prevé que la próxima versión de OpenSL ES incluya más compatibilidad explícita para administrar errores en la fuente de datos. No obstante, para lograr la compatibilidad con objetos binarios en el futuro, tenemos la intención de continuar admitiendo el método actual para informar un error no recuperable.

En resumen, la secuencia de código recomendada es la siguiente:

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

Nota: La preparación y la carga previa tienen lugar aquí. Durante este tiempo, se llama a tu devolución de llamada con actualizaciones de estado periódicas.

Destrucción

Asegúrate de destruir todos los objetos cuando salgas de tu aplicación. Los objetos deben destruirse en el orden inverso de su creación, ya que no es seguro destruir un objeto que tenga otros dependientes. Por ejemplo, destruye los objetos en este orden: reproductores y grabadores de audio, combinación de salida y, por último, el motor.

OpenSL ES no admite la recolección automática de elementos no utilizados ni el recuento de referencia de interfaces. Una vez que llamas a Object::Destroy, todas las interfaces existentes que derivan del objeto asociado se vuelven indefinidas.

La implementación de OpenSL ES para Android no detecta el uso incorrecto de estas interfaces. Si continúas usando las interfaces una vez que el objeto se destruyó, la aplicación puede fallar o comportarse de manera imprevista.

Te recomendamos que configures de forma explícita la interfaz de objeto principal y todas las interfaces asociadas en NULL como parte de tu secuencia de destrucción de objetos, lo que evita el uso inadecuado accidental de un controlador de interfaz inactiva.

Distribución de sonido estéreo

Cuando se usa Volume::EnableStereoPosition para habilitar la distribución de sonido estéreo de un origen mono, se produce una reducción de 3 dB en el nivel total de potencia del sonido. Esto es necesario para permitir que el nivel total de potencia del sonido permanezca constante mientras el origen se desplaza de un canal a otro. Por lo tanto, solo debes habilitar el posicionamiento estéreo si lo necesitas. Para obtener más información, consulta el artículo de Wikipedia sobre distribución de sonido estéreo.

Devoluciones de llamadas y subprocesos

Los controladores de devoluciones de llamada generalmente se llaman de forma síncrona cuando la implementación detecta un evento. Este punto es asíncrono con respecto a la aplicación, de modo que debes usar un mecanismo de sincronización que no provoque bloqueos para controlar el acceso a cualquiera de las variables compartidas entre la aplicación y el controlador de devolución de llamada. En el ejemplo de código, como en el caso de las colas de búfer, omitimos esta sincronización o usamos sincronización con bloqueo para que resulte más simple. No obstante, la sincronización adecuada sin bloqueo es crítica para cualquier código de producción.

Los controladores de devoluciones de llamada reciben llamadas de subprocesos internos que no pertenecen a la aplicación y que no están conectados con el tiempo de ejecución de Android, por lo cual no pueden usar JNI. Debido a que estos subprocesos internos son críticos para la integridad de la implementación de OpenSL ES, un controlador de devolución de llamada tampoco debe bloquear ni realizar un trabajo excesivo.

Si tu controlador de devolución de llamada necesita usar JNI o ejecutar trabajo que no es proporcional a la devolución de llamada, el controlador debe publicar en cambio un evento para el procesamiento de otro subproceso. Entre algunos ejemplos de carga de trabajo de devolución de llamada aceptable se incluyen el procesamiento y la disposición en cola del siguiente búfer de salida (para un objeto AudioPlayer), el procesamiento del búfer de entrada recién cargado y la disposición en cola del siguiente búfer vacío (para un objeto AudioRecorder), o API sencillas como la mayoría de las que componen la familia Get. A continuación, consulta la sección Rendimiento relacionada con la carga de trabajo.

Ten en cuenta que lo opuesto es lo seguro: un subproceso de una aplicación para Android que ingresó en JNI puede llamar directamente a las API de OpenSL ES, incluidas aquellas que provocan bloqueos. No obstante, no se recomienda realizar llamadas de bloqueo desde el subproceso principal, ya que pueden mostrar el error Aplicación no responde (ANR).

La determinación del subproceso que llama a un controlador de devolución de llamada depende en gran medida de la implementación. El motivo de esta flexibilidad es permitir futuras optimizaciones, en especial en dispositivos con varios núcleos.

No se garantiza que el subproceso en el cual se ejecute el controlador de devolución de llamadas tenga la misma identidad en las diferentes llamadas. Por lo tanto, no confíes en que el objeto pthread_t mostrado por pthread_self() o el objeto pid_t mostrado por gettid() sean coherentes en todas las llamadas. Por el mismo motivo, no uses las APIs de almacenamiento local de subprocesos (TLS), como pthread_setspecific() y pthread_getspecific() de una devolución de llamada.

La implementación garantiza que no tengan lugar devoluciones de llamada simultáneas del mismo tipo para el mismo objeto. Sin embargo, sí pueden tener lugar devoluciones de llamada simultáneas de diferentes tipos para el mismo objeto en diferentes subprocesos.

Rendimiento

Dado que OpenSL ES es una API C nativa, los subprocesos de una aplicación que no se ejecuta en el tiempo de ejecución y que llaman a OpenSL ES no tienen una sobrecarga relacionada con el tiempo de ejecución, como pausas provocadas por la recolección de elementos no utilizados. Salvo en el caso de la excepción que se describe a continuación, el uso de OpenSL ES no ofrece otros beneficios de rendimiento. En particular, el uso de OpenSL ES no garantiza mejoras, como una menor latencia de audio o una mayor prioridad de programación, que superen las que la plataforma proporciona generalmente. Por otro lado, dado que la plataforma de Android y las implementaciones para dispositivos específicos continúan evolucionando, una aplicación de OpenSL ES podrá beneficiarse de las futuras mejoras en el rendimiento del sistema.

Una de estas mejoras es la compatibilidad con una menor latencia de salida de audio. Los fundamentos para la menor latencia de salida se introdujeron por primera vez en Android 4.1 (nivel de API 16) y, luego, se avanzó con esta mejora en Android 4.2 (nivel de API 17). Estas mejoras están disponibles a través de OpenSL ES para implementaciones en dispositivos que indican tener la característica android.hardware.audio.low_latency. Si el dispositivo no indica que tiene esta característica, pero admite Android 2.3 (nivel de API 9) o versiones posteriores, puedes de todos modos usar las API de OpenSL ES, pero la latencia de salida puede ser mayor. La ruta de acceso de la menor latencia de salida se usa solo si la aplicación solicita un tamaño del búfer y una tasa de muestreo que sean compatibles con la configuración de salida nativa del dispositivo. Estos parámetros son específicos del dispositivo y deben obtenerse de la forma que se describe a continuación.

A partir de Android 4.2 (nivel de API 17), una aplicación puede consultar la tasa de muestreo de salida óptima o nativa de la plataforma y el tamaño del búfer para la transmisión de salida principal del dispositivo. Cuando se combina con la prueba de características recién mencionada, una aplicación ahora puede configurarse automáticamente de forma correcta para lograr una latencia reducida de salida en dispositivos que indican compatibilidad.

Para Android 4.2 (nivel de API 17) y versiones anteriores, se requiere un recuento de búferes de dos o más para lograr una latencia reducida. A partir de Android 4.3 (nivel de API 18), un recuento de búferes de uno es suficiente para obtener una latencia reducida.

Todas las interfaces OpenSL ES para efectos de salida excluyen la ruta de acceso de latencia reducida.

La secuencia recomendada es la siguiente:

  1. Comprueba que el nivel de API sea 9 o superior para confirmar el uso de OpenSL ES.
  2. Comprueba la presencia de la función android.hardware.audio.low_latency con código como el siguiente:

    Kotlin

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

    Java

    import android.content.pm.PackageManager;
    ...
    PackageManager pm = getContext().getPackageManager();
    boolean claimsFeature = pm.hasSystemFeature(PackageManager.FEATURE_AUDIO_LOW_LATENCY);
    
  3. Comprueba que el nivel de API sea 17 o mayor para confirmar el uso de android.media.AudioManager.getProperty().
  4. Obtén la tasa de muestreo de salida nativa u óptima y el tamaño del búfer de la transmisión de salida principal de este dispositivo usando código como el siguiente:

    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)
    

    Java

    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);
    
    Ten en cuenta que sampleRate y framesPerBuffer son strings. Primero comprueba la presencia de un valor null y luego conviértelo en int con Integer.parseInt().
  5. Luego, usa OpenSL ES para crear un objeto AudioPlayer con localizador de datos de la cola de búfer PCM.

Nota: Puedes usar la app de prueba Tamaño del búfer de audio con el objetivo de determinar el tamaño del búfer nativo y la tasa de muestreo para aplicaciones de audio de OpenSL ES en tu dispositivo de audio. También puedes visitar GitHub para ver ejemplos de tamaños del búfer de audio.

La cantidad de reproductores de audio con baja latencia es limitada. Si tu aplicación requiere más que unas pocas fuentes de audio, considera combinar tu audio en el nivel de la aplicación. Asegúrate de destruir tus reproductores de audio cuando se detenga la actividad, ya que son un recurso global que se comparte con otras apps.

Para evitar fallas audibles, el controlador de devolución de llamada de la cola de búfer debe ejecutarse en un período breve y previsible. Esto generalmente implica que no se producirán bloqueos desvinculados en exclusiones mutuas, condiciones ni operaciones de E/S. Como alternativa, considera usar bloqueos de intentos, bloqueos y esperas con tiempos de espera y algoritmos sin bloqueo.

El cálculo requerido a fin de procesar el siguiente búfer (para AudioPlayer) o para consumir el búfer anterior (para AudioRecord) debe tardar aproximadamente el mismo tiempo para cada devolución de llamada. Evita los algoritmos que se ejecuten en un tiempo no determinista o sean intermitentes en sus cálculos. Un cálculo de devolución de llamada es intermitente si el tiempo de CPU empleado en cualquier devolución de llamada es considerablemente mayor que el promedio. En resumen, lo ideal es que el tiempo de ejecución del controlador en la CPU tenga una variación cercana a cero, y que el controlador no bloquee otros tiempos desvinculados.

El audio de baja latencia solo es posible para estos dispositivos de salida:

  • Bocinas integradas en el dispositivo
  • Auriculares con cable
  • Auriculares con micrófono con cable
  • Línea de salida
  • Audio digital USB

En algunos dispositivos, la latencia de las bocinas es mayor que en otras vías debido al procesamiento de señales digitales para la corrección y protección de las bocinas.

A partir de Android 5.0 (nivel de API 21), se admite la entrada de audio de latencia reducida en algunos dispositivos. Para aprovechar esta función, primero confirma que esté disponible la salida de latencia reducida, como se describió previamente. La capacidad para generar una salida de audio de latencia reducida es un requisito previo para esta función. Luego, crea un objeto AudioRecorder con la misma tasa de muestreo y el mismo tamaño del búfer que usarías para la salida. Las interfaces de OpenSL ES para efectos de entrada excluyen la ruta de acceso de latencia reducida. Para la latencia reducida, se debe usar el ajuste predeterminado de grabación SL_ANDROID_RECORDING_PRESET_VOICE_RECOGNITION; este ajuste inhabilita el procesamiento de señales digitales específicas del dispositivo que podrían agregar latencia a la ruta de acceso de entrada. Para obtener más información sobre los ajustes predeterminados de grabación, consulta la sección previa Interfaz de configuración de Android.

Para lograr entrada y salida simultáneas, se usan controladores de finalización de la cola de búfer independientes para cada lado. No se garantiza el orden relativo de estas devoluciones de llamada ni la sincronización de los relojes de audio, incluso cuando ambos extremos usen la misma tasa de muestreo. Tu aplicación debe almacenar los datos en el búfer con la sincronización de búfer adecuada.

Una consecuencia de los relojes de audio posiblemente independientes es la necesidad de realizar una conversión asíncrona de la tasa de muestreo. Una técnica sencilla (aunque no ideal en términos de la calidad de audio) para la conversión asincrónica de la tasa de muestreo es duplicar muestras, o bien omitirlas, según sea necesario, cerca de un punto de cruce en cero. También se pueden realizar conversiones más sofisticadas.

Modos de rendimiento

A partir de Android 7.1 (API nivel 25), OpenSL ES presenta una forma de especificar el modo de rendimiento de la ruta de acceso de audio. Las opciones son las siguientes:

  • SL_ANDROID_PERFORMANCE_NONE: No se requiere un rendimiento específico. Permite efectos de hardware y software.
  • SL_ANDROID_PERFORMANCE_LATENCY: Se le da prioridad a la latencia. No hay efectos de hardware ni software. Este es el modo predeterminado.
  • SL_ANDROID_PERFORMANCE_LATENCY_EFFECTS: Se le da prioridad a la latencia y, a la vez, se mantienen los efectos de hardware y software.
  • SL_ANDROID_PERFORMANCE_POWER_SAVING: Se le da prioridad a la conservación de energía. Permite efectos de hardware y software.

Nota: Si no necesitas una ruta de acceso de baja latencia y quieres aprovechar los efectos de audio integrados del dispositivo (por ejemplo, para mejorar la calidad acústica de la reproducción de video), debes configurar explícitamente el modo de rendimiento en SL_ANDROID_PERFORMANCE_NONE.

Para configurar el modo de rendimiento, debes llamar a SetConfiguration mediante la interfaz de configuración de Android, como se muestra a continuación:

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

Seguridad y permisos

En cuanto a lo que puede realizarse y a quienes puedan llevarlo a cabo, la seguridad en Android se aborda en el nivel del proceso. El código de lenguaje de programación Java solo puede generar código nativo y el código nativo solo puede producir código de lenguaje de programación Java. Las únicas diferencias entre ellos son las API disponibles.

Las aplicaciones que usan OpenSL ES deben solicitar los permisos que necesitarían para API no nativas similares. Por ejemplo, si tu aplicación graba audio, necesita el permiso android.permission.RECORD_AUDIO. Las aplicaciones que usan efectos de audio necesitan android.permission.MODIFY_AUDIO_SETTINGS. Las aplicaciones que reproducen recursos de URI de la red necesitan android.permission.NETWORK. Para obtener más información, consulta Cómo trabajar con permisos del sistema.

Según la versión de la plataforma y la implementación, los analizadores de contenido multimedia y los códecs de software pueden ejecutarse dentro del contexto de la aplicación para Android que llama a OpenSL ES (no se incluyen los códecs de hardware, pero dependen del dispositivo). El contenido con formato incorrecto diseñado para aprovechar vulnerabilidades del analizador y el códec es un vector de ataque conocido. Te recomendamos que reproduzcas medios solo de fuentes conocidas o que crees una partición de tu aplicación de modo que el código que maneje contenido multimedia de fuentes poco confiables se ejecute en un entorno relativamente vinculado a zonas de pruebas. Por ejemplo, puedes procesar contenido multimedia de fuentes poco confiables en un proceso independiente. Si bien ambos procesos se ejecutarían en el mismo UID (ID único), esta separación dificulta más la posibilidad de un ataque.