diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/audio/AudioMixingAlgorithm.java b/libraries/transformer/src/main/java/androidx/media3/transformer/audio/AudioMixingAlgorithm.java new file mode 100644 index 0000000000..8124f9c337 --- /dev/null +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/audio/AudioMixingAlgorithm.java @@ -0,0 +1,82 @@ +/* + * Copyright 2022 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.media3.transformer.audio; + +import android.annotation.SuppressLint; +import androidx.media3.common.C; +import androidx.media3.common.audio.AudioProcessor.AudioFormat; +import androidx.media3.common.audio.AudioProcessor.UnhandledAudioFormatException; +import androidx.media3.common.util.UnstableApi; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import java.nio.ByteBuffer; + +/** + * Algorithm for mixing source audio buffers into an audio mixing buffer. + * + *

Each instance is parameterized by the mixing (output) audio format provided to {@link + * #create(AudioFormat)}. An instance may support multiple source audio formats queried via {@link + * #supportsSourceAudioFormat(AudioFormat)}. + * + *

All implementations are stateless and can work with any number of source and mixing buffers. + */ +@UnstableApi +/* package */ interface AudioMixingAlgorithm { + + /** Indicates whether the algorithm supports mixing source buffers with the given audio format. */ + boolean supportsSourceAudioFormat(AudioFormat sourceAudioFormat); + + /** + * Mixes audio from {@code sourceBuffer} into {@code mixingBuffer}. + * + *

The method will read from {@code sourceBuffer} and write to {@code mixingBuffer}, advancing + * the positions of both. The frame count must be in bounds for both buffers. + * + *

The {@code channelMixingMatrix} input and output channel counts must match the channel count + * of the source audio format and mixing audio format respectively. + * + * @param sourceBuffer Source audio. + * @param sourceAudioFormat {@link AudioFormat} of {@code sourceBuffer}. Must be {@linkplain + * #supportsSourceAudioFormat(AudioFormat) supported}. + * @param channelMixingMatrix Scaling factors applied to source samples before mixing. + * @param frameCount Number of audio frames to mix. + * @param mixingBuffer Mixing buffer. + */ + @CanIgnoreReturnValue + ByteBuffer mix( + ByteBuffer sourceBuffer, + AudioFormat sourceAudioFormat, + ChannelMixingMatrix channelMixingMatrix, + int frameCount, + ByteBuffer mixingBuffer); + + /** + * Creates an instance that mixes into the given audio format. + * + * @param mixingAudioFormat The format of audio in the mixing buffer. + * @return The new algorithm instance. + * @throws UnhandledAudioFormatException If the specified format is not supported for mixing. + */ + @SuppressLint("SwitchIntDef") + public static AudioMixingAlgorithm create(AudioFormat mixingAudioFormat) + throws UnhandledAudioFormatException { + switch (mixingAudioFormat.encoding) { + case C.ENCODING_PCM_FLOAT: + return new FloatAudioMixingAlgorithm(mixingAudioFormat); + default: + throw new UnhandledAudioFormatException(mixingAudioFormat); + } + } +} diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/audio/ChannelMixingMatrix.java b/libraries/transformer/src/main/java/androidx/media3/transformer/audio/ChannelMixingMatrix.java new file mode 100644 index 0000000000..d1a4c010a2 --- /dev/null +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/audio/ChannelMixingMatrix.java @@ -0,0 +1,178 @@ +/* + * Copyright 2022 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.media3.transformer.audio; + +import static androidx.media3.common.util.Assertions.checkArgument; + +/** + * An immutable matrix that describes the mapping of input channels to output channels. + * + *

The matrix coefficients define the scaling factor to use when mixing samples from the input + * channel (row) to the output channel (column). + * + *

Examples: + * + *

+ */ +/* package */ final class ChannelMixingMatrix { + private final int inputChannelCount; + private final int outputChannelCount; + private final float[] coefficients; + private final boolean isZero; + private final boolean isDiagonal; + + /** + * Creates a standard channel mixing matrix that converts from {@code inputChannelCount} channels + * to {@code outputChannelCount} channels. + * + *

If the input and output channel counts match then a simple identity matrix will be returned. + * Otherwise, default matrix coefficients will be used to best match channel locations and overall + * power level. + * + * @param inputChannelCount Number of input channels. + * @param outputChannelCount Number of output channels. + * @return New channel mixing matrix. + * @throws UnsupportedOperationException If no default matrix coefficients are implemented for the + * given input and output channel counts. + */ + public static ChannelMixingMatrix create(int inputChannelCount, int outputChannelCount) { + return new ChannelMixingMatrix( + inputChannelCount, + outputChannelCount, + createMixingCoefficients(inputChannelCount, outputChannelCount)); + } + + private static float[] createMixingCoefficients(int inputChannelCount, int outputChannelCount) { + if (inputChannelCount == outputChannelCount) { + int channelCount = inputChannelCount; + float[] coefficients = new float[channelCount * channelCount]; + for (int c = 0; c < channelCount; c++) { + coefficients[channelCount * c + c] = 1f; + } + return coefficients; + } + if (inputChannelCount == 1 && outputChannelCount == 2) { + // Mono -> stereo. + return new float[] {1f, 1f}; + } + if (inputChannelCount == 2 && outputChannelCount == 1) { + // Stereo -> mono. + return new float[] {0.5f, 0.5f}; + } + throw new UnsupportedOperationException( + "Default channel mixing coefficients for " + + inputChannelCount + + "->" + + outputChannelCount + + " are not yet implemented."); + } + + /** + * Creates a matrix with the given coefficients in row-major order. + * + * @param inputChannelCount Number of input channels (rows in the matrix). + * @param outputChannelCount Number of output channels (columns in the matrix). + * @param coefficients Non-negative matrix coefficients in row-major order. + */ + public ChannelMixingMatrix(int inputChannelCount, int outputChannelCount, float[] coefficients) { + checkArgument(inputChannelCount > 0, "Input channel count must be positive."); + checkArgument(outputChannelCount > 0, "Output channel count must be positive."); + checkArgument( + coefficients.length == inputChannelCount * outputChannelCount, + "Coefficient array length is invalid."); + this.inputChannelCount = inputChannelCount; + this.outputChannelCount = outputChannelCount; + this.coefficients = checkCoefficientsValid(coefficients); + + // Calculate matrix properties. + boolean hasNonZero = false; + boolean hasNonZeroOutOfDiagonal = false; + for (int i = 0; i < inputChannelCount; i++) { + for (int o = 0; o < outputChannelCount; o++) { + if (getMixingCoefficient(i, o) != 0f) { + hasNonZero = true; + if (i != o) { + hasNonZeroOutOfDiagonal = true; + } + } + } + } + isZero = !hasNonZero; + isDiagonal = isSquare() && !hasNonZeroOutOfDiagonal; + } + + public int getInputChannelCount() { + return inputChannelCount; + } + + public int getOutputChannelCount() { + return outputChannelCount; + } + + /** Gets the scaling factor for the given input and output channel. */ + public float getMixingCoefficient(int inputChannel, int outputChannel) { + return coefficients[inputChannel * outputChannelCount + outputChannel]; + } + + /** Returns whether all mixing coefficients are zero. */ + public boolean isZero() { + return isZero; + } + + /** Returns whether the input and output channel count is the same. */ + public boolean isSquare() { + return inputChannelCount == outputChannelCount; + } + + /** Returns whether the matrix is square and all non-diagonal coefficients are zero. */ + public boolean isDiagonal() { + return isDiagonal; + } + + /** Returns a new matrix with the given scaling factor applied to all coefficients. */ + public ChannelMixingMatrix scaleBy(float scale) { + float[] scaledCoefficients = new float[coefficients.length]; + for (int i = 0; i < coefficients.length; i++) { + scaledCoefficients[i] = scale * coefficients[i]; + } + return new ChannelMixingMatrix(inputChannelCount, outputChannelCount, scaledCoefficients); + } + + private static float[] checkCoefficientsValid(float[] coefficients) { + for (int i = 0; i < coefficients.length; i++) { + if (coefficients[i] < 0f) { + throw new IllegalArgumentException("Coefficient at index " + i + " is negative."); + } + } + return coefficients; + } +} diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/audio/FloatAudioMixingAlgorithm.java b/libraries/transformer/src/main/java/androidx/media3/transformer/audio/FloatAudioMixingAlgorithm.java new file mode 100644 index 0000000000..5d9a4d7e30 --- /dev/null +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/audio/FloatAudioMixingAlgorithm.java @@ -0,0 +1,184 @@ +/* + * Copyright 2022 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.media3.transformer.audio; + +import static androidx.media3.common.util.Assertions.checkArgument; + +import android.annotation.SuppressLint; +import androidx.media3.common.C; +import androidx.media3.common.Format; +import androidx.media3.common.audio.AudioProcessor.AudioFormat; +import androidx.media3.common.util.UnstableApi; +import java.nio.ByteBuffer; + +/** An {@link AudioMixingAlgorithm} which mixes into float samples. */ +@UnstableApi +/* package */ class FloatAudioMixingAlgorithm implements AudioMixingAlgorithm { + + // Short.MIN_VALUE != -Short.MAX_VALUE so use different scaling factors for positive and + // negative samples. + private static final float SCALE_S16_FOR_NEGATIVE_INPUT = -1f / Short.MIN_VALUE; + private static final float SCALE_S16_FOR_POSITIVE_INPUT = 1f / Short.MAX_VALUE; + + private final AudioFormat mixingAudioFormat; + + public FloatAudioMixingAlgorithm(AudioFormat mixingAudioFormat) { + checkArgument(mixingAudioFormat.encoding == C.ENCODING_PCM_FLOAT); + checkArgument(mixingAudioFormat.channelCount != Format.NO_VALUE); + this.mixingAudioFormat = mixingAudioFormat; + } + + @Override + @SuppressLint("SwitchIntDef") + public boolean supportsSourceAudioFormat(AudioFormat sourceAudioFormat) { + if (sourceAudioFormat.sampleRate != mixingAudioFormat.sampleRate) { + return false; + } + switch (sourceAudioFormat.encoding) { + case C.ENCODING_PCM_16BIT: + case C.ENCODING_PCM_FLOAT: + return true; + default: + return false; + } + } + + @Override + @SuppressLint("SwitchIntDef") + public ByteBuffer mix( + ByteBuffer sourceBuffer, + AudioFormat sourceAudioFormat, + ChannelMixingMatrix channelMixingMatrix, + int frameCount, + ByteBuffer mixingBuffer) { + checkArgument( + supportsSourceAudioFormat(sourceAudioFormat), "Source audio format is not supported."); + checkArgument( + channelMixingMatrix.getInputChannelCount() == sourceAudioFormat.channelCount, + "Input channel count does not match source format."); + checkArgument( + channelMixingMatrix.getOutputChannelCount() == mixingAudioFormat.channelCount, + "Output channel count does not match mixing format."); + checkArgument( + sourceBuffer.remaining() >= frameCount * sourceAudioFormat.bytesPerFrame, + "Source buffer is too small."); + checkArgument( + mixingBuffer.remaining() >= frameCount * mixingAudioFormat.bytesPerFrame, + "Mixing buffer is too small."); + + switch (sourceAudioFormat.encoding) { + case C.ENCODING_PCM_FLOAT: + return mixFloatIntoFloat(sourceBuffer, channelMixingMatrix, frameCount, mixingBuffer); + case C.ENCODING_PCM_16BIT: + return mixS16IntoFloat(sourceBuffer, channelMixingMatrix, frameCount, mixingBuffer); + default: + throw new IllegalArgumentException("Source encoding is not supported."); + } + } + + private static ByteBuffer mixFloatIntoFloat( + ByteBuffer sourceBuffer, + ChannelMixingMatrix channelMixingMatrix, + int frameCount, + ByteBuffer mixingBuffer) { + if (channelMixingMatrix.isDiagonal()) { + return mixFloatIntoFloatDiagonal(sourceBuffer, channelMixingMatrix, frameCount, mixingBuffer); + } + int sourceChannelCount = channelMixingMatrix.getInputChannelCount(); + float[] sourceFrame = new float[sourceChannelCount]; + for (int i = 0; i < frameCount; i++) { + for (int sourceChannel = 0; sourceChannel < sourceChannelCount; sourceChannel++) { + sourceFrame[sourceChannel] = sourceBuffer.getFloat(); + } + mixFloatFrameIntoFloat(sourceFrame, channelMixingMatrix, mixingBuffer); + } + return mixingBuffer; + } + + private static void mixFloatFrameIntoFloat( + float[] sourceFrame, ChannelMixingMatrix channelMixingMatrix, ByteBuffer mixingBuffer) { + int mixingChannelCount = channelMixingMatrix.getOutputChannelCount(); + for (int mixingChannel = 0; mixingChannel < mixingChannelCount; mixingChannel++) { + float mixedSample = mixingBuffer.getFloat(mixingBuffer.position()); + for (int sourceChannel = 0; sourceChannel < sourceFrame.length; sourceChannel++) { + mixedSample += + channelMixingMatrix.getMixingCoefficient(sourceChannel, mixingChannel) + * sourceFrame[sourceChannel]; + } + mixingBuffer.putFloat(mixedSample); + } + } + + private static ByteBuffer mixFloatIntoFloatDiagonal( + ByteBuffer sourceBuffer, + ChannelMixingMatrix channelMixingMatrix, + int frameCount, + ByteBuffer mixingBuffer) { + int channelCount = channelMixingMatrix.getInputChannelCount(); + for (int i = 0; i < frameCount; i++) { + for (int c = 0; c < channelCount; c++) { + float sourceSample = sourceBuffer.getFloat(); + float mixedSample = + mixingBuffer.getFloat(mixingBuffer.position()) + + channelMixingMatrix.getMixingCoefficient(c, c) * sourceSample; + mixingBuffer.putFloat(mixedSample); + } + } + return mixingBuffer; + } + + private static ByteBuffer mixS16IntoFloat( + ByteBuffer sourceBuffer, + ChannelMixingMatrix channelMixingMatrix, + int frameCount, + ByteBuffer mixingBuffer) { + if (channelMixingMatrix.isDiagonal()) { + return mixS16IntoFloatDiagonal(sourceBuffer, channelMixingMatrix, frameCount, mixingBuffer); + } + int sourceChannelCount = channelMixingMatrix.getInputChannelCount(); + float[] sourceFrame = new float[sourceChannelCount]; + for (int i = 0; i < frameCount; i++) { + for (int sourceChannel = 0; sourceChannel < sourceChannelCount; sourceChannel++) { + sourceFrame[sourceChannel] = s16ToFloat(sourceBuffer.getShort()); + } + mixFloatFrameIntoFloat(sourceFrame, channelMixingMatrix, mixingBuffer); + } + return mixingBuffer; + } + + private static ByteBuffer mixS16IntoFloatDiagonal( + ByteBuffer sourceBuffer, + ChannelMixingMatrix channelMixingMatrix, + int frameCount, + ByteBuffer mixingBuffer) { + int channelCount = channelMixingMatrix.getInputChannelCount(); + for (int i = 0; i < frameCount; i++) { + for (int c = 0; c < channelCount; c++) { + float sourceSample = s16ToFloat(sourceBuffer.getShort()); + float mixedSample = + mixingBuffer.getFloat(mixingBuffer.position()) + + channelMixingMatrix.getMixingCoefficient(c, c) * sourceSample; + mixingBuffer.putFloat(mixedSample); + } + } + return mixingBuffer; + } + + private static float s16ToFloat(short shortValue) { + return shortValue + * (shortValue < 0 ? SCALE_S16_FOR_NEGATIVE_INPUT : SCALE_S16_FOR_POSITIVE_INPUT); + } +} diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/audio/package-info.java b/libraries/transformer/src/main/java/androidx/media3/transformer/audio/package-info.java new file mode 100644 index 0000000000..8a705c7e41 --- /dev/null +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/audio/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2022 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. + */ +@NonNullApi +package androidx.media3.transformer.audio; + +import androidx.media3.common.util.NonNullApi; diff --git a/libraries/transformer/src/test/java/androidx/media3/transformer/audio/FloatAudioMixingAlgorithmTest.java b/libraries/transformer/src/test/java/androidx/media3/transformer/audio/FloatAudioMixingAlgorithmTest.java new file mode 100644 index 0000000000..f3fd0a2ff1 --- /dev/null +++ b/libraries/transformer/src/test/java/androidx/media3/transformer/audio/FloatAudioMixingAlgorithmTest.java @@ -0,0 +1,222 @@ +/* + * Copyright 2022 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.media3.transformer.audio; + +import static com.google.common.truth.Truth.assertThat; + +import androidx.media3.common.C; +import androidx.media3.common.audio.AudioProcessor.AudioFormat; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit tests for {@link FloatAudioMixingAlgorithm}. */ +@RunWith(AndroidJUnit4.class) +public final class FloatAudioMixingAlgorithmTest { + private static final AudioFormat AUDIO_FORMAT_STEREO_PCM_FLOAT = + new AudioFormat(/* sampleRate= */ 44100, /* channelCount= */ 2, C.ENCODING_PCM_FLOAT); + private static final AudioFormat AUDIO_FORMAT_MONO_PCM_FLOAT = + new AudioFormat(/* sampleRate= */ 44100, /* channelCount= */ 1, C.ENCODING_PCM_FLOAT); + + private static final AudioFormat AUDIO_FORMAT_STEREO_PCM_16BIT = + new AudioFormat(/* sampleRate= */ 44100, /* channelCount= */ 2, C.ENCODING_PCM_16BIT); + private static final AudioFormat AUDIO_FORMAT_MONO_PCM_16BIT = + new AudioFormat(/* sampleRate= */ 44100, /* channelCount= */ 1, C.ENCODING_PCM_16BIT); + + private static final ChannelMixingMatrix STEREO_TO_STEREO = + ChannelMixingMatrix.create(/* inputChannelCount= */ 2, /* outputChannelCount= */ 2); + private static final ChannelMixingMatrix MONO_TO_STEREO = + ChannelMixingMatrix.create(/* inputChannelCount= */ 1, /* outputChannelCount= */ 2); + private static final ChannelMixingMatrix STEREO_TO_MONO = + ChannelMixingMatrix.create(/* inputChannelCount= */ 2, /* outputChannelCount= */ 1); + + @Test + public void supportsSourceAudioFormatsForStereoMixing() throws Exception { + AudioMixingAlgorithm algorithm = new FloatAudioMixingAlgorithm(AUDIO_FORMAT_STEREO_PCM_FLOAT); + assertThat(algorithm.supportsSourceAudioFormat(AUDIO_FORMAT_STEREO_PCM_FLOAT)).isTrue(); + assertThat(algorithm.supportsSourceAudioFormat(AUDIO_FORMAT_MONO_PCM_FLOAT)).isTrue(); + assertThat(algorithm.supportsSourceAudioFormat(AUDIO_FORMAT_STEREO_PCM_16BIT)).isTrue(); + assertThat(algorithm.supportsSourceAudioFormat(AUDIO_FORMAT_MONO_PCM_16BIT)).isTrue(); + } + + @Test + public void supportsSourceAudioFormatsForMonoMixing() throws Exception { + AudioMixingAlgorithm algorithm = new FloatAudioMixingAlgorithm(AUDIO_FORMAT_MONO_PCM_FLOAT); + assertThat(algorithm.supportsSourceAudioFormat(AUDIO_FORMAT_STEREO_PCM_FLOAT)).isTrue(); + assertThat(algorithm.supportsSourceAudioFormat(AUDIO_FORMAT_MONO_PCM_FLOAT)).isTrue(); + assertThat(algorithm.supportsSourceAudioFormat(AUDIO_FORMAT_STEREO_PCM_16BIT)).isTrue(); + assertThat(algorithm.supportsSourceAudioFormat(AUDIO_FORMAT_MONO_PCM_16BIT)).isTrue(); + } + + @Test + public void mixStereoFloatIntoStereoFloat() throws Exception { + AudioMixingAlgorithm algorithm = new FloatAudioMixingAlgorithm(AUDIO_FORMAT_STEREO_PCM_FLOAT); + + ByteBuffer mixingBuffer = createByteBuffer(new float[] {0.25f, -0.25f, 0.5f, -0.5f}); + ByteBuffer sourceBuffer = createByteBuffer(new float[] {-0.5f, 0.25f, -0.25f, 0.5f}); + algorithm.mix( + sourceBuffer, + AUDIO_FORMAT_STEREO_PCM_FLOAT, + STEREO_TO_STEREO.scaleBy(0.5f), + /* frameCount= */ 2, + mixingBuffer); + assertThat(sourceBuffer.remaining()).isEqualTo(0); + assertThat(mixingBuffer.remaining()).isEqualTo(0); + + assertThat(createFloatArray(mixingBuffer.flip())) + .isEqualTo(new float[] {0f, -0.125f, 0.375f, -0.25f}); + } + + @Test + public void mixMonoFloatIntoStereoFloat() throws Exception { + AudioMixingAlgorithm algorithm = new FloatAudioMixingAlgorithm(AUDIO_FORMAT_STEREO_PCM_FLOAT); + + ByteBuffer mixingBuffer = createByteBuffer(new float[] {0.25f, -0.25f, 0.5f, -0.5f}); + ByteBuffer sourceBuffer = createByteBuffer(new float[] {-0.5f, 0.5f}); + algorithm.mix( + sourceBuffer, + AUDIO_FORMAT_MONO_PCM_FLOAT, + MONO_TO_STEREO.scaleBy(0.5f), + /* frameCount= */ 2, + mixingBuffer); + assertThat(sourceBuffer.remaining()).isEqualTo(0); + assertThat(mixingBuffer.remaining()).isEqualTo(0); + + assertThat(createFloatArray(mixingBuffer.flip())) + .isEqualTo(new float[] {0f, -0.5f, 0.75f, -0.25f}); + } + + @Test + public void mixStereoFloatIntoMonoFloat() throws Exception { + AudioMixingAlgorithm algorithm = new FloatAudioMixingAlgorithm(AUDIO_FORMAT_MONO_PCM_FLOAT); + + ByteBuffer mixingBuffer = createByteBuffer(new float[] {0.25f, 0.5f}); + ByteBuffer sourceBuffer = createByteBuffer(new float[] {-0.5f, 0.25f, -0.25f, 0.5f}); + algorithm.mix( + sourceBuffer, + AUDIO_FORMAT_STEREO_PCM_FLOAT, + STEREO_TO_MONO.scaleBy(0.5f), + /* frameCount= */ 2, + mixingBuffer); + assertThat(sourceBuffer.remaining()).isEqualTo(0); + assertThat(mixingBuffer.remaining()).isEqualTo(0); + + assertThat(createFloatArray(mixingBuffer.flip())).isEqualTo(new float[] {0.1875f, 0.5625f}); + } + + @Test + public void mixStereoS16IntoStereoFloat() throws Exception { + AudioMixingAlgorithm algorithm = new FloatAudioMixingAlgorithm(AUDIO_FORMAT_STEREO_PCM_FLOAT); + + ByteBuffer mixingBuffer = createByteBuffer(new float[] {0.25f, -0.25f, 0.5f, -0.5f}); + ByteBuffer sourceBuffer = + createByteBuffer( + new short[] { + -16384 /* -0.5f */, + 8192 /* 0.25000762962f */, + -8192 /* -0.25f */, + 16384 /* 0.50001525925f */ + }); + algorithm.mix( + sourceBuffer, + AUDIO_FORMAT_STEREO_PCM_16BIT, + STEREO_TO_STEREO.scaleBy(0.5f), + /* frameCount= */ 2, + mixingBuffer); + assertThat(sourceBuffer.remaining()).isEqualTo(0); + assertThat(mixingBuffer.remaining()).isEqualTo(0); + + assertThat(createFloatArray(mixingBuffer.flip())) + .usingTolerance(1f / Short.MAX_VALUE) + .containsExactly(new float[] {0f, -0.125f, 0.375f, -0.25f}) + .inOrder(); + } + + @Test + public void mixMonoS16IntoStereoFloat() throws Exception { + AudioMixingAlgorithm algorithm = new FloatAudioMixingAlgorithm(AUDIO_FORMAT_STEREO_PCM_FLOAT); + + ByteBuffer mixingBuffer = createByteBuffer(new float[] {0.25f, -0.25f, 0.5f, -0.5f}); + ByteBuffer sourceBuffer = + createByteBuffer(new short[] {-16384 /* -0.5f */, 16384 /* 0.50001525925f */}); + algorithm.mix( + sourceBuffer, + AUDIO_FORMAT_MONO_PCM_16BIT, + MONO_TO_STEREO.scaleBy(0.5f), + /* frameCount= */ 2, + mixingBuffer); + assertThat(sourceBuffer.remaining()).isEqualTo(0); + assertThat(mixingBuffer.remaining()).isEqualTo(0); + + assertThat(createFloatArray(mixingBuffer.flip())) + .usingTolerance(1f / Short.MAX_VALUE) + .containsExactly(new float[] {0f, -0.5f, 0.75f, -0.25f}) + .inOrder(); + } + + @Test + public void doesNotSupportSampleRateConversion() throws Exception { + AudioMixingAlgorithm algorithm = + new FloatAudioMixingAlgorithm( + new AudioFormat(/* sampleRate= */ 44100, /* channelCount= */ 2, C.ENCODING_PCM_FLOAT)); + assertThat( + algorithm.supportsSourceAudioFormat( + new AudioFormat( + /* sampleRate= */ 48000, /* channelCount= */ 2, C.ENCODING_PCM_FLOAT))) + .isFalse(); + } + + @Test + public void doesNotSupportSampleFormats() throws Exception { + AudioMixingAlgorithm algorithm = + new FloatAudioMixingAlgorithm( + new AudioFormat(/* sampleRate= */ 44100, /* channelCount= */ 2, C.ENCODING_PCM_FLOAT)); + assertThat( + algorithm.supportsSourceAudioFormat( + new AudioFormat( + /* sampleRate= */ 44100, /* channelCount= */ 2, C.ENCODING_PCM_24BIT))) + .isFalse(); + assertThat( + algorithm.supportsSourceAudioFormat( + new AudioFormat( + /* sampleRate= */ 44100, /* channelCount= */ 2, C.ENCODING_PCM_32BIT))) + .isFalse(); + } + + private static ByteBuffer createByteBuffer(float[] content) { + ByteBuffer byteBuffer = + ByteBuffer.allocateDirect(content.length * 4).order(ByteOrder.nativeOrder()); + byteBuffer.asFloatBuffer().put(content); + return byteBuffer; + } + + private static ByteBuffer createByteBuffer(short[] content) { + ByteBuffer byteBuffer = + ByteBuffer.allocateDirect(content.length * 2).order(ByteOrder.nativeOrder()); + byteBuffer.asShortBuffer().put(content); + return byteBuffer; + } + + private static float[] createFloatArray(ByteBuffer byteBuffer) { + FloatBuffer buffer = byteBuffer.asFloatBuffer(); + float[] content = new float[buffer.remaining()]; + buffer.get(content); + return content; + } +}