יכולים להיות כמה פלטות אודיו שמחוברות למכשירי 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 }
לחלופין, אפשר לשלוח שאילתה כדי לראות אילו פרופילים נתמכים להפעלה ישירה של מדיה דרך מכשיר האודיו שמחובר כרגע. ההגדרה הזו לא כוללת פרופילים שלא נתמכים או שיומרו על ידי מסגרת 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);