Add support for FLOAT_PCM in ChannelMappingAudioProcessor

This was requested in Issue: androidx/media#2191 for playback of Opus and Vorbis
files with more than two channels with a float PCM pipeline.

Also, add ChannelMappingAudioProcessorTest.

PiperOrigin-RevId: 733766680
This commit is contained in:
ivanbuper 2025-03-05 09:37:57 -08:00 committed by Copybara-Service
parent d7574ffd66
commit f996a5e3e4
4 changed files with 145 additions and 5 deletions

View File

@ -10,6 +10,7 @@
* DataSource:
* Audio:
* Allow constant power upmixing/downmixing in DefaultAudioMixer.
* Add support for float PCM to `ChannelMappingAudioProcessor`.
* Video:
* Text:
* Metadata:

View File

@ -15,6 +15,8 @@
*/
package androidx.media3.exoplayer.audio;
import static androidx.media3.common.util.Util.getByteDepth;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.Format;
@ -22,6 +24,7 @@ import androidx.media3.common.audio.AudioProcessor;
import androidx.media3.common.audio.BaseAudioProcessor;
import androidx.media3.common.util.Assertions;
import java.nio.ByteBuffer;
import java.util.Arrays;
/**
* An {@link AudioProcessor} that applies a mapping from input channels onto specified output
@ -53,7 +56,8 @@ import java.nio.ByteBuffer;
return AudioFormat.NOT_SET;
}
if (inputAudioFormat.encoding != C.ENCODING_PCM_16BIT) {
if (inputAudioFormat.encoding != C.ENCODING_PCM_16BIT
&& inputAudioFormat.encoding != C.ENCODING_PCM_FLOAT) {
throw new UnhandledAudioFormatException(inputAudioFormat);
}
@ -61,12 +65,17 @@ import java.nio.ByteBuffer;
for (int i = 0; i < outputChannels.length; i++) {
int channelIndex = outputChannels[i];
if (channelIndex >= inputAudioFormat.channelCount) {
throw new UnhandledAudioFormatException(inputAudioFormat);
throw new UnhandledAudioFormatException(
"Channel map ("
+ Arrays.toString(outputChannels)
+ ") trying to access non-existent input channel.",
inputAudioFormat);
}
active |= (channelIndex != i);
}
return active
? new AudioFormat(inputAudioFormat.sampleRate, outputChannels.length, C.ENCODING_PCM_16BIT)
? new AudioFormat(
inputAudioFormat.sampleRate, outputChannels.length, inputAudioFormat.encoding)
: AudioFormat.NOT_SET;
}
@ -80,7 +89,17 @@ import java.nio.ByteBuffer;
ByteBuffer buffer = replaceOutputBuffer(outputSize);
while (position < limit) {
for (int channelIndex : outputChannels) {
buffer.putShort(inputBuffer.getShort(position + 2 * channelIndex));
int inputIndex = position + getByteDepth(inputAudioFormat.encoding) * channelIndex;
switch (inputAudioFormat.encoding) {
case C.ENCODING_PCM_16BIT:
buffer.putShort(inputBuffer.getShort(inputIndex));
break;
case C.ENCODING_PCM_FLOAT:
buffer.putFloat(inputBuffer.getFloat(inputIndex));
break;
default:
throw new IllegalStateException("Unexpected encoding: " + inputAudioFormat.encoding);
}
}
position += inputAudioFormat.bytesPerFrame;
}

View File

@ -601,7 +601,8 @@ public final class DefaultAudioSink implements AudioSink {
toIntPcmAvailableAudioProcessors =
ImmutableList.of(
new ToInt16PcmAudioProcessor(), channelMappingAudioProcessor, trimmingAudioProcessor);
toFloatPcmAvailableAudioProcessors = ImmutableList.of(new ToFloatPcmAudioProcessor());
toFloatPcmAvailableAudioProcessors =
ImmutableList.of(new ToFloatPcmAudioProcessor(), channelMappingAudioProcessor);
volume = 1f;
audioSessionId = C.AUDIO_SESSION_ID_UNSET;
auxEffectInfo = new AuxEffectInfo(AuxEffectInfo.NO_AUX_EFFECT_ID, 0f);

View File

@ -0,0 +1,119 @@
/*
* Copyright 2025 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
*
* https://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.exoplayer.audio;
import static androidx.media3.test.utils.TestUtil.createByteBuffer;
import static androidx.media3.test.utils.TestUtil.createFloatArray;
import static androidx.media3.test.utils.TestUtil.createShortArray;
import static com.google.common.truth.Truth.assertThat;
import androidx.media3.common.C;
import androidx.media3.common.audio.AudioProcessor;
import androidx.media3.common.audio.AudioProcessor.AudioFormat;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit tests for {@link ChannelMappingAudioProcessor} */
@RunWith(AndroidJUnit4.class)
public class ChannelMappingAudioProcessorTest {
private static final AudioFormat PCM_FLOAT_LCR_FORMAT =
new AudioFormat(
/* sampleRate= */ 44100, /* channelCount= */ 3, /* encoding= */ C.ENCODING_PCM_FLOAT);
private static final AudioFormat PCM_16BIT_STEREO_FORMAT =
new AudioFormat(
/* sampleRate= */ 44100, /* channelCount= */ 2, /* encoding= */ C.ENCODING_PCM_16BIT);
@Test
public void channelMap_withPcmFloatSamples_mapsOutputCorrectly()
throws AudioProcessor.UnhandledAudioFormatException {
ChannelMappingAudioProcessor processor = new ChannelMappingAudioProcessor();
processor.setChannelMap(new int[] {2, 1, 0});
processor.configure(PCM_FLOAT_LCR_FORMAT);
processor.flush();
processor.queueInput(createByteBuffer(new float[] {1f, 2f, 3f, 4f, 5f, 6f}));
float[] output = createFloatArray(processor.getOutput());
assertThat(output).isEqualTo(new float[] {3f, 2f, 1f, 6f, 5f, 4f});
}
@Test
public void channelMap_withPcm16Samples_mapsOutputCorrectly()
throws AudioProcessor.UnhandledAudioFormatException {
ChannelMappingAudioProcessor processor = new ChannelMappingAudioProcessor();
processor.setChannelMap(new int[] {1, 0});
processor.configure(PCM_16BIT_STEREO_FORMAT);
processor.flush();
processor.queueInput(createByteBuffer(new short[] {1, 2, 3, 4, 5, 6}));
short[] output = createShortArray(processor.getOutput());
assertThat(output).isEqualTo(new short[] {2, 1, 4, 3, 6, 5});
}
@Test
public void channelMap_withMoreOutputChannels_duplicatesSamples()
throws AudioProcessor.UnhandledAudioFormatException {
ChannelMappingAudioProcessor processor = new ChannelMappingAudioProcessor();
processor.setChannelMap(new int[] {1, 0, 1});
processor.configure(PCM_16BIT_STEREO_FORMAT);
processor.flush();
processor.queueInput(createByteBuffer(new short[] {1, 2, 3, 4}));
short[] output = createShortArray(processor.getOutput());
assertThat(output).isEqualTo(new short[] {2, 1, 2, 4, 3, 4});
}
@Test
public void channelMap_withLessOutputChannels_ignoresSamples()
throws AudioProcessor.UnhandledAudioFormatException {
ChannelMappingAudioProcessor processor = new ChannelMappingAudioProcessor();
processor.setChannelMap(new int[] {0, 1});
processor.configure(PCM_FLOAT_LCR_FORMAT);
processor.flush();
processor.queueInput(createByteBuffer(new float[] {1f, 2f, 3f, 4f, 5f, 6f}));
float[] output = createFloatArray(processor.getOutput());
assertThat(output).isEqualTo(new float[] {1f, 2f, 4f, 5f});
}
@Test
public void setChannelMap_withNonExistentInputChannels_throwsInConfigure()
throws AudioProcessor.UnhandledAudioFormatException {
ChannelMappingAudioProcessor processor = new ChannelMappingAudioProcessor();
processor.setChannelMap(new int[] {1, 0, 2});
Assert.assertThrows(
AudioProcessor.UnhandledAudioFormatException.class,
() -> processor.configure(PCM_16BIT_STEREO_FORMAT));
}
@Test
public void configure_withoutChannelMapSet_returnNotSet()
throws AudioProcessor.UnhandledAudioFormatException {
ChannelMappingAudioProcessor processor = new ChannelMappingAudioProcessor();
assertThat(processor.configure(PCM_16BIT_STEREO_FORMAT)).isEqualTo(AudioFormat.NOT_SET);
}
@Test
public void configure_withDifferentInputAndOutputChannelCounts_returnsOutputChannelCount()
throws AudioProcessor.UnhandledAudioFormatException {
ChannelMappingAudioProcessor processor = new ChannelMappingAudioProcessor();
processor.setChannelMap(new int[] {0});
assertThat(processor.configure(PCM_FLOAT_LCR_FORMAT).channelCount).isEqualTo(1);
}
}