יכולות אודיו

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

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

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