צלצולים

במאמר הזה מתואר ניגון של צליל התראה במעבד הרינדור של זמינות גבוהה (HAR). ‫Audio crate חושף את AudioManager לאפליקציית HAR, ששולטת בהשמעת הצלצול.

כדי לשמור על השהיה נמוכה, השרשורים של ההפעלה פועלים לאורך כל משך החיים של האפליקציה, במצב המתנה ובהעברה כשלא מושמע אודיו.

הסברים על המונחים

נכס
AudioAsset מתייחס לאודיו שאפשר להפעיל. הנכסים מוכרים בדרך כלל וקיימים בזמן הריצה של האפליקציה.
מכשיר
AudioDevice מתייחס לאוטובוס נפרד להשמעת אודיו. המכשיר הוא היחידה הכי פרטנית שקשורה לחומרה שהמערכת ניגשת אליה. בהטמעה רגילה של SDVM, ‏ AudioDevice מתייחס ל-PCM יחיד של Advanced Linux Sound Architecture‏ (ALSA).
זרם
מופע של הפעלת נכס במכשיר. הסטרימינג נמשך מהרגע שבו הוא מתוזמן ועד שהוא מסתיים, מבוטל או מסתיים בשגיאה.

רכיבים

איור 1 מציג את דיאגרמת הרכיבים של Chime:

תרשים רכיבים

איור 1. תרשים רכיבים.

מכשיר אודיו ו-PCM

הגדרת חומרת האודיו מתבצעת לפי העיצוב של שכבת ההפשטה של פלטפורמת HAR, והיא נמצאת ב-har-platform-api.

תיבת ה-HAR Audio מגדירה מבנה חדש ל-AudioDevice, שמגדיר שדות לכל מבני הנתונים שמשפיעים על תיבת ה-HAR הפנימית Audio ועל ההפעלה. ‫AudioDevice משתמש גם בפרמטרים גנריים כדי לעטוף פרמטרים נוספים פוטנציאליים שספציפיים לפלטפורמה. במקרה של tinyalsa,‏ PlatformAudioDevice מכיל את התיאורים והמאפיינים של 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
}

נכסי אודיו

בקטע הזה מוסבר איך מגדירים ומטמיעים נכסי אודיו.

הגדרות אישיות

ההטמעה הראשונית של אודיו ב-HAR תומכת בנכסי אודיו שהוגדרו באופן סטטי. הגדרת JSON מגדירה אילו נכסים זמינים ואילו נכסים מוגדרים כקבצי WAV.

ההטמעה תומכת גם בנכסי אודיו מסונתזים ובנכסי אודיו שמוזרמים, באמצעות הטמעה כללית יותר של נכסים שמקבלת פונקציה ליצירת נתוני אודיו.

הטמעה

מטמיעים נכסים באמצעות שני מבנים נפרדים, AudioAsset ו-AudioStream.

AudioAsset מגדיר את המאפיינים הסטטיים של נכס, ומשמש כמאגר לנתונים פנימיים פוטנציאליים שקשורים לנכס. אפשר לגזור את AudioAsset AudioStream, שהוא מופע יחיד של הנכס שאפשר להזרים. AudioStream מכיל מצב פנימי שקשור להפעלה של הזרם היחיד.

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

הפעלת מנגנון הצלצול

בקטע הזה מתוארים ה-API וההליך להפעלת צליל התראה. הפעלה של צליל יחיד נקראת זרם.

מחזור החיים של שידור

איור 2 מציג את מחזור החיים של זרם:

הפעלת שידורים ואירועים

איור 2. הפעלת סטרימינג ואירועים.

באיור 2 מתוארים השלבים האלה:

  1. הפעלה: תזמון של שידור חי להפעלה.

  2. קביעת עדיפות: קביעת עדיפות של הפעלה מחליטה אם:

    • השמעת צלצול עכשיו (האירוע התחיל כשהתקבלו הבייטים הראשונים)
    • הפעלת הצלצול מאוחר יותר (אירוע מושהה או שהופעלה בו הפעלה מחדש)
    • הפחתת העדיפות של הצליל (אירוע שבוטל)
  3. הגדרות המיקסר: אם צריך, מעדכנים את הגדרות המיקסר בהתאם להתנהגויות שהוגדרו.

  4. כתיבת בייטים: כתיבת נתח של בייטים אל AudioDevice.

  5. עוד נתונים: אם יש עוד נתונים במקור, חוזרים לשלב 2.

  6. חזרה על השידור: אם רוצים שהשידור יחזור על עצמו, מאפסים וחוזרים לשלב 2 (האירוע מופעל מחדש).

  7. הושלם: הסטרימינג הושלם בהצלחה (FinishedSuccessfully אירוע).

אפשר להפריע לצליל ההודעה על ידי השהיה, הפעלה מחדש או הפסקת השיחות בכל שלב.

עדיפויות הצלצול

הלוגיקה הזו קובעת את סדר העדיפויות של הצלילים:

  1. שינוי מצב ההפעלה. לדוגמה, צלצול במצב דעיכה תמיד מקבל עדיפות עליונה עד לסיום הדעיכה.

  2. העדיפות שצוינה.

  3. אם העדיפות השווה היא מהזמן האחרון, הצליל יושמע קודם.

כשצלילי ההתראה הם בעדיפות שווה, המערכת יוצרת מופע של AudioManager עם ערך של enum.

API

אירועים

אם מסופק ערוץ אירועים כשהצליל מתחיל, HAR Audio פולט מספר אירועים במהלך ההפעלה. האירועים הנתמכים מוצגים בדוגמה הזו:

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

שליטה במיקסר

בקטע הזה מוסבר איך שולטים בעוצמת הקול ובמיקום שלו במרחב.

עוצמת הקול

ב-HAR, עוצמת הקול מוגדרת באופן עקבי במיליבלים. ‫har-platform-api crate מטפל בהמרה ממיליבלים לאות בקרה.

הקשר בין מיליבלים לבין פלט עוצמת הקול של החומרה הוא לוגריתמי, והוא משתנה מאוד בין חומרה שונה והגדרות שונות של רמקולים. לכן, צריך לספק הגדרה בין הערכים כחלק מההגדרה של AudioDevice (Audio Device and PCM), וההמרה צריכה להתבצע לפני הקריאה לשכבת הפלטפורמה.

כתוצאה מכך, ההטמעה ב-PAL API מגדירה שתי פונקציות.

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

fn set_volume_control(AudioDeviceID, ControlValue);

ההטמעה שמוגדרת כברירת מחדל עבור set_volume_millibel משתמשת בהגדרות שסופקו עבור AudioDevice, כולל קבוצה של צמדי מפתח/ערך למיליבל הפניה – שליטה, המרה של המיליבל לערכי בקרה ואז קריאה לפונקציה set_volume_control עם הערך שהומר.

העיצוב הזה מספק ברירת מחדל ומאפשר הטמעות עוקבות שיבטלו את מיפוי ברירת המחדל.

תהליך העבודה של אודיו ב-HAR

איור 3. זרימת אודיו של HAR.

הוספת אפקט מרחבי

ממשק Audio API חושף פונקציונליות לשליטה באזור המרחבי שבו נתוני האודיו צריכים להיות מושמעים. הפרמטרים האלה מועברים לשכבת ה-PAL, ומוחלים בהמשך באמצעות אמצעי בקרה של החומרה. האפשרויות מוגדרות כחלק מ-PAL API באופן הבא:

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

רמות של שליטה במיקסר

אתם יכולים להגדיר עוצמת קול ומיקום מרחבי בנכס ובזרם. אם מגדירים עדיפות לסטרימינג, הסטרימינג מבטל את אמצעי הבקרה שהוגדרו על ידי הנכס.

ניהול שרשורים

מנהל האודיו שומר על שרשור אחד לכל מופע של AudioDevice. כל שרשור פועל באופן עצמאי. האינטראקציה בין AudioManager לבין השרשור של ההפעלה מתבצעת באמצעות תור משותף של סטרימינג שממוין לפי עדיפות.

קריאות ל-ALSA משתמשות בכתיבות ASYNC עם סקר כדי לקבוע מתי הנתונים מעובדים.

רצף ניהול השרשורים

איור 4. רצף הפעולות לניהול שרשורים.

שליטה באותות במהלך שליחת בקשות

כשממתינים לעיבוד הבייטים על ידי כרטיס הקול, אפשר להנפיק אותות בקרה. לדוגמה, כדי לשנות את האפקט של הדהייה או את המיקום של האודיו במרחב. התשאול כדי לקבל את המצב של מכשיר האודיו מוגדר ברמה AudioManager או שמוגדר כברירת מחדל לאלפית השנייה. אחרי כל מחזור של סקר, השרשור של ההפעלה מעכל את הפקודות של השליטה המתוזמנת ומוציא אותן.

ניהול מאגר נתונים זמני

כדי לצמצם את זמן האחזור של ההפרעות, גודלי מאגר הנתונים הזמני שנכתבים למכשיר נשמרים קטנים. כשמשתמשים ב-TinyALSA כברירת מחדל, גודל שטח האחסון הזמני מוגדר להיות זהה לפרמטר startup_threshold. ב-TinyALSA, ברירת המחדל מוגדרת כמאגר המכשיר המוקצה כולו חלקי שניים.

הפרעה לשידור

כשזרמים מופרעים, הם שומרים על העדיפות של ה-thread עד שכל הנתונים שהם כתבו בכרטיס מסתיימים. כתוצאה מכך, יש תקופת מעבר בין ההפרעה לבין השידור החדש.

לדוגמה, אם קטע אודיו לדוגמה בקובץ HAR משתמש ב:

  • גודל של 3,072
  • תדירות של 48,000
  • גודל מדגם של שניים

המאגר הזמני מחושב כ-3,072 ו-6,144 פריימים, מה שמוביל להשהיית הפרעה של 64 עד 128 אלפיות השנייה. ביישום בסביבת ייצור נדרש מאגר קטן יותר.

ניהול שגיאות וסיכונים

בקטע הזה מוסבר איך המערכת מטפלת בשגיאות ומהם הסיכונים הפוטנציאליים.

שידורים לא פעילים ובעיות בתור

אפשר להשהות את AudioStream, וההפעלה יכולה להתבצע רק ממופע AudioStream בעדיפות הגבוהה ביותר, ולכן קיים סיכון לתור הולך וגדל של סטרימינג שמונע מסטרימינג בעדיפות נמוכה להתבצע.

כדי למנוע את המצב הזה, הגודל של כל תור מוגבל. אם הערך הזה חורג, הזרם עם העדיפות הכי נמוכה נמחק.

מעקב והתראות

בשלב הייצור, אמצעי הבקרה למניעת פגיעה עוקב אחרי תכונות אודיו כדי לוודא שההפעלה מתבצעת כמצופה.

AudioManager עוקב אחרי נתונים סטטיסטיים פנימיים שספציפיים לזמני השהיה, וגם אחרי דגל שמגדיר את ביצועי הרישום ביומן. אחרי שמגדירים את ערכי הסף האלה, נוצרים יומני אזהרה לכל גרסאות ה-debug כשמתרחשים המקרים הבאים:

  • משך הזמן בין התזמון לבין תחילת ההפעלה חורג מ-x אלפיות השנייה.
  • (לסטרימינג ללא הפרעות) יש הבדל של יותר מ-y אחוזים בין אורך הנכס לבין זמן ההפעלה.

המכשיר נחסם

תמיד יש סיכון קטן שמכשיר אודיו לא יגיב, למשל אם הוא מוקצה ונכתב על ידי תהליך אחר במערכת. ההשמעה מתבצעת באופן אסינכרוני בשרשורים נפרדים, והצלילים יכולים להיכנס לתור להשמעה מאוחר יותר, כך שהאפליקציה שמתקשרת לא יודעת על כך.

כדי לזהות את הבעיה הזו, מתבצעת בדיקת תקינות של השרשור בכל פעם שמתוזמן צליל חדש להשמעה. אם תור ההמתנה של שרשור ההשמעה מלא ולא נצרכו בו בייטים חדשים בשנייה האחרונה, המערכת מחזירה שגיאה.

יכול להיות שבעתיד יהיה צורך לנסות להפעיל מחדש מכשירים או לפתוח אותם, אבל בהטמעה הראשונית, השגיאות לא אמורות להיות בלתי נראות.

מבנה הקוד

באופן כללי, הקוד שקשור להשמעת צלצולים נמצא בתיקיות הבאות:

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

אפליקציית ה-HAR הקיימת, שמבצעת קריאות להפעלת צלצולים.

NEW CRATE: display-safety/crates/audio

חדש: Crate לניהול של בקרת אודיו והפעלה (כאן נמצא רוב הפונקציונליות).

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

PAL כולל את כל קריאות המערכת שנדרשות לאודיו.

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

שיחות אל tinyalsa-rs להפעלה באמצעות TinyALSA. התמיכה ב-QNX לא מוטמעת בפתרון הראשוני, והיא תגדל ככל שיתווספו פלטפורמות נתמכות.

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

קוד ספציפי ל-TinyALSA להשמעה. הוא משמש להטמעות בפלטפורמות Android ו-Linux.

CRATE: display-safety/crates/tinyalsa-rs

‫Rust bindings for TinyALSA C implementation

פרטי ההטמעה של Rust

כמה פרטים ספציפיים לגבי ההטמעה:

  • כל פונקציות ה-API מחזירות Result<X, AudioError> כאשר X הוא () או ערך החזרה.
  • אף פונקציית API לא מסומנת בסימן unsafe.
  • מנגנוני ה-Mutex והסנכרון הם פנימיים ולא נחשפים ב-API‏ AudioManager.

מודל הבעלות ו-AudioManager

  • כל האינטראקציות של האפליקציה עם מערכת האודיו מתבצעות דרך AudioManager או אובייקטים שמוחזרים מ-AudioManager.

  • AudioManager is thread safe.

  • המשתנה AudioManager מוגדר פעם אחת באפליקציית HARry, והמשתנה Moved, כדי שלמשתנה Looper תהיה בעלות.

  • AudioManager משתמש באסימון tokio_util::CancellationToken כדי לנהל את השרשורים של ההפעלה שהתחילו, ומוודא שהשרשורים מסתיימים והמשאבים משוחררים אם AudioManager הוא Dropped.

  • AudioManager לא מונעת באופן מפורש יצירה של כמה מופעים. אם קיימים יותר ממופע אחד, הרישום מתבצע ברמה warn.

בעלות משותפת

מספר אובייקטים כוללים בעלות משותפת, והם מסונכרנים עם גישה בלעדית. המנגנונים האלה לא נחשפים ב-API של AudioManager, אבל הם פנימיים להטמעות של אודיו ו-PAL.

  • AudioDevice – לכל הפניה לחומרה (לדוגמה, TinyALSA PCM) שנפתחה (יש לה נקודת אחיזה) יש גישה בלעדית. מידע נוסף על עיצוב SMP

  • למופעי AudioStream יש גישה בלעדית אחרי שהם מתוזמנים להפעלה, כי האפליקציה יכולה לשלוט בהם ובו-זמנית לשרשור ההפעלה יש גישה אליהם.

    ה-thread של ההפעלה לא מחזיק נעילות במהלך ההפעלה, אבל הוא יוצר תמונת מצב בלתי משתנה של המאגר הבא להפעלה, ולא מתייחס לשינויים עד שהמאגר הבא עובר עיבוד.

  • לכל שרשור הפעלה יש תור הפעלה, הפניה משותפת בין AudioManager לבין שרשור ההפעלה. לכן, השרשור צריך גישה בלעדית למוטציות.

  • שרשורים ללא זרמים עוברים למצב בלי פעילות עם המשתנה Condvar כדי לקבל אירועי הוצאה ממצב שינה כשמזוהים נתונים חדשים. המנגנון הזה מבוסס על בעלות משותפת.

פניות קשורות

מטרתם של ארגזי תוכנה וארגזי אודיו היא לצמצם את התלות בארגזי תוכנה שלא אושרו לבנייה בעץ המקור של Android. כאן מופיעה רשימה של חבילות (crates) שכלולות.

הטמעות של פלטפורמות במורד הזרם עבור Android ו-Linux תלויות ב-TinyALSA וב-tinyalsa-rs crate הקיים של בטיחות התצוגה.

מאפייני איכות

אמינות

הפעלת אודיו היא קריטית לבטיחות, אבל העיצוב הזה לא כולל את ההטמעה של ניטור בטיחות. כדאי להטמיע את זה בנפרד כדי לוודא שהפעלת האודיו אמינה בחומרה ובסביבת הייצור.

מדרגיות

הגישה של שרשור אחד לכל מכשיר נועדה להתרחב להגדרות חומרה שונות. בהתחשב בכך שכל thread נמצא בעיקר במצב המתנה, ממתין לנתונים או ממתין שהמכשיר יעבד נתונים שנכתבו, הוא לא אמור להעמיס על המעבד או על הביצועים של המערכת.

ההחלטה העיצובית להפעיל נתונים רק במכשיר אחד, בשילוב עם פקודות בקרה של המיקסר לכל פלט נוסף, מבטיחה שהפלט המדויק יטופל על ידי חומרת הסאונד, ושהמערכת תהיה ניתנת להרחבה בעתיד.

זמן אחזור

השהיה היא קריטית למערכת האודיו, ולכן אחרי ההטמעה מוגדרים יעדים למדידת רמת השירות (SLO) להשהיה של המערכת. כדי לעקוב באופן רציף אחרי תקינות השהייה, המעקב ביומני המערכת לא עומד ביעדי ה-SLO המוגדרים בכל גרסאות הניפוי באגים.

בגרסאות הייצור, נתוני המעקב מועברים למערכת חיצונית כלשהי ליישום האודיו, במקום להסתמך על יומנים.

בדיקה ואסטרטגיית בדיקה

הארגזים והארגז לאודיו מתוכננים עם כיסוי בדיקה. הוספנו הטמעה של פלטפורמה מדומה כדי לוודא שכל היכולות נבדקות.

מורכבות החומרה והקשרים מונעת כיסוי נרחב של בדיקות להטמעות של פלטפורמות. אנחנו מספקים יישומי דוגמה כדי לבדוק את הפתרון באופן ידני בחומרה ובאמולטור Cuttlefish.

מאמרי עזרה

בקובץ README.md בתיקייה Audio crates/audio מוסבר איך להשתמש ב-AudioManager. ‫crates/audio/examples כולל דוגמאות ל:

  • מטמיעים פלטפורמה.
  • יוצרים מופע של AudioManager.
  • הפעל את WavAsset.
  • הפעלה חוזרת של נכס של פונקציה בהתאמה אישית.
  • תיעוד אירועי הפעלה ביומן.