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

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

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

ارائه چندین کدگذاری برای بهترین تجربه بر اساس قابلیت‌های دستگاه. به عنوان مثال، در صورت پشتیبانی، از Dolby Digital استفاده می‌شود؛ در غیر این صورت، برنامه به 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);