הערות תכנות של OpenSL ES

ההערות בקטע הזה הן תוספת OpenSL ES 1.0.1 המפרט.

אובייקטים ואתחול ממשק

שני היבטים של מודל התכנות OpenSL ES שייתכן שמפתחים חדשים לא מכירים בין אובייקטים וממשקים לבין רצף האתחול.

בקצרה, אובייקט OpenSL ES דומה לקונספט האובייקט שפות תכנות כמו Java ו-C++, מלבד אובייקט OpenSL ES שניתן לצפייה רק דרך הממשקים המשויכים אליו. המידע הזה כולל הממשק הראשוני של כל האובייקטים, שנקרא SLObjectItf. אין כינוי לאובייקט עצמו, רק נקודת אחיזה לממשק SLObjectItf של האובייקט.

קודם כל נוצר אובייקט OpenSL ES ומחזיר SLObjectItf, ואז שמומש. הדבר דומה לדפוס התכנות הנפוץ שבו מרכיבים תחילה (שאף פעם לא ייכשל, למעט עקב מחסור בזיכרון או פרמטרים לא חוקיים), ואז השלמת האתחול (שעשוי להיכשל בגלל מחסור במשאבים). שלב המימוש מעניק להטמיע מקום הגיוני להקצאת משאבים נוספים במקרה הצורך.

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

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

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

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

שליפה מראש של נגן אודיו

לנגן אודיו עם מקור נתונים של URI, Object::Realize מקצה אבל לא להתחבר למקור הנתונים (להכין) או להתחיל באחזור נתונים מראש. הרכיבים האלה מתרחשים ברגע מצב הנגן מוגדר כ-SL_PLAYSTATE_PAUSED או כ-SL_PLAYSTATE_PLAYING.

יכול להיות שחלק מהמידע עדיין לא יהיה ידוע עד מאוחר יחסית ברצף הזה. לחשבון ספציפית, בשלב הראשון Player::GetDuration מחזירה SL_TIME_UNKNOWN הפונקציה MuteSolo::GetChannelCount מחזירה בהצלחה כאשר מספר הערוצים הוא אפס, או תוצאת השגיאה SL_RESULT_PRECONDITIONS_VIOLATED. ממשקי ה-API האלה מחזירים את הערכים הנכונים מהרגע שהם ידועים.

מאפיינים אחרים שאינם ידועים בהתחלה כוללים את תדירות הדגימה סוג תוכן המדיה בפועל מבוסס על עיון בכותרת התוכן (בניגוד לכותרת של התוכן) סוג MIME שצוין על ידי האפליקציה, וגם סוג המאגר). הבעיות ייקבעו גם בהמשך במהלך להכין מראש/שליפה מראש, אבל אין ממשקי API לאחזר אותם.

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

גם הממשק של סטטוס השליפה מראש (prefetch) שימושי לזיהוי שגיאות. רישום קריאה חוזרת (callback) ולהפעיל SL_PREFETCHEVENT_FILLLEVELCHANGE וSL_PREFETCHEVENT_STATUSCHANGE לפחות אירועים. אם שני האירועים האלה נשלחים בו-זמנית, וגם PrefetchStatus::GetFillLevel מדווח על רמת אפס, PrefetchStatus::GetPrefetchStatus דוחות SL_PREFETCHSTATUS_UNDERFLOW, אז זה מציין שגיאה במקור הנתונים שאי אפשר לשחזר. הן כוללות את חוסר היכולת להתחבר אל מקור הנתונים כי שם הקובץ המקומי לא קיים או שה-URI של הרשת לא חוקי.

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

לסיכום, רצף הקוד המומלץ הוא:

  1. Engine::CreateAudioPlayer
  2. Object:Realize
  3. Object::GetInterface למשך SL_IID_PREFETCHSTATUS
  4. PrefetchStatus::SetCallbackEventsMask
  5. PrefetchStatus::SetFillUpdatePeriod
  6. PrefetchStatus::RegisterCallback
  7. Object::GetInterface למשך SL_IID_PLAY
  8. Play::SetPlayState אל SL_PLAYSTATE_PAUSED, או SL_PLAYSTATE_PLAYING

הערה: ההכנה והשליפה מראש (prefetch) מתרחשות כאן. בפרק הזמן הזה תישמע התקשרות חזרה אל עדכוני סטטוס תקופתיים.

להרוס

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

ב-OpenSL ES אין תמיכה באיסוף אשפה אוטומטי הפניה וספירה של ממשקים. אחרי השיחה אל Object::Destroy, כל הפרטים הקיימים ממשקים שנגזר מהאובייקט המשויך הופך ללא מוגדר.

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

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

הזזת סטריאו

אם משתמשים ב-Volume::EnableStereoPosition כדי להפעיל הזזת סטריאו של מקור מונו, יש הפחתה של 3 דציבלים בסך הכול עוצמת הצליל רמה. הדבר נדרש כדי שרמת עוצמת הקול הכוללת תישאר קבועה המקור הוא בוצעה הזזה מערוץ אחד לערוץ אחר. לכן, הפעל את מיקום הסטריאו רק אם יש צורך את זה. ניתן לקרוא מידע נוסף במאמר בוויקיפדיה בנושא הזזה של אודיו.

קריאות חוזרות ושרשורים

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

הגורמים המטפלים בקריאות חוזרות (callback) מופעלים משרשורים פנימיים שאינם של אפליקציות, שלא מצורפים בסביבת זמן הריצה של Android, לא ניתן להשתמש ב-JNI. כי השרשורים הפנימיים האלה קריטי ל- התקינות של הטמעת OpenSL ES, גם handler של קריאה חוזרת לא צריך לחסום או לבצע עבודה מופרזת.

אם ה-handler של הקריאה החוזרת צריך להשתמש ב-JNI או לבצע עבודה שלא פרופורציונלית וה-handler צריך לפרסם אירוע בשביל שרשור אחר. דוגמאות של עומס עבודה קביל של קריאה חוזרת (callback) כולל עיבוד והעברה למאגר הנתונים הזמני הבא של הפלט (לנגן AudioPlayer), מעבד את מאגר הנתונים הזמני למילוי אוטומטי ומעביר את הסרטון הבא לתור מאגר נתונים זמני ריק (למכשיר AudioRecorder) או ממשקי API פשוטים, כמו רוב משפחת Get. לצפייה הקטע ביצועים שבהמשך לגבי עומס העבודה.

שימו לב שההיפך הוא בטוח: שרשור של אפליקציה ל-Android שנכנסו ל-JNI מורשה: לקרוא ישירות לממשקי API של OpenSL ES, כולל אלה שחוסמים. עם זאת, חסימת שיחות מומלצים מהשרשור הראשי, כי הם עלולים לגרום האפליקציה לא מגיבה (ANR).

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

לא בטוח שה-thread שבו פועל ה-handler של הקריאה החוזרת יהיה זהה קריאות שונות. לכן אין להסתמך על ה-pthread_t שהוחזר על ידי pthread_self() או pid_t שהוחזרו על ידי gettid() כדי באופן עקבי בכל השיחות. מאותה סיבה, אל תשתמשו בממשקי API של אחסון מקומי של שרשור (TLS), כמו pthread_setspecific() ו-pthread_getspecific() משיחה חוזרת.

ההטמעה מבטיחה קריאות חוזרות (callback) בו-זמניות מאותו סוג, עבור אותו אובייקט, לא יתרחשו. עם זאת, אפשר לבצע בו-זמנית קריאות חוזרות (callback) מסוגים שונים לאותו אובייקט שרשורים שונים.

ביצועים

OpenSL ES הוא ממשק API מקורי של C, ולכן לשרשורים של אפליקציות שאינן בזמן ריצה שנקראות OpenSL ES אין תקורה שקשורה לזמן ריצה, למשל השהיות לאיסוף אשפה. חריג אחד שמתואר בהמשך, אין יתרון ביצועים נוסף לשימוש ב-OpenSL ES מלבד זה. באופן ספציפי, השימוש ב-OpenSL ES לא מבטיח שיפורים כמו זמן אחזור קצר יותר של אודיו או טעינה ארוכה יותר תזמון של לוח זמנים על פני מה שהפלטפורמה מספקת בדרך כלל. לעומת זאת, כיוון פלטפורמת Android והטמעות ספציפיות של מכשירים ממשיכים להתפתח, אפליקציית OpenSL ES יכולים להפיק תועלת משיפורים עתידיים בביצועי המערכת.

אחת אבולוציה כזו היא תמיכה במודלים זמן אחזור של פלט אודיו היסודות להשגת זמן האחזור של הפלט נכלל לראשונה ב-Android 4.1 (רמת API 16), ולאחר מכן ההתקדמות המתמשכת התרחשה ב-Android 4.2 (רמת API 17). שיפורים אלה זמינים דרך OpenSL ES להטמעות מכשירים לתבוע בעלות על התכונה android.hardware.audio.low_latency. אם המכשיר לא משתמש בתכונה הזו אבל תומך ב-Android 2.3 (רמת API 9) ואילך, עדיין תוכלו להשתמש בממשקי OpenSL ES API, אבל זמן האחזור של הפלט עשוי להיות ארוך יותר. נמוכה יותר נעשה שימוש בנתיב זמן האחזור של הפלט רק אם האפליקציה מבקשת גודל מאגר נתונים זמני וקצב דגימה שהם תואם לתצורת הפלט המקורי של המכשיר. הפרמטרים האלה ספציפיות למכשיר, הוא כפי שמתואר בהמשך.

החל מ-Android 4.2 (רמת API 17), אפליקציה יכולה לשלוח שאילתה קצב דגימה וגודל של מאגר נתונים זמני ומקורי בפלטפורמה נייטיב או אופטימלי עבור הפלט הראשי של המכשיר . בשילוב עם בדיקת התכונה שצוינה, אפליקציה יכולה עכשיו להגדיר את עצמה בהתאם לפלט זמן אחזור קצר במכשירים שתומכים בפלט.

עבור Android 4.2 (רמת API 17) ומטה, המספר הזמני של מאגר הנתונים הזמני או יותר הוא שנדרש לזמן אחזור קצר. החל מ-Android 4.3 (רמת API 18), מאגר נתונים זמני מספר אחד מספיק כדי לקצר את זמן האחזור.

כל ממשקי OpenSL ES לאפקטים של פלט מונעים את נתיב זמן האחזור הנמוך יותר.

הרצף המומלץ הוא:

  1. צריך לבדוק אם יש API ברמה 9 ואילך כדי לאשר את השימוש ב-OpenSL ES.
  2. מחפשים את התכונה android.hardware.audio.low_latency באמצעות קוד כמו:

    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. צריך לבדוק אם רמת ה-API היא 17 ומעלה כדי לאשר את השימוש android.media.AudioManager.getProperty()
  4. קבלת קצב הדגימה וגודל מאגר הנתונים הזמני של הפלט המקורי או האופטימלי למכשיר הזה פלט ראשי זרם באמצעות קוד כמו:

    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);
    
    שימו לב ש-sampleRate ו-framesPerBuffer הן מחרוזות. בדיקה ראשונה של null ואז המרה ל-int באמצעות Integer.parseInt().
  5. עכשיו משתמשים ב-OpenSL ES כדי ליצור AudioPlayer עם מאתר נתונים בתור מאגר נתונים זמני של PCM.

הערה: אפשר להשתמש הגודל של מאגר האודיו הזמני אפליקציית בדיקה לקביעת הגודל של מאגר הנתונים הזמני וקצב הדגימה של אודיו OpenSL ES יישומים במכשיר האודיו שלך. אפשר גם להיכנס ל-GitHub כדי לראות את דוגמאות של אודיו מאגר נתונים זמני.

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

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

החישוב הנדרש כדי לעבד את מאגר הנתונים הזמני הבא (בשביל AudioPlayer) או לצרוך את השימוש הקודם מאגר הנתונים הזמני (ב-AudioRecord) צריך להימשך בערך אותו משך זמן בכל קריאה חוזרת. עדיף להימנע מאלגוריתמים שפועלים בפרק זמן לא קבוע או שהם תפוצלים את החישובים שלהם. החישוב של הקריאה החוזרת (callback) מתפוצץ אם זמן המעבד (CPU) עבר בקריאה חוזרת כלשהי גדול באופן משמעותי מהממוצע. לסיכום, זמן הביצוע האידאלי הוא השונות של ה-handler תהיה קרובה לאפס, וה-handler לא יחסום אותו למשך זמנים בלתי מוגבלים.

אפשר לקצר את זמן האחזור של האודיו רק בפלטים הבאים:

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

החל מ-Android 5.0 (רמת API 21), זמן אחזור קצר יותר קלט אודיו נתמך במכשירים נבחרים. כדי להשתמש בתכונה הזו, צריך קודם לאשר שפלט של זמן אחזור קצר זמין כמו שמתואר למעלה. היכולת לקצר את זמן האחזור הוא דרישה מוקדמת לתכונת הקלט עם זמן אחזור קצר. לאחר מכן, יוצרים מקליט אודיו עם אותו את קצב הדגימה ואת הגודל של מאגר הנתונים הזמני שישמשו לפלט. ממשקי OpenSL ES לאפקטים של קלט את הנתיב של זמן האחזור הנמוך יותר. ההגדרה הקבועה מראש של ההקלטה צריך להשתמש ב-SL_ANDROID_RECORDING_PRESET_VOICE_RECOGNITION לזמן אחזור נמוך יותר. הזה ההגדרה משביתה מראש את עיבוד האותות הדיגיטליים שספציפיים למכשיר ועשויה להוסיף זמן אחזור לנתיב הקלט. למידע נוסף על הגדרות קבועות מראש להקלטה, הגדרות Android הממשק שלמעלה.

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

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

מצבי ביצועים

החל מ-Android 7.1 (API ברמה 25), ב-OpenSL ES הוספנו דרך לציין מצב ביצועים של נתיב האודיו. האפשרויות הן:

  • SL_ANDROID_PERFORMANCE_NONE: אין דרישה ספציפית לביצועים. הפעלת אפקטים של חומרה ותוכנה.
  • SL_ANDROID_PERFORMANCE_LATENCY: העדיפות ניתנת לזמן האחזור. ללא חומרה או את האפקטים של התוכנה. זהו מצב ברירת המחדל.
  • SL_ANDROID_PERFORMANCE_LATENCY_EFFECTS: עדיפות לזמן אחזור ועדיין מאפשרים אפקטים של חומרה ותוכנה.
  • SL_ANDROID_PERFORMANCE_POWER_SAVING: עדיפות שניתנה לשימור אנרגיה. הפעלת אפקטים של חומרה ותוכנה.

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

כדי להגדיר את מצב הביצועים, צריך להפעיל את SetConfiguration באמצעות Android ממשק תצורה, כפי שמוצג בהמשך:

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

אבטחה והרשאות

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

אפליקציות שמשתמשות ב-OpenSL ES חייבות לבקש את ההרשאות שהן צריכות כדי ליצור אפליקציות דומות של ממשקי API לא מקוריים. לדוגמה, אם האפליקציה מקליטה אודיו, היא צריכה ההרשאה android.permission.RECORD_AUDIO. לאפליקציות שמשתמשות באפקטים קוליים נדרשות android.permission.MODIFY_AUDIO_SETTINGS אפליקציות שמפעילות משאבי URI של רשת נדרשות android.permission.NETWORK. מידע נוסף זמין במאמר הבא: עבודה עם המערכת הרשאות.

בהתאם לגרסה ולהטמעה של הפלטפורמה, כלים לניתוח תוכן מדיה וכן יכול להיות שרכיבי קודק התוכנה פועלות בהקשר של אפליקציית Android שקוראת ל-OpenSL ES (קודקי חומרה מופשטים אבל תלויים במכשיר). תוכן בפורמט שגוי שנועד לנצל את המנתח וקודק נקודות חולשה הן וקטור תוקף ידוע. מומלץ להפעיל מדיה רק ממקורות מהימנים מקורות אחרים של האפליקציה, או תחלק את האפליקציה למחיצות, כך שהקוד שמטפל במדיה מקורות לא מהימנים פועלים בסביבה של ארגז חול יחסית. לדוגמה, אפשר: לעבד מדיה ממקורות לא מהימנים בתהליך נפרד. למרות ששני התהליכים עדיין פועלות באותו UID, אבל ההפרדה הזו מקשה על התקפה.