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

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