Notas de programación de OpenSL ES

Las notas en 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 objetos e interfaces, y la secuencia de inicialización.

Dicho de forma resumida, 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 mediante sus interfaces asociadas. Esto incluye la interfaz inicial para todos los objetos, llamada SLObjectItf. No hay controladores para un objeto, solo un controlador para la interfaz SLObjectItf del objeto.

Un objeto OpenSL ES primero debe crearse, con lo cual se 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 una matriz de interfaces deseadas que prevé adquirir más adelante. Ten en cuenta que esa matriz no adquiere las interfaces automáticamente; simplemente 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 la matriz si se prevé adquirirla más adelante. No es necesario indicar una interfaz implícita en la matriz 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 la matriz 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 origen de datos de URI requiere un poco más de preparación para poder detectar errores de conexión. Consulta la sección Captura previa 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.

Captura previa del reproductor de audio

Para un reproductor de audio con origen de datos de URI, Object::Realize asigna recursos, pero no se conecta al origen de datos (prepara) ni comienza la captura previa 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. En particular, inicialmente Player::GetDuration muestra SL_TIME_UNKNOWN y MuteSolo::GetChannelCount muestra correctamente un recuento de canales de cero o el resultado del error SL_RESULT_PRECONDITIONS_VIOLATED. Estas API muestran los valores correspondientes una vez que se conocen.

Entre otras propiedades que inicialmente se desconocen 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 al tipo de contenedor especificados por la aplicación). Estas propiedades también se determinan más adelante durante la preparación y captura previas, pero no hay API para recuperarlas.

La interfaz con estado de captura previa 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 captura previa 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 una 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 manejar errores en la fuente de datos. No obstante, para lograr la compatibilidad con ejecutables en el futuro, pretendemos 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 para SL_PLAYSTATE_PAUSED o SL_PLAYSTATE_PLAYING

Nota: La preparación y la captura 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 tiene otros objetos dependientes. Por ejemplo, destruye los objetos en este orden: los reproductores y grabadores de audio, la combinación de salida y, por último, el motor.

OpenSL ES no admite la recolección automática de basura 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 bloquearse o comportarse de manera imprevista.

Te recomendamos establecer de manera 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 cual evita el uso indebido accidental de un controlador de interfaces caduco.

Distribución de sonido estéreo

Cuando se usa Volume::EnableStereoPosition para habilitar la distribución 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 de los datos 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 audio estéreo.

Devoluciones de llamadas y subprocesos

Los controladores de devoluciones de llamada generalmente se llaman de forma sincrónica cuando la implementación detecta un evento. Este punto es asincrónico 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 devoluciones 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 devoluciones de llamada tampoco debe bloquear ni realizar un trabajo excesivo.

Si tu controlador de devoluciones de llamadas necesita usar JNI o ejecutar trabajo que no es proporcional a la devolución de llamada, el controlador no debe publicar un evento para el procesamiento de otro subproceso. Entre algunos ejemplos de carga de trabajo de devolución de llamada aceptable se incluyen la representación y colocació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 colocació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. Consulta, a continuación, 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 Android que ingresó en JNI puede llamar directamente a las OpenSL ES API, incluidas aquellas que provocan bloqueos. No obstante, no se recomienda realizar llamadas de bloqueo desde el subproceso principal, ya que pueden hacer que la aplicación no responda (ANR).

La determinación del subproceso que llama a un controlador de devoluciones de llamada depende ampliamente 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 devoluciones de llamadas tenga la misma identidad en las diferentes llamadas. Por lo tanto, no confíes en que el pthread_t mostrado por pthread_self() o el pid_t mostrado por gettid() sean constantes en todas las llamadas. Por el mismo motivo, no uses las API de almacenamiento local de subprocesos (TLS), como pthread_setspecific() y pthread_getspecific(), desde 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

Como 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 una 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 generalmente proporciona. Por otro lado, dado que la plataforma de Android y las implementaciones para dispositivos específicos continúan evolucionando, una aplicación 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 función android.hardware.audio.low_latency. Si el dispositivo no indica que tiene esta función, pero admite Android 2.3 (nivel de API 9) o versiones posteriores, puedes de todos modos usar las OpenSL ES API, 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 de búfer y una tasa de muestreo que son compatibles con la configuración de salida nativa del dispositivo. Estos parámetros son específicos para el 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 de búfer para la transmisión de salida principal del dispositivo. Cuando se combina con la prueba de funciones recién mencionada, una aplicación ahora puede configurarse automáticamente de forma correcta para lograr una menor latencia 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 más baja. A partir de Android 4.3 (nivel de API 18), un recuento de búfer de uno es suficiente para obtener una latencia más baja.

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 mayor para confirmar el uso de OpenSL ES.
  2. Comprueba la presencia de la función android.hardware.audio.low_latency usando código como este:

    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 el 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 de 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 cadenas. Primero comprueba la presencia de un valor null y luego conviértelo en int usando 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 aplicación de prueba Audio Buffer Size para determinar el tamaño del búfer y la tasa de muestreo nativa para aplicaciones de audio OpenSL ES en tu dispositivo de audio. También puedes visitar GitHub para ver ejemplos de tamaño de búfer de audio.

La cantidad de reproductores de audio con baja latencia es limitada. Si tu aplicación requiere más que unos pocos orígenes de audio, considera combinar tu audio en el nivel de la aplicación. Asegúrate de destruir tus reproductores de audio cuando se pause la actividad, ya que son un recurso global que se comparte con otras aplicaciones.

Para evitar fallas audibles, el controlador de devoluciones de llamada de la cola de búfer debe ejecutarse en un lapso 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, tiempos de espera con límites y algoritmos que no generen bloqueos.

El cómputo requerido para representar 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ómputos. Un cómputo 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:

  • Altavoces integrados al dispositivo.
  • Auriculares con cable.
  • Auriculares-micrófono con cable.
  • Línea de salida
  • Audio digital USB

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

A partir de Android 5.0 (nivel de API 21), se admite la entrada de audio de baja latencia en algunos dispositivos. Para aprovechar esta función, primero confirma que esté disponible la salida de baja latencia, como se describe previamente. La capacidad para generar salida de audio de baja latencia es un requisito previo para la función de entrada de audio de baja latencia. Luego, crea un objeto AudioRecorder con la misma tasa de muestreo y el mismo tamaño de búfer que usarías para la salida. Las interfaces OpenSL ES para efectos de entrada excluyen la ruta de acceso de baja latencia. Para baja latencia, se debe usar el valor preestablecido de grabación SL_ANDROID_RECORDING_PRESET_VOICE_RECOGNITION; este valor preestablecido inhabilita el procesamiento de señales digitales que podrían agregar latencia a la ruta de acceso de entrada. Para obtener más información sobre los valores preestablecidos 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 extremo. 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 asincrónica de la tasa de muestreo. Una técnica sencilla (aunque no ideal respecto de la calidad de audio) para la conversión asincrónica de la tasa de muestreo es duplicar u omitir muestras, 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 (nivel de API 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 integrado 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 de 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 orígenes conocidos o que dividas tu aplicación de modo que el código que maneje contenido multimedia de orígenes poco confiables se ejecute en un entorno relativamente vinculado a zonas de prueba. Por ejemplo, puedes procesar contenido multimedia de orígenes poco confiables en un proceso independiente. Si bien ambos procesos se ejecutarían en el mismo UID, esta separación no dificulta más la posibilidad de un ataque.