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

يمكن توصيل أجهزة 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().

النشط حاليًا.

مسار الصوت التوقّعي

طرح الإصدار 13 من نظام التشغيل Android (المستوى 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);