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

يمكن أن تحتوي أجهزة 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) في نظام التشغيل Android مسارات صوتية استباقية. يمكنك توقع توافق سمة الصوت للجهاز وإعداد المقاطع الصوتية للجهاز الصوتي النشط. يمكنك استخدام 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);