Skip to content

expand format support for audio processors used by DefaultAudioSink #2259

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions RELEASENOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
* Extractors:
* DataSource:
* Audio:
* Add support for all linear PCM sample formats in
`ChannelMappingAudioProcessor` and `TrimmingAudioProcessor`.
* Video:
* Improve smooth video frame release at startup when audio samples don't
start at exactly the requested position.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3459,6 +3459,50 @@ public static long getNowUnixTimeMs(long elapsedRealtimeEpochOffsetMs) {
: SystemClock.elapsedRealtime() + elapsedRealtimeEpochOffsetMs;
}

/**
* Returns the sign-extended 24-bit integer value at {@code index}.
*
* @param buffer The buffer from which to read the 24-bit integer.
* @param index The index of the 24-bit integer.
*/
@UnstableApi
public static int getInt24(ByteBuffer buffer, int index) {
byte component1 = buffer.get(buffer.order() == ByteOrder.BIG_ENDIAN ? index : index + 2);
byte component2 = buffer.get(index + 1);
byte component3 = buffer.get(buffer.order() == ByteOrder.BIG_ENDIAN ? index + 2 : index);
return ((component1 << 24) & 0xff000000
| (component2 << 16) & 0xff0000
| (component3 << 8) & 0xff00)
>> 8;
}

/**
* Writes a 24-bit integer value to a buffer at its current {@link ByteBuffer#position()}.
*
* <p>This is a relative operation that affects the buffer's position.
*
* @param buffer The buffer on which to write the integer.
* @param value The integer value to write.
* @throws IllegalArgumentException If {@code value} is out of range for a 24-bit integer.
*/
@UnstableApi
public static void putInt24(ByteBuffer buffer, int value) {
checkArgument(
(value & ~0xffffff) == 0 || (value & ~0x7fffff) == 0xff800000,
"Value out of range of 24-bit integer: " + Integer.toHexString(value));
checkArgument(buffer.remaining() >= 3);
byte component1 =
buffer.order() == ByteOrder.BIG_ENDIAN
? (byte) ((value & 0xFF0000) >> 16)
: (byte) (value & 0xFF);
byte component2 = (byte) ((value & 0xFF00) >> 8);
byte component3 =
buffer.order() == ByteOrder.BIG_ENDIAN
? (byte) (value & 0xFF)
: (byte) ((value & 0xFF0000) >> 16);
buffer.put(component1).put(component2).put(component3);
}

/**
* Moves the elements starting at {@code fromIndex} to {@code newFromIndex}.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@
import static androidx.media3.common.util.Util.contentHashCode;
import static androidx.media3.common.util.Util.escapeFileName;
import static androidx.media3.common.util.Util.getCodecsOfType;
import static androidx.media3.common.util.Util.getInt24;
import static androidx.media3.common.util.Util.getStringForTime;
import static androidx.media3.common.util.Util.gzip;
import static androidx.media3.common.util.Util.maxValue;
import static androidx.media3.common.util.Util.minValue;
import static androidx.media3.common.util.Util.parseXsDateTime;
import static androidx.media3.common.util.Util.parseXsDuration;
import static androidx.media3.common.util.Util.percentFloat;
import static androidx.media3.common.util.Util.putInt24;
import static androidx.media3.common.util.Util.unescapeFileName;
import static androidx.media3.test.utils.TestUtil.buildTestData;
import static androidx.media3.test.utils.TestUtil.buildTestString;
Expand Down Expand Up @@ -1677,6 +1679,58 @@ public void contentHashCode_sparseArraysWithDifferentContent_returnsDifferentCon
assertThat(contentHashCode(sparseArray1)).isNotEqualTo(contentHashCode(sparseArray2));
}

@Test
public void putInt24_withOutOfRangeInts_throws() {
ByteBuffer buffer = ByteBuffer.allocateDirect(6).order(ByteOrder.LITTLE_ENDIAN);
assertThrows(IllegalArgumentException.class, () -> putInt24(buffer, 0xFF000000));
assertThrows(IllegalArgumentException.class, () -> putInt24(buffer, 0x8FFFFFFF));
assertThrows(IllegalArgumentException.class, () -> putInt24(buffer, 0x01FFFFFF));
}

@Test
public void putInt24_littleEndianBuffer_respectsOrdering() {
ByteBuffer buffer = ByteBuffer.allocateDirect(6).order(ByteOrder.LITTLE_ENDIAN);
putInt24(buffer, -1);
putInt24(buffer, 0x123456);
buffer.rewind();
assertThat(createByteArray(buffer))
.isEqualTo(new byte[] {(byte) 0xff, (byte) 0xff, (byte) 0xff, 0x56, 0x34, 0x12});
}

@Test
public void getInt24_littleEndianBuffer_returnsExpectedValues() {
ByteBuffer buffer = ByteBuffer.allocateDirect(9).order(ByteOrder.LITTLE_ENDIAN);
buffer.put(
new byte[] {
(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, 0x56, 0x34, 0x12, 0x01, 0x00, (byte) 0xFF
});
assertThat(getInt24(buffer, 0)).isEqualTo(-1);
assertThat(getInt24(buffer, 3)).isEqualTo(0x00123456);
assertThat(getInt24(buffer, 6)).isEqualTo(0xFFFF0001);
}

@Test
public void putInt24_bigEndianBuffer_respectsOrdering() {
ByteBuffer buffer = ByteBuffer.allocateDirect(6).order(ByteOrder.BIG_ENDIAN);
putInt24(buffer, -1);
putInt24(buffer, 0x123456);
buffer.rewind();
assertThat(createByteArray(buffer))
.isEqualTo(new byte[] {(byte) 0xff, (byte) 0xff, (byte) 0xff, 0x12, 0x34, 0x56});
}

@Test
public void getInt24_bigEndianBuffer_returnsExpectedValues() {
ByteBuffer buffer = ByteBuffer.allocateDirect(9).order(ByteOrder.BIG_ENDIAN);
buffer.put(
new byte[] {
(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, 0x12, 0x34, 0x56, (byte) 0xFF, 0x00, 0x01
});
assertThat(getInt24(buffer, 0)).isEqualTo(-1);
assertThat(getInt24(buffer, 3)).isEqualTo(0x00123456);
assertThat(getInt24(buffer, 6)).isEqualTo(0xFFFF0001);
}

private static void assertEscapeUnescapeFileName(String fileName, String escapedFileName) {
assertThat(escapeFileName(fileName)).isEqualTo(escapedFileName);
assertThat(unescapeFileName(escapedFileName)).isEqualTo(fileName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
package androidx.media3.exoplayer.audio;

import static androidx.media3.common.util.Util.getByteDepth;
import static androidx.media3.common.util.Util.getInt24;
import static androidx.media3.common.util.Util.isEncodingLinearPcm;
import static androidx.media3.common.util.Util.putInt24;

import androidx.annotation.Nullable;
import androidx.media3.common.C;
Expand Down Expand Up @@ -58,8 +61,7 @@ public AudioFormat onConfigure(AudioFormat inputAudioFormat)
return AudioFormat.NOT_SET;
}

if (inputAudioFormat.encoding != C.ENCODING_PCM_16BIT
&& inputAudioFormat.encoding != C.ENCODING_PCM_FLOAT) {
if (!isEncodingLinearPcm(inputAudioFormat.encoding)) {
throw new UnhandledAudioFormatException(inputAudioFormat);
}

Expand Down Expand Up @@ -93,9 +95,21 @@ public void queueInput(ByteBuffer inputBuffer) {
for (int channelIndex : outputChannels) {
int inputIndex = position + getByteDepth(inputAudioFormat.encoding) * channelIndex;
switch (inputAudioFormat.encoding) {
case C.ENCODING_PCM_8BIT:
buffer.put(inputBuffer.get(inputIndex));
break;
case C.ENCODING_PCM_16BIT:
case C.ENCODING_PCM_16BIT_BIG_ENDIAN:
buffer.putShort(inputBuffer.getShort(inputIndex));
break;
case C.ENCODING_PCM_24BIT:
case C.ENCODING_PCM_24BIT_BIG_ENDIAN:
putInt24(buffer, getInt24(inputBuffer, inputIndex));
break;
case C.ENCODING_PCM_32BIT:
case C.ENCODING_PCM_32BIT_BIG_ENDIAN:
buffer.putInt(inputBuffer.getInt(inputIndex));
break;
case C.ENCODING_PCM_FLOAT:
buffer.putFloat(inputBuffer.getFloat(inputIndex));
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -538,8 +538,9 @@ public DefaultAudioSink build() {
private final boolean enableFloatOutput;
private final ChannelMappingAudioProcessor channelMappingAudioProcessor;
private final TrimmingAudioProcessor trimmingAudioProcessor;
private final ImmutableList<AudioProcessor> toIntPcmAvailableAudioProcessors;
private final ImmutableList<AudioProcessor> toFloatPcmAvailableAudioProcessors;
private final ToInt16PcmAudioProcessor toInt16PcmAudioProcessor;
private final ToFloatPcmAudioProcessor toFloatPcmAudioProcessor;
private final ImmutableList<AudioProcessor> availableAudioProcessors;
private final AudioTrackPositionTracker audioTrackPositionTracker;
private final ArrayDeque<MediaPositionParameters> mediaPositionParametersCheckpoints;
private final boolean preferAudioTrackPlaybackParams;
Expand Down Expand Up @@ -620,12 +621,10 @@ private DefaultAudioSink(Builder builder) {
audioTrackPositionTracker = new AudioTrackPositionTracker(new PositionTrackerListener());
channelMappingAudioProcessor = new ChannelMappingAudioProcessor();
trimmingAudioProcessor = new TrimmingAudioProcessor();
toIntPcmAvailableAudioProcessors =
ImmutableList.of(
new ToInt16PcmAudioProcessor(), channelMappingAudioProcessor, trimmingAudioProcessor);
toFloatPcmAvailableAudioProcessors =
ImmutableList.of(
new ToFloatPcmAudioProcessor(), channelMappingAudioProcessor, trimmingAudioProcessor);
toInt16PcmAudioProcessor = new ToInt16PcmAudioProcessor();
toFloatPcmAudioProcessor = new ToFloatPcmAudioProcessor();
availableAudioProcessors =
ImmutableList.of(trimmingAudioProcessor, channelMappingAudioProcessor);
volume = 1f;
audioSessionId = C.AUDIO_SESSION_ID_UNSET;
auxEffectInfo = new AuxEffectInfo(AuxEffectInfo.NO_AUX_EFFECT_ID, 0f);
Expand Down Expand Up @@ -727,10 +726,11 @@ public void configure(Format inputFormat, int specifiedBufferSize, @Nullable int
inputPcmFrameSize = Util.getPcmFrameSize(inputFormat.pcmEncoding, inputFormat.channelCount);

ImmutableList.Builder<AudioProcessor> pipelineProcessors = new ImmutableList.Builder<>();
pipelineProcessors.addAll(availableAudioProcessors);
if (shouldUseFloatOutput(inputFormat.pcmEncoding)) {
pipelineProcessors.addAll(toFloatPcmAvailableAudioProcessors);
pipelineProcessors.add(toFloatPcmAudioProcessor);
} else {
pipelineProcessors.addAll(toIntPcmAvailableAudioProcessors);
pipelineProcessors.add(toInt16PcmAudioProcessor);
pipelineProcessors.add(audioProcessorChain.getAudioProcessors());
}
audioProcessingPipeline = new AudioProcessingPipeline(pipelineProcessors.build());
Expand Down Expand Up @@ -1619,12 +1619,12 @@ public void flush() {
@Override
public void reset() {
flush();
for (AudioProcessor audioProcessor : toIntPcmAvailableAudioProcessors) {
audioProcessor.reset();
}
for (AudioProcessor audioProcessor : toFloatPcmAvailableAudioProcessors) {
for (AudioProcessor audioProcessor : availableAudioProcessors) {
audioProcessor.reset();
}
toInt16PcmAudioProcessor.reset();
toFloatPcmAudioProcessor.reset();

if (audioProcessingPipeline != null) {
audioProcessingPipeline.reset();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
*/
package androidx.media3.exoplayer.audio;

import static androidx.media3.common.util.Util.isEncodingLinearPcm;
import static java.lang.Math.min;

import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.audio.BaseAudioProcessor;
import androidx.media3.common.util.UnstableApi;
Expand Down Expand Up @@ -80,8 +80,7 @@ public long getDurationAfterProcessorApplied(long durationUs) {
@Override
public AudioFormat onConfigure(AudioFormat inputAudioFormat)
throws UnhandledAudioFormatException {
if (inputAudioFormat.encoding != C.ENCODING_PCM_16BIT
&& inputAudioFormat.encoding != C.ENCODING_PCM_FLOAT) {
if (!isEncodingLinearPcm(inputAudioFormat.encoding)) {
throw new UnhandledAudioFormatException(inputAudioFormat);
}
reconfigurationPending = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,12 @@
*/
package androidx.media3.exoplayer.audio;

import static androidx.media3.test.utils.TestUtil.createByteArray;
import static androidx.media3.test.utils.TestUtil.createByteBuffer;
import static androidx.media3.test.utils.TestUtil.createFloatArray;
import static androidx.media3.test.utils.TestUtil.createInt24Array;
import static androidx.media3.test.utils.TestUtil.createInt24ByteBuffer;
import static androidx.media3.test.utils.TestUtil.createIntArray;
import static androidx.media3.test.utils.TestUtil.createShortArray;
import static com.google.common.truth.Truth.assertThat;

Expand All @@ -36,10 +40,22 @@ public class ChannelMappingAudioProcessorTest {
new AudioFormat(
/* sampleRate= */ 44100, /* channelCount= */ 3, /* encoding= */ C.ENCODING_PCM_FLOAT);

private static final AudioFormat PCM_32BIT_STEREO_FORMAT =
new AudioFormat(
/* sampleRate= */ 44100, /* channelCount= */ 2, /* encoding= */ C.ENCODING_PCM_32BIT);

private static final AudioFormat PCM_24BIT_STEREO_FORMAT =
new AudioFormat(
/* sampleRate= */ 44100, /* channelCount= */ 2, /* encoding= */ C.ENCODING_PCM_24BIT);

private static final AudioFormat PCM_16BIT_STEREO_FORMAT =
new AudioFormat(
/* sampleRate= */ 44100, /* channelCount= */ 2, /* encoding= */ C.ENCODING_PCM_16BIT);

private static final AudioFormat PCM_8BIT_STEREO_FORMAT =
new AudioFormat(
/* sampleRate= */ 44100, /* channelCount= */ 2, /* encoding= */ C.ENCODING_PCM_8BIT);

@Test
public void channelMap_withPcmFloatSamples_mapsOutputCorrectly()
throws AudioProcessor.UnhandledAudioFormatException {
Expand All @@ -53,6 +69,32 @@ public void channelMap_withPcmFloatSamples_mapsOutputCorrectly()
assertThat(output).isEqualTo(new float[] {3f, 2f, 1f, 6f, 5f, 4f});
}

@Test
public void channelMap_withPcm32Samples_mapsOutputCorrectly()
throws AudioProcessor.UnhandledAudioFormatException {
ChannelMappingAudioProcessor processor = new ChannelMappingAudioProcessor();
processor.setChannelMap(new int[] {1, 0});
processor.configure(PCM_32BIT_STEREO_FORMAT);
processor.flush();

processor.queueInput(createByteBuffer(new int[] {1, 2, 3, 4, 5, 6}));
int[] output = createIntArray(processor.getOutput());
assertThat(output).isEqualTo(new int[] {2, 1, 4, 3, 6, 5});
}

@Test
public void channelMap_withPcm24Samples_mapsOutputCorrectly()
throws AudioProcessor.UnhandledAudioFormatException {
ChannelMappingAudioProcessor processor = new ChannelMappingAudioProcessor();
processor.setChannelMap(new int[] {1, 0});
processor.configure(PCM_24BIT_STEREO_FORMAT);
processor.flush();

processor.queueInput(createInt24ByteBuffer(new int[] {0xff0001, 0x00ff02, 0x0300ff, 0x40ff00}));
int[] output = createInt24Array(processor.getOutput());
assertThat(output).isEqualTo(new int[] {0x00ff02, 0xffff0001, 0x40ff00, 0x0300ff});
}

@Test
public void channelMap_withPcm16Samples_mapsOutputCorrectly()
throws AudioProcessor.UnhandledAudioFormatException {
Expand All @@ -66,6 +108,19 @@ public void channelMap_withPcm16Samples_mapsOutputCorrectly()
assertThat(output).isEqualTo(new short[] {2, 1, 4, 3, 6, 5});
}

@Test
public void channelMap_withPcm8Samples_mapsOutputCorrectly()
throws AudioProcessor.UnhandledAudioFormatException {
ChannelMappingAudioProcessor processor = new ChannelMappingAudioProcessor();
processor.setChannelMap(new int[] {1, 0});
processor.configure(PCM_8BIT_STEREO_FORMAT);
processor.flush();

processor.queueInput(createByteBuffer(new byte[] {1, 2, 3, 4, 5, 6}));
byte[] output = createByteArray(processor.getOutput());
assertThat(output).isEqualTo(new byte[] {2, 1, 4, 3, 6, 5});
}

@Test
public void channelMap_withMoreOutputChannels_duplicatesSamples()
throws AudioProcessor.UnhandledAudioFormatException {
Expand Down
Loading