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

‫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 برای دستگاه‌های پایین‌تر از میانای برنامه‌سازی کاربردی ۲۳ ثبت کنید، زیرا 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);