Read gapless playback metadata in files with Xing metadata.
------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=112699916
This commit is contained in:
parent
4bea0b184f
commit
3f0244e214
@ -31,13 +31,6 @@ import java.util.regex.Pattern;
|
||||
*/
|
||||
/* package */ final class Id3Util {
|
||||
|
||||
public static final class Metadata {
|
||||
|
||||
public int encoderDelay;
|
||||
public int encoderPadding;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* The maximum valid length for metadata in bytes.
|
||||
*/
|
||||
@ -54,12 +47,12 @@ import java.util.regex.Pattern;
|
||||
* Peeks data from the input and parses ID3 metadata.
|
||||
*
|
||||
* @param input The {@link ExtractorInput} from which data should be peeked.
|
||||
* @param out {@link Metadata} to populate based on the input.
|
||||
* @param out {@link Mp3Extractor.Metadata} to populate based on the input.
|
||||
* @return The number of bytes peeked from the input.
|
||||
* @throws IOException If an error occurred peeking from the input.
|
||||
* @throws InterruptedException If the thread was interrupted.
|
||||
*/
|
||||
public static int parseId3(ExtractorInput input, Metadata out)
|
||||
public static int parseId3(ExtractorInput input, Mp3Extractor.Metadata out)
|
||||
throws IOException, InterruptedException {
|
||||
out.encoderDelay = 0;
|
||||
out.encoderPadding = 0;
|
||||
@ -100,7 +93,8 @@ import java.util.regex.Pattern;
|
||||
&& !(majorVersion == 4 && (flags & 0x0F) != 0);
|
||||
}
|
||||
|
||||
private static void parseMetadata(ParsableByteArray frame, int version, int flags, Metadata out) {
|
||||
private static void parseMetadata(ParsableByteArray frame, int version, int flags,
|
||||
Mp3Extractor.Metadata out) {
|
||||
unescape(frame, version, flags);
|
||||
|
||||
// Skip any extended header.
|
||||
|
@ -56,7 +56,7 @@ public final class Mp3Extractor implements Extractor {
|
||||
private final long forcedFirstSampleTimestampUs;
|
||||
private final ParsableByteArray scratch;
|
||||
private final MpegAudioHeader synchronizedHeader;
|
||||
private final Id3Util.Metadata metadata;
|
||||
private final Metadata metadata;
|
||||
|
||||
// Extractor outputs.
|
||||
private ExtractorOutput extractorOutput;
|
||||
@ -86,7 +86,7 @@ public final class Mp3Extractor implements Extractor {
|
||||
this.forcedFirstSampleTimestampUs = forcedFirstSampleTimestampUs;
|
||||
scratch = new ParsableByteArray(4);
|
||||
synchronizedHeader = new MpegAudioHeader();
|
||||
metadata = new Id3Util.Metadata();
|
||||
metadata = new Metadata();
|
||||
basisTimeUs = -1;
|
||||
}
|
||||
|
||||
@ -259,64 +259,51 @@ public final class Mp3Extractor implements Extractor {
|
||||
* the next two frames were already peeked during synchronization.
|
||||
*/
|
||||
private void setupSeeker(ExtractorInput input) throws IOException, InterruptedException {
|
||||
// Read the first frame which may contain a Xing or VBRI header with seeking metadata.
|
||||
ParsableByteArray frame = new ParsableByteArray(synchronizedHeader.frameSize);
|
||||
input.peekFully(frame.data, 0, synchronizedHeader.frameSize);
|
||||
if (parseSeekerFrame(frame, input.getPosition(), input.getLength())) {
|
||||
input.skipFully(synchronizedHeader.frameSize);
|
||||
if (seeker != null) {
|
||||
return;
|
||||
|
||||
long position = input.getPosition();
|
||||
long length = input.getLength();
|
||||
|
||||
// Check if there is a Xing header.
|
||||
int xingBase = (synchronizedHeader.version & 1) != 0
|
||||
? (synchronizedHeader.channels != 1 ? 36 : 21) // MPEG 1
|
||||
: (synchronizedHeader.channels != 1 ? 21 : 13); // MPEG 2 or 2.5
|
||||
frame.setPosition(xingBase);
|
||||
int headerData = frame.readInt();
|
||||
if (headerData == XING_HEADER || headerData == INFO_HEADER) {
|
||||
seeker = XingSeeker.create(synchronizedHeader, frame, position, length);
|
||||
if (seeker != null && metadata.encoderDelay == 0 && metadata.encoderPadding == 0) {
|
||||
// If there is a Xing header, read gapless playback metadata at a fixed offset.
|
||||
input.resetPeekPosition();
|
||||
input.advancePeekPosition(xingBase + 141);
|
||||
input.peekFully(scratch.data, 0, 3);
|
||||
scratch.setPosition(0);
|
||||
int gaplessMetadata = scratch.readUnsignedInt24();
|
||||
metadata.encoderDelay = gaplessMetadata >> 12;
|
||||
metadata.encoderPadding = gaplessMetadata & 0x0FFF;
|
||||
}
|
||||
// If there was a header but it was not usable, synchronize to the next frame so we don't use
|
||||
// an invalid bitrate for CBR seeking.
|
||||
input.skipFully(synchronizedHeader.frameSize);
|
||||
} else {
|
||||
// Check if there is a VBRI header.
|
||||
frame.setPosition(36); // MPEG audio header (4 bytes) + 32 bytes.
|
||||
headerData = frame.readInt();
|
||||
if (headerData == VBRI_HEADER) {
|
||||
seeker = VbriSeeker.create(synchronizedHeader, frame, position);
|
||||
input.skipFully(synchronizedHeader.frameSize);
|
||||
}
|
||||
}
|
||||
|
||||
if (seeker == null) {
|
||||
// Repopulate the synchronized header in case we had to skip an invalid seeking header, which
|
||||
// would give an invalid CBR bitrate.
|
||||
input.resetPeekPosition();
|
||||
input.peekFully(scratch.data, 0, 4);
|
||||
scratch.setPosition(0);
|
||||
MpegAudioHeader.populateHeader(scratch.readInt(), synchronizedHeader);
|
||||
seeker = new ConstantBitrateSeeker(input.getPosition(), synchronizedHeader.bitrate, length);
|
||||
}
|
||||
seeker = new ConstantBitrateSeeker(input.getPosition(), synchronizedHeader.bitrate * 1000,
|
||||
input.getLength());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to read seeking metadata from the given {@code frame}. If there is no seeking metadata,
|
||||
* returns {@code false} and sets {@link #seeker} to null. If seeking metadata is present and
|
||||
* unusable, returns {@code true} and sets {@link #seeker} to null. Otherwise, returns
|
||||
* {@code true} and assigns {@link #seeker}.
|
||||
*/
|
||||
private boolean parseSeekerFrame(ParsableByteArray frame, long headerPosition, long inputLength) {
|
||||
// Check if there is a Xing header.
|
||||
int xingBase;
|
||||
if ((synchronizedHeader.version & 1) == 1) {
|
||||
// MPEG 1.
|
||||
if (synchronizedHeader.channels != 1) {
|
||||
xingBase = 32;
|
||||
} else {
|
||||
xingBase = 17;
|
||||
}
|
||||
} else {
|
||||
// MPEG 2 or 2.5.
|
||||
if (synchronizedHeader.channels != 1) {
|
||||
xingBase = 17;
|
||||
} else {
|
||||
xingBase = 9;
|
||||
}
|
||||
}
|
||||
frame.setPosition(4 + xingBase);
|
||||
int headerData = frame.readInt();
|
||||
if (headerData == XING_HEADER || headerData == INFO_HEADER) {
|
||||
seeker = XingSeeker.create(synchronizedHeader, frame, headerPosition, inputLength);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if there is a VBRI header.
|
||||
frame.setPosition(36); // MPEG audio header (4 bytes) + 32 bytes.
|
||||
headerData = frame.readInt();
|
||||
if (headerData == VBRI_HEADER) {
|
||||
seeker = VbriSeeker.create(synchronizedHeader, frame, headerPosition);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Neither header is present.
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -338,4 +325,11 @@ public final class Mp3Extractor implements Extractor {
|
||||
|
||||
}
|
||||
|
||||
/* package */ static final class Metadata {
|
||||
|
||||
public int encoderDelay;
|
||||
public int encoderPadding;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -172,7 +172,8 @@ public final class MpegAudioHeader {
|
||||
|
||||
String mimeType = MIME_TYPE_BY_LAYER[3 - layer];
|
||||
int channels = ((headerData >> 6) & 3) == 3 ? 1 : 2;
|
||||
header.setValues(version, mimeType, frameSize, sampleRate, channels, bitrate, samplesPerFrame);
|
||||
header.setValues(version, mimeType, frameSize, sampleRate, channels, bitrate * 1000,
|
||||
samplesPerFrame);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -186,7 +187,7 @@ public final class MpegAudioHeader {
|
||||
public int sampleRate;
|
||||
/** Number of audio channels in the frame. */
|
||||
public int channels;
|
||||
/** Bitrate of the frame in kbit/s. */
|
||||
/** Bitrate of the frame in bit/s. */
|
||||
public int bitrate;
|
||||
/** Number of samples stored in the frame. */
|
||||
public int samplesPerFrame;
|
||||
|
Loading…
x
Reference in New Issue
Block a user