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:
|
* DataSource:
|
||||||
* Audio:
|
* Audio:
|
||||||
* Allow constant power upmixing/downmixing in DefaultAudioMixer.
|
* Allow constant power upmixing/downmixing in DefaultAudioMixer.
|
||||||
|
* Add support for float PCM to `ChannelMappingAudioProcessor`.
|
||||||
* Video:
|
* Video:
|
||||||
* Text:
|
* Text:
|
||||||
* Metadata:
|
* Metadata:
|
||||||
|
@ -15,6 +15,8 @@
|
|||||||
*/
|
*/
|
||||||
package androidx.media3.exoplayer.audio;
|
package androidx.media3.exoplayer.audio;
|
||||||
|
|
||||||
|
import static androidx.media3.common.util.Util.getByteDepth;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.media3.common.C;
|
import androidx.media3.common.C;
|
||||||
import androidx.media3.common.Format;
|
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.audio.BaseAudioProcessor;
|
||||||
import androidx.media3.common.util.Assertions;
|
import androidx.media3.common.util.Assertions;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An {@link AudioProcessor} that applies a mapping from input channels onto specified output
|
* 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;
|
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);
|
throw new UnhandledAudioFormatException(inputAudioFormat);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,12 +65,17 @@ import java.nio.ByteBuffer;
|
|||||||
for (int i = 0; i < outputChannels.length; i++) {
|
for (int i = 0; i < outputChannels.length; i++) {
|
||||||
int channelIndex = outputChannels[i];
|
int channelIndex = outputChannels[i];
|
||||||
if (channelIndex >= inputAudioFormat.channelCount) {
|
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);
|
active |= (channelIndex != i);
|
||||||
}
|
}
|
||||||
return active
|
return active
|
||||||
? new AudioFormat(inputAudioFormat.sampleRate, outputChannels.length, C.ENCODING_PCM_16BIT)
|
? new AudioFormat(
|
||||||
|
inputAudioFormat.sampleRate, outputChannels.length, inputAudioFormat.encoding)
|
||||||
: AudioFormat.NOT_SET;
|
: AudioFormat.NOT_SET;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,7 +89,17 @@ import java.nio.ByteBuffer;
|
|||||||
ByteBuffer buffer = replaceOutputBuffer(outputSize);
|
ByteBuffer buffer = replaceOutputBuffer(outputSize);
|
||||||
while (position < limit) {
|
while (position < limit) {
|
||||||
for (int channelIndex : outputChannels) {
|
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;
|
position += inputAudioFormat.bytesPerFrame;
|
||||||
}
|
}
|
||||||
|
@ -601,7 +601,8 @@ public final class DefaultAudioSink implements AudioSink {
|
|||||||
toIntPcmAvailableAudioProcessors =
|
toIntPcmAvailableAudioProcessors =
|
||||||
ImmutableList.of(
|
ImmutableList.of(
|
||||||
new ToInt16PcmAudioProcessor(), channelMappingAudioProcessor, trimmingAudioProcessor);
|
new ToInt16PcmAudioProcessor(), channelMappingAudioProcessor, trimmingAudioProcessor);
|
||||||
toFloatPcmAvailableAudioProcessors = ImmutableList.of(new ToFloatPcmAudioProcessor());
|
toFloatPcmAvailableAudioProcessors =
|
||||||
|
ImmutableList.of(new ToFloatPcmAudioProcessor(), channelMappingAudioProcessor);
|
||||||
volume = 1f;
|
volume = 1f;
|
||||||
audioSessionId = C.AUDIO_SESSION_ID_UNSET;
|
audioSessionId = C.AUDIO_SESSION_ID_UNSET;
|
||||||
auxEffectInfo = new AuxEffectInfo(AuxEffectInfo.NO_AUX_EFFECT_ID, 0f);
|
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