Notas de programação do OpenSL ES

As notas desta seção complementam a especificação do OpenSL ES 1.0.1.

Inicialização de interfaces e objetos

Dois aspectos do modelo de programação do OpenSL ES que talvez os novos desenvolvedores desconheçam são a distinção entre objetos e interfaces e a sequência de inicialização.

Em resumo, um objeto do OpenSL ES é similar ao conceito de objeto de linguagens de programação, como o Java e o C++, mas um objeto do OpenSL ES só é visível por meio das interfaces associadas a ele. Isso inclui a interface inicial de todos os objetos, chamada SLObjectItf. Não há gerenciamento de um objeto em si, somente para a interface SLObjectItf do objeto.

Primeiramente, um objeto do OpenSL ES é criado, o que retorna um SLObjectItf, e depois efetivado. Isso é semelhante ao padrão de programação comum de criar um objeto antes (que não pode falhar por motivos que não seja falta de memória ou parâmetros inválidos) e depois concluir a inicialização (que pode falhar por falta de recursos). A etapa de efetivação dá à implementação um local lógico para alocar recursos adicionais, se necessário.

Como parte da API para criar um objeto, o aplicativo especifica uma matriz de interfaces desejadas que ele planeja adquirir depois. Observe que essa matriz não adquire as interfaces automaticamente, ela simplesmente indica uma intenção futura de adquiri-las. As interfaces são separadas em implícitas e explícitas. É preciso listar uma interface explícita na matriz para adquiri-la no futuro. Uma interface implícita não precisa ser listada na matriz de criação do objeto, mas listá-la não causará problemas. O OpenSL ES tem mais um tipo de interface chamada de dinâmica, que não precisa ser especificada na matriz de criação do objeto e pode ser adicionada depois da criação do objeto. A implementação do Android oferece um recurso conveniente para evitar essa complexidade, que é descrito em Interfaces dinâmicas na criação de objetos.

Depois de criar e efetivar o objeto, o aplicativo precisa adquirir interfaces para cada recurso necessário usando GetInterface no SLObjectItf inicial.

Por fim, o objeto estará disponível para uso por meio das interfaces, embora alguns objetos precisem de configurações adicionais. Isso é relevante principalmente para players de áudio com fontes de dados de URI, que precisam de um pouco mais de preparação para detectar erros de conexão. Consulte a seção Pré-busca para player de áudio para ver mais detalhes.

Depois que o aplicativo terminar as tarefas com o objeto, será necessário destruí-lo explicitamente: consulte a seção Destruir abaixo.

Pré-busca para player de áudio

Em players de áudio com fontes de dados de URI, Object::Realize aloca recursos, mas não se conecta à fonte de dados (preparar) nem inicia a pré-busca de dados. Isso ocorre quando o estado do player está definido como SL_PLAYSTATE_PAUSED ou SL_PLAYSTATE_PLAYING.

Algumas informações ainda podem ser desconhecidas até os momentos mais avançados dessa sequência. Em especial, Player::GetDuration inicialmente retorna SL_TIME_UNKNOWN e MuteSolo::GetChannelCount retorna a contagem do canal igual a zero ou o resultado do erro SL_RESULT_PRECONDITIONS_VIOLATED. Essas APIs retornam os valores corretos quando eles são conhecidos.

Outras propriedades que são inicialmente desconhecidas incluem a taxa de amostragem e o tipo real do conteúdo de mídia baseado na avaliação do cabeçalho do conteúdo (em oposição ao tipo MIME e ao tipo de contêiner especificados pelo aplicativo). Elas também são determinadas mais à frente durante a preparação/pré-busca, mas não há APIs para recuperá-las.

A interface de status da pré-busca é útil para identificar quando todas as informações estão disponíveis ou quando o aplicativo pode pesquisar periodicamente. Observe que algumas informações, como a duração de um streaming de MP3, podem nunca ser conhecidas.

A interface de status da pré-busca também é útil para detectar erros. Registre um callback e ative pelo menos os eventos SL_PREFETCHEVENT_FILLLEVELCHANGE e SL_PREFETCHEVENT_STATUSCHANGE. Se ambos os eventos forem fornecidos simultaneamente, PrefetchStatus::GetFillLevel comunicar um nível zero e PrefetchStatus::GetPrefetchStatus comunicar SL_PREFETCHSTATUS_UNDERFLOW, isso indica que há um erro irrecuperável na fonte de dados. Isso inclui a incapacidade de se conectar à fonte de dados, porque o nome de arquivo local não existe ou o URI da rede é inválido.

Espera-se que a próxima versão do OpenSL ES inclua mais suporte explícito para gerenciamento de erros na fonte de dados. No entanto, para compatibilidade binária futura, pretendemos continuar a oferecer suporte ao método atual para comunicar erros irrecuperáveis.

Em resumo, é recomendável usar a sequência de código a seguir:

  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 ou SL_PLAYSTATE_PLAYING

Observação: a preparação e a pré-busca ocorrem aqui. Durante esse período, seu callback é chamado com atualizações de status periódicas.

Destruir

Destrua todos os objetos ao sair do aplicativo. Os objetos precisam ser destruídos na ordem inversa à da criação, já que não é seguro destruir um objeto que tem objetos dependentes. Por exemplo, destrua nesta ordem: players e gravadores de áudio, mix de saída e mecanismo.

O OpenSL ES não aceita a coleta de lixo automática nem a contagem referencial de interfaces. Depois de chamar Object::Destroy, todas as interfaces remanescentes derivadas do objeto associado ficam indefinidas.

A implementação do OpenSL ES para Android não detecta o uso incorreto dessas interfaces. Se elas ainda forem usadas depois que o objeto for destruído, o aplicativo poderá falhar ou se comportar de forma inesperada.

Recomendamos definir a interface principal do objeto e as associadas como NULL explicitamente como parte da sequência de destruição do objeto porque isso impede o uso acidental de um gerenciador de interface antigo.

Panning estéreo

Ao usar Volume::EnableStereoPosition para ativar o panning estéreo de uma fonte mono, há uma redução de 3 dB no nível total de potência do som. Isso é necessário para permitir que o nível total de potência do som permaneça constante, já que a fonte é distribuída entre os canais. Portanto, só ative o posicionamento estéreo se realmente precisar. Para saber mais, consulte o artigo da Wikipédia sobre panning de áudio.

Callbacks e threads

Os gerenciadores de callback geralmente são chamados de forma síncrona quando a implementação detecta um evento. Esse ponto é assíncrono em relação ao aplicativo, por isso é preciso usar um mecanismo de sincronização que não bloqueie para controlar o acesso às variáveis compartilhadas entre o aplicativo e o gerenciador de callback. No exemplo de código, assim como nas filas de buffer, omitimos essa sincronização ou usamos a sincronização de bloqueio para simplificar. No entanto, a sincronização que não bloqueia é fundamental para todo código de produção.

Os gerenciadores de callback são chamados pelos threads internos (não os do aplicativo) que não estão conectados ao tempo de execução do Android e não podem usar JNI. Como esses threads internos são fundamentais para a integridade da implementação do OpenSL ES, o gerenciador de callback também não deve bloquear nem trabalhar excessivamente.

Caso seu gerenciador de callback precise usar JNI ou trabalhar de modo não proporcional com o callback, ele precisará publicar um evento para processamento por outro thread. Exemplos de carga de trabalho aceitáveis para o callback incluem renderizar e enfileirar o próximo buffer de saída (para um AudioPlayer), processar o buffer de entrada preenchido recentemente e enfileirar o próximo buffer vazio (para um AudioRecorder) ou APIs simples como a maior parte da família Get. Consulte abaixo a seção Desempenho sobre a carga de trabalho.

A conversão é segura: um thread do aplicativo Android que acessou a JNI pode chamar as APIs do OpenSL ES diretamente, inclusive as que ele bloqueia. No entanto, recomendamos não bloquear chamadas do thread principal, já que isso pode resultar em erros do tipo O app não está respondendo (ANR).

A determinação do thread que chama um gerenciador de callback é deixada para a implementação. O motivo dessa flexibilidade é permitir otimizações futuras, principalmente em dispositivos com vários núcleos.

O thread em que o gerenciador de callback é executado pode não ter a mesma identidade em chamadas diferentes. Portanto, não confie somente no pthread_t retornado por pthread_self() nem no pid_t retornado por gettid() para ter consistência entre chamadas. Pelo mesmo motivo, não use as APIs de armazenamento local do thread (TLS), como pthread_setspecific() e pthread_getspecific(), de um callback.

A implementação garante que não ocorram callbacks concomitantes do mesmo tipo para o mesmo objeto. No entanto, callbacks concomitantes de tipos diferentes para o mesmo objeto são possíveis em threads diferentes.

Desempenho

Como o OpenSL ES é uma API em C nativa, os threads do aplicativo que não são de tempo de execução que chamam o OpenSL ES não têm sobrecarga relacionada ao tempo de execução, como pausas para coleta de lixo. Com a exceção descrita abaixo, não há benefícios de desempenho com o uso do OpenSL ES além desse. O uso do OpenSL ES não garante melhorias, como menor latência de áudio e maior prioridade de agendamento em relação ao que a plataforma geralmente fornece. Por outro lado, como as implementações da plataforma Android e específicas de dispositivos continuam evoluindo, os aplicativos que usam OpenSL ES podem se beneficiar de possíveis melhorias de desempenho futuras do sistema.

Uma dessas evoluções é a compatibilidade com uma menor latência na saída de áudio. As bases para a redução da latência na saída foram incluídas no Android 4.1 (nível de API 16) e evoluídas no Android 4.2 (nível de API 17). Essas melhorias são disponibilizadas pelo OpenSL ES para implementações de dispositivos que solicitam o recurso android.hardware.audio.low_latency. Se o dispositivo não solicitar esse recurso, mas for compatível com o Android 2.3 (nível de API 9) ou posteriores, você ainda poderá usar as APIs do OpenSL ES, mas talvez a latência de saída seja maior. O menor caminho de latência de saída é usado somente se o aplicativo solicitar um tamanho de buffer e uma taxa de amostragem compatível com a configuração de saída nativa do dispositivo. Esses parâmetros são específicos do dispositivo e devem ser recebidos conforme descrito abaixo.

A partir do Android 4.2 (nível de API 17), os aplicativos podem consultar a taxa de amostragem de saída e o tamanho de buffer ideais ou nativos da plataforma para o stream de saída principal do dispositivo. Quando combinado com o teste de recurso mencionado acima, um aplicativo pode se ajustar de forma adequada para saídas de menor latência em dispositivos compatíveis.

Para o Android 4.2 (nível de API 17) e anteriores, é necessário ter uma contagem de buffers de dois ou mais para uma menor latência. A partir do Android 4.3 (nível de API 18), é suficiente uma contagem de buffer de um para uma menor latência.

Todas as interfaces do OpenSL ES para efeitos de saída impedem o caminho de menor latência.

A sequência recomendada é a seguinte:

  1. Procure o nível de API 9 ou posterior para confirmar o uso do OpenSL ES.
  2. Procure o recurso android.hardware.audio.low_latency usando o código da seguinte forma:

    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. Procure o nível de API 17 ou posterior para confirmar o uso de android.media.AudioManager.getProperty().
  4. Encontre a taxa de amostragem e o tamanho de buffer de saída ideais ou nativas para o stream de saída principal deste dispositivo usando código da seguinte forma:

    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);
        
    Observe que sampleRate e framesPerBuffer são strings. Primeiramente, busque null e converta para int usando Integer.parseInt().
  5. Agora use o OpenSL ES para criar um AudioPlayer com localizador de dados de fila de buffer de PCM.

Observação: use o aplicativo de teste Tamanho do buffer de áudio para determinar o tamanho do buffer e a taxa de amostragem nativos de aplicativos de áudio que usam OpenSL ES no seu dispositivo de áudio. Também é possível acessar o GitHub para ver exemplos de tamanho de buffer de áudio.

O número de players de áudio de baixa latência é limitado. Se o aplicativo exigir muitas fontes de áudio, é recomendável mixar o áudio no nível do aplicativo. Lembre-se de destruir os players de áudio quando a atividade for pausada, já que são um recurso global compartilhado com outros aplicativos.

Para evitar falhas audíveis, é necessário executar o gerenciador de callback da fila de buffer dentro de um intervalo pequeno e previsível. Isso normalmente implica na ausência de bloqueios ilimitados em mutexes, condições ou operações de E/S. Em vez disso, use try-locks, locks e waits com limite de tempo e algoritmos sem bloqueio.

A computação necessária para renderizar o buffer seguinte (para o AudioPlayer) ou consumir o buffer anterior (para AudioRecord) precisa usar aproximadamente o mesmo tempo para cada callback. Evite algoritmos executados em períodos não determinísticos ou com computações em burst. A computação de callback é chamada de “em burst” se o tempo gasto pelo CPU for muito maior do que a média. Em resumo, o ideal é que o tempo de execução do CPU para o gerenciador tenha variância próxima a zero e que o gerenciador não bloqueie por períodos ilimitados.

É possível alcançar uma menor latência de áudio somente para as seguintes saídas:

  • Alto-falantes do dispositivo
  • Fones de ouvido com fio.
  • Fones de ouvido + microfone com fio.
  • Saída de linha.
  • Áudio digital USB

Em alguns dispositivos, a latência dos alto-falantes é superior a outros caminhos devido ao processamento digital do sinal para a correção e a proteção dos alto-falantes.

A partir do Android 5.0 (API de nível 21), a entrada de áudio com menor latência é compatível com determinados dispositivos. Para utilizar esse recurso, confirme se a saída de menor latência está disponível conforme descrito acima. O recurso de saída de menor latência é necessário para o recurso de entrada de menor latência. Em seguida, crie um AudioRecorder com a mesma taxa de amostragem e o mesmo tamanho de buffer que seria usado para a saída. As interfaces do OpenSL ES para efeitos de entrada impedem o caminho de menor latência. A predefinição de gravação SL_ANDROID_RECORDING_PRESET_VOICE_RECOGNITION precisa ser usada para uma menor latência. Essa predefinição desativa o processamento digital de sinal específico do dispositivo que pode adicionar latência ao caminho de entrada. Para saber mais sobre predefinições de gravação, consulte a seção Interface de configuração do Android acima.

Para entrada e saída simultâneas, são usados gerenciadores de conclusão de fila de buffer separados para cada lado. Não há garantia de ordem relativa desses callbacks nem da sincronização dos relógios de áudio, mesmo quando ambos os lados usam a mesma taxa de amostragem. Seu app deve armazenar os dados em buffer com uma sincronização de buffer adequada.

Uma consequência de usar relógios de áudio independentes é a necessidade de conversão assíncrona da taxa de amostragem. Uma técnica simples (embora não ideal em termos de qualidade do áudio) para a conversão assíncrona da taxa de amostragem é duplicar ou remover amostras conforme a necessidade, até quase um ponto de interseção zero. É possível fazer conversões mais sofisticadas.

Modos de desempenho

A partir do Android 7.1 (nível de API 25), o OpenSL ES introduziu uma maneira de especificar um modo de desempenho para o caminho de áudio. As opções são as seguintes:

  • SL_ANDROID_PERFORMANCE_NONE: nenhum requisito de desempenho específico. Permite efeitos de hardware e software.
  • SL_ANDROID_PERFORMANCE_LATENCY: a prioridade é a latência. Sem efeitos de hardware ou software. Esse é o modo padrão.
  • SL_ANDROID_PERFORMANCE_LATENCY_EFFECTS: a prioridade é a latência, permitindo efeitos de hardware e software.
  • SL_ANDROID_PERFORMANCE_POWER_SAVING: a prioridade é a conservação de energia. Permite efeitos de hardware e software.

Observação: se você não precisar de um caminho de baixa latência e quiser utilizar os efeitos de áudio integrados do dispositivo (por exemplo, para melhorar a qualidade acústica da reprodução de vídeo), será necessário definir explicitamente o modo de desempenho como SL_ANDROID_PERFORMANCE_NONE.

Para definir o modo de desempenho, é necessário chamar SetConfiguration usando a interface de configuração do Android, conforme exibido abaixo:

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

Segurança e permissões

Até o limite da capacidade de cada um, a segurança no Android é feita no nível do processo. O código em Java não pode fazer o que o código nativo não possa e vice-versa. As únicas diferenças entre eles são as APIs disponíveis.

Os aplicativos que usam o OpenSL ES precisam solicitar as permissões necessárias para APIs não nativas similares. Por exemplo, se o aplicativo gravar áudio, ele precisará da permissão android.permission.RECORD_AUDIO. Os aplicativos que usam efeitos de áudio precisam da permissão android.permission.MODIFY_AUDIO_SETTINGS. Os aplicativos que reproduzem recursos de URI de rede precisam da permissão android.permission.NETWORK. Para saber mais, consulte Trabalhar com permissões do sistema.

Dependendo da implementação e da versão da plataforma, os analisadores de conteúdo de mídia e codecs de software podem ser executados dentro do contexto do aplicativo Android que chama o OpenSL ES (codecs de hardware são separados, mas dependem do dispositivo). Os conteúdos maliciosos projetados para explorar vulnerabilidades do codec e do analisador são um vetor de ataque conhecido. Recomendamos reproduzir mídia somente a partir de fontes confiáveis ou particionar o aplicativo de forma que o código que lida com mídia de fontes não confiáveis seja executado em um ambiente relativamente restrito. Por exemplo, é possível processar mídia de fontes não confiáveis em um processo separado. Embora ambos os processos ainda sejam executados no mesmo UID, essa separação dificulta um eventual ataque.