Notes de programmation d'OpenSL ES

AVERTISSEMENT: OpenSL ES est obsolète. Les développeurs doivent utiliser la bibliothèque Oboe Open Source, disponible sur GitHub. Oboe est un wrapper C++ qui fournit une API ressemblant fortement à AAudio. Il appelle AAudio s'il est disponible et utilise OpenSL ES dans le cas contraire.

Les notes de cette section complètent la spécification OpenSL ES 1.0.1.

Initialisation des objets et de l'interface

Deux aspects du modèle de programmation OpenSL ES que les nouveaux développeurs peuvent ne pas connaître sont la distinction entre les objets et les interfaces, et la séquence d'initialisation.

En bref, un objet OpenSL ES est semblable au concept d'objet dans les langages de programmation tels que Java et C++, à la différence qu'un objet OpenSL ES n'est visible que via ses interfaces associées. Cela inclut l'interface initiale de tous les objets, appelée SLObjectItf. Il n'existe pas de handle pour un objet lui-même, mais seulement un handle pour l'interface SLObjectItf de l'objet.

Un objet OpenSL ES est d'abord créé, ce qui renvoie un objet SLObjectItf, puis est réalisé. Cette méthode est semblable à la pratique de programmation courante qui consiste à créer d'abord un objet (qui ne devrait jamais échouer sauf en cas de manque de mémoire ou de paramètres non valides), puis à effectuer l'initialisation (qui peut échouer en raison d'un manque de ressources). L'étape de réalisation donne à l'implémentation un emplacement logique auquel allouer des ressources supplémentaires si nécessaire.

Dans le cadre de l'API permettant de créer un objet, une application spécifie un tableau des interfaces souhaitées qu'elle prévoit d'acquérir ultérieurement. Notez que ce tableau n'acquiert pas automatiquement les interfaces. Il indique simplement une intention future de les acquérir. On distingue les interfaces implicites ou explicites. Une interface explicite doit être indiquée dans le tableau si elle est acquise ultérieurement. Vous pouvez indiquer une interface implicite dans le tableau de création d'objets, bien que cela ne soit pas nécessaire. OpenSL ES dispose d'un autre type d'interface appelé dynamique, qui n'a pas besoin d'être spécifié dans le tableau de création d'objets et qui pourra être ajouté ultérieurement après la création de l'objet. L'implémentation Android fournit une fonctionnalité pratique permettant d'éviter cette complexité, décrite dans la section Interfaces dynamiques lors de la création d'objets.

Une fois l'objet créé et réalisé, l'application doit acquérir des interfaces pour chaque fonctionnalité dont elle a besoin, en utilisant GetInterface au niveau de l'élément SLObjectItf initial.

Enfin, l'objet peut être utilisé via ses interfaces. Notez toutefois que certains objets nécessitent une configuration plus avancée. Plus spécifiquement, un lecteur audio avec une source de données URI nécessite un peu plus de préparation pour détecter les erreurs de connexion. Pour en savoir plus, consultez la section Préchargement du lecteur audio.

Une fois l'application n'a plus besoin de l'objet, vous devez le détruire explicitement. consultez la section Détruire un objet ci-dessous.

Préchargement du lecteur audio

Pour un lecteur audio avec source de données URI, Object::Realize alloue des ressources, mais ne se connecte pas à la source de données (préparation) et ne commence pas à précharger les données. Ces opérations se produisent lorsque l'état du lecteur est défini sur SL_PLAYSTATE_PAUSED ou SL_PLAYSTATE_PLAYING.

Il est possible que certaines informations soient encore inconnues jusqu'à un stade relativement avancé de cette séquence. En particulier, initialement, Player::GetDuration renvoie SL_TIME_UNKNOWN, et MuteSolo::GetChannelCount renvoie soit un nombre de canaux égal à zéro, soit le résultat d'erreur SL_RESULT_PRECONDITIONS_VIOLATED. Ces API renvoient les valeurs appropriées une fois qu'elles sont connues.

Parmi les autres propriétés initialement inconnues figurent le taux d'échantillonnage et le type de contenu multimédia réel, d'après l'examen de l'en-tête du contenu (par opposition au type MIME et au type de conteneur spécifiés par l'application). Ces informations seront également déterminées ultérieurement lors de la préparation ou du préchargement, mais il n'existe aucune API permettant de les récupérer.

L'interface d'état du préchargement permet de détecter la disponibilité de toutes les informations. Vous pouvez aussi interroger régulièrement votre application. Notez que certaines informations, telles que la durée d'un flux MP3, peuvent ne jamais être connues.

L'interface de l'état de préchargement est également utile pour détecter les erreurs. Enregistrez un rappel et activez au moins les événements SL_PREFETCHEVENT_FILLLEVELCHANGE et SL_PREFETCHEVENT_STATUSCHANGE. Si ces deux événements sont diffusés simultanément et que PrefetchStatus::GetFillLevel indique un niveau zéro, tandis que PrefetchStatus::GetPrefetchStatus renvoie SL_PREFETCHSTATUS_UNDERFLOW, cela indique une erreur non récupérable dans la source de données. Il est alors impossible de se connecter à la source de données, car le nom de fichier local n'existe pas ou l'URI du réseau n'est pas valide.

La prochaine version d'OpenSL ES devrait permettre une gestion plus explicite des erreurs dans la source de données. Toutefois, pour une future compatibilité binaire, nous prévoyons de continuer à prendre en charge la méthode actuelle permettant de signaler une erreur non récupérable.

En résumé, voici une séquence de code recommandée :

  1. Engine::CreateAudioPlayer
  2. Object:Realize
  3. Object::GetInterface pour SL_IID_PREFETCHSTATUS
  4. PrefetchStatus::SetCallbackEventsMask
  5. PrefetchStatus::SetFillUpdatePeriod
  6. PrefetchStatus::RegisterCallback
  7. Object::GetInterface pour SL_IID_PLAY
  8. Play::SetPlayState à SL_PLAYSTATE_PAUSED, ou SL_PLAYSTATE_PLAYING

Remarque : La préparation et le préchargement ont lieu ici. Pendant ce temps, le rappel est appelé avec des mises à jour périodiques de l'état.

Détruire un objet

Veillez à détruire tous les objets lorsque vous quittez l'application. Les objets doivent être détruits dans l'ordre inverse de leur création, car il est risqué de détruire un objet ayant des dépendances. Par exemple, détruisez les objets dans cet ordre : les lecteurs et les enregistreurs audio, le mixage de sortie, puis le moteur.

OpenSL ES n'est pas compatible avec la récupération automatique de mémoire ni avec le comptage de référence des interfaces. Une fois que vous avez appelé Object::Destroy, toutes les interfaces existantes qui sont dérivées de l'objet associé ne sont plus définies.

L'implémentation Android OpenSL ES ne détecte pas l'utilisation incorrecte de ces interfaces. Si vous continuez à utiliser ces interfaces après la destruction de l'objet, votre application peut planter ou se comporter de manière imprévisible.

Nous vous recommandons de définir explicitement l'interface principale des objets et toutes les interfaces associées sur NULL dans le cadre de la séquence de destruction d'objets. Vous éviterez ainsi tout usage accidentel d'un handle d'interface obsolète.

Panoramique stéréo

Lorsque Volume::EnableStereoPosition est utilisé pour effectuer un panoramique stéréo d'une source mono, le niveau de puissance totale du son diminue de 3 dB. Cela est nécessaire pour permettre au niveau sonore total de rester constant, car la source passe d'un canal à l'autre. Par conséquent, n'activez le positionnement stéréo que si vous en avez besoin. Pour en savoir plus, consultez l'article Wikipédia sur le panoramique audio.

Rappels et threads

Les gestionnaires de rappel sont généralement appelés de manière synchrone lorsque l'implémentation détecte un événement. Ce point est asynchrone par rapport à l'application. Vous devez donc utiliser un mécanisme de synchronisation non bloquant pour contrôler l'accès aux variables partagées entre l'application et le gestionnaire de rappel. Dans l'exemple de code, notamment pour les files d'attente de tampon, nous avons omis cette synchronisation ou utilisé la synchronisation bloquante par souci de simplicité. Cependant, une synchronisation non bloquante appropriée est essentielle pour tout code de production.

Les gestionnaires de rappel sont appelés à partir de threads internes non applicatif qui ne sont pas associés à l'environnement d'exécution Android. Ils ne peuvent donc pas utiliser JNI. Ces threads internes étant essentiels à l'intégrité de l'implémentation d'OpenSL ES, un gestionnaire de rappel ne doit pas bloquer ni effectuer un travail excessif.

Si votre gestionnaire de rappel doit utiliser JNI ou exécuter une tâche qui n'est pas proportionnelle au rappel, le handler doit publier un événement pour qu'un autre thread puisse le traiter. Voici quelques exemples de charges de travail de rappel acceptables : afficher et mettre en file d'attente le prochain tampon de sortie (pour un lecteur audio), traiter le tampon d'entrée qui vient d'être rempli et mettre en file d'attente le tampon vide suivant (pour un enregistrement audio) ou des API simples comme la plupart de celles issues de la famille Get. Consultez la section Performances ci-dessous concernant la charge de travail.

Notez que l'inverse est sans risque : un thread d'application Android qui utilise JNI est autorisé à appeler directement les API OpenSL ES, y compris celles qui sont bloquantes. Toutefois, le blocage des appels n'est pas recommandé à partir du thread principal, car ils peuvent entraîner l'erreur L'application ne répond pas (ou ARN).

La détermination du thread qui appelle un gestionnaire de rappel dépend en grande partie de l'implémentation. Cette flexibilité permet les futures optimisations, en particulier sur les appareils multicœurs.

Il n'est pas garanti que le thread sur lequel s'exécute le gestionnaire de rappel ait la même identité pour différents appels. Par conséquent, ne vous appuyez pas sur l'élément pthread_t renvoyé par pthread_self() ni sur l'élément pid_t renvoyé par gettid() pour la cohérence des appels. Pour la même raison, n'utilisez pas les API TLS (thread local storage) telles que pthread_setspecific() et pthread_getspecific() à partir d'un rappel.

L'implémentation garantit que les rappels simultanés du même genre ne se produisent pas pour le même objet. Toutefois, des rappels simultanés de genres différents pour le même objet sont possibles sur des threads distincts.

Performances

Étant donné qu'OpenSL ES est une API C native, les threads d'application qui ne sont pas liés à l'exécution, et qui appellent OpenSL ES, ne subissent pas de surcharge liée à l'exécution, comme l'interruption de la récupération de mémoire. Hormis l'une des exceptions décrites ci-dessous, l'utilisation d'OpenSL ES ne présente pas d'autres avantages de performances. Par exemple, l'utilisation d'OpenSL ES ne garantit pas d'améliorations telles qu'une latence audio plus faible et une priorité plus élevée que celle généralement fournie par la plate-forme. En revanche, à mesure que la plate-forme Android et que des implémentations d'appareils spécifiques évoluent, une application OpenSL ES peut espérer bénéficier des futures améliorations des performances système.

L'une de ces évolutions est la compatibilité avec la latence des sorties audio. Les fondements d'une latence de sortie réduite ont été d'abord inclus dans Android 4.1 (niveau d'API 16), puis ont continué de progresser dans Android 4.2 (niveau d'API 17). Ces améliorations sont disponibles via OpenSL ES pour les implémentations d'appareils qui revendiquent la fonctionnalité android.hardware.audio.low_latency. Si l'appareil ne revendique pas cette fonctionnalité, mais est compatible avec Android 2.3 (API de niveau 9) ou une version ultérieure, vous pouvez toujours utiliser les API OpenSL ES, mais la latence de sortie peut être plus élevée. Le chemin de latence de sortie plus faible n'est utilisé que si l'application demande une taille de tampon et un taux d'échantillonnage compatibles avec la configuration de sortie native de l'appareil. Ces paramètres sont propres à chaque appareil et doivent être obtenus comme décrit ci-dessous.

À partir d'Android 4.2 (niveau d'API 17), une application peut interroger le taux d'échantillonnage de sortie et la taille de la mémoire tampon natifs ou optimaux de la plate-forme pour le flux de sortie principal de l'appareil. Lorsqu'elle est combinée au test de fonctionnalité mentionné précédemment, une application peut désormais se configurer de manière appropriée pour une sortie à faible latence sur les appareils compatibles.

Pour Android 4.2 (niveau d'API 17) et les versions antérieures, un nombre de tampons égal ou supérieur à 2 est requis pour réduire la latence. À partir d'Android 4.3 (niveau d'API 18), un seul tampon suffit pour réduire la latence.

Toutes les interfaces OpenSL ES pour les effets de sortie empêchent le chemin de latence faible.

La séquence recommandée est la suivante :

  1. Vérifiez le niveau d'API 9 ou supérieur pour confirmer l'utilisation d'OpenSL ES.
  2. Recherchez la fonctionnalité android.hardware.audio.low_latency à l'aide du code suivant :

    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. Vérifiez le niveau d'API 17 ou supérieur pour confirmer l'utilisation de android.media.AudioManager.getProperty().
  4. Obtenez le taux d'échantillonnage et la taille de tampon optimaux pour le flux de sortie principal de cet appareil à l'aide du code suivant :

    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);
    Notez que sampleRate et framesPerBuffer sont des chaînes. Commencez par rechercher la valeur "null", puis convertissez-la en valeur int via Integer.parseInt().
  5. Utilisez maintenant OpenSL ES pour créer un lecteur audio avec un localisateur de données de la file d'attente de tampon PCM.

Remarque : Vous pouvez utiliser l'application de test Audio Buffer Size pour déterminer la taille de la mémoire tampon et le taux d'échantillonnage natifs de l'audio OpenSL ES sur votre appareil audio. Vous pouvez également accéder à GitHub pour consulter des exemples de fichiers audio-buffer-size.

Le nombre de lecteurs audio à faible latence est limité. Si votre application nécessite plusieurs sources audio, envisagez de les combiner au niveau de l'application. Assurez-vous de détruire vos lecteurs audio lorsque votre activité est suspendue, car ils constituent une ressource globale partagée avec d'autres applications.

Pour éviter les problèmes de son, le gestionnaire de rappel de la file d'attente de tampon doit s'exécuter dans un délai court et prévisible. En général, cela n'implique aucun blocage illimité sur les mutex, les conditions ou les opérations d'E/S. Envisagez plutôt d'utiliser des verrouillages d'essai, des verrouillages et des temps d'attente avec des délais avant expiration, ainsi que des algorithmes non bloquants.

Le calcul requis pour afficher le tampon suivant (pour AudioPlayer) ou pour utiliser le tampon précédent (pour AudioRecord) devrait prendre à peu près le même temps pour chaque rappel. Évitez les algorithmes qui s'exécutent dans un laps de temps non déterministe ou dont les calculs sont intensifs. Un calcul de rappel est intensif si le temps CPU passé dans un rappel donné est nettement supérieur à la moyenne. En résumé, l'idéal est que le temps d'exécution du processeur du handler ait une variance proche de zéro et que le handler ne bloque pas les durées illimitées.

Il est possible de réduire la latence de l'audio pour ces sorties uniquement :

  • Haut-parleurs intégrés à l'appareil
  • Écouteurs filaires
  • Casques filaires
  • Sortie de ligne
  • Audio USB numérique

Sur certains appareils, la latence du haut-parleur est plus élevée que celle d'autres canaux en raison du traitement des signaux numériques pour la correction et la protection des haut-parleurs.

À partir d'Android 5.0 (niveau d'API 21), une entrée audio à faible latence est compatible avec certains appareils. Pour profiter de cette fonctionnalité, commencez par vérifier que vous pouvez utiliser une sortie à latence faible, comme décrit ci-dessus. La capacité à générer une sortie à faible latence est une condition préalable à la fonctionnalité d'entrée à faible latence. Créez ensuite un enregistrement audio avec les mêmes taux d'échantillonnage et taille de tampon que ceux utilisés pour la sortie. Les interfaces OpenSL ES pour les effets d'entrée empêchent le chemin de latence faible. Le préréglage d'enregistrement SL_ANDROID_RECORDING_PRESET_VOICE_RECOGNITION doit être utilisé pour réduire la latence. Cette présélection désactive le traitement des signaux numériques spécifiques à l'appareil, ce qui peut augmenter la latence du chemin d'entrée. Pour en savoir plus sur les préréglages d'enregistrement, consultez la section Interface de configuration Android ci-dessus.

Pour les entrées et les sorties simultanées, des gestionnaires de finalisation de file d'attente de tampon distincts sont utilisés de chaque côté. Il n'existe aucune garantie quant à l'ordre relatif de ces rappels ni à la synchronisation des horloges audio, même lorsque les deux côtés utilisent le même taux d'échantillonnage. Votre application doit mettre les données en mémoire tampon avec une synchronisation de tampon appropriée.

Cette indépendance potentielle des horloges audio entraîne la nécessité d'une conversion de taux d'échantillonnage asynchrone. Une technique simple (mais pas idéale pour la qualité audio) de conversion de taux d'échantillonnage asynchrone consiste à dupliquer ou à supprimer des échantillons selon les besoins, à proximité d'un point de passage à zéro. Des conversions plus sophistiquées sont possibles.

Modes de performances

À partir d'Android 7.1 (niveau d'API 25), OpenSL ES permet de spécifier un mode Performances pour la piste audio. Pour ce faire, vous disposez des options suivantes :

  • SL_ANDROID_PERFORMANCE_NONE : aucune exigence spécifique en termes de performances. Autorise les effets matériels et logiciels.
  • SL_ANDROID_PERFORMANCE_LATENCY : la priorité est donnée à la latence. Aucun effet matériel ni logiciel. Il s'agit du mode par défaut.
  • SL_ANDROID_PERFORMANCE_LATENCY_EFFECTS : la priorité est donnée à la latence, tout en autorisant les effets matériels et logiciels.
  • SL_ANDROID_PERFORMANCE_POWER_SAVING : priorité accordée à l'économie d'énergie. Autorise les effets matériels et logiciels.

Remarque : Si vous n'avez pas besoin d'un chemin à faible latence et que vous souhaitez profiter des effets audio intégrés à l'appareil (par exemple, pour améliorer la qualité acoustique des lectures vidéo), vous devez définir explicitement le mode Performances sur SL_ANDROID_PERFORMANCE_NONE.

Pour définir le mode Performances, vous devez appeler SetConfiguration à l'aide de l'interface de configuration Android, comme indiqué ci-dessous :

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

Sécurité et autorisations

La sécurité d'Android s'effectue au niveau des processus. Le code du langage de programmation Java ne peut pas aller plus loin que le code natif, et inversement. Les seules différences entre eux sont les API disponibles.

Les applications utilisant OpenSL ES doivent demander les autorisations dont elles ont besoin pour les API non natives similaires. Par exemple, si votre application enregistre du contenu audio, elle a besoin de l'autorisation android.permission.RECORD_AUDIO. Les applications qui utilisent des effets audio nécessitent android.permission.MODIFY_AUDIO_SETTINGS. Les applications qui lisent des ressources URI réseau ont besoin de android.permission.NETWORK. Pour en savoir plus, consultez la page Fonctionnement des autorisations système.

Selon la version et l'implémentation de la plate-forme, les analyseurs de contenu multimédia et les codecs logiciels peuvent s'exécuter dans le contexte de l'application Android qui appelle OpenSL ES (les codecs matériels sont extraits, mais dépendent de l'appareil). Le contenu mal formé conçu pour exploiter les failles de l'analyseur et du codec est un vecteur d'attaque connu. Nous vous recommandons de ne lire les contenus multimédias qu'à partir de sources fiables ou de partitionner votre application de sorte que le code qui gère les contenus multimédias provenant de sources non fiables s'exécute dans un environnement relativement protégé tel qu'un bac à sable. Par exemple, vous pouvez traiter les contenus multimédias provenant de sources non fiables dans un processus distinct. Bien que ces deux processus s'exécutent toujours sous le même UID, cette séparation rend les attaques plus difficiles.