إمكانات الصوت

يمكن توصيل أجهزة Android TV متعددة بمخرجات صوت متعددة في الوقت نفسه: مكبّرات صوت التلفزيون والسينما المنزلية المتصلة بكابل HDMI وسماعات رأس بلوتوث وما إلى ذلك. ويمكن أن تتوافق أجهزة إخراج الصوت هذه مع إمكانات مختلفة للصوت، مثل الترميزات (Dolby Digital+ وDTS وPCM)، ومعدّل البيانات، والقنوات. على سبيل المثال، تدعم أجهزة التلفزيون المتصلة بكابل HDMI العديد من الترميزات، بينما تكون سماعات الرأس المتصلة عبر البلوتوث عادةً متوافقة مع PCM فقط.

يمكن أيضًا أن تتغيّر قائمة الأجهزة السماعية المتاحة وجهاز الصوت الموجه من خلال توصيل أجهزة HDMI بمنفذ ساخن أو توصيل أو إلغاء توصيل سماعات رأس بلوتوث أو تغيير المستخدم لإعدادات الصوت. وبما أنّ إمكانات إخراج الصوت يمكن أن تتغيّر حتى عند تشغيل التطبيقات للوسائط، يجب أن تتكيّف التطبيقات مع هذه التغييرات بسلاسة ومواصلة التشغيل على الجهاز السماعي الجديد وإمكاناته. فقد يؤدي إخراج تنسيق صوتي غير صحيح إلى حدوث أخطاء أو عدم تشغيل صوت.

تمتلك التطبيقات القدرة على إخراج المحتوى نفسه بترميزات متعددة لتقديم أفضل تجربة صوتية للمستخدم بناءً على إمكانات الجهاز السمعي. على سبيل المثال، يتم تشغيل بث صوتي مرمّز بتقنية Dolby Digital إذا كان التلفزيون يتيح ذلك، في حين يتمّ اختيار بث صوتي PCM متوافق على نطاق أوسع في حال عدم توفّر تقنية Dolby Digital. يمكنك الاطّلاع على قائمة برامج فك ترميز Android المدمَجة المستخدمة لتحويل بث صوتي إلى PCM في تنسيقات الوسائط المتوافقة.

في وقت التشغيل، يجب أن ينشئ تطبيق البثّ تنسيق AudioTrack يتضمّن أفضل رمز AudioFormat متوافق مع جهاز إخراج الصوت.

إنشاء مقطع صوتي بالتنسيق الصحيح

يجب أن تنشئ التطبيقات AudioTrack وتبدأ في تشغيله وتتصل بـ getRoutedDevice() لتحديد جهاز الصوت التلقائي الذي سيتم تشغيل الصوت منه. يمكن مثلاً أن يكون هذا المقطع الصوتي الآمن والقصير المرمّز باستخدام PCM بهدف تحديد الجهاز الموجّه وإمكاناته الصوتية فقط.

الحصول على ترميزات متوافقة

استخدِم getAudioProfiles() (المستوى 31 من واجهة برمجة التطبيقات والمستويات الأعلى) أو getEncodings() (المستوى 23 من واجهة برمجة التطبيقات والمستويات الأعلى) لتحديد تنسيقات الصوت المتوفّرة على الجهاز الصوتي التلقائي.

التحقّق من الملفات الشخصية والتنسيقات المتوافقة

استخدِم AudioProfile (المستوى 31 أو أعلى لواجهة برمجة التطبيقات) أو isDirectPlaybackSupported() (المستوى 29 والأعلى من واجهة برمجة التطبيقات) للاطّلاع على التركيبات المتوافقة من التنسيقات وعدد القنوات ومعدّل النماذج.

تستطيع بعض أجهزة Android دعم ترميزات غير تلك المتوافقة مع جهاز إخراج الصوت. يجب التعرّف على هذه التنسيقات الإضافية من خلال "isDirectPlaybackSupported()". وفي هذه الحالات، تتم إعادة ترميز البيانات الصوتية إلى تنسيق يتوافق مع جهاز إخراج الصوت. استخدِم isDirectPlaybackSupported() للتحقّق بشكل صحيح من إمكانية استخدام التنسيق المطلوب حتى لو لم يكن متوفرًا في القائمة التي يعرضها getEncodings().

مسار الصوت التنبؤي

وفّر نظام Android 13 (المستوى 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 الأعلى جودة الذي يتوافق مع الجهاز الصوتي التلقائي (ويتوفّر للمحتوى المحدَّد).

اعتراض تغييرات الأجهزة السماعية

لاعتراض تغييرات الأجهزة السماعية والتفاعل معها، يجب أن:

  • بالنسبة إلى مستويات واجهة برمجة التطبيقات التي تساوي أو تزيد عن 24، أضِف OnRoutingChangedListener لمراقبة التغييرات في الأجهزة الصوتية (منفذ HDMI والبلوتوث وغير ذلك).
  • بالنسبة إلى المستوى 23 من واجهة برمجة التطبيقات، عليك تسجيل AudioDeviceCallback لتلقّي التغييرات في قائمة الأجهزة السماعية المتاحة.
  • بالنسبة إلى المستويَين 21 و22 من واجهة برمجة التطبيقات، راقِب أحداث مقبس HDMI واستخدِم البيانات الإضافية من عمليات البث.
  • يمكنك أيضًا تسجيل BroadcastReceiver لمراقبة تغييرات حالة BluetoothDevice للأجهزة التي تقل عن المستوى 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);