Skip to content

Commit 06c2466

Browse files
committed
Support ID3/RIFF INFO/ReplayGain metadata in WAV extractor
Including flag for returning prior behavior if seeking to end of file to find metadata chunks there (as old WAV readers do not support chunks between format and data, metadata is often put after data) is undesirable. Currently sample_rf64 test fails due to seeking more than max int value, and I will need some guidance on how exactly you can seek after the data segment if it's more than integer max value, as I can neither skipFully() nor seek numbers that high. The RIFF INFO tag names have been pieced together from "Multimedia Programming Interface and Data Specifications 1.0" (1991), exiftool, Hydrogen Audio, FFmpeg and wavemetatools. Samples created from other samples are also included, these were made with Kid3 and Awave Audio.
1 parent e41b755 commit 06c2466

42 files changed

Lines changed: 1647 additions & 56 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

libraries/common/src/main/java/androidx/media3/common/util/WavUtil.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,21 @@ public final class WavUtil {
4343
/** Four character code for "ds64". */
4444
public static final int DS64_FOURCC = 0x64733634;
4545

46+
/** Four character code for "LIST". */
47+
public static final int LIST_FOURCC = 0x4c495354;
48+
49+
/** Four character code for "INFO". */
50+
public static final int INFO_FOURCC = 0x494e464f;
51+
52+
/** Four character code for "ID3 ". */
53+
public static final int ID3_FOURCC = 0x49443320;
54+
55+
/** Four character code for "id3 ". */
56+
public static final int ID3_LOWER_FOURCC = 0x69643320;
57+
58+
/** Four character code for "rgad". */
59+
public static final int RGAD_FOURCC = 0x72676164;
60+
4661
/** WAVE type value for integer PCM audio data. */
4762
public static final int TYPE_PCM = 0x0001;
4863

libraries/extractor/src/main/java/androidx/media3/extractor/DefaultExtractorsFactory.java

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory {
147147
private @Mp4Extractor.Flags int mp4Flags;
148148
private @FragmentedMp4Extractor.Flags int fragmentedMp4Flags;
149149
private @Mp3Extractor.Flags int mp3Flags;
150+
private @WavExtractor.Flags int wavFlags;
150151
private @TsExtractor.Mode int tsMode;
151152
private @DefaultTsPayloadReaderFactory.Flags int tsFlags;
152153
// TODO (b/261183220): Initialize tsSubtitleFormats in constructor once shrinking bug is fixed.
@@ -322,6 +323,19 @@ public synchronized DefaultExtractorsFactory setMp3ExtractorFlags(@Mp3Extractor.
322323
return this;
323324
}
324325

326+
/**
327+
* Sets flags for {@link WavExtractor} instances created by the factory.
328+
*
329+
* @see WavExtractor#WavExtractor(int)
330+
* @param flags The flags to use.
331+
* @return The factory, for convenience.
332+
*/
333+
@CanIgnoreReturnValue
334+
public synchronized DefaultExtractorsFactory setWavExtractorFlags(@WavExtractor.Flags int flags) {
335+
wavFlags = flags;
336+
return this;
337+
}
338+
325339
/**
326340
* Sets the mode for {@link TsExtractor} instances created by the factory.
327341
*
@@ -581,7 +595,10 @@ private void addExtractorsForFileType(@FileTypes.Type int fileType, List<Extract
581595
tsTimestampSearchBytes));
582596
break;
583597
case FileTypes.WAV:
584-
extractors.add(new WavExtractor());
598+
extractors.add(
599+
new WavExtractor(
600+
wavFlags
601+
| (disableArtworkMetadata ? WavExtractor.FLAG_DISABLE_ARTWORK_METADATA : 0)));
585602
break;
586603
case FileTypes.JPEG:
587604
extractors.add(new JpegExtractor(jpegFlags));

libraries/extractor/src/main/java/androidx/media3/extractor/mp3/Mp3InfoReplayGain.java renamed to libraries/extractor/src/main/java/androidx/media3/extractor/metadata/ReplayGainInfo.java

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
package androidx.media3.extractor.mp3;
16+
package androidx.media3.extractor.metadata;
1717

1818
import static java.lang.annotation.ElementType.TYPE_USE;
1919

@@ -27,9 +27,11 @@
2727
import java.lang.annotation.Target;
2828
import java.util.Objects;
2929

30-
/** Representation of the ReplayGain data stored in a LAME Xing or Info frame. */
30+
/**
31+
* Representation of the ReplayGain data stored in a LAME Xing or Info frame, or a RIFF RGAD chunk.
32+
*/
3133
@UnstableApi
32-
public final class Mp3InfoReplayGain implements Metadata.Entry {
34+
public final class ReplayGainInfo implements Metadata.Entry {
3335

3436
/** A gain field can store one gain adjustment with name and originator metadata. */
3537
public static final class GainField {
@@ -202,7 +204,7 @@ public int hashCode() {
202204
*/
203205
@Nullable public GainField field2;
204206

205-
private Mp3InfoReplayGain(float peak, @Nullable GainField field1, @Nullable GainField field2) {
207+
private ReplayGainInfo(float peak, @Nullable GainField field1, @Nullable GainField field2) {
206208
this.peak = peak;
207209
this.field1 = field1;
208210
this.field2 = field2;
@@ -214,13 +216,13 @@ private Mp3InfoReplayGain(float peak, @Nullable GainField field1, @Nullable Gain
214216
* <p>Returns null if the representation is invalid or should be ignored.
215217
*/
216218
@Nullable
217-
public static Mp3InfoReplayGain parse(float peak, int field1, int field2) {
219+
public static ReplayGainInfo parse(float peak, int field1, int field2) {
218220
GainField parsedField1 = GainField.parse(field1);
219221
GainField parsedField2 = GainField.parse(field2);
220222
if (peak <= 0 && parsedField1 == null && parsedField2 == null) {
221223
return null;
222224
}
223-
return new Mp3InfoReplayGain(peak, parsedField1, parsedField2);
225+
return new ReplayGainInfo(peak, parsedField1, parsedField2);
224226
}
225227

226228
@Override
@@ -236,10 +238,10 @@ public String toString() {
236238

237239
@Override
238240
public boolean equals(@Nullable Object o) {
239-
if (!(o instanceof Mp3InfoReplayGain)) {
241+
if (!(o instanceof ReplayGainInfo)) {
240242
return false;
241243
}
242-
Mp3InfoReplayGain that = (Mp3InfoReplayGain) o;
244+
ReplayGainInfo that = (ReplayGainInfo) o;
243245
return Float.compare(peak, that.peak) == 0
244246
&& Objects.equals(field1, that.field1)
245247
&& Objects.equals(field2, that.field2);
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/*
2+
* Copyright (C) 2016 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+
* http://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.metadata.riff;
17+
18+
import static com.google.common.base.Preconditions.checkArgument;
19+
20+
import androidx.annotation.Nullable;
21+
import androidx.media3.common.MediaMetadata;
22+
import androidx.media3.common.Metadata;
23+
import androidx.media3.common.util.UnstableApi;
24+
import com.google.common.collect.ImmutableList;
25+
import java.util.List;
26+
import java.util.Objects;
27+
28+
/** The data from one or more chunks in the RIFF INFO list with the same fourcc. */
29+
@UnstableApi
30+
public final class RiffInfoChunk implements Metadata.Entry {
31+
32+
/** The fourcc converted to a String. */
33+
public final String id;
34+
35+
/** The text values of these chunks. Will always have at least one element. */
36+
public final ImmutableList<String> values;
37+
38+
public RiffInfoChunk(String id, List<String> values) {
39+
checkArgument(!values.isEmpty());
40+
41+
this.id = id;
42+
this.values = ImmutableList.copyOf(values);
43+
}
44+
45+
/**
46+
* Uses the first element in {@link #values} to set the relevant field in {@link MediaMetadata}
47+
* (as determined by {@link #id}).
48+
*/
49+
@Override
50+
public void populateMediaMetadata(MediaMetadata.Builder builder) {
51+
switch (id) {
52+
case "IART":
53+
builder.setArtist(values.get(0));
54+
break;
55+
case "ICRD":
56+
try {
57+
int year = Integer.parseInt(values.get(0).substring(0, 4));
58+
int month = Integer.parseInt(values.get(0).substring(5, 7));
59+
int day = Integer.parseInt(values.get(0).substring(8, 10));
60+
builder.setRecordingYear(year);
61+
builder.setRecordingMonth(month);
62+
builder.setRecordingDay(day);
63+
} catch (NumberFormatException | IndexOutOfBoundsException e) {
64+
// Do nothing, invalid input.
65+
}
66+
break;
67+
case "IGNR":
68+
case "GENR":
69+
builder.setGenre(values.get(0));
70+
break;
71+
case "IPRD":
72+
case "IALB":
73+
builder.setAlbumTitle(values.get(0));
74+
break;
75+
case "ICOM":
76+
case "IMUS":
77+
builder.setComposer(values.get(0));
78+
break;
79+
case "IWRI":
80+
builder.setWriter(values.get(0));
81+
break;
82+
case "ISBJ":
83+
builder.setDescription(values.get(0));
84+
break;
85+
case "INAM":
86+
case "TITL":
87+
builder.setTitle(values.get(0));
88+
break;
89+
case "IYER":
90+
case "YEAR":
91+
try {
92+
builder.setReleaseYear(Integer.parseInt(values.get(0)));
93+
} catch (NumberFormatException e) {
94+
// Do nothing, invalid input.
95+
}
96+
break;
97+
case "IFRM":
98+
try {
99+
builder.setTotalTrackCount(Integer.parseInt(values.get(0)));
100+
} catch (NumberFormatException e) {
101+
// Do nothing, invalid input.
102+
}
103+
break;
104+
case "ITRK":
105+
case "TRCK":
106+
case "IPRT":
107+
try {
108+
builder.setTrackNumber(Integer.parseInt(values.get(0)));
109+
} catch (NumberFormatException e) {
110+
// Do nothing, invalid input.
111+
}
112+
break;
113+
default:
114+
break;
115+
}
116+
}
117+
118+
@Override
119+
public boolean equals(@Nullable Object obj) {
120+
if (this == obj) {
121+
return true;
122+
}
123+
if (obj == null || getClass() != obj.getClass()) {
124+
return false;
125+
}
126+
RiffInfoChunk other = (RiffInfoChunk) obj;
127+
return Objects.equals(id, other.id) && values.equals(other.values);
128+
}
129+
130+
@Override
131+
public int hashCode() {
132+
int result = 17;
133+
result = 31 * result + id.hashCode();
134+
result = 31 * result + values.hashCode();
135+
return result;
136+
}
137+
138+
@Override
139+
public String toString() {
140+
return id + ": values=" + values;
141+
}
142+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
* Copyright (C) 2019 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+
* http://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+
@NonNullApi
17+
package androidx.media3.extractor.metadata.riff;
18+
19+
import androidx.media3.common.util.NonNullApi;

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import androidx.media3.common.util.ParsableByteArray;
2222
import androidx.media3.common.util.Util;
2323
import androidx.media3.extractor.MpegAudioUtil;
24+
import androidx.media3.extractor.metadata.ReplayGainInfo;
2425

2526
/** Representation of a LAME Xing or Info frame. */
2627
/* package */ final class XingFrame {
@@ -37,7 +38,7 @@
3738
public final long dataSize;
3839

3940
/** ReplayGain data. Only present if this frame is an Info or the LAME variant of a Xing frame. */
40-
@Nullable public final Mp3InfoReplayGain replayGain;
41+
@Nullable public final ReplayGainInfo replayGain;
4142

4243
/**
4344
* The number of samples to skip at the start of the stream, or {@link C#LENGTH_UNSET} if not
@@ -62,7 +63,7 @@ private XingFrame(
6263
long frameCount,
6364
long dataSize,
6465
@Nullable long[] tableOfContents,
65-
@Nullable Mp3InfoReplayGain replayGain,
66+
@Nullable ReplayGainInfo replayGain,
6667
int encoderDelay,
6768
int encoderPadding) {
6869
this.header = new MpegAudioUtil.Header(header);
@@ -104,7 +105,7 @@ public static XingFrame parse(MpegAudioUtil.Header mpegAudioHeader, ParsableByte
104105
frame.skipBytes(4); // Quality indicator
105106
}
106107

107-
@Nullable Mp3InfoReplayGain replayGain;
108+
@Nullable ReplayGainInfo replayGain;
108109
int encoderDelay;
109110
int encoderPadding;
110111
// Skip: version string (9), revision & VBR method (1), lowpass filter (1).
@@ -117,7 +118,7 @@ public static XingFrame parse(MpegAudioUtil.Header mpegAudioHeader, ParsableByte
117118
float peak = frame.readFloat();
118119
int field1 = frame.readUnsignedShort();
119120
int field2 = frame.readUnsignedShort();
120-
replayGain = Mp3InfoReplayGain.parse(peak, field1, field2);
121+
replayGain = ReplayGainInfo.parse(peak, field1, field2);
121122

122123
frame.skipBytes(bytesToSkipAfterReplayGain);
123124
int encoderDelayAndPadding = frame.readUnsignedInt24();

0 commit comments

Comments
 (0)