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

يتيح Android TV إمكانية إخراج الصوت بشكل متزامن من مصادر متعددة، مثل مكبّرات صوت التلفزيون ونظام السينما المنزلية المتصل بمنفذ HDMI والبلوتوث وغيرها. تتيح هذه الأجهزة استخدام ترميزات مختلفة (مثل Dolby Digital+‎ وDTS وPCM) ومعدّلات عيّنات وقنوات مختلفة. تتوافق أجهزة التلفزيون المتصلة بمنفذ HDMI مع العديد من التنسيقات، ولكن يتوافق البلوتوث مع تنسيق PCM فقط.

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

توفير عمليات ترميز متعددة للحصول على أفضل تجربة استنادًا إلى إمكانات الجهاز على سبيل المثال، يتم استخدام Dolby Digital إذا كان متاحًا، وإلا سيعود التطبيق إلى PCM. يمكنك الاطّلاع على تنسيقات الوسائط المتوافقة لمعرفة برامج فك الترميز على 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);