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

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

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

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

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

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

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

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

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

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

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

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

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

اندروید ۱۳ (سطح API ۳۳) مسیرهای صوتی پیش‌بینی‌شده را معرفی کرد. می‌توانید پشتیبانی از ویژگی صوتی دستگاه را پیش‌بینی کنید و آهنگ‌ها را برای دستگاه صوتی فعال آماده کنید. می‌توانید از 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
}

از طرف دیگر، می‌توانید پرس‌وجو کنید که کدام پروفایل‌ها برای پخش مستقیم رسانه از طریق دستگاه صوتی روت‌شده‌ی فعلی پشتیبانی می‌شوند. این، پروفایل‌هایی را که پشتیبانی نمی‌شوند یا مثلاً توسط چارچوب اندروید کدگذاری می‌شوند، حذف می‌کند:

کاتلین

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، بلوتوث و غیره) را رصد کند.
  • برای API سطح ۲۳، یک 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);