יכולים להיות כמה פלטות אודיו שמחוברות למכשירי Android TV בו-זמנית: רמקולים של הטלוויזיה, קולנוע ביתי שמחובר באמצעות HDMI, אוזניות Bluetooth וכו'. מכשירי פלט אודיו יכולים לתמוך ביכולות אודיו שונות, כמו קידודים (Dolby Digital+, DTS ו-PCM), קצב דגימה וערוצים. לדוגמה, לטלוויזיות שמחוברות באמצעות HDMI יש תמיכה בהרבה קידודים, בזמן שאוזניות Bluetooth שמחוברות בדרך כלל תומכות רק ב-PCM.
הרשימה של מכשירי האודיו הזמינים ומכשיר האודיו שאליו מנותב האודיו יכולים להשתנות גם אם מחברים מכשירי HDMI או מנתקים אותם, אם מחברים אוזניות Bluetooth או מנתקים אותן, או אם המשתמש משנה את הגדרות האודיו. יכולות פלט האודיו יכולות להשתנות גם כשאפליקציות מפעילות מדיה, ולכן האפליקציות צריכות להתאים את עצמן לשינויים האלה ולהמשיך את ההפעלה במכשיר האודיו החדש ובאפשרויות שלו. פלט בפורמט אודיו שגוי עלול לגרום לשגיאות או לכך שלא יושמע צליל.
אפליקציות יכולות להפיק את אותו תוכן בכמה קידודים כדי לספק למשתמש את חוויית האודיו הטובה ביותר, בהתאם ליכולות של מכשיר האודיו. לדוגמה, אם הטלוויזיה תומכת בסטרימינג של אודיו שמקודד ב-Dolby Digital, הוא יופעל. אם אין תמיכה ב-Dolby Digital, יופעל סטרימינג של אודיו ב-PCM, שנתמך באופן נרחב יותר. רשימת מפענחי Android המובנים שמשמשים להמרת זרם אודיו ל-PCM מופיעה במאמר פורמטים נתמכים של מדיה.
בזמן ההפעלה, אפליקציית הסטרימינג צריכה ליצור AudioTrack
עם AudioFormat
הכי טוב שנתמך על ידי מכשיר הפלט של האודיו.
יצירת טראק בפורמט הנכון
האפליקציות צריכות ליצור AudioTrack
, להתחיל להפעיל אותו ולקרוא ל-getRoutedDevice()
כדי לקבוע את מכשיר האודיו שמוגדר כברירת מחדל להשמעת צלילים.
לדוגמה, יכול להיות שזה יהיה קטע שקט קצר ומאובטח עם קידוד PCM, שמשמש רק כדי לקבוע את המכשיר שאליו מנותב האודיו ואת יכולות האודיו שלו.
קבלת קידודים נתמכים
משתמשים ב-getAudioProfiles()
(רמת API 31 ואילך) או ב-getEncodings()
(רמת API 23 ואילך) כדי לקבוע את פורמטי האודיו שזמינים במכשיר האודיו שמוגדר כברירת מחדל.
בדיקת הפורמטים והפרופילים הנתמכים של אודיו
אפשר להשתמש ב-AudioProfile
(רמת API 31 ומעלה) או ב-isDirectPlaybackSupported()
(רמת API 29 ומעלה) כדי לבדוק שילובים נתמכים של פורמט, מספר ערוצים וקצב דגימה.
חלק ממכשירי Android יכולים לתמוך בקידודים מעבר לאלה שנתמכים על ידי מכשיר האודיו של הפלט. צריך לזהות את הפורמטים הנוספים האלה באמצעות isDirectPlaybackSupported()
. במקרים כאלה, נתוני האודיו מקודדים מחדש לפורמט שנתמך על ידי מכשיר פלט האודיו. אפשר להשתמש ב-isDirectPlaybackSupported()
כדי לבדוק בצורה נכונה את התמיכה בפורמט הרצוי, גם אם הוא לא מופיע ברשימה שמוחזרת על ידי getEncodings()
.
ניתוב אודיו צפוי
ב-Android 13 (רמת API 33) הוצגו נתיבי אודיו צפויים. אתם יכולים לצפות לתמיכה במאפייני אודיו של מכשירים ולהכין רצועות למכשיר האודיו הפעיל. אפשר להשתמש ב-getDirectPlaybackSupport()
כדי לבדוק אם יש תמיכה בהפעלה ישירה בפורמט ובמאפיינים מסוימים במכשיר האודיו שאליו מנותב כרגע האודיו:
Kotlin
val format = AudioFormat.Builder() .setEncoding(AudioFormat.ENCODING_E_AC3) .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1) .setSampleRate(48000) .build() val attributes = AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_MEDIA) .build() if (AudioManager.getDirectPlaybackSupport(format, attributes) != AudioManager.DIRECT_PLAYBACK_NOT_SUPPORTED ) { // The format and attributes are supported for direct playback // on the currently active routed audio path } else { // The format and attributes are NOT supported for direct playback // on the currently active routed audio path }
Java
AudioFormat format = new AudioFormat.Builder() .setEncoding(AudioFormat.ENCODING_E_AC3) .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1) .setSampleRate(48000) .build(); AudioAttributes attributes = new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_MEDIA) .build(); if (AudioManager.getDirectPlaybackSupport(format, attributes) != AudioManager.DIRECT_PLAYBACK_NOT_SUPPORTED) { // The format and attributes are supported for direct playback // on the currently active routed audio path } else { // The format and attributes are NOT supported for direct playback // on the currently active routed audio path }
אפשר גם לשאול אילו פרופילים נתמכים להפעלה ישירה של מדיה דרך מכשיר האודיו שמחובר כרגע. לא נכללים פרופילים שלא נתמכים או שיועברו המרה (transcoding) על ידי מסגרת Android:
Kotlin
private fun findBestAudioFormat(audioAttributes: AudioAttributes): AudioFormat { val preferredFormats = listOf( AudioFormat.ENCODING_E_AC3, AudioFormat.ENCODING_AC3, AudioFormat.ENCODING_PCM_16BIT, AudioFormat.ENCODING_DEFAULT ) val audioProfiles = audioManager.getDirectProfilesForAttributes(audioAttributes) val bestAudioProfile = preferredFormats.firstNotNullOf { format -> audioProfiles.firstOrNull { it.format == format } } val sampleRate = findBestSampleRate(bestAudioProfile) val channelMask = findBestChannelMask(bestAudioProfile) return AudioFormat.Builder() .setEncoding(bestAudioProfile.format) .setSampleRate(sampleRate) .setChannelMask(channelMask) .build() }
Java
private AudioFormat findBestAudioFormat(AudioAttributes audioAttributes) { Stream<Integer> preferredFormats = Stream.<Integer>builder() .add(AudioFormat.ENCODING_E_AC3) .add(AudioFormat.ENCODING_AC3) .add(AudioFormat.ENCODING_PCM_16BIT) .add(AudioFormat.ENCODING_DEFAULT) .build(); Stream<AudioProfile> audioProfiles = audioManager.getDirectProfilesForAttributes(audioAttributes).stream(); AudioProfile bestAudioProfile = (AudioProfile) preferredFormats.map(format -> audioProfiles.filter(profile -> profile.getFormat() == format) .findFirst() .orElseThrow(NoSuchElementException::new) ); Integer sampleRate = findBestSampleRate(bestAudioProfile); Integer channelMask = findBestChannelMask(bestAudioProfile); return new AudioFormat.Builder() .setEncoding(bestAudioProfile.getFormat()) .setSampleRate(sampleRate) .setChannelMask(channelMask) .build(); }
בדוגמה הזו, preferredFormats
היא רשימה של מופעי AudioFormat
. הן מסודרות לפי העדפה, כך שהאפשרות המועדפת ביותר מופיעה ראשונה ברשימה והאפשרות הכי פחות מועדפת מופיעה אחרונה.
getDirectProfilesForAttributes()
מחזירה רשימה של אובייקטים נתמכים של AudioProfile
עבור
מכשיר האודיו שמוגדר כרגע לניתוב עם AudioAttributes
שסופק. המערכת עוברת על רשימת הפריטים המועדפים AudioFormat
עד שהיא מוצאת פריט תואם שנתמך AudioProfile
. AudioProfile
הזה מאוחסן כ-bestAudioProfile
.
שיעורי הדגימה האופטימליים ומסכות הערוצים נקבעים מתוך bestAudioProfile
.
לבסוף, נוצרת מכונת AudioFormat
מתאימה.
יצירת טראק אודיו
האפליקציות צריכות להשתמש במידע הזה כדי ליצור AudioTrack
באיכות הכי גבוהה AudioFormat
שנתמכת על ידי מכשיר האודיו שמוגדר כברירת מחדל (ושזמינה לתוכן שנבחר).
יירוט שינויים במכשיר האודיו
כדי ליירט שינויים במכשיר האודיו ולהגיב להם, האפליקציות צריכות:
- לרמות API ששווה ל-24 או יותר, מוסיפים את התג
OnRoutingChangedListener
כדי לעקוב אחרי שינויים במכשיר האודיו (HDMI, Bluetooth וכו'). - ב-API ברמה 23, צריך לרשום
AudioDeviceCallback
כדי לקבל שינויים ברשימת מכשירי האודיו הזמינים. - במקרה של רמות API 21 ו-22, צריך לעקוב אחרי אירועים של חיבור HDMI ולהשתמש בנתונים הנוספים מהשידורים.
- צריך גם לרשום
BroadcastReceiver
כדי לעקוב אחרי שינויים במצב של מכשירים עם API בגרסה נמוכה מ-23, כיAudioDeviceCallback
עדיין לא נתמך.BluetoothDevice
כשמזוהה שינוי בהתקן האודיו של AudioTrack
, האפליקציה צריכה לבדוק את יכולות האודיו המעודכנות, ואם צריך, ליצור מחדש את AudioTrack
עם AudioFormat
אחר. כדאי לעשות את זה אם יש תמיכה בקידוד באיכות גבוהה יותר או אם הקידוד שבו השתמשתם בעבר כבר לא נתמך.
קוד לדוגמה
Kotlin
// audioPlayer is a wrapper around an AudioTrack // which calls a callback for an AudioTrack write error audioPlayer.addAudioTrackWriteErrorListener { // error code can be checked here, // in case of write error try to recreate the audio track restartAudioTrack(findDefaultAudioDeviceInfo()) } audioPlayer.audioTrack.addOnRoutingChangedListener({ audioRouting -> audioRouting?.routedDevice?.let { audioDeviceInfo -> // use the updated audio routed device to determine // what audio format should be used if (needsAudioFormatChange(audioDeviceInfo)) { restartAudioTrack(audioDeviceInfo) } } }, handler)
Java
// audioPlayer is a wrapper around an AudioTrack // which calls a callback for an AudioTrack write error audioPlayer.addAudioTrackWriteErrorListener(new AudioTrackPlayer.AudioTrackWriteError() { @Override public void audioTrackWriteError(int errorCode) { // error code can be checked here, // in case of write error try to recreate the audio track restartAudioTrack(findDefaultAudioDeviceInfo()); } }); audioPlayer.getAudioTrack().addOnRoutingChangedListener(new AudioRouting.OnRoutingChangedListener() { @Override public void onRoutingChanged(AudioRouting audioRouting) { if (audioRouting != null && audioRouting.getRoutedDevice() != null) { AudioDeviceInfo audioDeviceInfo = audioRouting.getRoutedDevice(); // use the updated audio routed device to determine // what audio format should be used if (needsAudioFormatChange(audioDeviceInfo)) { restartAudioTrack(audioDeviceInfo); } } } }, handler);