mirror of
https://github.com/samsonjs/media.git
synced 2026-03-29 10:05:48 +00:00
Add low level audio mixing algorithms
Adds the AudioMixerAlgorithm interface which allows for specialized implementations of audio mixing that also efficiently convert between source and mixing formats. Initial implementation has two algorithms: 1. Float -> float (with channel mixing) 2. S16 -> float (with channel mixing) PiperOrigin-RevId: 496686805
This commit is contained in:
parent
d8c964cfe6
commit
9465fe2f98
5 changed files with 685 additions and 0 deletions
|
|
@ -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.
|
||||
*
|
||||
* <p>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)}.
|
||||
*
|
||||
* <p>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}.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
*
|
||||
* <p>The matrix coefficients define the scaling factor to use when mixing samples from the input
|
||||
* channel (row) to the output channel (column).
|
||||
*
|
||||
* <p>Examples:
|
||||
*
|
||||
* <ul>
|
||||
* <li>Stereo to mono with each channel at half volume:
|
||||
* <pre>[0.5 0.5]
|
||||
* <li>
|
||||
* <li>Stereo to stereo with no mixing or scaling:
|
||||
* <pre>
|
||||
* [1 0
|
||||
* 0 1]
|
||||
* </pre>
|
||||
* </li>
|
||||
* <li>Stereo to stereo with 0.7 volume:
|
||||
* <pre>
|
||||
* [0.7 0
|
||||
* 0 0.7]
|
||||
* </pre>
|
||||
* </li>
|
||||
* </ul>
|
||||
*/
|
||||
/* 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.
|
||||
*
|
||||
* <p>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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue