יכולות אודיו

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

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

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