AudioUtils.java

/*
 * Copyright 2023 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.audio;

import static android.media.AudioFormat.ENCODING_PCM_16BIT;
import static android.media.AudioFormat.ENCODING_PCM_24BIT_PACKED;
import static android.media.AudioFormat.ENCODING_PCM_32BIT;
import static android.media.AudioFormat.ENCODING_PCM_8BIT;
import static android.media.AudioFormat.ENCODING_PCM_FLOAT;

import static androidx.core.util.Preconditions.checkArgument;

import android.media.AudioFormat;

import androidx.annotation.RequiresApi;

import java.util.concurrent.TimeUnit;

/** Utility class for audio-related operations and calculations. */
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
public final class AudioUtils {

    // Prevent instantiation
    private AudioUtils() {
    }

    /**
     * Converts a channel count to the channel config of {@link AudioFormat}.
     *
     * @return channel config.
     *
     * @see AudioFormat#CHANNEL_IN_MONO
     * @see AudioFormat#CHANNEL_IN_STEREO
     */
    public static int channelCountToChannelConfig(int channelCount) {
        return channelCount == 1 ? AudioFormat.CHANNEL_IN_MONO : AudioFormat.CHANNEL_IN_STEREO;
    }

    /**
     * Converts a channel count to the channel mask of {@link AudioFormat}.
     *
     * @return channel mask.
     *
     * @see AudioFormat#CHANNEL_IN_MONO
     * @see AudioFormat#CHANNEL_IN_STEREO
     */
    public static int channelCountToChannelMask(int channelCount) {
        // Currently equivalent to channelCountToChannelConfig, but keep this logic separate
        // since technically channel masks are different from the legacy channel config and we don't
        // want any future updates to break things.
        return channelCount == 1 ? AudioFormat.CHANNEL_IN_MONO : AudioFormat.CHANNEL_IN_STEREO;
    }

    /**
     * Gets the size in bytes per frame.
     *
     * @param audioEncoding the audio encoding of {@link AudioFormat}.
     * @param channelCount  the channel count.
     * @return bytes per frame.
     * @throws IllegalArgumentException if the channel count or audio encoding is invalid.
     *
     * @see AudioFormat#ENCODING_PCM_8BIT
     * @see AudioFormat#ENCODING_PCM_16BIT
     * @see AudioFormat#ENCODING_PCM_24BIT_PACKED
     * @see AudioFormat#ENCODING_PCM_32BIT
     * @see AudioFormat#ENCODING_PCM_FLOAT
     */
    public static int getBytesPerFrame(int audioEncoding, int channelCount) {
        checkArgument(channelCount > 0, "Invalid channel count: " + channelCount);
        switch (audioEncoding) {
            case ENCODING_PCM_8BIT:
                return channelCount;
            case ENCODING_PCM_16BIT:
                return channelCount * 2;
            case ENCODING_PCM_24BIT_PACKED:
                return channelCount * 3;
            case ENCODING_PCM_32BIT:
            case ENCODING_PCM_FLOAT:
                return channelCount * 4;
            default:
                throw new IllegalArgumentException("Invalid audio encoding: " + audioEncoding);
        }
    }

    /**
     * Calculates the frame count by the input size in bytes and the per frame size.
     *
     * <p>If the size is not divisible by the per frame size, the decimal will be rounded down.
     *
     * <p>Negative size is allowed which is useful to calculate frame count difference.
     *
     * @param sizeInBytes   size in bytes.
     * @param bytesPerFrame bytes per frame. Must be greater than 0.
     * @return frame count.
     * @throws IllegalArgumentException if bytesPerFrame is not greater than 0.
     */
    public static long sizeToFrameCount(long sizeInBytes, int bytesPerFrame) {
        checkArgument(bytesPerFrame > 0L, "bytesPerFrame must be greater than 0.");
        return sizeInBytes / bytesPerFrame;
    }

    /**
     * Calculates the size in bytes by the input frame count and the per frame size.
     *
     * <p>Negative frame count is allowed which is useful to calculate size difference.
     *
     * @param frameCount    frame count.
     * @param bytesPerFrame bytes per frame. Must be greater than 0.
     * @return size in bytes.
     * @throws IllegalArgumentException if bytesPerFrame is not greater than 0.
     */
    public static long frameCountToSize(long frameCount, int bytesPerFrame) {
        checkArgument(bytesPerFrame > 0L, "bytesPerFrame must be greater than 0.");
        return frameCount * bytesPerFrame;
    }

    /**
     * Calculates the duration in nanoseconds by the input frame count and sample rate.
     *
     * <p>Negative frame count is allowed which is useful to calculate negative duration offset.
     *
     * @param frameCount the frame count.
     * @param sampleRate the sample rate. Must be greater than 0.
     * @return duration in nanoseconds.
     * @throws IllegalArgumentException if sampleRate is not greater than 0.
     */
    public static long frameCountToDurationNs(long frameCount, int sampleRate) {
        checkArgument(sampleRate > 0L, "sampleRate must be greater than 0.");
        return TimeUnit.SECONDS.toNanos(1) * frameCount / sampleRate;
    }
}