AudioConfigUtil.java

/*
 * Copyright 2021 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.camera.video.internal.config;

import android.util.Range;
import android.util.Rational;

import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.camera.core.Logger;
import androidx.camera.video.AudioSpec;
import androidx.camera.video.internal.AudioSource;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * A collection of utilities used for resolving and debugging audio configurations.
 */
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
public final class AudioConfigUtil {
    private static final String TAG = "AudioConfigUtil";

    // Default to 44100 for now as it's guaranteed supported on devices.
    static final int AUDIO_SAMPLE_RATE_DEFAULT = 44100;
    // Default to mono since that should be supported on the most devices.
    static final int AUDIO_CHANNEL_COUNT_DEFAULT = AudioSpec.CHANNEL_COUNT_MONO;
    // Defaults to PCM_16BIT as it's guaranteed supported on devices.
    static final int AUDIO_SOURCE_FORMAT_DEFAULT = AudioSpec.SOURCE_FORMAT_PCM_16BIT;
    // Defaults to Camcorder as this should be the source closest to the camera
    static final int AUDIO_SOURCE_DEFAULT = AudioSpec.SOURCE_CAMCORDER;

    // Should not be instantiated.
    private AudioConfigUtil() {
    }

    static int resolveAudioSource(@NonNull AudioSpec audioSpec) {
        int resolvedAudioSource = audioSpec.getSource();
        if (resolvedAudioSource == AudioSpec.SOURCE_AUTO) {
            resolvedAudioSource = AUDIO_SOURCE_DEFAULT;
            Logger.d(TAG, "Using default AUDIO source: " + resolvedAudioSource);
        } else {
            Logger.d(TAG, "Using provided AUDIO source: " + resolvedAudioSource);
        }

        return resolvedAudioSource;
    }

    static int resolveAudioSourceFormat(@NonNull AudioSpec audioSpec) {
        int resolvedAudioSourceFormat = audioSpec.getSourceFormat();
        if (resolvedAudioSourceFormat == AudioSpec.SOURCE_FORMAT_AUTO) {
            // TODO: This should come from a priority list and may need to be combined with
            //  AudioSource.isSettingsSupported.
            resolvedAudioSourceFormat = AUDIO_SOURCE_FORMAT_DEFAULT;
            Logger.d(TAG, "Using default AUDIO source format: " + resolvedAudioSourceFormat);
        } else {
            Logger.d(
                    TAG, "Using provided AUDIO source format: " + resolvedAudioSourceFormat);
        }

        return resolvedAudioSourceFormat;
    }

    static int selectSampleRateOrNearestSupported(@NonNull Range<Integer> targetRange,
            int channelCount, int sourceFormat, int initialTargetSampleRate) {
        int selectedSampleRate = initialTargetSampleRate;
        // Sample rates sorted by proximity to initial target.
        List<Integer> sortedCommonSampleRates = null;
        int i = 0;
        do {
            if (targetRange.contains(selectedSampleRate)) {
                if (AudioSource.isSettingsSupported(selectedSampleRate, channelCount,
                        sourceFormat)) {
                    return selectedSampleRate;
                } else {
                    Logger.d(TAG, "Sample rate " + selectedSampleRate + "Hz is not supported by "
                            + "audio source with channel count " + channelCount + " and source "
                            + "format " + sourceFormat);
                }
            } else {
                Logger.d(TAG, "Sample rate " + selectedSampleRate + "Hz is not in target range "
                        + targetRange);
            }

            // If the initial target isn't supported, sort the array of published common sample
            // rates by closeness to target  and step through until we've found one that is
            // supported.
            if (sortedCommonSampleRates == null) {
                Logger.d(TAG,
                        "Trying common sample rates in proximity order to target "
                                + initialTargetSampleRate + "Hz");
                sortedCommonSampleRates = new ArrayList<>(AudioSource.COMMON_SAMPLE_RATES);
                Collections.sort(sortedCommonSampleRates, (x, y) -> {
                    int relativeDifference = Math.abs(x - initialTargetSampleRate) - Math.abs(
                            y - initialTargetSampleRate);
                    // If the relative difference is zero, i.e., the target is halfway
                    // between the two, always prefer the larger sample rate for quality.
                    if (relativeDifference == 0) {
                        return (int) Math.signum(x - y);
                    }

                    return (int) Math.signum(relativeDifference);
                });
            }

            if (i < sortedCommonSampleRates.size()) {
                selectedSampleRate = sortedCommonSampleRates.get(i++);
            } else {
                break;
            }
        } while (true);

        // No supported sample rate found. The default sample rate should work on most devices. May
        // consider throw an exception or have other way to notify users that the specified
        // sample rate can not be satisfied.
        Logger.d(TAG, "No sample rate found in target range or supported by audio source. Falling"
                + " back to default sample rate of " + AUDIO_SAMPLE_RATE_DEFAULT + "Hz");
        return AUDIO_SAMPLE_RATE_DEFAULT;
    }

    static int scaleAndClampBitrate(int baseBitrate,
            int actualChannelCount, int baseChannelCount,
            int actualSampleRate, int baseSampleRate,
            Range<Integer> clampedRange) {
        // Scale bitrate based on source number of channels relative to base channel count.
        Rational channelCountRatio = new Rational(actualChannelCount, baseChannelCount);
        // Scale bitrate based on source sample rate relative to profile sample rate.
        Rational sampleRateRatio = new Rational(actualSampleRate, baseSampleRate);

        int resolvedBitrate = (int) (baseBitrate * channelCountRatio.doubleValue()
                * sampleRateRatio.doubleValue());

        String debugString = "";
        if (Logger.isDebugEnabled(TAG)) {
            debugString = String.format("Base Bitrate(%dbps) * Channel Count Ratio(%d / %d) * "
                            + "Sample Rate Ratio(%d / %d) = %d", baseBitrate, actualChannelCount,
                    baseChannelCount, actualSampleRate, baseSampleRate, resolvedBitrate);
        }

        if (!AudioSpec.BITRATE_RANGE_AUTO.equals(clampedRange)) {
            resolvedBitrate = clampedRange.clamp(resolvedBitrate);
            if (Logger.isDebugEnabled(TAG)) {
                debugString += String.format("\nClamped to range %s -> %dbps", clampedRange,
                        resolvedBitrate);
            }
        }
        Logger.d(TAG, debugString);
        return resolvedBitrate;
    }
}