יכולות אודיו

ב-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);