במכשירי Android TV אפשר לחבר כמה יציאות אודיו בו-זמנית: רמקולים של הטלוויזיה, מערכת קולנוע ביתית שמחוברת ל-HDMI, אוזניות Bluetooth וכו'. מכשירי הפלט של האודיו האלה יכולים לתמוך ביכולות אודיו שונות, כמו קידוד (Dolby Digital+, DTS ו-PCM), קצב דגימה וערוצים. לדוגמה, טלוויזיות שמחוברות באמצעות HDMI תומכות במגוון קידודים, ואילו אוזניות Bluetooth מחוברות תומכות בדרך כלל רק ב-PCM.
רשימת מכשירי האודיו הזמינים ומכשיר האודיו המנותב יכולים להשתנות גם אם מחברים מכשירים עם HDMI במהלך הפעולה, מחברים או מנתקים אוזניות Bluetooth או משנים את הגדרות האודיו של המשתמש. יכולות הפלט של האודיו יכולות להשתנות גם כשאפליקציות מפעילות מדיה, ולכן האפליקציות צריכות להתאים את עצמן בצורה חלקה לשינויים האלה ולהמשיך את ההפעלה במכשיר האודיו החדש שממנו מנותב האודיו, בהתאם ליכולות שלו. הפקת פורמט אודיו שגוי עלולה לגרום לשגיאות או לכך שהצליל לא יושמע.
לאפליקציות יש אפשרות להפיק את אותו תוכן בכמה קידודים כדי לספק למשתמש את חוויית האודיו הטובה ביותר, בהתאם ליכולות של מכשיר האודיו. לדוגמה, אם הטלוויזיה תומכת בקודק Dolby Digital, יופעל סטרימינג של אודיו מקודד ב-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
כדי לעקוב אחרי שינויים במצב שלBluetoothDevice
במכשירים עם גרסה ישנה יותר מ-API 23, כיAudioDeviceCallback
עדיין לא נתמך.
כשזוהה שינוי בהתקן האודיו של 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);