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

يتيح 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().

المسار الصوتي الاستباقي

يقدّم الإصدار 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 وBluetooth وما إلى ذلك).
  • بالنسبة إلى مستوى واجهة برمجة التطبيقات 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);