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

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

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

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

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

قطعه‌ای با قالب صحیح بسازید

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

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

از getAudioProfiles() (سطح میانای برنامه‌سازی کاربردی ۳۱ و بالاتر) یا getEncodings() (سطح میانای برنامه‌سازی کاربردی ۲۳ و بالاتر) برای تعیین قالب‌های صوتی دردسترس در دستگاه صوتی پیش‌فرض استفاده کنید.

بررسی قالب‌ها و نمایه‌های صوتی پشتیبانی‌شده

از AudioProfile (سطح میانای برنامه‌سازی کاربردی ۳۱ و بالاتر) یا isDirectPlaybackSupported() (سطح میانای برنامه‌سازی کاربردی ۲۹ و بالاتر) برای بررسی ترکیب‌های پشتیبانی‌شده از قالب، تعداد کانال، و نرخ نمونه استفاده کنید.

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

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

‫Android 13 (سطح میانای برنامه‌سازی کاربردی ۳۳) مسیرهای صوتی پیش‌بینی‌کننده را معرفی کرد. می‌توانید از پشتیبانی ویژگی صدای دستگاه پیش‌بینی کنید و قطعه‌هایی را برای دستگاه صوتی فعال آماده کنید. می‌توانید از 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 برابر یا بزرگ‌تر از ۲۴، OnRoutingChangedListener را برای نظارت بر تغییرات دستگاه صوتی (HDMI، بلوتوث، و غیره) اضافه کنید.
  • برای سطح میانای برنامه‌سازی کاربردی ۲۳، AudioDeviceCallback را ثبت کنید تا تغییرات در فهرست دستگاه‌های صوتی دردسترس را دریافت کنید.
  • برای سطوح API ۲۱ و ۲۲، رویدادهای اتصال 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);