Sygnały dźwiękowe

Ten artykuł opisuje odtwarzanie dzwonka w renderowaniu o wysokiej dostępności (HAR). Audio skrzynka udostępnia AudioManager aplikacji HAR, która steruje odtwarzaniem dzwonka.

Aby utrzymać niskie opóźnienie, wątki odtwarzania działają przez cały czas działania aplikacji, przechodząc w stan bezczynności i zwalniając zasoby, gdy nie jest odtwarzany żaden dźwięk.

Terminologia

zasób
AudioAsset dotyczy odtwarzalnego dźwięku. Komponenty są powszechnie znane i istnieją w środowisku wykonawczym aplikacji.
urządzenie
AudioDevice odnosi się do osobnej magistrali do odtwarzania dźwięku. Urządzenie jest najmniejszą jednostką związaną ze sprzętem, do której system ma dostęp. W standardowej implementacji SDVM symbol AudioDevice odnosi się do pojedynczego interfejsu PCM w architekturze ALSA (Advanced Linux Sound Architecture).
strumień
Odtwarzanie komponentu na urządzeniu. Strumienie są przechowywane od momentu zaplanowania do momentu zakończenia, anulowania lub zakończenia z błędem.

Komponenty

Na rysunku 1 przedstawiono diagram komponentów dzwonka:

Diagram komponentów

Rysunek 1. Diagram komponentów.

Urządzenie audio i PCM

Konfiguracja sprzętu audio jest zgodna ze standardową warstwą abstrakcji platformy HAR i har-platform-api ją zawiera.

Pakiet HAR Audio definiuje nową strukturę dla AudioDevice, która określa pola dla wszystkich struktur danych wpływających na wewnętrzny pakiet HAR Audio i odtwarzanie. AudioDevice używa też typów ogólnych do opakowywania potencjalnych dodatkowych parametrów specyficznych dla platformy. W przypadku tinyalsa PlatformAudioDevice zawiera deskryptory i właściwości ALSA PCM.

/// 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
}

Zasoby audio

W tej sekcji opisujemy, jak konfigurować i wdrażać komponenty audio.

Konfiguracja

Początkowa implementacja dźwięku HAR obsługuje statycznie skonfigurowane komponenty audio. Konfiguracja JSON określa, które komponenty są dostępne, a które są zdefiniowane jako pliki WAV.

Implementacja obsługuje też syntetyzowane i strumieniowane komponenty audio, ale w bardziej ogólny sposób, który akceptuje funkcję generowania danych audio.

Implementacja

Wdrażaj komponenty za pomocą 2 oddzielnych konstrukcji: AudioAssetAudioStream.

AudioAsset określa statyczne właściwości zasobu i jest kontenerem potencjalnych danych wewnętrznych związanych z zasobem. Z AudioAsset AudioStream można utworzyć pojedyncze wystąpienie zasobu, które można przesyłać strumieniowo. AudioStream zawiera stan wewnętrzny związany z odtwarzaniem pojedynczego strumienia.

/// 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.
  ...
}

Odtwarzanie gongu

W tej sekcji opisujemy interfejs API i procedurę odtwarzania dzwonka. Pojedyncze odtworzenie dzwonka jest nazywane strumieniem.

Cykl życia strumienia

Na ilustracji 2 przedstawiono cykl życia strumienia:

Odtwarzanie strumienia i zdarzenia

Rysunek 2. odtwarzanie strumieni i wydarzeń;

Rysunek 2 przedstawia te kroki:

  1. Odtwórz: zaplanuj odtwarzanie transmisji.

  2. Priorytet: priorytet odtwarzania określa, czy:

    • Odtwórz gong (wydarzenie rozpoczęte po otrzymaniu pierwszych bajtów)
    • Odtwórz dzwonek później (wstrzymane lub wznowione wydarzenie)
    • Obniżanie priorytetu dzwonka (anulowane wydarzenie)
  3. Elementy sterujące mikserem: w razie potrzeby zaktualizuj elementy sterujące mikserem na podstawie skonfigurowanych zachowań.

  4. Write bytes (Zapisz bajty): zapisz fragment bajtów w AudioDevice.

  5. Więcej danych: jeśli strumień zawiera więcej danych, wróć do kroku 2.

  6. Powtórz: jeśli transmisja ma być powtórzona, zresetuj ją i wróć do kroku 2 (ponownie uruchomione wydarzenie).

  7. Ukończono: strumień został ukończony (FinishedSuccessfullyzdarzenie).

Dźwięk można w każdej chwili przerwać, wstrzymując, wznawiając lub kończąc połączenia.

Priorytety gongu

Ta logika określa priorytety dzwonka:

  1. Zastąpienia trybu odtwarzania. Na przykład sygnał dźwiękowy w trybie wyciszania zawsze ma najwyższy priorytet, dopóki wyciszanie nie zostanie zakończone.

  2. Określony priorytet.

  3. Jeśli priorytet równy jest nowszy, najpierw odtworzony zostanie dzwonek.

Gdy dzwonki mają równy priorytet, makro AudioManager jest tworzone z wartością enum.

Interfejs API

Wydarzenia

Jeśli podczas uruchamiania dzwonka podany jest kanał zdarzeń, HARAudio emituje podczas odtwarzania kilka zdarzeń. Obsługiwane zdarzenia są widoczne w tym przykładzie:

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

Sterowanie mikserem

W tej sekcji opisujemy, jak kontrolować głośność i przestrzenność.

Głośność

HAR określa głośność w milibelach. Pakiet har-platform-api zajmuje się konwersją z milibelów na sygnał sterujący.

Relacja między milibelami a mocą wyjściową sprzętu jest logarytmiczna i znacznie różni się w zależności od konfiguracji sprzętu i głośników. W związku z tym w ramach konfiguracji AudioDevice (Urządzenie audio i PCM) podaj konfigurację między wartościami, a konwersja musi nastąpić przed wywołaniem warstwy platformy.

W rezultacie implementacja w interfejsie PAL API definiuje 2 funkcje.

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

fn set_volume_control(AudioDeviceID, ControlValue);

Domyślna implementacja funkcji set_volume_millibel korzysta z konfiguracji podanej dla funkcji AudioDevice, w tym z zestawu par klucz-wartość dla odniesienia millibel – sterowanie, przekształca millibel na wartości sterujące, a następnie wywołuje funkcję set_volume_control z przekonwertowaną wartością.

Takie rozwiązanie zapewnia domyślne mapowanie i umożliwia późniejsze zastąpienie go w kolejnych implementacjach.

Przepływ dźwięku HAR

Rysunek 3. Przepływ audio HAR.

Nadawanie trójwymiarowości

Interfejs Audio API udostępnia funkcje, które pozwalają określać, w jakim obszarze przestrzennym mają być odtwarzane dane audio. Te parametry są przekazywane do warstwy PAL i mogą być stosowane w dalszej części procesu za pomocą elementów sterujących sprzętu. Opcje są zdefiniowane w ramach interfejsu PAL API jako:

/// 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
}

Poziomy kontroli miksera

Możesz określić głośność i przestrzenność zasobu oraz strumienia. Jeśli określisz priorytet strumienia, zastąpi on ustawienia zdefiniowane przez komponent.

Zarządzanie wątkami

Menedżer audio utrzymuje 1 wątek na instancję AudioDevice. Każdy wątek działa niezależnie. Interakcja między AudioManager a wątkiem odtwarzania odbywa się za pomocą wspólnej kolejki strumieni posortowanej według priorytetu.

Wywołania ALSA używają zapisów ASYNC z odpytywaniem, aby określić, kiedy dane są przetwarzane.

Sekwencja zarządzania wątkami

Rysunek 4. Sekwencja zarządzania wątkami.

Sygnały sterujące podczas odpytywania

Podczas oczekiwania na przetworzenie bajtów przez kartę dźwiękową można wysyłać sygnały sterujące. Na przykład aby zmienić zanikanie lub przestrzenność dźwięku. Odpytywanie w celu uzyskania stanu urządzenia audio jest konfigurowane na poziomie AudioManager lub domyślnie ustawione na 1 milisekundę. Po każdym cyklu odpytywania wątek odtwarzania przetwarza i wydaje polecenia sterowania czasowego.

Zarządzanie buforem

Aby zminimalizować opóźnienie przerw, rozmiary buforów zapisywanych na urządzeniu są niewielkie. Gdy TinyALSA jest używany jako domyślny, rozmiar bufora jest taki sam jak parametr startup_threshold. Biblioteka TinyALSA definiuje wartość domyślną jako połowę całego przydzielonego bufora urządzenia.

Przerwanie transmisji

Gdy strumienie zostaną przerwane, zachowują priorytet wątku, dopóki dane zapisane na karcie nie zostaną wyczerpane. W rezultacie między przerwą a nowym strumieniem występuje okres przejściowy.

Na przykład jeśli próbka dźwięku w pliku HAR używa:

  • Rozmiar 3072
  • Stawka: 48 000
  • Próbka o rozmiarze 2

Bufor oczekujący jest obliczany jako 3072 i 6144 klatki, co powoduje opóźnienie przerwy od 64 do 128 milisekund. Wdrożenie produkcyjne wymagałoby mniejszego bufora.

Zarządzanie błędami i ryzyko

W tej sekcji opisujemy, jak zarządza się błędami i jakie są potencjalne zagrożenia.

Nieaktualne strumienie i brak zasobów w kolejce

Ponieważ AudioStream można wstrzymać, a odtwarzanie może się odbywać tylko z instancji AudioStream o najwyższym priorytecie, istnieje ryzyko, że rosnąca kolejka będzie blokować strumienie o niskim priorytecie.

Aby tego uniknąć, każda kolejka ma konfigurowalny limit rozmiaru. Gdy ta wartość zostanie przekroczona, strumień o najniższym priorytecie zostanie odrzucony.

Monitorowanie i alerty

W środowisku produkcyjnym monitor bezpieczeństwa śledzi funkcje audio, aby sprawdzić, czy odtwarzanie przebiega zgodnie z oczekiwaniami.

AudioManager monitoruje wewnętrzne statystyki dotyczące opóźnień i flagę określającą wydajność logowania. Po ustawieniu tych progów w przypadku wszystkich wersji debugowania generowane są logi ostrzeżeń, gdy:

  • Czas między zaplanowaniem a rozpoczęciem odtwarzania przekracza x milisekund.
  • (W przypadku nieprzerwanego strumienia) długość zasobu i czas odtwarzania różnią się o więcej niż y%.

Urządzenie zablokowane

Zawsze istnieje niewielkie ryzyko, że urządzenie audio przestanie odpowiadać, na przykład jeśli zostanie przydzielone i zapisane przez inny proces w systemie. Odtwarzanie odbywa się asynchronicznie w osobnych wątkach, a dźwięki mogą być umieszczane w kolejce do późniejszego odtworzenia, więc jest to całkowicie niewidoczne dla aplikacji wywołującej.

Aby to wykryć, za każdym razem, gdy zaplanowane jest odtworzenie nowego dzwonka, przeprowadzana jest kontrola stanu wątku. Jeśli wątek odtwarzania ma wypełnioną kolejkę i w ciągu ostatniej sekundy nie przetworzył żadnych nowych bajtów, zwracany jest błąd.

W przyszłości może być konieczne ponowne uruchomienie lub otwarcie urządzeń, ale w przypadku początkowej implementacji błędy nie powinny być niewidoczne.

Struktura kodu

Ogólnie kod związany z odtwarzaniem dzwonków znajduje się w tych pakietach:

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

Istniejąca aplikacja HAR, która wywołuje odtwarzanie dzwonków.

NEW CRATE: display-safety/crates/audio

NOWOŚĆ: skrzynka do zarządzania sterowaniem dźwiękiem i odtwarzaniem (zawiera większość funkcji).

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

PAL, w tym wszystkie wywołania systemowe wymagane w przypadku dźwięku.

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

Połączenia z tinyalsa-rs w celu odtwarzania za pomocą TinyALSA. Obsługa QNX nie jest zaimplementowana w początkowym rozwiązaniu, ale będzie się rozwijać wraz z obsługą kolejnych platform.

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

Kod specyficzny dla TinyALSA do odtwarzania. Jest on używany w implementacjach na platformy Android i Linux.

CRATE: display-safety/crates/tinyalsa-rs

Powiązania Rusta z implementacją TinyALSA w języku C

Szczegóły implementacji w Rust

Szczegóły implementacji:

  • Wszystkie funkcje interfejsu API zwracają Result<X, AudioError>, gdzie X to () lub wartość zwracana.
  • Żadne funkcje interfejsu API nie są oznaczone jako unsafe.
  • Mechanizmy mutex i synchronizacji są wewnętrzne i nie są udostępniane w interfejsie AudioManagerAPI.

Model własności i AudioManager

  • Cała interakcja aplikacji z systemem audio odbywa się za pomocą interfejsu AudioManager lub obiektów zwracanych przez AudioManager.

  • AudioManager jest wątkowo bezpieczny.

  • AudioManager jest tworzony raz w aplikacji HARry, a Moved, aby Looper miał własność.

  • AudioManager używa tokena tokio_util::CancellationToken do zarządzania rozpoczętymi wątkami odtwarzania, co zapewnia zakończenie wątków i zwolnienie zasobów, jeśli AudioManager jest Dropped;

  • AudioManager nie uniemożliwia wyraźnie tworzenia wielu instancji. Jeśli istnieje więcej niż 1 instancja, rejestruje się ją na poziomie warn.

Wspólna własność

Liczba obiektów, które mają wspólną własność i są zsynchronizowane z wyłącznym dostępem. Te mechanizmy nie są dostępne w interfejsie AudioManager API, ale są wewnętrzne w implementacjach audio i PAL.

  • AudioDevice – każda referencja sprzętowa (np. TinyALSA PCM), która jest otwarta (ma uchwyt), ma wyłączny dostęp. Zobacz SMP Design.

  • Instancje AudioStream mają wyłączny dostęp po zaplanowaniu odtwarzania, ponieważ mogą być sterowane przez aplikację i jednocześnie dostępne dla wątku odtwarzania.

    Wątek odtwarzania nie blokuje zasobów podczas odtwarzania, ale tworzy niezmienny zrzut następnego bufora do odtworzenia i nie uwzględnia zmian, dopóki nie przetworzy następnego bufora.

  • Każdy wątek odtwarzania ma kolejkę odtwarzania, czyli wspólne odniesienie między AudioManager a wątkiem odtwarzania. W związku z tym wątek potrzebuje wyłącznego dostępu do mutacji.

  • Wątki bez strumieni przechodzą w stan bezczynności ze zmienną Condvar, aby otrzymywać zdarzenia wybudzania po wykryciu nowych danych. Ten mechanizm ma współwłasność.

Zależności

Skrzynie i skrzynie audio mają na celu zmniejszenie zależności od skrzyń, które nie są zatwierdzone do budowania w drzewie źródłowym Androida. Zobacz listę uwzględnionych skrzyń.

Implementacje platformy niższego szczebla na Androida i Linuksa zależą od TinyALSA i istniejącego pakietu tinyalsa-rs bezpieczeństwa wyświetlania.

Atrybuty jakości

Niezawodność

Odtwarzanie dźwięku jest kluczowe dla bezpieczeństwa, ale ten projekt nie obejmuje wdrożenia monitorowania bezpieczeństwa. Wdróż to w ramach osobnego projektu, aby sprawdzić niezawodność odtwarzania dźwięku na sprzęcie i w środowisku produkcyjnym.

Skalowalność

Podejście polegające na używaniu jednego wątku na urządzenie ma na celu skalowanie do różnych konfiguracji sprzętowych. Każdy wątek jest w większości bezczynny, czeka na dane lub na to, aż urządzenie przetworzy zapisane dane, więc nie powinien obciążać procesora ani systemu.

Decyzja projektowa, aby odtwarzać dane tylko na jednym urządzeniu, w połączeniu z poleceniami sterowania mikserem dla wszystkich dalszych elementów sterujących wyjściem zapewnia, że dokładne wyjście jest obsługiwane przez sprzęt dźwiękowy i powinno być skalowalne w przypadku przyszłych systemów.

Czas oczekiwania

Opóźnienie ma kluczowe znaczenie dla systemu audio, dlatego po wdrożeniu definiuje się zestaw docelowych poziomów usług (SLO) dla opóźnienia systemu. Aby stale monitorować stan opóźnienia, monitoruj dzienniki systemowe, które nie spełniają zdefiniowanych poziomów usług w przypadku wszystkich wersji debugowania.

W przypadku wersji produkcyjnych dane monitorowania są przekazywane do systemu zewnętrznego w stosunku do implementacji audio, a nie do logów.

Testowanie i strategia testowania

Kraty i krata audio są zaprojektowane z uwzględnieniem pokrycia testami. Dodaliśmy implementację platformy testowej, aby potwierdzić, że wszystkie funkcje są testowane.

Złożoność sprzętu i powiązań uniemożliwia przeprowadzenie kompleksowych testów implementacji platformy. Udostępniamy przykładowe implementacje, aby umożliwić ręczne testowanie rozwiązania na sprzęcie i emulatorze Cuttlefish.

Dokumentacja

Plik README.mdAudio crates/audio opisuje, jak korzystać z AudioManager. crates/audio/examples zawiera przykłady:

  • Wdróż platformę.
  • Utwórz instancję AudioManager.
  • Odtwórz WavAsset.
  • Odtwarzaj komponent funkcji niestandardowej w pętli.
  • rejestrować zdarzenia odtwarzania;