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