Toques de áudio

Este conteúdo descreve a reprodução de sons no renderizador de alta disponibilidade (HAR). Um crate Audio expõe AudioManager ao app HAR, que controla a reprodução de sons.

Para manter a latência baixa, as linhas de execução de reprodução são executadas durante todo o ciclo de vida do app, ficando inativas e gerando quando nenhum áudio é reproduzido.

Terminologia

recurso
AudioAsset pertence ao áudio reproduzível. Os recursos são conhecidos e existem no ambiente de execução do app.
dispositivo
AudioDevice se refere a um barramento separado para reprodução de áudio. O dispositivo é a unidade mais granular relacionada ao hardware acessado pelo sistema. Na implementação padrão do SDVM, AudioDevice se refere a um único PCM (link em inglês) da Advanced Linux Sound Architecture (ALSA).
stream
Uma instância de reprodução de um recurso em um dispositivo. Os streams persistem desde o momento em que são programados até serem concluídos, cancelados ou terminarem com erro.

Componentes

A Figura 1 mostra o diagrama de componentes para o som:

Diagrama de componentes

Figura 1. Diagrama de componentes.

Dispositivo de áudio e PCM

A configuração de hardware de áudio segue o design da camada de abstração da plataforma HAR padrão, e har-platform-api a contém.

O crate Audio do HAR define uma nova estrutura para AudioDevice, que define campos para todas as estruturas de dados que afetam o crate Audio interno do HAR e a reprodução. AudioDevice também usa tipos genéricos para encapsular outros parâmetros adicionais específicos da plataforma. No caso de tinyalsa, PlatformAudioDevice contém os descritores e as propriedades de um PCM ALSA.

/// NOTE: The following code is a sample definition to help understanding, it is not a
/// representation of the final code/implementation.

AudioDevice<PlatformAudioDevice> {
  /// Internal HAR Identifier for the device.
  AudioDeviceID,

  /// The size (in bytes) for chunks of audio data to stream to the device.
  ChunkSize,

  /// Properties necessary to control volume (details in "Mixer control" section).
  VolumeControl,

  /// Properties necessary to control spatialization (details in "Mixer control"
  /// section).
  SpatialControl,

  /// Platform specific data for the AudioDevice.
  /// E.g. ALSA properties and reference to opened PCM.
  PlatformAudioDevice
}

/// Elaboration of the previously mentioned VolumeControl
VolumeControl {
  /// Identifier for the control used to change volume.
  ControlID,

  /// Mapping between Decibel and control values. (see Mixer control section)
  VolumeOutputIndex
}

Recursos de áudio

Esta seção descreve como os recursos de áudio são configurados e implementados.

Configuração

A implementação inicial de áudio do HAR oferece suporte a recursos de áudio configurados estaticamente. Uma configuração JSON define quais recursos estão disponíveis e quais são definidos como arquivos WAV.

A implementação também oferece suporte a recursos de áudio sintetizados e transmitidos por stream por meio de uma implementação de recurso mais genérica, que aceita uma função para gerar dados de áudio.

Implementação

Implemente recursos usando duas construções separadas, AudioAsset e AudioStream.

AudioAsset define as propriedades estáticas de um recurso e um contêiner para possíveis dados internos relacionados ao recurso. De AudioAsset AudioStream pode ser derivado, que é uma única instância de stream do recurso. AudioStream contém um estado interno relacionado à reprodução de stream singular.

/// NOTE: The following code is a sample definition to help understanding, it is not a
/// representation of the final code/implementation.

/// Static properties and definition of an Asset.
AudioAsset {
  /// Perform optional initialization steps, e.g. load bytes from file into memory.
  /// Can also define lazy loading, to load data at first playback instead.
  fn initialize(LazyLoad);

  /// Create a new AudioStream from the asset.
  fn create_stream() -> AudioStream;

  /// More functions for metadata etc. of the asset.
  ...
}

/// Single streamable instance of an AudioAsset
AudioStream {
  /// Gets the next bytes to play from the Asset together with if the current chunk of
  /// bytes contains any control signals (e.g. fade-out).
  fn get_playback(num_bytes: usize) -> ([u8], ControlSignals);

  /// Gets playback Mode details used to handle special states of playback
  /// e.g. when a chime gets is interrupted and put in "fade-out" mode.
  fn playback_mode() -> PlaybackMode;

  /// [0.0, 1.0] indication of how much of the stream was played.
  fn progress() -> f32;

  /// Reset the stream, e.g. if it should play again.
  fn reset();

  /// Time of which the stream was created.
  fn created_at() -> Instant;

  /// Additional metadata etc. for the stream.
  ...
}

Reprodução de sons

Esta seção descreve a API e o procedimento para reprodução de um som. Uma reprodução de som singular é chamada de stream.

Ciclo de vida de um stream

A Figura 2 ilustra o ciclo de vida de um stream:

Reprodução e eventos de stream

Figura 2. Reprodução de stream e eventos.

A Figura 2 descreve estas etapas:

  1. Reproduzir:programa o stream para reprodução.

  2. Priorizar:a priorização da reprodução decide se:

    • Reproduzir o som agora (evento iniciado quando os primeiros bytes)
    • Reproduzir o som mais tarde (evento pausado ou retomado)
    • Despriorizar o som (evento cancelado)
  3. Controles do mixer:se necessário, atualize os controles do mixer com base nos comportamentos configurados.

  4. Gravar bytes:grava um bloco de bytes em AudioDevice.

  5. Mais dados:se o stream tiver mais dados, volte à etapa 2.

  6. Repetir:se o stream precisar ser repetido, redefina e volte à etapa 2 (evento reiniciado).

  7. Concluído:o stream foi concluído (FinishedSuccessfully event).

O som pode ser interrompido com chamadas de pausa, retomada ou parada a qualquer momento.

Prioridades de som

Esta lógica define as prioridades de som:

  1. Substituições do modo de reprodução. Por exemplo, um som no modo de esmaecimento sempre recebe a maior prioridade até que o esmaecimento seja concluído.

  2. Prioridade especificada.

  3. Se a prioridade igual for mais recente, o som será reproduzido primeiro.

Quando os sons têm a mesma prioridade, AudioManager é instanciado com um valor enum.

API

Eventos

Se um canal de eventos for fornecido quando o som começar, o Audio do HAR vai emitir vários eventos durante a reprodução. Os eventos com suporte são mostrados neste exemplo:

/// NOTE: The following code is a sample definition to help understanding, it is not a
/// representation of the final code/implementation.

StreamBehaviors<PlatformStreamBehaviors> {
  /// What should happen if the stream is interrupted for a higher priority stream.
  /// e.g. pause-and-resume or cancel, will also define preference for fade-out.

  OverrunBehavior,
  /// Urgency, if interrupted streams are allowed to "fade-out", or if the stream should
  /// urgently disrupt any other playback.
  Optional<Urgency>,

  /// Priority for the stream (or minimum if not specified).
  Optional<StreamPriority>
  /// Descriptor if a stream should be played on repeat.
  Optional<RepeatBehavior>
  /// Volume, if the stream should play at a specific volume.
  Optional<Volume>
  /// Spatialization, if the stream should play with specific spatialization.
  Optional<Spatialization>

  /// Optional generic for future expandability of the API, or pass-through of platform
  /// specific Stream Behaviors
  Optional<PlatformStreamBehaviors>
}

/// Plays a chime on specified device with given behaviors. StreamEvents are delivered
/// using the provided event transmitter. This method won't wait for any events.
fn play(AudioDeviceID, AssetID, StreamBehaviors, Option<EventTransmitter>) -> StreamController

/// Object used to control a Stream.
StreamController {
  /// Gets the current state/metadata of a stream (e.g. ID, progress, playback_state).
  fn metadata() -> StreamMetadata

  /// Stops the stream.
  fn stop()

  /// Pauses a given stream, if the specified duration expires the stream is cancelled.
  /// Timeout is required to make sure there are no paused streams left indefinitely
  /// pending resumption.
  fn pause(TimeoutDuration)

  /// Resumes a paused stream.
  fn resume()

  /// Updates the spatialization of a playing stream.
  fn set_spatialization(Spatialization)

  /// Updates the volume of a playing stream.
  fn set_volume(Volume)
}

Controle do mixer

Esta seção descreve como o volume e a espacialização são controlados.

Volume

O HAR define o volume de forma consistente em milibel. O crate har-platform-api processa a conversão de milibel para sinal de controle.

A relação entre milibel e saída de energia de hardware é logarítmica e varia muito entre diferentes configurações de hardware e alto-falante. Como resultado, forneça a configuração entre os valores como parte da configuração AudioDevice (dispositivo de áudio e PCM), e a conversão precisa ocorrer antes de chamar a camada da plataforma.

Como resultado, a implementação na API PAL define duas funções.

fn set_volume_millibel(AudioDeviceID, Millibel) {
  /// Default implementation with conversion using DeviceConfig.
}

fn set_volume_control(AudioDeviceID, ControlValue);

A implementação padrão para set_volume_millibel usa a configuração fornecida para AudioDevice, incluindo um conjunto de pares de chave-valor para milibel de referência - controle, transforma o milibel para controlar valores e, em seguida, chama a função set_volume_control com o valor convertido.

Esse design fornece um padrão e permite que implementações subsequentes substituam o mapeamento padrão.

Fluxo de áudio HAR

Figura 3. Fluxo de áudio do HAR.

Espacialização

A API Audio expõe a funcionalidade para controlar em qual área espacial os dados de áudio devem ser reproduzidos. Esses parâmetros são transmitidos para a camada PAL e aplicados downstream usando controles de hardware. As opções são definidas como parte da API PAL como:

/// NOTE: The following code is a sample definition to help understanding, it is not a
/// representation of the final code/implementation.

enum Spatialization {
  Front,
  FrontLeft,
  FrontRight,
  Center, // No spatialization
  Rear,
  RearLeft,
  RearRight,
  Right,
  Left
}

Níveis de controle do mixer

É possível definir o volume e a espacialização em um recurso e em um stream. Se você definir uma prioridade de stream, o stream vai substituir os controles definidos pelo recurso.

Gerenciamento de linhas de execução

O gerenciador de áudio mantém uma linha de execução por instância AudioDevice. Cada linha de execução opera de forma independente. A interação entre AudioManager e a linha de execução de reprodução usa uma fila de stream compartilhada classificada por prioridade.

As chamadas ALSA usam gravações ASYNC com pesquisa para determinar quando os dados são processados.

Sequência de gerenciamento de linhas de execução

Figura 4. Sequência de gerenciamento de linhas de execução.

Sinais de controle durante a pesquisa

Ao aguardar o processamento de bytes pela placa de som, os sinais de controle podem ser emitidos. Por exemplo, para mudar o esmaecimento ou a espacialização do áudio. A pesquisa para receber o estado do dispositivo de áudio é configurada no nível AudioManager ou padrão para 1 milissegundo. Após cada ciclo de pesquisa, a linha de execução de reprodução processa e emite comandos de controle temporizados.

Gerenciamento de buffer

Para minimizar a latência de interrupção, os tamanhos de buffer gravados no dispositivo são mantidos pequenos. Ao usar o TinyALSA como padrão, o tamanho do buffer é configurado para ser o mesmo que o parâmetro startup_threshold. O TinyALSA define o padrão como todo o buffer de dispositivo alocado dividido por dois.

Interrupção de stream

Quando os streams são interrompidos, eles mantêm a prioridade da linha de execução até que os dados gravados no card sejam drenados. Como resultado, um período de transição ocorre entre a interrupção e o novo stream.

Por exemplo,se uma amostra de áudio no HAR usar um:

  • Tamanho de 3.072
  • Taxa de 48.000
  • Tamanho da amostra de dois

O buffer pendente é calculado como 3.072 e 6.144 frames, o que resulta em um atraso de interrupção de 64 a 128 milissegundos. Uma implementação de produção exigiria um buffer menor.

Gerenciamento de erros e riscos

Esta seção descreve como os erros são gerenciados e os possíveis riscos.

Streams obsoletos e falta de fila

Como AudioStream pode ser pausado e a reprodução só pode ocorrer na instância AudioStream de maior prioridade, surge o risco de uma fila crescente que priva streams de baixa prioridade.

Para evitar essa ocorrência, cada fila é limitada a um tamanho configurável. Quando esse valor é excedido, o stream de menor prioridade é descartado.

Monitorar e alertar

Na produção, o monitor de segurança rastreia recursos de áudio para acompanhar se a reprodução ocorre conforme o esperado.

AudioManager monitora as estatísticas internas específicas para latências e um flag que define o desempenho do registro. Depois de definir esses limites, os registros de aviso são gerados para todas as builds de depuração quando:

  • A duração entre o agendamento e o início da reprodução excede x milissegundos.
  • O comprimento do recurso e o tempo de reprodução (para um stream não interrompido) diferem em mais de y por cento.

Dispositivo bloqueado

Sempre há um pequeno risco de um dispositivo de áudio ficar sem resposta, por exemplo, se ele for alocado e gravado por outro processo no sistema. Como a reprodução é executada de forma assíncrona em linhas de execução separadas e os sons podem ser enfileirados para reprodução posterior, isso é completamente transparente para o app de chamada.

Para detectar isso, uma verificação de integridade da linha de execução é feita sempre que um novo som é programado para ser reproduzido, retornando um erro se uma linha de execução de reprodução tiver uma fila preenchida e não tiver processado nenhum byte novo no último segundo.

Para fins futuros, pode ser necessário tentar reiniciar / abrir dispositivos, mas, para a implementação inicial, os erros não devem ser invisíveis.

Estrutura do código

Em um nível alto, o código relacionado à reprodução de sons existe nos seguintes crates:

CRATE: display-safety/crates/(harry-app|harry)

O app HAR atual, que emite chamadas para reproduzir sons.

NEW CRATE: display-safety/crates/audio

NOVO: crate para gerenciar o controle e a reprodução de áudio (é aqui que a maioria das funcionalidades existe).

CRATE: display-safety/crates/har-platform-api/audio

PAL, incluindo todas as chamadas de sistema necessárias para áudio.

CRATE: display-safety/crates/har-platform-(android|linux)/audio

Chamadas para tinyalsa-rs para reprodução usando TinyALSA. O suporte ao QNX não é implementado na solução inicial, e isso vai aumentar à medida que mais plataformas forem compatíveis.

TINYALSA PAL: display-safety/crates/tinyalsa-audio

Código específico do TinyALSA para reprodução. Isso é usado pelas implementações de plataforma Android e Linux.

CRATE: display-safety/crates/tinyalsa-rs

Vinculações Rust para implementação C do TinyALSA

Detalhes da implementação do Rust

Alguns detalhes específicos da implementação:

  • Todas as funções da API retornam Result<X, AudioError>, em que X é () ou um valor de retorno.
  • Nenhuma função da API é marcada como unsafe.
  • Os mecanismos de mutex e sincronização são internos e não são expostos na API AudioManager.

Modelo de propriedade e AudioManager

  • Toda a interação do app com o sistema de áudio ocorre por meio de AudioManager ou objetos retornados de AudioManager.

  • AudioManager é seguro para linhas de execução.

  • AudioManager é instanciado uma vez no app HARry e Moved, para que Looper tenha a propriedade.

  • AudioManager usa um token tokio_util::CancellationToken para gerenciar suas linhas de execução de reprodução iniciadas, garantindo que as linhas de execução sejam encerradas e os recursos liberados se AudioManager for Dropped.

  • AudioManager não impede explicitamente que várias instâncias sejam criadas. Se houver mais de uma instância, ela será registrada com o nível warn.

Propriedade compartilhada

Vários objetos têm propriedade compartilhada encapsulada e sincronizada com acesso exclusivo. Esses mecanismos não são expostos na API AudioManager, mas são internos às implementações de áudio e PAL.

  • AudioDevice - cada referência de hardware (por exemplo, TinyALSA PCM) que é aberta (tem um identificador) tem acesso exclusivo. Consulte Design de SMP.

  • As instâncias AudioStream têm acesso exclusivo depois de serem programadas para reprodução, porque podem ser controladas pelo app e acessadas simultaneamente pela linha de execução de reprodução.

    A linha de execução de reprodução não mantém bloqueios durante a reprodução, mas faz um snapshot imutável do próximo buffer a ser reproduzido e não considera as mudanças até que o próximo buffer seja processado.

  • Cada linha de execução de reprodução tem uma fila de reprodução, uma referência compartilhada entre AudioManager e a linha de execução de reprodução. Como resultado, a linha de execução precisa de acesso exclusivo para mutações.

  • As linhas de execução sem streams ficam inativas com a Condvar variável para receber eventos de ativação quando novos dados são detectados. Esse mecanismo tem propriedade compartilhada.

Dependências

Os crates e o crate de áudio são projetados para reduzir as dependências de crates que não são aprovados para serem criados na árvore de origem do Android. Consulte esta lista de crates incluídos .

As implementações de plataforma downstream para Android e Linux dependem de TinyALSA e do crate tinyalsa-rs de segurança de exibição atual.

Atributos de qualidade

Confiabilidade

Embora a reprodução de áudio seja essencial para a segurança, esse design não abrange a implementação de um monitoramento de segurança. Implemente isso em um esforço separado para verificar a confiabilidade da reprodução de áudio no hardware e na produção.

Escalonabilidade

A abordagem de uma linha de execução por dispositivo tem como objetivo ser escalonada para diferentes configurações de hardware. Como cada linha de execução fica principalmente inativa, aguardando dados ou aguardando o dispositivo processar dados gravados, ela não deve ser exigente no processador ou no desempenho do sistema.

A decisão de design de reproduzir dados apenas em um único dispositivo, combinada com comandos de controle do mixer para todo o controle de saída adicional, garante que a saída exata seja processada pelo hardware de som e possa ser escalonada para sistemas futuros.

Latência

A latência é essencial para o sistema de áudio. Portanto, após a implementação, um conjunto de objetivos de nível de serviço (SLOs) é definido para a latência do sistema. Para monitorar continuamente a integridade da latência, o monitoramento nos registros do sistema não atende aos SLOs definidos em todas as builds de depuração.

Para as versões de produção, os dados de monitoramento são transmitidos para algum sistema externo à implementação de áudio, em vez de depender de registros.

Teste e estratégia de teste

Os crates e o crate de áudio são projetados com cobertura de teste. Adicionamos uma implementação de plataforma simulada para confirmar que todos os recursos são testados.

A complexidade do hardware e das vinculações impede a cobertura de testes extensiva para implementações de plataforma. Fornecemos implementações de amostra para testar manualmente a solução no hardware e no emulador Cuttlefish.

Documentação

O arquivo README.md em Audio crates/audio descreve como usar AudioManager. crates/audio/examples contém exemplos para:

  • Implementar uma plataforma.
  • Criar uma instância de AudioManager.
  • Reproduzir WavAsset.
  • Reproduzir um recurso de função personalizada repetidamente.
  • Registrar eventos de reprodução.