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

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

فعلى سبيل المثال، في فيلم، قد يبدأ الصوت الصادر من سيارة خلف المستخدم، ثم يتحرك إلى الأمام، ثم ينطلق إلى مسافة بعيدة. في دردشة الفيديو، يمكن فصل الأصوات ووضعها حول المستخدم، مما يسهل التعرف على المتحدثين.

إذا كان المحتوى يستخدم تنسيقًا صوتيًا متوافقًا، يمكنك إضافة ميزة "الصوت المكاني" إلى تطبيقك بدءًا من Android 13 (المستوى 33 من واجهة برمجة التطبيقات).

طلب بحث عن الإمكانيات

يمكنك استخدام الفئة Spatializer للاستعلام عن إمكانات الفصل المكاني للجهاز وسلوكه. ابدأ باسترداد مثيل من Spatializer من AudioManager:

Kotlin

val spatializer = audioManager.spatializer

Java

Spatializer spatializer = AudioManager.getSpatializer();

بعد الحصول على Spatializer، تحقَّق من الشروط الأربعة التي يجب أن تثبت نجاح الجهاز في إخراج صوت مكاني:

المعايير التحقّق
هل يتيح الجهاز استخدام ميزة الانتقال المكاني؟ getImmersiveAudioLevel() ليس SPATIALIZER_IMMERSIVE_LEVEL_NONE
هل تتيح ميزة تحديد الموقع الجغرافي؟
يعتمد مدى التوفُّر على التوافق مع توجيه إخراج الصوت الحالي.
قيمة isAvailable() هي true.
هل ميزة "الموقع الجغرافي" مفعّلة؟ قيمة isEnabled() هي true.
هل يمكن إنشاء موقع صوتي إذا كان يتضمّن معلَمات معيّنة؟ قيمة canBeSpatialized() هي true.

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

تتبُّع حركة الرأس

من خلال سمّاعات الرأس المتوافقة، يمكن للنظام الأساسي ضبط موضع الصوت استنادًا إلى وضع رأس المستخدم. للتحقّق مما إذا كان جهاز تتبُّع الرأس متاحًا لتوجيه إخراج الصوت الحالي، يمكنك استدعاء isHeadTrackerAvailable().

المحتوى المتوافق

يشير الرمز Spatializer.canBeSpatialized() إلى ما إذا كان يمكن استبدال الصوت بخصائص معيّنة باستخدام توجيه جهاز الإخراج الحالي. تتطلّب هذه الطريقة السمة AudioAttributes والسمة AudioFormat، وكلاهما موصوف بمزيد من التفصيل أدناه.

AudioAttributes

يصف عنصر AudioAttributes استخدام بث صوتي (على سبيل المثال، صوت لعبة أو وسائط عادية)، بالإضافة إلى سلوكيات التشغيل ونوع المحتوى.

عند طلب الرقم canBeSpatialized()، يُرجى استخدام مثيل AudioAttributes نفسه الذي تم ضبطه على جهاز Player. على سبيل المثال، إذا كنت تستخدم مكتبة Jetpack Media3 ولم تخصص AudioAttributes، يمكنك استخدام AudioAttributes.DEFAULT.

إيقاف ميزة "الصوت المكاني"

للإشارة إلى أنّه تم تحديد مكان المحتوى في وقت سابق، يمكنك الاتصال بالرمز setIsContentSpatialized(true) كي لا تتم معالجة الصوت مرتين. يمكنك بدلاً من ذلك تعديل سلوك التمييز المكاني لإيقاف هذه الميزة تمامًا من خلال طلب الرمز setSpatializationBehavior(AudioAttributes.SPATIALIZATION_BEHAVIOR_NEVER).

AudioFormat

يصف عنصر AudioFormat تفاصيل حول تنسيق المقطع الصوتي وإعدادات القناة.

عند إنشاء مثيل AudioFormat لتمريره إلى canBeSpatialized()، اضبط الترميز على نفس تنسيق الإخراج المتوقع من برنامج فك الترميز. يجب أيضًا ضبط قناع قناة يتطابق مع إعدادات القناة في المحتوى الخاص بك. راجِع قسم السلوك التلقائي للتحديد المكاني للحصول على إرشادات حول القيم المحدّدة التي يجب استخدامها.

التعرّف على التغييرات التي تطرأ على Spatializer

للاستماع إلى التغييرات التي تطرأ على حالة "Spatializer"، يمكنك إضافة مستمع باستخدام Spatializer.addOnSpatializerStateChangedListener(). وبالمثل، للاستماع إلى أي تغييرات في مدى توفّر جهاز تتبُّع الرأس، يمكنك الاتصال بالرقم Spatializer.addOnHeadTrackerAvailableListener().

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

ExoPlayer والصوت المكاني

تسهّل الإصدارات الحديثة من ExoPlayer استخدام ميزة "الصوت المكاني". إذا كنت تستخدم مكتبة ExoPlayer المستقلة (اسم الحزمة com.google.android.exoplayer2)، يضبط الإصدار 2.17 النظام الأساسي لإخراج صوت مكاني، بينما يفرض الإصدار 2.18 قيودًا على عدد القنوات الصوتية. إذا كنت تستخدم وحدة ExoPlayer من مكتبة Media3 (اسم الحزمة androidx.media3)، ستتضمّن الإصدارات 1.0.0-beta01 والإصدارات الأحدث هذه التحديثات نفسها.

بعد تحديث اعتمادك على ExoPlayer إلى أحدث إصدار، لا يحتاج تطبيقك إلا إلى تضمين محتوى يمكن ترتيبه في مكان.

القيود المفروضة على عدد القنوات الصوتية

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

لإيقاف القيود المفروضة على عدد القنوات الصوتية، اضبط معايير اختيار المقطع الصوتي في المشغّل كما هو موضّح أدناه:

Kotlin

exoPlayer.trackSelectionParameters = DefaultTrackSelector.Parameters.Builder(context)
  .setConstrainAudioChannelCountToDeviceCapabilities(false)
  .build()

Java

exoPlayer.setTrackSelectionParameters(
  new DefaultTrackSelector.Parameters.Builder(context)
    .setConstrainAudioChannelCountToDeviceCapabilities(false)
    .build()
);

بالمثل، يمكنك تعديل معلَمات أداة اختيار المقاطع الصوتية الحالية لإيقاف قيود عدد القنوات الصوتية على النحو التالي:

Kotlin

val trackSelector = DefaultTrackSelector(context)
...
trackSelector.parameters = trackSelector.buildUponParameters()
  .setConstrainAudioChannelCountToDeviceCapabilities(false)
  .build()

Java

DefaultTrackSelector trackSelector = new DefaultTrackSelector(context);
...
trackSelector.setParameters(
  trackSelector
    .buildUponParameters()
    .setConstrainAudioChannelCountToDeviceCapabilities(false)
    .build()
);

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

اختيار المقطع الصوتي

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

تغيير معلمات اختيار قناة الإصدار

لتغيير معلَمات اختيار المسارات في ExoPlayer، يمكنك استخدام Player.setTrackSelectionParameters(). وبالمثل، يمكنك الحصول على معلَمات ExoPlayer الحالية باستخدام Player.getTrackSelectionParameters(). على سبيل المثال، لاختيار مقطع صوتي استيريو في منتصف التشغيل:

Kotlin

exoPlayer.trackSelectionParameters = exoPlayer.trackSelectionParameters
  .buildUpon()
  .setMaxAudioChannelCount(2)
  .build()

Java

exoPlayer.setTrackSelectionParameters(
  exoPlayer.getTrackSelectionParameters()
    .buildUpon()
    .setMaxAudioChannelCount(2)
    .build()
);

تجدر الإشارة إلى أنّ تغيير معلَمات اختيار المقاطع الصوتية في منتصف التشغيل قد يؤدي إلى انقطاع في التشغيل. ويمكنك الاطّلاع على مزيد من المعلومات حول ضبط معلمات اختيار المقطع الصوتي للمشغّل في قسم اختيار المقاطع الصوتية في مستندات ExoPlayer.

السلوك التلقائي لظهور المحتوى في مكان معيّن

يشمل السلوك المكاني التلقائي في Android السلوكيات التالية التي يمكن أن يخصّصها المصنّعون الأصليون للأجهزة:

  • ويتمّ تحديد المحتوى المتعدّد القنوات فقط في مكاني، ولا يتمّ ذلك عن طريق المحتوى الاستيريو. إذا كنت لا تستخدم ExoPlayer، استنادًا إلى تنسيق المحتوى الصوتي المتعدد القنوات، قد تحتاج إلى ضبط الحد الأقصى لعدد القنوات التي يمكن إخراجها من خلال برنامج فك ترميز الصوت على عدد كبير. ويضمن ذلك أن يعمل برنامج فك ترميز الصوت على إخراج PCM متعدّد القنوات للنظام الأساسي

    Kotlin

    val mediaFormat = MediaFormat()
    mediaFormat.setInteger(MediaFormat.KEY_MAX_OUTPUT_CHANNEL_COUNT, 99)
    

    Java

    MediaFormat mediaFormat = new MediaFormat();
    mediaFormat.setInteger(MediaFormat.KEY_MAX_OUTPUT_CHANNEL_COUNT, 99);
    

    للاطّلاع على مثال عملي، يُرجى الاطّلاع على MediaCodecAudioRenderer.java من ExoPlayer. لإيقاف ميزة "الصوت المكاني" بنفسك، بصرف النظر عن تخصيص المصنِّع الأصلي للجهاز، يُرجى الاطّلاع على إيقاف ميزة الصوت المكاني.

  • AudioAttributes: يكون الصوت مؤهلاً للعرض المكاني في حال ضبط usage على USAGE_MEDIA أو USAGE_GAME.

  • AudioFormat: استخدِم قناع قناة يتضمّن على الأقل قنوات AudioFormat.CHANNEL_OUT_QUAD (من الأمام إلى اليسار وأمام اليمين وخلف اليسار و"الخلف" في الصورة) حتى يكون المقطع الصوتي مؤهّلاً لاستخدام المقطع الصوتي. في المثال أدناه، نستخدم AudioFormat.CHANNEL_OUT_5POINT1 لمقطع صوتي بجودة 5.1. لإنشاء مقطع صوتي استيريو، استخدِم AudioFormat.CHANNEL_OUT_STEREO.

    إذا كنت تستخدم Media3، يمكنك استخدام Util.getAudioTrackChannelConfig(int channelCount) لتحويل عدد قنوات إلى قناع قناة.

    بالإضافة إلى ذلك، اضبط الترميز على AudioFormat.ENCODING_PCM_16BIT إذا ضبطت برنامج فك الترميز على إخراج PCM متعدد القنوات.

    Kotlin

    val audioFormat = AudioFormat.Builder()
      .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
      .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1)
      .build()
    

    Java

    AudioFormat audioFormat = new AudioFormat.Builder()
      .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
      .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1)
      .build();
    

اختبار الصوت المكاني

تأكَّد من أنّ ميزة "الصوت المكاني" مفعَّلة على جهاز الاختبار:

  • بالنسبة إلى سمّاعات الرأس السلكية، انتقِل إلى إعدادات النظام > الصوت والاهتزاز > الصوت المكاني.
  • بالنسبة إلى سماعات الرأس اللاسلكية، يمكنك الانتقال إلى إعدادات النظام > الأجهزة المتصلة > رمز الترس لجهازك اللاسلكي > الصوت المكاني.

للتحقّق من توفّر ميزة "الصوت المكاني" للتوجيه الحالي، شغِّل الأمر adb shell dumpsys audio على جهازك. من المفترض أن تظهر المَعلمات التالية في الإخراج عندما يكون التشغيل نشطًا:

Spatial audio:
mHasSpatializerEffect:true (effect present)
isSpatializerEnabled:true (routing dependent)