Skip to content

Commit 5ca3bd3

Browse files
committed
Parse ReplayGain data from LAME Xing/Info header
see http://gabriel.mp3-tech.org/mp3infotag.html#replaygain
1 parent ce7ca3b commit 5ca3bd3

File tree

3 files changed

+271
-15
lines changed

3 files changed

+271
-15
lines changed

libraries/extractor/src/main/java/androidx/media3/extractor/mp3/Mp3Extractor.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ public final class Mp3Extractor implements Extractor {
174174
private int synchronizedHeaderData;
175175

176176
@Nullable private Metadata metadata;
177+
@Nullable private Metadata infoMetadata;
177178
private long basisTimeUs;
178179
private long samplesRead;
179180
private long firstSamplePosition;
@@ -289,6 +290,12 @@ private int readInternal(ExtractorInput input) throws IOException {
289290
if (seeker == null) {
290291
seeker = computeSeeker(input);
291292
extractorOutput.seekMap(seeker);
293+
@Nullable Metadata finalMetadata = (flags & FLAG_DISABLE_ID3_METADATA) != 0 ? null : metadata;
294+
if (finalMetadata != null) {
295+
finalMetadata = finalMetadata.copyWithAppendedEntriesFrom(infoMetadata);
296+
} else {
297+
finalMetadata = infoMetadata;
298+
}
292299
Format.Builder format =
293300
new Format.Builder()
294301
.setContainerMimeType(MimeTypes.AUDIO_MPEG)
@@ -298,7 +305,7 @@ private int readInternal(ExtractorInput input) throws IOException {
298305
.setSampleRate(synchronizedHeader.sampleRate)
299306
.setEncoderDelay(gaplessInfoHolder.encoderDelay)
300307
.setEncoderPadding(gaplessInfoHolder.encoderPadding)
301-
.setMetadata((flags & FLAG_DISABLE_ID3_METADATA) != 0 ? null : metadata);
308+
.setMetadata(finalMetadata);
302309
if (seeker.getAverageBitrate() != C.RATE_UNSET_INT) {
303310
format.setAverageBitrate(seeker.getAverageBitrate());
304311
}
@@ -579,6 +586,7 @@ private Seeker maybeReadSeekFrame(ExtractorInput input) throws IOException {
579586
gaplessInfoHolder.encoderDelay = xingFrame.encoderDelay;
580587
gaplessInfoHolder.encoderPadding = xingFrame.encoderPadding;
581588
}
589+
infoMetadata = xingFrame.getMetadata();
582590
long startPosition = input.getPosition();
583591
if (input.getLength() != C.LENGTH_UNSET
584592
&& xingFrame.dataSize != C.LENGTH_UNSET
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/*
2+
* Copyright 2025 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package androidx.media3.extractor.mp3;
17+
18+
import androidx.media3.common.Metadata;
19+
import androidx.media3.common.util.UnstableApi;
20+
import java.util.Objects;
21+
22+
/** Representation of the ReplayGain data stored in a LAME Xing or Info frame. */
23+
@UnstableApi
24+
public final class Mp3InfoReplayGain implements Metadata.Entry {
25+
/**
26+
* 32 bit floating point "Peak signal amplitude".
27+
*
28+
* <p>1.0 is maximal signal amplitude store-able in decoding format. 0.8 is 80% of maximal signal
29+
* amplitude store-able in decoding format. 1.5 is 150% of maximal signal amplitude store-able in
30+
* decoding format.
31+
*
32+
* <p>A value above 1.0 can occur for example due to "true peak" measurement. A value of 0.0 means
33+
* the peak signal amplitude is unknown.
34+
*/
35+
public final float peak;
36+
37+
/**
38+
* NAME of Gain adjustment in the first field, also called "Radio Replay Gain" field:
39+
*
40+
* <p>b000 = not set
41+
*
42+
* <p>b001 = radio
43+
*
44+
* <p>b010 = audiophile
45+
*/
46+
public final byte field1Name;
47+
48+
/**
49+
* ORIGINATOR of Gain adjustment in the first field, also called "Radio Replay Gain" field:
50+
*
51+
* <p>b000 = not set
52+
*
53+
* <p>b001 = set by artist
54+
*
55+
* <p>b010 = set by user
56+
*
57+
* <p>b011 = set by ReplayGain model
58+
*
59+
* <p>b100 = set by simple RMS average
60+
*/
61+
public final byte field1Originator;
62+
63+
/**
64+
* Absolute gain adjustment in the first field, also called "Radio Replay Gain" field.
65+
*
66+
* <p>Stored in the header with 1 decimal of precision by being multiplied by 10; this field is
67+
* already divided by 10 again.
68+
*/
69+
public final float field1Value;
70+
71+
/**
72+
* NAME of Gain adjustment in the second field, also called "Audiophile Replay Gain" field:
73+
*
74+
* <p>b000 = not set
75+
*
76+
* <p>b001 = radio
77+
*
78+
* <p>b010 = audiophile
79+
*/
80+
public final byte field2Name;
81+
82+
/**
83+
* ORIGINATOR of Gain adjustment in the second field, also called "Audiophile Replay Gain" field:
84+
*
85+
* <p>b000 = not set
86+
*
87+
* <p>b001 = set by artist
88+
*
89+
* <p>b010 = set by user
90+
*
91+
* <p>b011 = set by ReplayGain model
92+
*
93+
* <p>b100 = set by simple RMS average
94+
*/
95+
public final byte field2Originator;
96+
97+
/**
98+
* Absolute gain adjustment in the second field, also called "Audiophile Replay Gain" field.
99+
*
100+
* <p>Stored in the header with 1 decimal of precision by being multiplied by 10; this field is
101+
* already divided by 10 again.
102+
*/
103+
public final float field2Value;
104+
105+
/* package */ Mp3InfoReplayGain(XingFrame frame) {
106+
this.peak = frame.replayGainPeak;
107+
this.field1Name = frame.replayGainField1Name;
108+
this.field1Originator = frame.replayGainField1Originator;
109+
this.field1Value = frame.replayGainField1Value;
110+
this.field2Name = frame.replayGainField2Name;
111+
this.field2Originator = frame.replayGainField2Originator;
112+
this.field2Value = frame.replayGainField2Value;
113+
}
114+
115+
@Override
116+
public String toString() {
117+
return "ReplayGain Xing/Info: "
118+
+ "peak="
119+
+ peak
120+
+ ", f1 name="
121+
+ field1Name
122+
+ ", f1 orig="
123+
+ field1Originator
124+
+ ", f1 val="
125+
+ field1Value
126+
+ ", f2 name="
127+
+ field2Name
128+
+ ", f2 orig="
129+
+ field2Originator
130+
+ ", f2 val="
131+
+ field2Value;
132+
}
133+
134+
@Override
135+
public boolean equals(Object o) {
136+
if (!(o instanceof Mp3InfoReplayGain)) return false;
137+
Mp3InfoReplayGain that = (Mp3InfoReplayGain) o;
138+
return Float.compare(peak, that.peak) == 0
139+
&& field1Name == that.field1Name
140+
&& field1Originator == that.field1Originator
141+
&& Float.compare(field1Value, that.field1Value) == 0
142+
&& field2Name == that.field2Name
143+
&& field2Originator == that.field2Originator
144+
&& Float.compare(field2Value, that.field2Value) == 0;
145+
}
146+
147+
@Override
148+
public int hashCode() {
149+
return Objects.hash(
150+
peak, field1Name, field1Originator, field1Value, field2Name, field2Originator, field2Value);
151+
}
152+
}

libraries/extractor/src/main/java/androidx/media3/extractor/mp3/XingFrame.java

Lines changed: 110 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import androidx.annotation.Nullable;
1919
import androidx.media3.common.C;
20+
import androidx.media3.common.Metadata;
2021
import androidx.media3.common.util.ParsableByteArray;
2122
import androidx.media3.common.util.Util;
2223
import androidx.media3.extractor.MpegAudioUtil;
@@ -35,6 +36,44 @@
3536
*/
3637
public final long dataSize;
3738

39+
/** Whether this frame is the LAME variant of a Xing frame, and hence has ReplayGain data. */
40+
public final boolean hasReplayGain;
41+
42+
/**
43+
* @see Mp3InfoReplayGain#peak
44+
*/
45+
public final float replayGainPeak;
46+
47+
/**
48+
* @see Mp3InfoReplayGain#field1Name
49+
*/
50+
public final byte replayGainField1Name;
51+
52+
/**
53+
* @see Mp3InfoReplayGain#field1Originator
54+
*/
55+
public final byte replayGainField1Originator;
56+
57+
/**
58+
* @see Mp3InfoReplayGain#field1Value
59+
*/
60+
public final float replayGainField1Value;
61+
62+
/**
63+
* @see Mp3InfoReplayGain#field2Name
64+
*/
65+
public final byte replayGainField2Name;
66+
67+
/**
68+
* @see Mp3InfoReplayGain#field2Originator
69+
*/
70+
public final byte replayGainField2Originator;
71+
72+
/**
73+
* @see Mp3InfoReplayGain#field2Value
74+
*/
75+
public final float replayGainField2Value;
76+
3877
/**
3978
* The number of samples to skip at the start of the stream, or {@link C#LENGTH_UNSET} if not
4079
* present in the header.
@@ -58,12 +97,28 @@ private XingFrame(
5897
long frameCount,
5998
long dataSize,
6099
@Nullable long[] tableOfContents,
100+
boolean hasReplayGain,
101+
float replayGainPeak,
102+
byte replayGainField1Name,
103+
byte replayGainField1Originator,
104+
float replayGainField1Value,
105+
byte replayGainField2Name,
106+
byte replayGainField2Originator,
107+
float replayGainField2Value,
61108
int encoderDelay,
62109
int encoderPadding) {
63110
this.header = new MpegAudioUtil.Header(header);
64111
this.frameCount = frameCount;
65112
this.dataSize = dataSize;
66113
this.tableOfContents = tableOfContents;
114+
this.hasReplayGain = hasReplayGain;
115+
this.replayGainPeak = replayGainPeak;
116+
this.replayGainField1Name = replayGainField1Name;
117+
this.replayGainField1Originator = replayGainField1Originator;
118+
this.replayGainField1Value = replayGainField1Value;
119+
this.replayGainField2Name = replayGainField2Name;
120+
this.replayGainField2Originator = replayGainField2Originator;
121+
this.replayGainField2Value = replayGainField2Value;
67122
this.encoderDelay = encoderDelay;
68123
this.encoderPadding = encoderPadding;
69124
}
@@ -98,23 +153,56 @@ public static XingFrame parse(MpegAudioUtil.Header mpegAudioHeader, ParsableByte
98153
frame.skipBytes(4); // Quality indicator
99154
}
100155

101-
int encoderDelay;
102-
int encoderPadding;
103-
// Skip: version string (9), revision & VBR method (1), lowpass filter (1), replay gain (8),
104-
// encoding flags & ATH type (1), bitrate (1).
105-
int bytesToSkipBeforeEncoderDelayAndPadding = 9 + 1 + 1 + 8 + 1 + 1;
106-
if (frame.bytesLeft() >= bytesToSkipBeforeEncoderDelayAndPadding + 3) {
107-
frame.skipBytes(bytesToSkipBeforeEncoderDelayAndPadding);
108-
int encoderDelayAndPadding = frame.readUnsignedInt24();
109-
encoderDelay = (encoderDelayAndPadding & 0xFFF000) >> 12;
110-
encoderPadding = (encoderDelayAndPadding & 0xFFF);
111-
} else {
112-
encoderDelay = C.LENGTH_UNSET;
113-
encoderPadding = C.LENGTH_UNSET;
156+
boolean hasReplayGain = false;
157+
float replayGainPeak = 0f;
158+
byte replayGainField1Name = 0;
159+
byte replayGainField1Originator = 0;
160+
float replayGainField1Value = 0;
161+
byte replayGainField2Name = 0;
162+
byte replayGainField2Originator = 0;
163+
float replayGainField2Value = 0;
164+
int encoderDelay = C.LENGTH_UNSET;
165+
int encoderPadding = C.LENGTH_UNSET;
166+
// Skip: version string (9), revision & VBR method (1), lowpass filter (1).
167+
int bytesToSkipBeforeReplayGain = 9 + 1 + 1;
168+
if (frame.bytesLeft() >= bytesToSkipBeforeReplayGain + 8) {
169+
frame.skipBytes(bytesToSkipBeforeReplayGain);
170+
hasReplayGain = true;
171+
replayGainPeak = frame.readFloat();
172+
short field1 = frame.readShort();
173+
replayGainField1Name = (byte) ((field1 >> 13) & 7);
174+
replayGainField1Originator = (byte) ((field1 >> 10) & 7);
175+
replayGainField1Value = ((field1 & 0x1ff) * ((field1 & 0x200) != 0 ? -1 : 1)) / 10f;
176+
short field2 = frame.readShort();
177+
replayGainField2Name = (byte) ((field2 >> 13) & 7);
178+
replayGainField2Originator = (byte) ((field2 >> 10) & 7);
179+
replayGainField2Value = ((field2 & 0x1ff) * ((field2 & 0x200) != 0 ? -1 : 1)) / 10f;
180+
181+
// Skip: encoding flags & ATH type (1), bitrate (1).
182+
int bytesToSkipBeforeEncoderDelayAndPadding = 1 + 1;
183+
if (frame.bytesLeft() >= bytesToSkipBeforeEncoderDelayAndPadding + 3) {
184+
frame.skipBytes(bytesToSkipBeforeEncoderDelayAndPadding);
185+
int encoderDelayAndPadding = frame.readUnsignedInt24();
186+
encoderDelay = (encoderDelayAndPadding & 0xFFF000) >> 12;
187+
encoderPadding = (encoderDelayAndPadding & 0xFFF);
188+
}
114189
}
115190

116191
return new XingFrame(
117-
mpegAudioHeader, frameCount, dataSize, tableOfContents, encoderDelay, encoderPadding);
192+
mpegAudioHeader,
193+
frameCount,
194+
dataSize,
195+
tableOfContents,
196+
hasReplayGain,
197+
replayGainPeak,
198+
replayGainField1Name,
199+
replayGainField1Originator,
200+
replayGainField1Value,
201+
replayGainField2Name,
202+
replayGainField2Originator,
203+
replayGainField2Value,
204+
encoderDelay,
205+
encoderPadding);
118206
}
119207

120208
/**
@@ -132,4 +220,12 @@ public long computeDurationUs() {
132220
return Util.sampleCountToDurationUs(
133221
(frameCount * header.samplesPerFrame) - 1, header.sampleRate);
134222
}
223+
224+
/** Provide the metadata derived from this Xing frame, such as ReplayGain data. */
225+
public @Nullable Metadata getMetadata() {
226+
if (hasReplayGain) {
227+
return new Metadata(new Mp3InfoReplayGain(this));
228+
}
229+
return null;
230+
}
135231
}

0 commit comments

Comments
 (0)