mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
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:
parent
d7574ffd66
commit
f996a5e3e4
@ -10,6 +10,7 @@
|
||||
* DataSource:
|
||||
* Audio:
|
||||
* Allow constant power upmixing/downmixing in DefaultAudioMixer.
|
||||
* Add support for float PCM to `ChannelMappingAudioProcessor`.
|
||||
* Video:
|
||||
* Text:
|
||||
* Metadata:
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user