قابلیت های صوتی

دستگاه‌های Android TV می‌توانند چندین خروجی صوتی را به طور همزمان متصل کنند: بلندگوهای تلویزیون، سینمای خانگی متصل به HDMI، هدفون بلوتوث و غیره. این دستگاه‌های خروجی صدا می‌توانند از قابلیت‌های صوتی مختلفی مانند رمزگذاری (Dolby Digital+، DTS و PCM)، نرخ نمونه‌برداری و کانال‌ها پشتیبانی کنند. به عنوان مثال، تلویزیون های متصل به HDMI از تعداد زیادی کدگذاری پشتیبانی می کنند در حالی که هدفون های بلوتوث متصل معمولاً فقط از PCM پشتیبانی می کنند.

لیست دستگاه‌های صوتی موجود و دستگاه صوتی مسیریابی شده نیز می‌تواند با وصل کردن دستگاه‌های HDMI، اتصال یا قطع اتصال هدفون بلوتوث یا تغییر تنظیمات صدا توسط کاربر تغییر کند. از آنجایی که قابلیت‌های خروجی صدا می‌توانند حتی زمانی که برنامه‌ها در حال پخش رسانه هستند، تغییر کند، برنامه‌ها باید به‌خوبی خود را با این تغییرات وفق دهند و به پخش در دستگاه صوتی مسیریابی جدید و قابلیت‌های آن ادامه دهند. خروجی فرمت صوتی اشتباه می تواند منجر به خطا یا عدم پخش صدا شود.

برنامه‌ها این قابلیت را دارند که محتوای یکسانی را در چندین رمزگذاری خروجی بگیرند تا بسته به قابلیت‌های دستگاه صوتی بهترین تجربه صوتی را به کاربر ارائه دهند. به عنوان مثال، در صورتی که تلویزیون از آن پشتیبانی کند، یک جریان صوتی رمزگذاری شده Dolby Digital پخش می شود، در حالی که وقتی از Dolby Digital پشتیبانی نمی شود، یک جریان صوتی PCM با پشتیبانی گسترده تر انتخاب می شود. فهرست رمزگشاهای داخلی Android که برای تبدیل یک جریان صوتی به PCM استفاده می‌شوند را می‌توانید در قالب‌های رسانه پشتیبانی شده پیدا کنید.

در زمان پخش، برنامه پخش باید یک AudioTrack با بهترین AudioFormat که توسط دستگاه صوتی خروجی پشتیبانی می‌شود، ایجاد کند.

یک آهنگ با فرمت مناسب ایجاد کنید

برنامه‌ها باید یک AudioTrack ایجاد کنند، شروع به پخش کنند و getRoutedDevice() را فراخوانی کنند تا دستگاه صوتی پیش‌فرض که از آن پخش می‌شود را تعیین کنند. برای مثال، این می‌تواند یک تراک رمزگذاری شده PCM بی‌صدا کوتاه باشد که فقط برای تعیین دستگاه مسیریابی و قابلیت‌های صوتی آن استفاده می‌شود.

کدهای پشتیبانی شده را دریافت کنید

از getAudioProfiles() (سطح API 31 و بالاتر) یا getEncodings() (سطح API 23 و بالاتر) برای تعیین فرمت های صوتی موجود در دستگاه صوتی پیش فرض استفاده کنید.

نمایه ها و فرمت های صوتی پشتیبانی شده را بررسی کنید

از AudioProfile (سطح API 31 و بالاتر) یا isDirectPlaybackSupported() (سطح API 29 و بالاتر) برای بررسی ترکیبات پشتیبانی شده از قالب، تعداد کانال و نرخ نمونه استفاده کنید.

برخی از دستگاه‌های Android قادر به پشتیبانی از رمزگذاری‌هایی فراتر از آنهایی هستند که توسط دستگاه صوتی خروجی پشتیبانی می‌شوند. این فرمت های اضافی باید از طریق isDirectPlaybackSupported() شناسایی شوند. در این موارد، داده های صوتی به فرمتی که توسط دستگاه صوتی خروجی پشتیبانی می شود، دوباره کدگذاری می شوند. از isDirectPlaybackSupported() برای بررسی صحیح پشتیبانی از فرمت مورد نظر استفاده کنید، حتی اگر در لیستی که توسط getEncodings() برگردانده نشده باشد.

مسیر صوتی پیش بینی شده

اندروید 13 (سطح API 33) مسیرهای صوتی پیش بینی شده را معرفی کرد. می‌توانید پشتیبانی از ویژگی صوتی دستگاه را پیش‌بینی کنید و آهنگ‌هایی را برای دستگاه صوتی فعال آماده کنید. می‌توانید از getDirectPlaybackSupport() برای بررسی اینکه آیا پخش مستقیم در دستگاه صوتی مسیریابی فعلی برای فرمت و ویژگی‌های معین پشتیبانی می‌شود یا خیر استفاده کنید:

کاتلین

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
}

جاوا

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 رمزگذاری می‌شوند، مستثنی می‌شود:

کاتلین

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()
}

جاوا

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 با بالاترین کیفیت که توسط دستگاه صوتی پیش‌فرض پشتیبانی می‌شود (و برای محتوای انتخابی موجود است) استفاده کنند.

رهگیری تغییرات دستگاه صوتی

برای رهگیری و واکنش به تغییرات دستگاه صوتی، برنامه‌ها باید:

  • برای سطوح API مساوی یا بیشتر از 24، یک OnRoutingChangedListener اضافه کنید تا تغییرات دستگاه صوتی (HDMI، بلوتوث و غیره) را نظارت کنید.
  • برای سطح API 23، AudioDeviceCallback را برای دریافت تغییرات در لیست دستگاه های صوتی موجود ثبت کنید.
  • برای سطوح API 21 و 22، رویدادهای پلاگین HDMI را نظارت کنید و از داده های اضافی پخش شده استفاده کنید.
  • همچنین یک BroadcastReceiver برای نظارت بر تغییرات وضعیت BluetoothDevice برای دستگاه‌های پایین‌تر از API 23 ثبت کنید، زیرا AudioDeviceCallback هنوز پشتیبانی نمی‌شود.

هنگامی که تغییر دستگاه صوتی برای AudioTrack شناسایی شد، برنامه باید قابلیت‌های صوتی به‌روزرسانی‌شده را بررسی کند و در صورت نیاز، AudioTrack با AudioFormat متفاوت دوباره ایجاد کند. اگر اکنون کدگذاری با کیفیت بالاتر پشتیبانی می‌شود یا کدگذاری قبلاً استفاده شده دیگر پشتیبانی نمی‌شود، این کار را انجام دهید.

کد نمونه

کاتلین

// 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)

جاوا

// 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);