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

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