Commit 110c8f6f by Oliver Woodman

Improvements to ID3 decoder

parent 50aeb20c
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.metadata.id3; package com.google.android.exoplayer2.metadata.id3;
import android.util.Log;
import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.MetadataDecoder; import com.google.android.exoplayer2.metadata.MetadataDecoder;
import com.google.android.exoplayer2.metadata.MetadataDecoderException; import com.google.android.exoplayer2.metadata.MetadataDecoderException;
...@@ -31,6 +32,8 @@ import java.util.Locale; ...@@ -31,6 +32,8 @@ import java.util.Locale;
*/ */
public final class Id3Decoder implements MetadataDecoder { public final class Id3Decoder implements MetadataDecoder {
private static final String TAG = "Id3Decoder";
private static final int ID3_TEXT_ENCODING_ISO_8859_1 = 0; private static final int ID3_TEXT_ENCODING_ISO_8859_1 = 0;
private static final int ID3_TEXT_ENCODING_UTF_16 = 1; private static final int ID3_TEXT_ENCODING_UTF_16 = 1;
private static final int ID3_TEXT_ENCODING_UTF_16BE = 2; private static final int ID3_TEXT_ENCODING_UTF_16BE = 2;
...@@ -45,199 +48,185 @@ public final class Id3Decoder implements MetadataDecoder { ...@@ -45,199 +48,185 @@ public final class Id3Decoder implements MetadataDecoder {
public Metadata decode(byte[] data, int size) throws MetadataDecoderException { public Metadata decode(byte[] data, int size) throws MetadataDecoderException {
List<Id3Frame> id3Frames = new ArrayList<>(); List<Id3Frame> id3Frames = new ArrayList<>();
ParsableByteArray id3Data = new ParsableByteArray(data, size); ParsableByteArray id3Data = new ParsableByteArray(data, size);
Id3Header id3Header = decodeId3Header(id3Data);
int framesBytesLeft = id3Header.framesSize; Id3Header id3Header = decodeHeader(id3Data);
if (id3Header.isUnsynchronized) { if (id3Header == null) {
id3Data = removeUnsynchronization(id3Data, id3Header.framesSize); return null;
framesBytesLeft = id3Data.bytesLeft();
} }
while (framesBytesLeft > 0) { int startPosition = id3Data.getPosition();
int frameId0 = id3Data.readUnsignedByte(); int framesSize = id3Header.framesSize;
int frameId1 = id3Data.readUnsignedByte(); if (id3Header.isUnsynchronized) {
int frameId2 = id3Data.readUnsignedByte(); framesSize = removeUnsynchronization(id3Data, id3Header.framesSize);
int frameId3 = id3Header.majorVersion > 2 ? id3Data.readUnsignedByte() : 0; }
int frameSize = id3Header.majorVersion == 2 ? id3Data.readUnsignedInt24() : id3Data.setLimit(startPosition + framesSize);
id3Header.majorVersion == 3 ? id3Data.readInt() : id3Data.readSynchSafeInt();
if (frameSize <= 1) {
break;
}
// Frame flags.
boolean isCompressed = false;
boolean isEncrypted = false;
boolean isUnsynchronized = false;
boolean hasGroupIdentifier = false;
boolean hasDataLength = false;
if (id3Header.majorVersion > 2) {
int flags = id3Data.readShort();
if (id3Header.majorVersion == 3) {
isCompressed = (flags & 0x0080) != 0;
isEncrypted = (flags & 0x0040) != 0;
hasDataLength = isCompressed;
} else {
isCompressed = (flags & 0x0008) != 0;
isEncrypted = (flags & 0x0004) != 0;
isUnsynchronized = (flags & 0x0002) != 0;
hasGroupIdentifier = (flags & 0x0040) != 0;
hasDataLength = (flags & 0x0001) != 0;
}
}
int headerSize = id3Header.majorVersion == 2 ? 6 : 10;
if (hasGroupIdentifier) {
++headerSize;
--frameSize;
id3Data.skipBytes(1);
}
if (isEncrypted) {
++headerSize;
--frameSize;
id3Data.skipBytes(1);
}
if (hasDataLength) {
headerSize += 4;
frameSize -= 4;
id3Data.skipBytes(4);
}
framesBytesLeft -= frameSize + headerSize;
if (isCompressed || isEncrypted) { int frameHeaderSize = id3Header.majorVersion == 2 ? 6 : 10;
id3Data.skipBytes(frameSize); while (id3Data.bytesLeft() >= frameHeaderSize) {
} else { Id3Frame frame = decodeFrame(id3Header, id3Data);
try { if (frame != null) {
Id3Frame frame; id3Frames.add(frame);
ParsableByteArray frameData = id3Data;
if (isUnsynchronized) {
frameData = removeUnsynchronization(id3Data, frameSize);
frameSize = frameData.bytesLeft();
}
if (frameId0 == 'T' && frameId1 == 'X' && frameId2 == 'X' && frameId3 == 'X') {
frame = decodeTxxxFrame(frameData, frameSize);
} else if (frameId0 == 'P' && frameId1 == 'R' && frameId2 == 'I' && frameId3 == 'V') {
frame = decodePrivFrame(frameData, frameSize);
} else if (frameId0 == 'G' && frameId1 == 'E' && frameId2 == 'O' && frameId3 == 'B') {
frame = decodeGeobFrame(frameData, frameSize);
} else if (frameId0 == 'A' && frameId1 == 'P' && frameId2 == 'I' && frameId3 == 'C') {
frame = decodeApicFrame(frameData, frameSize);
} else if (frameId0 == 'T') {
String id = frameId3 != 0 ?
String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3) :
String.format(Locale.US, "%c%c%c", frameId0, frameId1, frameId2);
frame = decodeTextInformationFrame(frameData, frameSize, id);
} else if (frameId0 == 'C' && frameId1 == 'O' && frameId2 == 'M' &&
(frameId3 == 'M' || frameId3 == 0)) {
frame = decodeCommentFrame(frameData, frameSize);
} else {
String id = frameId3 != 0 ?
String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3) :
String.format(Locale.US, "%c%c%c", frameId0, frameId1, frameId2);
frame = decodeBinaryFrame(frameData, frameSize, id);
}
id3Frames.add(frame);
} catch (UnsupportedEncodingException e) {
throw new MetadataDecoderException("Unsupported character encoding");
}
} }
} }
return new Metadata(id3Frames); return new Metadata(id3Frames);
} }
private static int indexOfEos(byte[] data, int fromIndex, int encoding) { /**
int terminationPos = indexOfZeroByte(data, fromIndex); * @param data A {@link ParsableByteArray} from which the header should be read.
* @return The parsed header, or null if the ID3 tag is unsupported.
// For single byte encoding charsets, we're done. * @throws MetadataDecoderException If the first three bytes differ from "ID3".
if (encoding == ID3_TEXT_ENCODING_ISO_8859_1 || encoding == ID3_TEXT_ENCODING_UTF_8) { */
return terminationPos; private static Id3Header decodeHeader(ParsableByteArray data)
throws MetadataDecoderException {
int id1 = data.readUnsignedByte();
int id2 = data.readUnsignedByte();
int id3 = data.readUnsignedByte();
if (id1 != 'I' || id2 != 'D' || id3 != '3') {
throw new MetadataDecoderException(String.format(Locale.US,
"Unexpected ID3 tag identifier, expected \"ID3\", actual \"%c%c%c\".", id1, id2, id3));
} }
// Otherwise ensure an even index and look for a second zero byte. int majorVersion = data.readUnsignedByte();
while (terminationPos < data.length - 1) { data.skipBytes(1); // Skip minor version.
if (terminationPos % 2 == 0 && data[terminationPos + 1] == (byte) 0) { int flags = data.readUnsignedByte();
return terminationPos; int framesSize = data.readSynchSafeInt();
if (majorVersion == 2) {
boolean isCompressed = (flags & 0x40) != 0;
if (isCompressed) {
Log.w(TAG, "Skipped ID3 tag with majorVersion=1 and undefined compression scheme");
return null;
} }
terminationPos = indexOfZeroByte(data, terminationPos + 1); } else if (majorVersion == 3) {
boolean hasExtendedHeader = (flags & 0x40) != 0;
if (hasExtendedHeader) {
int extendedHeaderSize = data.readInt(); // Size excluding size field.
data.skipBytes(extendedHeaderSize);
framesSize -= (extendedHeaderSize + 4);
}
} else if (majorVersion == 4) {
boolean hasExtendedHeader = (flags & 0x40) != 0;
if (hasExtendedHeader) {
int extendedHeaderSize = data.readSynchSafeInt(); // Size including size field.
data.skipBytes(extendedHeaderSize - 4);
framesSize -= extendedHeaderSize;
}
boolean hasFooter = (flags & 0x10) != 0;
if (hasFooter) {
framesSize -= 10;
}
} else {
Log.w(TAG, "Skipped ID3 tag with unsupported majorVersion=" + majorVersion);
return null;
} }
return data.length; // isUnsynchronized is advisory only in version 4. Frame level flags are used instead.
boolean isUnsynchronized = majorVersion < 4 && (flags & 0x80) != 0;
return new Id3Header(majorVersion, isUnsynchronized, framesSize);
} }
private static int indexOfZeroByte(byte[] data, int fromIndex) { private Id3Frame decodeFrame(Id3Header id3Header, ParsableByteArray id3Data)
for (int i = fromIndex; i < data.length; i++) { throws MetadataDecoderException {
if (data[i] == (byte) 0) { int frameId0 = id3Data.readUnsignedByte();
return i; int frameId1 = id3Data.readUnsignedByte();
int frameId2 = id3Data.readUnsignedByte();
int frameId3 = id3Header.majorVersion >= 3 ? id3Data.readUnsignedByte() : 0;
int frameSize;
if (id3Header.majorVersion == 4) {
frameSize = id3Data.readUnsignedIntToInt();
if ((frameSize & 0x808080L) == 0) {
// Parse the frame size as a syncsafe integer, as per the spec.
frameSize = (frameSize & 0xFF) | (((frameSize >> 8) & 0xFF) << 7)
| (((frameSize >> 16) & 0xFF) << 14) | (((frameSize >> 24) & 0xFF) << 21);
} else {
// Proceed using the frame size read as an unsigned integer.
Log.w(TAG, "Frame size not specified as syncsafe integer");
} }
} else if (id3Header.majorVersion == 3) {
frameSize = id3Data.readUnsignedIntToInt();
} else /* id3Header.majorVersion == 2 */ {
frameSize = id3Data.readUnsignedInt24();
} }
return data.length;
}
private static int delimiterLength(int encodingByte) {
return (encodingByte == ID3_TEXT_ENCODING_ISO_8859_1 || encodingByte == ID3_TEXT_ENCODING_UTF_8)
? 1 : 2;
}
/** int flags = id3Header.majorVersion >= 2 ? id3Data.readShort() : 0;
* @param id3Buffer A {@link ParsableByteArray} from which data should be read. if (frameId0 == 0 && frameId1 == 0 && frameId2 == 0 && frameId3 == 0 && frameSize == 0
* @return The parsed header. && flags == 0) {
* @throws MetadataDecoderException If ID3 file identifier != "ID3". // We must be reading zero padding at the end of the tag.
*/ id3Data.setPosition(id3Data.limit());
private static Id3Header decodeId3Header(ParsableByteArray id3Buffer) return null;
throws MetadataDecoderException {
int id1 = id3Buffer.readUnsignedByte();
int id2 = id3Buffer.readUnsignedByte();
int id3 = id3Buffer.readUnsignedByte();
if (id1 != 'I' || id2 != 'D' || id3 != '3') {
throw new MetadataDecoderException(String.format(Locale.US,
"Unexpected ID3 file identifier, expected \"ID3\", actual \"%c%c%c\".", id1, id2, id3));
} }
int majorVersion = id3Buffer.readUnsignedByte(); int nextFramePosition = id3Data.getPosition() + frameSize;
id3Buffer.skipBytes(1); // Skip minor version.
boolean isUnsynchronized = false;
int flags = id3Buffer.readUnsignedByte(); // Frame flags.
int framesSize = id3Buffer.readSynchSafeInt(); boolean isCompressed = false;
boolean isEncrypted = false;
boolean isUnsynchronized = false;
boolean hasDataLength = false;
boolean hasGroupIdentifier = false;
if (id3Header.majorVersion == 3) {
isCompressed = (flags & 0x0080) != 0;
isEncrypted = (flags & 0x0040) != 0;
hasGroupIdentifier = (flags & 0x0020) != 0;
hasDataLength = isCompressed;
} else if (id3Header.majorVersion == 4) {
hasGroupIdentifier = (flags & 0x0040) != 0;
isCompressed = (flags & 0x0008) != 0;
isEncrypted = (flags & 0x0004) != 0;
isUnsynchronized = (flags & 0x0002) != 0;
hasDataLength = (flags & 0x0001) != 0;
}
if (majorVersion < 4) { if (isCompressed || isEncrypted) {
// this flag is advisory in version 4, use the frame flags instead Log.w(TAG, "Skipping unsupported compressed or encrypted frame");
isUnsynchronized = (flags & 0x80) != 0; id3Data.setPosition(nextFramePosition);
return null;
} }
if (majorVersion == 3) { if (hasGroupIdentifier) {
// check for extended header frameSize--;
if ((flags & 0x40) != 0) { id3Data.skipBytes(1);
int extendedHeaderSize = id3Buffer.readInt(); // size excluding size field }
if (extendedHeaderSize == 6 || extendedHeaderSize == 10) { if (hasDataLength) {
id3Buffer.skipBytes(extendedHeaderSize); frameSize -= 4;
framesSize -= (extendedHeaderSize + 4); id3Data.skipBytes(4);
} }
} if (isUnsynchronized) {
} else if (majorVersion >= 4) { frameSize = removeUnsynchronization(id3Data, frameSize);
// check for extended header }
if ((flags & 0x40) != 0) {
int extendedHeaderSize = id3Buffer.readSynchSafeInt(); // size including size field
if (extendedHeaderSize > 4) {
id3Buffer.skipBytes(extendedHeaderSize - 4);
}
framesSize -= extendedHeaderSize;
}
// Check if footer presents. try {
if ((flags & 0x10) != 0) { Id3Frame frame;
framesSize -= 10; if (frameId0 == 'T' && frameId1 == 'X' && frameId2 == 'X' && frameId3 == 'X') {
frame = decodeTxxxFrame(id3Data, frameSize);
} else if (frameId0 == 'P' && frameId1 == 'R' && frameId2 == 'I' && frameId3 == 'V') {
frame = decodePrivFrame(id3Data, frameSize);
} else if (frameId0 == 'G' && frameId1 == 'E' && frameId2 == 'O' && frameId3 == 'B') {
frame = decodeGeobFrame(id3Data, frameSize);
} else if (frameId0 == 'A' && frameId1 == 'P' && frameId2 == 'I' && frameId3 == 'C') {
frame = decodeApicFrame(id3Data, frameSize);
} else if (frameId0 == 'T') {
String id = frameId3 != 0 ?
String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3) :
String.format(Locale.US, "%c%c%c", frameId0, frameId1, frameId2);
frame = decodeTextInformationFrame(id3Data, frameSize, id);
} else if (frameId0 == 'C' && frameId1 == 'O' && frameId2 == 'M' &&
(frameId3 == 'M' || frameId3 == 0)) {
frame = decodeCommentFrame(id3Data, frameSize);
} else {
String id = frameId3 != 0 ?
String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3) :
String.format(Locale.US, "%c%c%c", frameId0, frameId1, frameId2);
frame = decodeBinaryFrame(id3Data, frameSize, id);
} }
return frame;
} catch (UnsupportedEncodingException e) {
throw new MetadataDecoderException("Unsupported character encoding");
} finally {
id3Data.setPosition(nextFramePosition);
} }
return new Id3Header(majorVersion, isUnsynchronized, framesSize);
} }
private static TxxxFrame decodeTxxxFrame(ParsableByteArray id3Data, int frameSize) private static TxxxFrame decodeTxxxFrame(ParsableByteArray id3Data, int frameSize)
...@@ -368,34 +357,22 @@ public final class Id3Decoder implements MetadataDecoder { ...@@ -368,34 +357,22 @@ public final class Id3Decoder implements MetadataDecoder {
} }
/** /**
* Undo the unsynchronization applied to one or more frames. * Performs in-place removal of unsynchronization for {@code length} bytes starting from
* @param dataSource The original data, positioned at the beginning of a frame. * {@link ParsableByteArray#getPosition()}
* @param count The number of valid bytes in the frames to be processed. *
* @return replacement data for the frames. * @param data Contains the data to be processed.
* @param length The length of the data to be processed.
* @return The length of the data after processing.
*/ */
private static ParsableByteArray removeUnsynchronization(ParsableByteArray dataSource, int count) { private static int removeUnsynchronization(ParsableByteArray data, int length) {
byte[] source = dataSource.data; byte[] bytes = data.data;
int sourceIndex = dataSource.getPosition(); for (int i = data.getPosition(); i + 1 < length; i++) {
int limit = sourceIndex + count; if ((bytes[i] & 0xFF) == 0xFF && bytes[i + 1] == 0x00) {
byte[] dest = new byte[count]; System.arraycopy(bytes, i + 2, bytes, i + 1, length - i - 2);
int destIndex = 0; length--;
while (sourceIndex < limit) {
byte b = source[sourceIndex++];
if ((b & 0xFF) == 0xFF) {
int nextIndex = sourceIndex+1;
if (nextIndex < limit) {
int b2 = source[nextIndex];
if (b2 == 0) {
// skip the 0 byte
++sourceIndex;
}
}
} }
dest[destIndex++] = b;
} }
return length;
return new ParsableByteArray(dest, destIndex);
} }
/** /**
...@@ -418,6 +395,39 @@ public final class Id3Decoder implements MetadataDecoder { ...@@ -418,6 +395,39 @@ public final class Id3Decoder implements MetadataDecoder {
} }
} }
private static int indexOfEos(byte[] data, int fromIndex, int encoding) {
int terminationPos = indexOfZeroByte(data, fromIndex);
// For single byte encoding charsets, we're done.
if (encoding == ID3_TEXT_ENCODING_ISO_8859_1 || encoding == ID3_TEXT_ENCODING_UTF_8) {
return terminationPos;
}
// Otherwise ensure an even index and look for a second zero byte.
while (terminationPos < data.length - 1) {
if (terminationPos % 2 == 0 && data[terminationPos + 1] == (byte) 0) {
return terminationPos;
}
terminationPos = indexOfZeroByte(data, terminationPos + 1);
}
return data.length;
}
private static int indexOfZeroByte(byte[] data, int fromIndex) {
for (int i = fromIndex; i < data.length; i++) {
if (data[i] == (byte) 0) {
return i;
}
}
return data.length;
}
private static int delimiterLength(int encodingByte) {
return (encodingByte == ID3_TEXT_ENCODING_ISO_8859_1 || encodingByte == ID3_TEXT_ENCODING_UTF_8)
? 1 : 2;
}
public static String decodeGenre(int code) { public static String decodeGenre(int code) {
return (0 < code && code <= standardGenres.length) ? standardGenres[code - 1] : null; return (0 < code && code <= standardGenres.length) ? standardGenres[code - 1] : null;
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment