יכולות אודיו

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

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

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

  • לרמות API ששווה ל-24 או גבוהה מ-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);