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
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ public final class Mp3Extractor implements Extractor {
private int synchronizedHeaderData;

@Nullable private Metadata metadata;
@Nullable private Metadata infoMetadata;
private long basisTimeUs;
private long samplesRead;
private long firstSamplePosition;
Expand Down Expand Up @@ -289,6 +290,12 @@ private int readInternal(ExtractorInput input) throws IOException {
if (seeker == null) {
seeker = computeSeeker(input);
extractorOutput.seekMap(seeker);
@Nullable Metadata finalMetadata = (flags & FLAG_DISABLE_ID3_METADATA) != 0 ? null : metadata;
if (finalMetadata != null) {
finalMetadata = finalMetadata.copyWithAppendedEntriesFrom(infoMetadata);
} else {
finalMetadata = infoMetadata;
}
Format.Builder format =
new Format.Builder()
.setContainerMimeType(MimeTypes.AUDIO_MPEG)
Expand All @@ -298,7 +305,7 @@ private int readInternal(ExtractorInput input) throws IOException {
.setSampleRate(synchronizedHeader.sampleRate)
.setEncoderDelay(gaplessInfoHolder.encoderDelay)
.setEncoderPadding(gaplessInfoHolder.encoderPadding)
.setMetadata((flags & FLAG_DISABLE_ID3_METADATA) != 0 ? null : metadata);
.setMetadata(finalMetadata);
if (seeker.getAverageBitrate() != C.RATE_UNSET_INT) {
format.setAverageBitrate(seeker.getAverageBitrate());
}
Expand Down Expand Up @@ -579,6 +586,7 @@ private Seeker maybeReadSeekFrame(ExtractorInput input) throws IOException {
gaplessInfoHolder.encoderDelay = xingFrame.encoderDelay;
gaplessInfoHolder.encoderPadding = xingFrame.encoderPadding;
}
infoMetadata = xingFrame.getMetadata();
long startPosition = input.getPosition();
if (input.getLength() != C.LENGTH_UNSET
&& xingFrame.dataSize != C.LENGTH_UNSET
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/*
* 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.extractor.mp3;

import androidx.media3.common.Metadata;
import androidx.media3.common.util.UnstableApi;
import java.util.Objects;

/** Representation of the ReplayGain data stored in a LAME Xing or Info frame. */
@UnstableApi
public final class Mp3InfoReplayGain implements Metadata.Entry {
/**
* 32 bit floating point "Peak signal amplitude".
*
* <p>1.0 is maximal signal amplitude store-able in decoding format. 0.8 is 80% of maximal signal
* amplitude store-able in decoding format. 1.5 is 150% of maximal signal amplitude store-able in
* decoding format.
*
* <p>A value above 1.0 can occur for example due to "true peak" measurement. A value of 0.0 means
* the peak signal amplitude is unknown.
*/
public final float peak;

/**
* NAME of Gain adjustment in the first field, also called "Radio Replay Gain" field:
*
* <p>b000 = not set
*
* <p>b001 = radio
*
* <p>b010 = audiophile
*/
public final byte field1Name;

/**
* ORIGINATOR of Gain adjustment in the first field, also called "Radio Replay Gain" field:
*
* <p>b000 = not set
*
* <p>b001 = set by artist
*
* <p>b010 = set by user
*
* <p>b011 = set by ReplayGain model
*
* <p>b100 = set by simple RMS average
*/
public final byte field1Originator;

/**
* Absolute gain adjustment in the first field, also called "Radio Replay Gain" field.
*
* <p>Stored in the header with 1 decimal of precision by being multiplied by 10; this field is
* already divided by 10 again.
*/
public final float field1Value;

/**
* NAME of Gain adjustment in the second field, also called "Audiophile Replay Gain" field:
*
* <p>b000 = not set
*
* <p>b001 = radio
*
* <p>b010 = audiophile
*/
public final byte field2Name;

/**
* ORIGINATOR of Gain adjustment in the second field, also called "Audiophile Replay Gain" field:
*
* <p>b000 = not set
*
* <p>b001 = set by artist
*
* <p>b010 = set by user
*
* <p>b011 = set by ReplayGain model
*
* <p>b100 = set by simple RMS average
*/
public final byte field2Originator;

/**
* Absolute gain adjustment in the second field, also called "Audiophile Replay Gain" field.
*
* <p>Stored in the header with 1 decimal of precision by being multiplied by 10; this field is
* already divided by 10 again.
*/
public final float field2Value;

/* package */ Mp3InfoReplayGain(XingFrame frame) {
this.peak = frame.replayGainPeak;
this.field1Name = frame.replayGainField1Name;
this.field1Originator = frame.replayGainField1Originator;
this.field1Value = frame.replayGainField1Value;
this.field2Name = frame.replayGainField2Name;
this.field2Originator = frame.replayGainField2Originator;
this.field2Value = frame.replayGainField2Value;
}

@Override
public String toString() {
return "ReplayGain Xing/Info: "
+ "peak="
+ peak
+ ", f1 name="
+ field1Name
+ ", f1 orig="
+ field1Originator
+ ", f1 val="
+ field1Value
+ ", f2 name="
+ field2Name
+ ", f2 orig="
+ field2Originator
+ ", f2 val="
+ field2Value;
}

@Override
public boolean equals(Object o) {
if (!(o instanceof Mp3InfoReplayGain)) return false;
Mp3InfoReplayGain that = (Mp3InfoReplayGain) o;
return Float.compare(peak, that.peak) == 0
&& field1Name == that.field1Name
&& field1Originator == that.field1Originator
&& Float.compare(field1Value, that.field1Value) == 0
&& field2Name == that.field2Name
&& field2Originator == that.field2Originator
&& Float.compare(field2Value, that.field2Value) == 0;
}

@Override
public int hashCode() {
return Objects.hash(
peak, field1Name, field1Originator, field1Value, field2Name, field2Originator, field2Value);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.Metadata;
import androidx.media3.common.util.ParsableByteArray;
import androidx.media3.common.util.Util;
import androidx.media3.extractor.MpegAudioUtil;
Expand All @@ -35,6 +36,44 @@
*/
public final long dataSize;

/** Whether this frame is the LAME variant of a Xing frame, and hence has ReplayGain data. */
public final boolean hasReplayGain;

/**
* @see Mp3InfoReplayGain#peak
*/
public final float replayGainPeak;

/**
* @see Mp3InfoReplayGain#field1Name
*/
public final byte replayGainField1Name;

/**
* @see Mp3InfoReplayGain#field1Originator
*/
public final byte replayGainField1Originator;

/**
* @see Mp3InfoReplayGain#field1Value
*/
public final float replayGainField1Value;

/**
* @see Mp3InfoReplayGain#field2Name
*/
public final byte replayGainField2Name;

/**
* @see Mp3InfoReplayGain#field2Originator
*/
public final byte replayGainField2Originator;

/**
* @see Mp3InfoReplayGain#field2Value
*/
public final float replayGainField2Value;

/**
* The number of samples to skip at the start of the stream, or {@link C#LENGTH_UNSET} if not
* present in the header.
Expand All @@ -58,12 +97,28 @@ private XingFrame(
long frameCount,
long dataSize,
@Nullable long[] tableOfContents,
boolean hasReplayGain,
float replayGainPeak,
byte replayGainField1Name,
byte replayGainField1Originator,
float replayGainField1Value,
byte replayGainField2Name,
byte replayGainField2Originator,
float replayGainField2Value,
int encoderDelay,
int encoderPadding) {
this.header = new MpegAudioUtil.Header(header);
this.frameCount = frameCount;
this.dataSize = dataSize;
this.tableOfContents = tableOfContents;
this.hasReplayGain = hasReplayGain;
this.replayGainPeak = replayGainPeak;
this.replayGainField1Name = replayGainField1Name;
this.replayGainField1Originator = replayGainField1Originator;
this.replayGainField1Value = replayGainField1Value;
this.replayGainField2Name = replayGainField2Name;
this.replayGainField2Originator = replayGainField2Originator;
this.replayGainField2Value = replayGainField2Value;
this.encoderDelay = encoderDelay;
this.encoderPadding = encoderPadding;
}
Expand Down Expand Up @@ -98,23 +153,56 @@ public static XingFrame parse(MpegAudioUtil.Header mpegAudioHeader, ParsableByte
frame.skipBytes(4); // Quality indicator
}

int encoderDelay;
int encoderPadding;
// Skip: version string (9), revision & VBR method (1), lowpass filter (1), replay gain (8),
// encoding flags & ATH type (1), bitrate (1).
int bytesToSkipBeforeEncoderDelayAndPadding = 9 + 1 + 1 + 8 + 1 + 1;
if (frame.bytesLeft() >= bytesToSkipBeforeEncoderDelayAndPadding + 3) {
frame.skipBytes(bytesToSkipBeforeEncoderDelayAndPadding);
int encoderDelayAndPadding = frame.readUnsignedInt24();
encoderDelay = (encoderDelayAndPadding & 0xFFF000) >> 12;
encoderPadding = (encoderDelayAndPadding & 0xFFF);
} else {
encoderDelay = C.LENGTH_UNSET;
encoderPadding = C.LENGTH_UNSET;
boolean hasReplayGain = false;
float replayGainPeak = 0f;
byte replayGainField1Name = 0;
byte replayGainField1Originator = 0;
float replayGainField1Value = 0;
byte replayGainField2Name = 0;
byte replayGainField2Originator = 0;
float replayGainField2Value = 0;
int encoderDelay = C.LENGTH_UNSET;
int encoderPadding = C.LENGTH_UNSET;
// Skip: version string (9), revision & VBR method (1), lowpass filter (1).
int bytesToSkipBeforeReplayGain = 9 + 1 + 1;
if (frame.bytesLeft() >= bytesToSkipBeforeReplayGain + 8) {
frame.skipBytes(bytesToSkipBeforeReplayGain);
hasReplayGain = true;
replayGainPeak = frame.readFloat();
short field1 = frame.readShort();
replayGainField1Name = (byte) ((field1 >> 13) & 7);
replayGainField1Originator = (byte) ((field1 >> 10) & 7);
replayGainField1Value = ((field1 & 0x1ff) * ((field1 & 0x200) != 0 ? -1 : 1)) / 10f;
short field2 = frame.readShort();
replayGainField2Name = (byte) ((field2 >> 13) & 7);
replayGainField2Originator = (byte) ((field2 >> 10) & 7);
replayGainField2Value = ((field2 & 0x1ff) * ((field2 & 0x200) != 0 ? -1 : 1)) / 10f;

// Skip: encoding flags & ATH type (1), bitrate (1).
int bytesToSkipBeforeEncoderDelayAndPadding = 1 + 1;
if (frame.bytesLeft() >= bytesToSkipBeforeEncoderDelayAndPadding + 3) {
frame.skipBytes(bytesToSkipBeforeEncoderDelayAndPadding);
int encoderDelayAndPadding = frame.readUnsignedInt24();
encoderDelay = (encoderDelayAndPadding & 0xFFF000) >> 12;
encoderPadding = (encoderDelayAndPadding & 0xFFF);
}
}

return new XingFrame(
mpegAudioHeader, frameCount, dataSize, tableOfContents, encoderDelay, encoderPadding);
mpegAudioHeader,
frameCount,
dataSize,
tableOfContents,
hasReplayGain,
replayGainPeak,
replayGainField1Name,
replayGainField1Originator,
replayGainField1Value,
replayGainField2Name,
replayGainField2Originator,
replayGainField2Value,
encoderDelay,
encoderPadding);
}

/**
Expand All @@ -132,4 +220,12 @@ public long computeDurationUs() {
return Util.sampleCountToDurationUs(
(frameCount * header.samplesPerFrame) - 1, header.sampleRate);
}

/** Provide the metadata derived from this Xing frame, such as ReplayGain data. */
public @Nullable Metadata getMetadata() {
if (hasReplayGain) {
return new Metadata(new Mp3InfoReplayGain(this));
}
return null;
}
}