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 {
|
/* package */ final class Id3Util {
|
||||||
|
|
||||||
public static final class Metadata {
|
|
||||||
|
|
||||||
public int encoderDelay;
|
|
||||||
public int encoderPadding;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The maximum valid length for metadata in bytes.
|
* 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.
|
* Peeks data from the input and parses ID3 metadata.
|
||||||
*
|
*
|
||||||
* @param input The {@link ExtractorInput} from which data should be peeked.
|
* @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.
|
* @return The number of bytes peeked from the input.
|
||||||
* @throws IOException If an error occurred peeking from the input.
|
* @throws IOException If an error occurred peeking from the input.
|
||||||
* @throws InterruptedException If the thread was interrupted.
|
* @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 {
|
throws IOException, InterruptedException {
|
||||||
out.encoderDelay = 0;
|
out.encoderDelay = 0;
|
||||||
out.encoderPadding = 0;
|
out.encoderPadding = 0;
|
||||||
@ -100,7 +93,8 @@ import java.util.regex.Pattern;
|
|||||||
&& !(majorVersion == 4 && (flags & 0x0F) != 0);
|
&& !(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);
|
unescape(frame, version, flags);
|
||||||
|
|
||||||
// Skip any extended header.
|
// Skip any extended header.
|
||||||
|
@ -56,7 +56,7 @@ public final class Mp3Extractor implements Extractor {
|
|||||||
private final long forcedFirstSampleTimestampUs;
|
private final long forcedFirstSampleTimestampUs;
|
||||||
private final ParsableByteArray scratch;
|
private final ParsableByteArray scratch;
|
||||||
private final MpegAudioHeader synchronizedHeader;
|
private final MpegAudioHeader synchronizedHeader;
|
||||||
private final Id3Util.Metadata metadata;
|
private final Metadata metadata;
|
||||||
|
|
||||||
// Extractor outputs.
|
// Extractor outputs.
|
||||||
private ExtractorOutput extractorOutput;
|
private ExtractorOutput extractorOutput;
|
||||||
@ -86,7 +86,7 @@ public final class Mp3Extractor implements Extractor {
|
|||||||
this.forcedFirstSampleTimestampUs = forcedFirstSampleTimestampUs;
|
this.forcedFirstSampleTimestampUs = forcedFirstSampleTimestampUs;
|
||||||
scratch = new ParsableByteArray(4);
|
scratch = new ParsableByteArray(4);
|
||||||
synchronizedHeader = new MpegAudioHeader();
|
synchronizedHeader = new MpegAudioHeader();
|
||||||
metadata = new Id3Util.Metadata();
|
metadata = new Metadata();
|
||||||
basisTimeUs = -1;
|
basisTimeUs = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -259,64 +259,51 @@ public final class Mp3Extractor implements Extractor {
|
|||||||
* the next two frames were already peeked during synchronization.
|
* the next two frames were already peeked during synchronization.
|
||||||
*/
|
*/
|
||||||
private void setupSeeker(ExtractorInput input) throws IOException, InterruptedException {
|
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);
|
ParsableByteArray frame = new ParsableByteArray(synchronizedHeader.frameSize);
|
||||||
input.peekFully(frame.data, 0, synchronizedHeader.frameSize);
|
input.peekFully(frame.data, 0, synchronizedHeader.frameSize);
|
||||||
if (parseSeekerFrame(frame, input.getPosition(), input.getLength())) {
|
|
||||||
input.skipFully(synchronizedHeader.frameSize);
|
long position = input.getPosition();
|
||||||
if (seeker != null) {
|
long length = input.getLength();
|
||||||
return;
|
|
||||||
|
// 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
|
input.skipFully(synchronizedHeader.frameSize);
|
||||||
// an invalid bitrate for CBR seeking.
|
} 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);
|
input.peekFully(scratch.data, 0, 4);
|
||||||
scratch.setPosition(0);
|
scratch.setPosition(0);
|
||||||
MpegAudioHeader.populateHeader(scratch.readInt(), synchronizedHeader);
|
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];
|
String mimeType = MIME_TYPE_BY_LAYER[3 - layer];
|
||||||
int channels = ((headerData >> 6) & 3) == 3 ? 1 : 2;
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -186,7 +187,7 @@ public final class MpegAudioHeader {
|
|||||||
public int sampleRate;
|
public int sampleRate;
|
||||||
/** Number of audio channels in the frame. */
|
/** Number of audio channels in the frame. */
|
||||||
public int channels;
|
public int channels;
|
||||||
/** Bitrate of the frame in kbit/s. */
|
/** Bitrate of the frame in bit/s. */
|
||||||
public int bitrate;
|
public int bitrate;
|
||||||
/** Number of samples stored in the frame. */
|
/** Number of samples stored in the frame. */
|
||||||
public int samplesPerFrame;
|
public int samplesPerFrame;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user