Skip to content
Open
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 @@ -77,6 +77,8 @@
([#3088](https://github.com/androidx/media/issues/3088)).
* MP3: Ignore Xing data length if it's longer than the known stream length
([#3117](https://github.com/androidx/media/issues/3117)).
* MP3: Adjust LAME/Xing encoder delay and padding metadata to match
decoded PCM trimming.
* Ignore `av1C` data with unsupported version.
* MP4: Add support for big-endian floating point PCM in `fpcm` boxes.
* Matroska: Parse chapter info to `Chapter` entries in a track's
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,8 @@ public void testPlayback_twoIdenticalMp3Files() throws Exception {
int bytesPerFrame = audioTrackListener.getAudioTrackOutputFormat().getFrameSizeInBytes();
int paddingBytes = max(0, playerAudioFormat.encoderPadding) * bytesPerFrame;
int delayBytes = max(0, playerAudioFormat.encoderDelay) * bytesPerFrame;
assertThat(paddingBytes).isEqualTo(2808);
assertThat(delayBytes).isEqualTo(1152);
assertThat(paddingBytes).isEqualTo(1750);
assertThat(delayBytes).isEqualTo(2210);

byte[] decoderOutputBytes = Bytes.concat(mp3Decoder.getAllOutputBytes().toArray(new byte[0][]));
int bytesPerAudioFile = decoderOutputBytes.length / 2;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@
/** Representation of a LAME Xing or Info frame. */
/* package */ final class XingFrame {

/**
* Offset between LAME/Xing delay/padding fields and decoded PCM trim samples. FFmpeg's MP3 muxer
* subtracts this offset when writing LAME metadata, and its demuxer adds it back when exposing
* decoded PCM skip/discard samples.
*/
private static final int LAME_TO_DECODED_PCM_TRIM_OFFSET_SAMPLES = 528 + 1;

/** The header of the Xing or Info frame. */
public final MpegAudioUtil.Header header;

Expand Down Expand Up @@ -113,7 +120,8 @@ public static XingFrame parse(MpegAudioUtil.Header mpegAudioHeader, ParsableByte
int bytesToSkipAfterReplayGain = 1 + 1;
// And account for values we parse, ReplayGain (8) and encoder delay & padding (3).
if (frame.bytesLeft() >= bytesToSkipBeforeReplayGain + 8 + bytesToSkipAfterReplayGain + 3) {
frame.skipBytes(bytesToSkipBeforeReplayGain);
String encoderIdentifier = frame.readString(9);
frame.skipBytes(bytesToSkipBeforeReplayGain - 9);
float peak = frame.readFloat();
int field1 = frame.readUnsignedShort();
int field2 = frame.readUnsignedShort();
Expand All @@ -123,6 +131,10 @@ public static XingFrame parse(MpegAudioUtil.Header mpegAudioHeader, ParsableByte
int encoderDelayAndPadding = frame.readUnsignedInt24();
encoderDelay = (encoderDelayAndPadding & 0xFFF000) >> 12;
encoderPadding = (encoderDelayAndPadding & 0xFFF);
if (usesLameGaplessEncoding(encoderIdentifier)) {
encoderDelay += LAME_TO_DECODED_PCM_TRIM_OFFSET_SAMPLES;
encoderPadding = Math.max(0, encoderPadding - LAME_TO_DECODED_PCM_TRIM_OFFSET_SAMPLES);
}
} else {
replayGain = null;
encoderDelay = C.LENGTH_UNSET;
Expand Down Expand Up @@ -155,6 +167,12 @@ public long computeDurationUs() {
(frameCount * header.samplesPerFrame) - 1, header.sampleRate);
}

private static boolean usesLameGaplessEncoding(String encoderIdentifier) {
return encoderIdentifier.startsWith("LAME")
|| encoderIdentifier.startsWith("Lavf")
|| encoderIdentifier.startsWith("Lavc");
}

/** Provide the metadata derived from this Xing frame, such as ReplayGain data. */
@Nullable
public Metadata getMetadata() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
* Copyright 2026 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.extractor.mp3;

import static com.google.common.truth.Truth.assertThat;

import androidx.media3.common.util.ParsableByteArray;
import androidx.media3.common.util.Util;
import androidx.media3.extractor.MpegAudioUtil;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import java.nio.ByteBuffer;
import org.junit.Test;
import org.junit.runner.RunWith;

/** Tests for {@link XingFrame}. */
@RunWith(AndroidJUnit4.class)
public final class XingFrameTest {

private static final int INFO_FRAME_HEADER_DATA = 0xFFFB40C0;

@Test
public void parse_withLameEncoderIdentifier_adjustsDelayAndPaddingForDecodedPcm() {
XingFrame frame =
createXingFrame(
INFO_FRAME_HEADER_DATA,
/* frameCount= */ 40,
/* dataSize= */ 8_541,
/* encoderDelay= */ 576,
/* encoderPadding= */ 1_404,
/* encoderIdentifier= */ "LAME3.99r");

assertThat(frame.encoderDelay).isEqualTo(1_105);
assertThat(frame.encoderPadding).isEqualTo(875);
}

@Test
public void parse_withLameEncoderIdentifierAndSmallPadding_clampsPaddingToZero() {
XingFrame frame =
createXingFrame(
INFO_FRAME_HEADER_DATA,
/* frameCount= */ 40,
/* dataSize= */ 8_541,
/* encoderDelay= */ 576,
/* encoderPadding= */ 398,
/* encoderIdentifier= */ "LAME3.99r");

assertThat(frame.encoderDelay).isEqualTo(1_105);
assertThat(frame.encoderPadding).isEqualTo(0);
}

private static XingFrame createXingFrame(
int headerData, int frameCount, int dataSize, int encoderDelay, int encoderPadding) {
return createXingFrame(
headerData,
frameCount,
dataSize,
encoderDelay,
encoderPadding,
/* encoderIdentifier= */ "");
}

private static XingFrame createXingFrame(
int headerData,
int frameCount,
int dataSize,
int encoderDelay,
int encoderPadding,
String encoderIdentifier) {
int encoderDelayAndPadding = (encoderDelay << 12) | encoderPadding;
ByteBuffer payload = ByteBuffer.allocate(4 + 4 + 4 + 11 + 8 + 2 + 3);
payload.putInt(0x03);
payload.putInt(frameCount);
payload.putInt(dataSize);
payload.put(createFixedLengthEncoderIdentifier(encoderIdentifier));
payload.position(payload.position() + 2 + 8 + 2);
payload.put((byte) (encoderDelayAndPadding >> 16));
payload.put((byte) (encoderDelayAndPadding >> 8));
payload.put((byte) encoderDelayAndPadding);
return createXingFrame(headerData, payload.array());
}

private static byte[] createFixedLengthEncoderIdentifier(String encoderIdentifier) {
byte[] fixedLengthIdentifier = new byte[9];
byte[] identifierBytes = Util.getUtf8Bytes(encoderIdentifier);
System.arraycopy(
identifierBytes,
/* srcPos= */ 0,
fixedLengthIdentifier,
/* destPos= */ 0,
Math.min(identifierBytes.length, fixedLengthIdentifier.length));
return fixedLengthIdentifier;
}

private static XingFrame createXingFrame(int headerData, byte[] payload) {
MpegAudioUtil.Header xingFrameHeader = new MpegAudioUtil.Header();
xingFrameHeader.setForHeaderData(headerData);
return XingFrame.parse(xingFrameHeader, new ParsableByteArray(payload));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ track 0:
maxInputSize = 4096
channelCount = 2
sampleRate = 48000
encoderDelay = 576
encoderPadding = 576
encoderDelay = 1105
encoderPadding = 47
metadata = entries=[TIT2: description=null: values=[Test title], TPE1: description=null: values=[Test Artist], TALB: description=null: values=[Test Album], TXXX: description=Test description: values=[Test user info], COMM: language=eng, description=Test description, text=Test comment, WXXX: url=Test URL, TSSE: description=null: values=[Lavf58.29.100], MLLT, PRIV: owner=test@gmail.com, UNKN, GEOB: mimeType=test/mime, filename=Testfilename.txt, description=Test description, CHAP, CHAP, CTOC, APIC: mimeType=image/jpeg, description=Test description, TCON: description=null: values=[9]]
sample 0:
time = 0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ track 0:
maxInputSize = 4096
channelCount = 2
sampleRate = 48000
encoderDelay = 576
encoderPadding = 576
encoderDelay = 1105
encoderPadding = 47
metadata = entries=[TIT2: description=null: values=[Test title], TPE1: description=null: values=[Test Artist], TALB: description=null: values=[Test Album], TXXX: description=Test description: values=[Test user info], COMM: language=eng, description=Test description, text=Test comment, WXXX: url=Test URL, TSSE: description=null: values=[Lavf58.29.100], MLLT, PRIV: owner=test@gmail.com, UNKN, GEOB: mimeType=test/mime, filename=Testfilename.txt, description=Test description, CHAP, CHAP, CTOC, APIC: mimeType=image/jpeg, description=Test description, TCON: description=null: values=[9]]
sample 0:
time = 943000
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ track 0:
maxInputSize = 4096
channelCount = 2
sampleRate = 48000
encoderDelay = 576
encoderPadding = 576
encoderDelay = 1105
encoderPadding = 47
metadata = entries=[TIT2: description=null: values=[Test title], TPE1: description=null: values=[Test Artist], TALB: description=null: values=[Test Album], TXXX: description=Test description: values=[Test user info], COMM: language=eng, description=Test description, text=Test comment, WXXX: url=Test URL, TSSE: description=null: values=[Lavf58.29.100], MLLT, PRIV: owner=test@gmail.com, UNKN, GEOB: mimeType=test/mime, filename=Testfilename.txt, description=Test description, CHAP, CHAP, CTOC, APIC: mimeType=image/jpeg, description=Test description, TCON: description=null: values=[9]]
sample 0:
time = 1879000
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ track 0:
maxInputSize = 4096
channelCount = 2
sampleRate = 48000
encoderDelay = 576
encoderPadding = 576
encoderDelay = 1105
encoderPadding = 47
metadata = entries=[TIT2: description=null: values=[Test title], TPE1: description=null: values=[Test Artist], TALB: description=null: values=[Test Album], TXXX: description=Test description: values=[Test user info], COMM: language=eng, description=Test description, text=Test comment, WXXX: url=Test URL, TSSE: description=null: values=[Lavf58.29.100], MLLT, PRIV: owner=test@gmail.com, UNKN, GEOB: mimeType=test/mime, filename=Testfilename.txt, description=Test description, CHAP, CHAP, CTOC, APIC: mimeType=image/jpeg, description=Test description, TCON: description=null: values=[9]]
tracksEnded = true
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ track 0:
maxInputSize = 4096
channelCount = 2
sampleRate = 48000
encoderDelay = 576
encoderPadding = 576
encoderDelay = 1105
encoderPadding = 47
metadata = entries=[TIT2: description=null: values=[Test title], TPE1: description=null: values=[Test Artist], TALB: description=null: values=[Test Album], TXXX: description=Test description: values=[Test user info], COMM: language=eng, description=Test description, text=Test comment, WXXX: url=Test URL, TSSE: description=null: values=[Lavf58.29.100], MLLT, PRIV: owner=test@gmail.com, UNKN, GEOB: mimeType=test/mime, filename=Testfilename.txt, description=Test description, CHAP, CHAP, CTOC, APIC: mimeType=image/jpeg, description=Test description, TCON: description=null: values=[9]]
sample 0:
time = 0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ track 0:
maxInputSize = 4096
channelCount = 2
sampleRate = 48000
encoderDelay = 576
encoderPadding = 576
encoderDelay = 1105
encoderPadding = 47
sample 0:
time = 0
flags = 1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ track 0:
maxInputSize = 4096
channelCount = 2
sampleRate = 48000
encoderDelay = 576
encoderPadding = 576
encoderDelay = 1105
encoderPadding = 47
sample 0:
time = 943000
flags = 1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ track 0:
maxInputSize = 4096
channelCount = 2
sampleRate = 48000
encoderDelay = 576
encoderPadding = 576
encoderDelay = 1105
encoderPadding = 47
sample 0:
time = 1879000
flags = 1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@ track 0:
maxInputSize = 4096
channelCount = 2
sampleRate = 48000
encoderDelay = 576
encoderPadding = 576
encoderDelay = 1105
encoderPadding = 47
tracksEnded = true
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ track 0:
maxInputSize = 4096
channelCount = 2
sampleRate = 48000
encoderDelay = 576
encoderPadding = 576
encoderDelay = 1105
encoderPadding = 47
sample 0:
time = 0
flags = 1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ track 0:
maxInputSize = 4096
channelCount = 2
sampleRate = 48000
encoderDelay = 576
encoderPadding = 576
encoderDelay = 1105
encoderPadding = 47
metadata = entries=[TIT2: description=null: values=[Test title], TPE1: description=null: values=[Test Artist], TALB: description=null: values=[Test Album], TXXX: description=Test description: values=[Test user info], COMM: language=eng, description=Test description, text=Test comment, WXXX: url=Test URL, TSSE: description=null: values=[Lavf58.29.100], MLLT, PRIV: owner=test@gmail.com, UNKN, GEOB: mimeType=test/mime, filename=Testfilename.txt, description=Test description, CHAP, CHAP, CTOC, APIC: mimeType=image/jpeg, description=Test description, TCON: description=null: values=[Gorpcore]]
sample 0:
time = 0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ track 0:
maxInputSize = 4096
channelCount = 2
sampleRate = 48000
encoderDelay = 576
encoderPadding = 576
encoderDelay = 1105
encoderPadding = 47
metadata = entries=[TIT2: description=null: values=[Test title], TPE1: description=null: values=[Test Artist], TALB: description=null: values=[Test Album], TXXX: description=Test description: values=[Test user info], COMM: language=eng, description=Test description, text=Test comment, WXXX: url=Test URL, TSSE: description=null: values=[Lavf58.29.100], MLLT, PRIV: owner=test@gmail.com, UNKN, GEOB: mimeType=test/mime, filename=Testfilename.txt, description=Test description, CHAP, CHAP, CTOC, APIC: mimeType=image/jpeg, description=Test description, TCON: description=null: values=[Gorpcore]]
sample 0:
time = 943000
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ track 0:
maxInputSize = 4096
channelCount = 2
sampleRate = 48000
encoderDelay = 576
encoderPadding = 576
encoderDelay = 1105
encoderPadding = 47
metadata = entries=[TIT2: description=null: values=[Test title], TPE1: description=null: values=[Test Artist], TALB: description=null: values=[Test Album], TXXX: description=Test description: values=[Test user info], COMM: language=eng, description=Test description, text=Test comment, WXXX: url=Test URL, TSSE: description=null: values=[Lavf58.29.100], MLLT, PRIV: owner=test@gmail.com, UNKN, GEOB: mimeType=test/mime, filename=Testfilename.txt, description=Test description, CHAP, CHAP, CTOC, APIC: mimeType=image/jpeg, description=Test description, TCON: description=null: values=[Gorpcore]]
sample 0:
time = 1879000
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ track 0:
maxInputSize = 4096
channelCount = 2
sampleRate = 48000
encoderDelay = 576
encoderPadding = 576
encoderDelay = 1105
encoderPadding = 47
metadata = entries=[TIT2: description=null: values=[Test title], TPE1: description=null: values=[Test Artist], TALB: description=null: values=[Test Album], TXXX: description=Test description: values=[Test user info], COMM: language=eng, description=Test description, text=Test comment, WXXX: url=Test URL, TSSE: description=null: values=[Lavf58.29.100], MLLT, PRIV: owner=test@gmail.com, UNKN, GEOB: mimeType=test/mime, filename=Testfilename.txt, description=Test description, CHAP, CHAP, CTOC, APIC: mimeType=image/jpeg, description=Test description, TCON: description=null: values=[Gorpcore]]
tracksEnded = true
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ track 0:
maxInputSize = 4096
channelCount = 2
sampleRate = 48000
encoderDelay = 576
encoderPadding = 576
encoderDelay = 1105
encoderPadding = 47
metadata = entries=[TIT2: description=null: values=[Test title], TPE1: description=null: values=[Test Artist], TALB: description=null: values=[Test Album], TXXX: description=Test description: values=[Test user info], COMM: language=eng, description=Test description, text=Test comment, WXXX: url=Test URL, TSSE: description=null: values=[Lavf58.29.100], MLLT, PRIV: owner=test@gmail.com, UNKN, GEOB: mimeType=test/mime, filename=Testfilename.txt, description=Test description, CHAP, CHAP, CTOC, APIC: mimeType=image/jpeg, description=Test description, TCON: description=null: values=[Gorpcore]]
sample 0:
time = 0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ track 0:
maxInputSize = 4096
channelCount = 2
sampleRate = 48000
encoderDelay = 576
encoderPadding = 576
encoderDelay = 1105
encoderPadding = 47
metadata = entries=[TSSE: description=null: values=[Lavf58.29.100]]
sample 0:
time = 0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ track 0:
maxInputSize = 4096
channelCount = 2
sampleRate = 48000
encoderDelay = 576
encoderPadding = 576
encoderDelay = 1105
encoderPadding = 47
metadata = entries=[TSSE: description=null: values=[Lavf58.29.100]]
sample 0:
time = 0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ track 0:
maxInputSize = 4096
channelCount = 2
sampleRate = 48000
encoderDelay = 576
encoderPadding = 576
encoderDelay = 1105
encoderPadding = 47
metadata = entries=[TSSE: description=null: values=[Lavf58.29.100]]
sample 0:
time = 943055
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ track 0:
maxInputSize = 4096
channelCount = 2
sampleRate = 48000
encoderDelay = 576
encoderPadding = 576
encoderDelay = 1105
encoderPadding = 47
metadata = entries=[TSSE: description=null: values=[Lavf58.29.100]]
sample 0:
time = 1879045
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ track 0:
maxInputSize = 4096
channelCount = 2
sampleRate = 48000
encoderDelay = 576
encoderPadding = 576
encoderDelay = 1105
encoderPadding = 47
metadata = entries=[TSSE: description=null: values=[Lavf58.29.100]]
tracksEnded = true
Loading