יכולות אודיו

במכשירי Android TV יכולים להיות מספר יציאות אודיו שמחוברות בו-זמנית: רמקולים של טלוויזיה, קולנוע ביתי שמחובר ל-HDMI, אוזניות Bluetooth וכן הלאה. המכשירים האלה לפלט אודיו יכולים לתמוך ביכולות אודיו שונות, כמו קידודים (Dolby Digital+ , DTS ו-PCM), קצב דגימה וערוצים. לדוגמה, טלוויזיות שמחוברות ל-HDMI תומכות במגוון קידודים, ואוזניות Bluetooth מחוברות תומכות בדרך כלל רק ב-PCM.

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

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