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