יכולות אודיו

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

יירוט שינויים במכשיר האודיו

כדי ליירט שינויים במכשיר האודיו ולהגיב להם, האפליקציות צריכות:

כשמזוהה שינוי בהתקן האודיו של 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);