In diesem Inhalt wird die Wiedergabe von Chimes im High Availability Renderer (HAR) beschrieben.
Ein Audio-Crate stellt AudioManager für die HAR-App bereit, die die Wiedergabe von Chimes steuert.
Um die Latenz gering zu halten, werden Wiedergabethreads während der gesamten Lebensdauer der App ausgeführt. Sie werden in den Leerlauf versetzt und geben die Ausführung ab, wenn kein Audio abgespielt wird.
Terminologie
- Asset
AudioAssetbezieht sich auf abspielbare Audioinhalte. Assets sind allgemein bekannt und in der App-Laufzeit vorhanden.- Gerät
AudioDevicebezieht sich auf einen separaten Bus für die Audiowiedergabe. Das Gerät ist die kleinste Einheit in Bezug auf Hardware, auf die das System zugreift. In der Standard-SDVM-Implementierung bezieht sichAudioDeviceauf ein einzelnes ALSA-PCM (Advanced Linux Sound Architecture).- stream
- Eine Instanz der Wiedergabe eines Assets auf einem Gerät. Streams bleiben vom Zeitpunkt der Planung bis zum Abschluss, zur Kündigung oder zum Fehler bestehen.
Komponenten
Abbildung 1 zeigt das Komponentendiagramm für Chime:
Abbildung 1. Komponentendiagramm.
Audiogerät und PCM
Die Konfiguration der Audiohardware folgt dem Standarddesign der HAR-Plattformabstraktionsschicht und ist in har-platform-api enthalten.
Das HAR-Audio-Crate definiert eine neue Struktur für AudioDevice, die Felder für alle Datenstrukturen definiert, die sich auf das interne HAR-Audio-Crate und die Wiedergabe auswirken. AudioDevice verwendet auch Generics, um potenzielle plattformspezifische zusätzliche Parameter zu umschließen. Im Fall von tinyalsa enthält PlatformAudioDevice die Deskriptoren und Attribute eines 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
}
Audio-Assets
In diesem Abschnitt wird beschrieben, wie Audio-Assets konfiguriert und implementiert werden.
Konfiguration
Die erste HAR-Audioimplementierung unterstützt statisch konfigurierte Audio-Assets. In einer JSON-Konfiguration wird definiert, welche Assets verfügbar sind und welche Assets als WAV-Dateien definiert sind.
Die Implementierung unterstützt auch synthetisierte und gestreamte Audio-Assets über eine allgemeinere Asset-Implementierung, die eine Funktion zum Generieren von Audiodaten akzeptiert.
Implementierung
Implementieren Sie Assets mit zwei separaten Konstrukten, AudioAsset und AudioStream.
AudioAsset definiert die statischen Eigenschaften eines Assets und ist ein Container für potenzielle interne Daten, die mit dem Asset verknüpft sind. Aus AudioAsset AudioStream kann AudioAsset AudioStream abgeleitet werden, eine einzelne streamfähige Instanz des Assets. AudioStream
enthält einen internen Status, der sich auf die Wiedergabe des einzelnen Streams bezieht.
/// 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.
...
}
Glockenton
In diesem Abschnitt werden die API und die Vorgehensweise für die Wiedergabe eines Glockentons beschrieben. Die Wiedergabe eines einzelnen Chimes wird als Stream bezeichnet.
Lebenszyklus eines Streams
Abbildung 2 veranschaulicht den Lebenszyklus eines Streams:
Abbildung 2. Streamwiedergabe und ‑ereignisse
Abbildung 2 veranschaulicht diese Schritte:
Wiedergabe:Hier kannst du einen Stream für die Wiedergabe planen.
Priorisieren: Mit der Priorisierung der Wiedergabe wird festgelegt, ob
- Chime jetzt abspielen (Event wurde beim Empfang der ersten Bytes gestartet)
- Glockenspiel später abspielen (pausiertes oder fortgesetztes Ereignis)
- Glocke weniger wichtig einstufen (abgesagter Termin)
Mixer-Steuerelemente:Aktualisieren Sie die Mixer-Steuerelemente bei Bedarf basierend auf den konfigurierten Verhaltensweisen.
Byte schreiben:Schreibt einen Byte-Chunk in
AudioDevice.Mehr Daten:Wenn der Stream mehr Daten enthält, kehren Sie zu Schritt 2 zurück.
Wiederholen:Wenn der Stream wiederholt werden soll, setze ihn zurück und kehre zu Schritt 2 zurück (neu gestartetes Ereignis).
Abgeschlossen:Der Stream wurde erfolgreich abgeschlossen (
FinishedSuccessfully-Ereignis).
Der Chime kann jederzeit durch Pausieren, Fortsetzen oder Beenden von Anrufen unterbrochen werden.
Prioritäten für Glockentöne
Diese Logik legt die Prioritäten für Chimes fest:
Überschreibungen des Wiedergabemodus Beispiel: Ein Glockenton im Ausblendmodus hat immer höchste Priorität, bis das Ausblenden abgeschlossen ist.
Angegebene Priorität.
Wenn die gleichwertige Priorität neuer ist, wird zuerst der Chime abgespielt.
Wenn Chimes die gleiche Priorität haben, wird AudioManager mit einem enum-Wert instanziiert.
API
Ereignisse
Wenn beim Starten des Chimes ein Ereignis-Channel angegeben wird, gibt HAR Audio während der Wiedergabe eine Reihe von Ereignissen aus. Die unterstützten Ereignisse sind in diesem Beispiel zu sehen:
/// 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)
}
Mixer-Steuerung
In diesem Abschnitt wird beschrieben, wie Lautstärke und Räumlichkeit gesteuert werden.
Lautstärke
In HAR wird die Lautstärke einheitlich in Millibel definiert. Das har-platform-api-Crate übernimmt die Umwandlung von Millibel in ein Steuersignal.
Die Beziehung zwischen Millibel und Hardware-Ausgangsleistung ist logarithmisch und variiert stark zwischen verschiedenen Hardware- und Lautsprecherkonfigurationen. Konfigurieren Sie daher die Werte als Teil der AudioDevice-Konfiguration (Audio Device and PCM). Die Konvertierung muss vor dem Aufrufen der Plattformebene erfolgen.
Daher werden in der PAL API zwei Funktionen implementiert.
fn set_volume_millibel(AudioDeviceID, Millibel) {
/// Default implementation with conversion using DeviceConfig.
}
fn set_volume_control(AudioDeviceID, ControlValue);
Bei der Standardimplementierung für set_volume_millibel wird die für AudioDevice bereitgestellte Konfiguration verwendet, einschließlich einer Reihe von Schlüssel/Wert-Paaren für den Referenz-Millibel. Der Millibel wird in Kontrollwerte umgewandelt und dann wird die Funktion set_volume_control mit dem umgewandelten Wert aufgerufen.
Dieses Design bietet eine Standardeinstellung und ermöglicht es nachfolgenden Implementierungen, die Standardzuordnung zu überschreiben.
Abbildung 3. HAR-Audioablauf.
Räumliche Darstellung
Die Audio API bietet Funktionen, mit denen Sie steuern können, in welchem räumlichen Bereich Audiodaten wiedergegeben werden sollen. Diese Parameter werden an die PAL-Ebene übergeben und nachgelagert über Hardware-Steuerelemente angewendet. Optionen werden als Teil der PAL API so definiert:
/// 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
}
Mixer-Kontrollstufen
Sie können Lautstärke und Räumlichkeit für ein Asset und für einen Stream definieren. Wenn Sie eine Stream-Priorität definieren, werden die vom Asset definierten Steuerelemente durch den Stream überschrieben.
Threadverwaltung
Der Audio-Manager verwaltet einen Thread pro AudioDevice-Instanz. Jeder Thread arbeitet unabhängig. Die Interaktion zwischen AudioManager und dem Wiedergabethread erfolgt über eine gemeinsame Streamwarteschlange, die nach Priorität sortiert ist.
Bei ALSA-Aufrufen werden ASYNC-Schreibvorgänge mit Polling verwendet, um zu ermitteln, wann Daten verarbeitet werden.
Abbildung 4. Sequenz für die Threadverwaltung.
Steuersignale während des Polling
Während die Soundkarte auf die Verarbeitung von Bytes wartet, können Steuersignale ausgegeben werden. So können Sie beispielsweise das Ein- und Ausblenden oder die Räumlichkeit des Audiosignals ändern. Das Polling zum Abrufen des Status des Audiogeräts ist entweder auf der AudioManager-Ebene konfiguriert oder wird standardmäßig auf 1 Millisekunde festgelegt. Nach jedem Polling-Zyklus verarbeitet der Wiedergabethread alle zeitgesteuerten Steuerbefehle.
Pufferverwaltung
Um die Unterbrechungslatenz zu minimieren, werden die auf das Gerät geschriebenen Puffergrößen klein gehalten. Wenn TinyALSA als Standard verwendet wird, wird die Puffergröße so konfiguriert, dass sie dem Parameter startup_threshold entspricht.
TinyALSA definiert den Standard als den gesamten zugewiesenen Gerätepuffer geteilt durch zwei.
Streamunterbrechung
Wenn Streams unterbrochen werden, behalten sie die Thread-Priorität bei, bis die Daten, die sie auf die Karte geschrieben haben, vollständig übertragen wurden. Daher gibt es einen Übergangszeitraum zwischen der Unterbrechung und dem neuen Stream.
Beispiel: Wenn in einem HAR-Audiobeispiel Folgendes verwendet wird:
- Größe von 3.072
- Rate von 48.000
- Stichprobengröße 2
Der ausstehende Puffer wird mit 3.072 und 6.144 Frames berechnet, was zu einer Unterbrechungsverzögerung von 64 bis 128 Millisekunden führt. Für eine Produktionsimplementierung wäre ein kleinerer Puffer erforderlich.
Fehlermanagement und Risiken
In diesem Abschnitt wird beschrieben, wie Fehler verwaltet werden und welche potenziellen Risiken bestehen.
Veraltete Streams und Warteschlangen-Starvation
Da AudioStream pausiert werden kann und die Wiedergabe nur über die AudioStream-Instanz mit der höchsten Priorität erfolgen kann, besteht das Risiko, dass eine wachsende Warteschlange Streams mit niedriger Priorität blockiert.
Um dies zu verhindern, ist jede Warteschlange auf eine konfigurierbare Größe begrenzt. Wenn dieser Wert überschritten wird, wird der Stream mit der niedrigsten Priorität verworfen.
Monitoring und Benachrichtigung
In der Produktion verfolgt der Sicherheitsmonitor Audiofunktionen, um sicherzustellen, dass die Wiedergabe wie erwartet erfolgt.
AudioManager überwacht die internen Statistiken zu Latenzen und ein Flag, das die Logging-Leistung definiert. Nachdem Sie diese Grenzwerte festgelegt haben, werden Warnungsprotokolle für alle Debug-Builds generiert, wenn:
- Die Dauer zwischen der Planung und dem Beginn der Wiedergabe überschreitet
xMillisekunden. - Bei einem nicht unterbrochenen Stream weichen die Asset-Länge und die Wiedergabezeit um mehr als
y% voneinander ab.
Gerät blockiert
Es besteht immer ein geringes Risiko, dass ein Audiogerät nicht mehr reagiert, z. B. wenn es von einem anderen Prozess im System zugewiesen und beschrieben wird. Da die Wiedergabe asynchron in separaten Threads erfolgt und Chimes in die Warteschlange gestellt werden können, um später wiedergegeben zu werden, ist dies für die aufrufende App völlig transparent.
Um dies zu erkennen, wird bei jeder geplanten Wiedergabe eines neuen Glockentons eine Thread-Systemdiagnose durchgeführt. Wenn ein Wiedergabe-Thread eine gefüllte Warteschlange hat und in der letzten Sekunde keine neuen Bytes verarbeitet wurden, wird ein Fehler zurückgegeben.
Für zukünftige Zwecke kann es erforderlich sein, Geräte neu zu starten oder zu öffnen. Bei der ersten Implementierung sollten Fehler jedoch nicht unsichtbar sein.
Codestruktur
Auf übergeordneter Ebene ist der Code für die Wiedergabe von Glockensignalen in den folgenden Crates enthalten:
CRATE: display-safety/crates/(harry-app|harry)
Die vorhandene HAR-App, die Anrufe zum Abspielen von Glockensignalen ausgibt.
NEW CRATE: display-safety/crates/audio
NEU: Crate zum Verwalten von Audiosteuerung und ‑wiedergabe (hier sind die meisten Funktionen enthalten).
CRATE: display-safety/crates/har-platform-api/audio
PAL mit allen für Audio erforderlichen Systemaufrufen.
CRATE: display-safety/crates/har-platform-(android|linux)/audio
Ruft tinyalsa-rs für die Wiedergabe mit TinyALSA auf. QNX wird in der ursprünglichen Lösung nicht unterstützt. Die Unterstützung wird jedoch mit der Zeit auf weitere Plattformen ausgeweitet.
TINYALSA PAL: display-safety/crates/tinyalsa-audio
TinyALSA-spezifischer Code für die Wiedergabe. Wird von den Android- und Linux-Plattformimplementierungen verwendet.
CRATE: display-safety/crates/tinyalsa-rs
Rust-Bindungen für die TinyALSA-C-Implementierung
Details zur Rust-Implementierung
Einige spezifische Implementierungsdetails:
- Alle API-Funktionen geben
Result<X, AudioError>zurück, wobeiXentweder () oder ein Rückgabewert ist. - Keine API-Funktionen sind als
unsafegekennzeichnet. - Mutex- und Synchronisationsmechanismen sind intern und werden nicht in der
AudioManagerAPI verfügbar gemacht.
Eigentumsmodell und AudioManager
Die gesamte App-Interaktion mit dem Audiosystem erfolgt über
AudioManageroder Objekte, die vonAudioManagerzurückgegeben werden.AudioManagerist threadsicher.AudioManagerwird einmal in der HARry-App instanziiert undMoved, damitLooperdie Inhaberschaft hat.AudioManagerverwendet eintokio_util::CancellationToken-Token, um die gestarteten Wiedergabenthreads zu verwalten. So wird dafür gesorgt, dass die Threads beendet und Ressourcen freigegeben werden, wennAudioManagerDroppedist.AudioManagerverhindert nicht explizit, dass mehrere Instanzen erstellt werden. Wenn mehrere Instanzen vorhanden sind, wird mit der Ebenewarnprotokolliert.
Gemeinsame Eigentumsrechte
Eine Reihe von Objekten haben eine gemeinsame Eigentümerschaft, die mit exklusivem Zugriff umbrochen und synchronisiert wurde. Diese Mechanismen sind in der AudioManager API nicht verfügbar, sondern sind intern in den Audio- und PAL-Implementierungen enthalten.
AudioDevice: Jede Hardware-Referenz (z. B. TinyALSA PCM), die geöffnet wird (einen Handle hat), hat exklusiven Zugriff. Weitere Informationen finden Sie unter SMP-Design.AudioStream-Instanzen haben nach der Planung für die Wiedergabe exklusiven Zugriff, da sie von der App gesteuert und gleichzeitig vom Wiedergabethread aufgerufen werden können.Der Wiedergabethread hält während der Wiedergabe keine Sperren, sondern erstellt einen unveränderlichen Snapshot des nächsten zu spielenden Puffers und berücksichtigt Änderungen erst, wenn der nächste Puffer verarbeitet wurde.
Jeder Wiedergabethread hat eine Wiedergabewarteschlange, eine gemeinsame Referenz zwischen
AudioManagerund dem Wiedergabethread. Daher benötigt der Thread exklusiven Zugriff für Mutationen.Threads ohne Streams werden mit der Variablen
Condvarin den Leerlauf versetzt, um Weckereignisse zu empfangen, wenn neue Daten erkannt werden. Dieser Mechanismus hat eine gemeinsame Inhaberschaft.
Abhängigkeiten
Crates und Audio-Crates sollen die Abhängigkeiten von Crates reduzieren, die nicht im Android-Quellbaum erstellt werden dürfen. Hier finden Sie eine Liste der enthaltenen Crates.
Downstream-Plattformimplementierungen für Android und Linux hängen von TinyALSA und dem vorhandenen tinyalsa-rs-Crate für die Displaysicherheit ab.
Qualitätsattribute
Zuverlässigkeit
Die Audiowiedergabe ist sicherheitskritisch, aber dieses Design deckt die Implementierung einer Sicherheitsüberwachung nicht ab. Implementieren Sie dies in einem separaten Schritt, um die Zuverlässigkeit der Audiowiedergabe auf der Hardware und in der Produktion zu überprüfen.
Skalierbarkeit
Der Ansatz mit einem Thread pro Gerät ist für verschiedene Hardwarekonfigurationen vorgesehen. Da jeder Thread hauptsächlich im Leerlauf ist, auf Daten wartet oder darauf wartet, dass das Gerät geschriebene Daten verarbeitet, sollte er den Prozessor nicht belasten und die Systemleistung nicht beeinträchtigen.
Die Designentscheidung, Daten nur auf einem einzigen Gerät wiederzugeben, in Kombination mit Mixer-Steuerbefehlen für alle weiteren Ausgaben sorgt dafür, dass die genaue Ausgabe von der Soundhardware verarbeitet wird und für zukünftige Systeme skaliert werden kann.
Latenz
Die Latenz ist für das Audiosystem von entscheidender Bedeutung. Daher werden nach der Implementierung eine Reihe von Service Level Objectives (SLOs) für die Latenz des Systems definiert. Um die Latenz kontinuierlich zu überwachen, werden in den Systemlogs alle Debug-Builds überwacht, die die definierten SLOs nicht erfüllen.
Bei den Produktionsversionen werden Überwachungsdaten an ein System übergeben, das sich außerhalb der Audioimplementierung befindet. Es wird also nicht auf Logs zurückgegriffen.
Test und Teststrategie
Die Crates und die Audio-Crate sind für die Testabdeckung konzipiert. Wir haben eine Mock-Plattformimplementierung hinzugefügt, um zu bestätigen, dass alle Funktionen getestet werden.
Aufgrund der Komplexität von Hardware und Bindungen ist eine umfassende Testabdeckung für Plattformimplementierungen nicht möglich. Wir stellen Beispielimplementierungen zur Verfügung, mit denen Sie die Lösung manuell auf Hardware und im Cuttlefish-Emulator testen können.
Dokumentation
In der Datei README.md in Audio crates/audio wird beschrieben, wie AudioManager verwendet wird. crates/audio/examples enthält Beispiele für:
- Implementieren Sie eine Plattform.
- Erstellen Sie eine Instanz von
AudioManager. WavAssetabspielen- Ein Asset mit einer benutzerdefinierten Funktion in Dauerschleife abspielen
- Wiedergabeereignisse protokollieren