Commit 7594f5b7 by Oliver Woodman

Further enhance ID3 decoder + support

parent e2ff401e
...@@ -16,6 +16,8 @@ ...@@ -16,6 +16,8 @@
package com.google.android.exoplayer2.extractor; package com.google.android.exoplayer2.extractor;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.id3.CommentFrame;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
...@@ -66,6 +68,25 @@ public final class GaplessInfoHolder { ...@@ -66,6 +68,25 @@ public final class GaplessInfoHolder {
} }
/** /**
* Populates the holder with data parsed from ID3 {@link Metadata}.
*
* @param metadata The metadata from which to parse the gapless information.
* @return Whether the holder was populated.
*/
public boolean setFromMetadata(Metadata metadata) {
for (int i = 0; i < metadata.length(); i++) {
Metadata.Entry entry = metadata.get(i);
if (entry instanceof CommentFrame) {
CommentFrame commentFrame = (CommentFrame) entry;
if (setFromComment(commentFrame.description, commentFrame.text)) {
return true;
}
}
}
return false;
}
/**
* Populates the holder with data parsed from a gapless playback comment (stored in an ID3 header * Populates the holder with data parsed from a gapless playback comment (stored in an ID3 header
* or MPEG 4 user data), if valid and non-zero. * or MPEG 4 user data), if valid and non-zero.
* *
......
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.extractor.mp3;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.MetadataDecoderException;
import com.google.android.exoplayer2.metadata.id3.Id3Decoder;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
/**
* Utility for parsing ID3 version 2 metadata in MP3 files.
*/
/* package */ final class Id3Util {
/**
* The maximum valid length for metadata in bytes.
*/
private static final int MAXIMUM_METADATA_SIZE = 3 * 1024 * 1024;
private static final int ID3_TAG = Util.getIntegerCodeForString("ID3");
/**
* Peeks data from the input and parses ID3 metadata, including gapless playback information.
*
* @param input The {@link ExtractorInput} from which data should be peeked.
* @return The metadata, if present, {@code null} otherwise.
* @throws IOException If an error occurred peeking from the input.
* @throws InterruptedException If the thread was interrupted.
*/
public static Metadata parseId3(ExtractorInput input)
throws IOException, InterruptedException {
Metadata result = null;
ParsableByteArray scratch = new ParsableByteArray(10);
int peekedId3Bytes = 0;
while (true) {
input.peekFully(scratch.data, 0, 10);
scratch.setPosition(0);
if (scratch.readUnsignedInt24() != ID3_TAG) {
break;
}
int majorVersion = scratch.readUnsignedByte();
int minorVersion = scratch.readUnsignedByte();
int flags = scratch.readUnsignedByte();
int length = scratch.readSynchSafeInt();
int frameLength = length + 10;
try {
if (canParseMetadata(majorVersion, minorVersion, flags, length)) {
input.resetPeekPosition();
byte[] frame = new byte[frameLength];
input.peekFully(frame, 0, frameLength);
return new Id3Decoder().decode(frame, frameLength);
} else {
input.advancePeekPosition(length);
}
} catch (MetadataDecoderException e) {
e.printStackTrace();
}
peekedId3Bytes += frameLength;
}
input.resetPeekPosition();
input.advancePeekPosition(peekedId3Bytes);
return result;
}
private static boolean canParseMetadata(int majorVersion, int minorVersion, int flags,
int length) {
return minorVersion != 0xFF && majorVersion >= 2 && majorVersion <= 4
&& length <= MAXIMUM_METADATA_SIZE
&& !(majorVersion == 2 && ((flags & 0x3F) != 0 || (flags & 0x40) != 0))
&& !(majorVersion == 3 && (flags & 0x1F) != 0)
&& !(majorVersion == 4 && (flags & 0x0F) != 0);
}
private Id3Util() {}
}
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.extractor.mp3; package com.google.android.exoplayer2.extractor.mp3;
import android.util.Log;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.ParserException;
...@@ -28,7 +29,8 @@ import com.google.android.exoplayer2.extractor.PositionHolder; ...@@ -28,7 +29,8 @@ import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.id3.CommentFrame; import com.google.android.exoplayer2.metadata.MetadataDecoderException;
import com.google.android.exoplayer2.metadata.id3.Id3Decoder;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.io.EOFException; import java.io.EOFException;
...@@ -51,6 +53,8 @@ public final class Mp3Extractor implements Extractor { ...@@ -51,6 +53,8 @@ public final class Mp3Extractor implements Extractor {
}; };
private static final String TAG = "Mp3Extractor";
/** /**
* The maximum number of bytes to search when synchronizing, before giving up. * The maximum number of bytes to search when synchronizing, before giving up.
*/ */
...@@ -59,6 +63,18 @@ public final class Mp3Extractor implements Extractor { ...@@ -59,6 +63,18 @@ public final class Mp3Extractor implements Extractor {
* The maximum number of bytes to peek when sniffing, excluding the ID3 header, before giving up. * The maximum number of bytes to peek when sniffing, excluding the ID3 header, before giving up.
*/ */
private static final int MAX_SNIFF_BYTES = MpegAudioHeader.MAX_FRAME_SIZE_BYTES; private static final int MAX_SNIFF_BYTES = MpegAudioHeader.MAX_FRAME_SIZE_BYTES;
/**
* First three bytes of a well formed ID3 tag header.
*/
private static final int ID3_TAG = Util.getIntegerCodeForString("ID3");
/**
* Length of an ID3 tag header.
*/
private static final int ID3_HEADER_LENGTH = 10;
/**
* Maximum length of data read into {@link #scratch}.
*/
private static final int SCRATCH_LENGTH = 10;
/** /**
* Mask that includes the audio header values that must match between frames. * Mask that includes the audio header values that must match between frames.
...@@ -100,7 +116,7 @@ public final class Mp3Extractor implements Extractor { ...@@ -100,7 +116,7 @@ public final class Mp3Extractor implements Extractor {
*/ */
public Mp3Extractor(long forcedFirstSampleTimestampUs) { public Mp3Extractor(long forcedFirstSampleTimestampUs) {
this.forcedFirstSampleTimestampUs = forcedFirstSampleTimestampUs; this.forcedFirstSampleTimestampUs = forcedFirstSampleTimestampUs;
scratch = new ParsableByteArray(4); scratch = new ParsableByteArray(SCRATCH_LENGTH);
synchronizedHeader = new MpegAudioHeader(); synchronizedHeader = new MpegAudioHeader();
gaplessInfoHolder = new GaplessInfoHolder(); gaplessInfoHolder = new GaplessInfoHolder();
basisTimeUs = C.TIME_UNSET; basisTimeUs = C.TIME_UNSET;
...@@ -147,7 +163,7 @@ public final class Mp3Extractor implements Extractor { ...@@ -147,7 +163,7 @@ public final class Mp3Extractor implements Extractor {
trackOutput.format(Format.createAudioSampleFormat(null, synchronizedHeader.mimeType, null, trackOutput.format(Format.createAudioSampleFormat(null, synchronizedHeader.mimeType, null,
Format.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, synchronizedHeader.channels, Format.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, synchronizedHeader.channels,
synchronizedHeader.sampleRate, Format.NO_VALUE, gaplessInfoHolder.encoderDelay, synchronizedHeader.sampleRate, Format.NO_VALUE, gaplessInfoHolder.encoderDelay,
gaplessInfoHolder.encoderPadding, null, null, 0, null, metadata)); gaplessInfoHolder.encoderPadding, null, null, 0, null, null));
} }
return readSample(input); return readSample(input);
} }
...@@ -202,18 +218,7 @@ public final class Mp3Extractor implements Extractor { ...@@ -202,18 +218,7 @@ public final class Mp3Extractor implements Extractor {
int searchLimitBytes = sniffing ? MAX_SNIFF_BYTES : MAX_SYNC_BYTES; int searchLimitBytes = sniffing ? MAX_SNIFF_BYTES : MAX_SYNC_BYTES;
input.resetPeekPosition(); input.resetPeekPosition();
if (input.getPosition() == 0) { if (input.getPosition() == 0) {
metadata = Id3Util.parseId3(input); peekId3Data(input);
if (!gaplessInfoHolder.hasGaplessInfo()) {
for (int i = 0; i < metadata.length(); i++) {
Metadata.Entry entry = metadata.get(i);
if (entry instanceof CommentFrame) {
CommentFrame commentFrame = (CommentFrame) entry;
if (gaplessInfoHolder.setFromComment(commentFrame.description, commentFrame.text)) {
break;
}
}
}
}
peekedId3Bytes = (int) input.getPeekPosition(); peekedId3Bytes = (int) input.getPeekPosition();
if (!sniffing) { if (!sniffing) {
input.skipFully(peekedId3Bytes); input.skipFully(peekedId3Bytes);
...@@ -268,6 +273,49 @@ public final class Mp3Extractor implements Extractor { ...@@ -268,6 +273,49 @@ public final class Mp3Extractor implements Extractor {
} }
/** /**
* Peeks ID3 data from the input, including gapless playback information.
*
* @param input The {@link ExtractorInput} from which data should be peeked.
* @throws IOException If an error occurred peeking from the input.
* @throws InterruptedException If the thread was interrupted.
*/
private void peekId3Data(ExtractorInput input) throws IOException, InterruptedException {
int peekedId3Bytes = 0;
while (true) {
input.peekFully(scratch.data, 0, ID3_HEADER_LENGTH);
scratch.setPosition(0);
if (scratch.readUnsignedInt24() != ID3_TAG) {
// Not an ID3 tag.
break;
}
scratch.skipBytes(3); // Skip major version, minor version and flags.
int framesLength = scratch.readSynchSafeInt();
int tagLength = ID3_HEADER_LENGTH + framesLength;
try {
if (metadata == null) {
byte[] id3Data = new byte[tagLength];
System.arraycopy(scratch.data, 0, id3Data, 0, ID3_HEADER_LENGTH);
input.peekFully(id3Data, ID3_HEADER_LENGTH, framesLength);
metadata = new Id3Decoder().decode(id3Data, tagLength);
if (metadata != null) {
gaplessInfoHolder.setFromMetadata(metadata);
}
} else {
input.advancePeekPosition(framesLength);
}
} catch (MetadataDecoderException e) {
Log.e(TAG, "Failed to decode ID3 tag", e);
}
peekedId3Bytes += tagLength;
}
input.resetPeekPosition();
input.advancePeekPosition(peekedId3Bytes);
}
/**
* Returns a {@link Seeker} to seek using metadata read from {@code input}, which should provide * Returns a {@link Seeker} to seek using metadata read from {@code input}, which should provide
* data from the start of the first frame in the stream. On returning, the input's position will * data from the start of the first frame in the stream. On returning, the input's position will
* be set to the start of the first frame of audio. * be set to the start of the first frame of audio.
......
...@@ -63,7 +63,7 @@ public final class Id3Decoder implements MetadataDecoder { ...@@ -63,7 +63,7 @@ public final class Id3Decoder implements MetadataDecoder {
int frameHeaderSize = id3Header.majorVersion == 2 ? 6 : 10; int frameHeaderSize = id3Header.majorVersion == 2 ? 6 : 10;
while (id3Data.bytesLeft() >= frameHeaderSize) { while (id3Data.bytesLeft() >= frameHeaderSize) {
Id3Frame frame = decodeFrame(id3Header, id3Data); Id3Frame frame = decodeFrame(id3Header.majorVersion, id3Data);
if (frame != null) { if (frame != null) {
id3Frames.add(frame); id3Frames.add(frame);
} }
...@@ -72,6 +72,40 @@ public final class Id3Decoder implements MetadataDecoder { ...@@ -72,6 +72,40 @@ public final class Id3Decoder implements MetadataDecoder {
return new Metadata(id3Frames); return new Metadata(id3Frames);
} }
// TODO: Move the following three methods nearer to the bottom of the file.
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;
}
/** /**
* @param data A {@link ParsableByteArray} from which the header should be read. * @param data A {@link ParsableByteArray} from which the header should be read.
* @return The parsed header, or null if the ID3 tag is unsupported. * @return The parsed header, or null if the ID3 tag is unsupported.
...@@ -126,15 +160,15 @@ public final class Id3Decoder implements MetadataDecoder { ...@@ -126,15 +160,15 @@ public final class Id3Decoder implements MetadataDecoder {
return new Id3Header(majorVersion, isUnsynchronized, framesSize); return new Id3Header(majorVersion, isUnsynchronized, framesSize);
} }
private Id3Frame decodeFrame(Id3Header id3Header, ParsableByteArray id3Data) private Id3Frame decodeFrame(int majorVersion, ParsableByteArray id3Data)
throws MetadataDecoderException { throws MetadataDecoderException {
int frameId0 = id3Data.readUnsignedByte(); int frameId0 = id3Data.readUnsignedByte();
int frameId1 = id3Data.readUnsignedByte(); int frameId1 = id3Data.readUnsignedByte();
int frameId2 = id3Data.readUnsignedByte(); int frameId2 = id3Data.readUnsignedByte();
int frameId3 = id3Header.majorVersion >= 3 ? id3Data.readUnsignedByte() : 0; int frameId3 = majorVersion >= 3 ? id3Data.readUnsignedByte() : 0;
int frameSize; int frameSize;
if (id3Header.majorVersion == 4) { if (majorVersion == 4) {
frameSize = id3Data.readUnsignedIntToInt(); frameSize = id3Data.readUnsignedIntToInt();
if ((frameSize & 0x808080L) == 0) { if ((frameSize & 0x808080L) == 0) {
// Parse the frame size as a syncsafe integer, as per the spec. // Parse the frame size as a syncsafe integer, as per the spec.
...@@ -144,13 +178,13 @@ public final class Id3Decoder implements MetadataDecoder { ...@@ -144,13 +178,13 @@ public final class Id3Decoder implements MetadataDecoder {
// Proceed using the frame size read as an unsigned integer. // Proceed using the frame size read as an unsigned integer.
Log.w(TAG, "Frame size not specified as syncsafe integer"); Log.w(TAG, "Frame size not specified as syncsafe integer");
} }
} else if (id3Header.majorVersion == 3) { } else if (majorVersion == 3) {
frameSize = id3Data.readUnsignedIntToInt(); frameSize = id3Data.readUnsignedIntToInt();
} else /* id3Header.majorVersion == 2 */ { } else /* id3Header.majorVersion == 2 */ {
frameSize = id3Data.readUnsignedInt24(); frameSize = id3Data.readUnsignedInt24();
} }
int flags = id3Header.majorVersion >= 2 ? id3Data.readShort() : 0; int flags = majorVersion >= 3 ? id3Data.readShort() : 0;
if (frameId0 == 0 && frameId1 == 0 && frameId2 == 0 && frameId3 == 0 && frameSize == 0 if (frameId0 == 0 && frameId1 == 0 && frameId2 == 0 && frameId3 == 0 && frameSize == 0
&& flags == 0) { && flags == 0) {
// We must be reading zero padding at the end of the tag. // We must be reading zero padding at the end of the tag.
...@@ -159,6 +193,9 @@ public final class Id3Decoder implements MetadataDecoder { ...@@ -159,6 +193,9 @@ public final class Id3Decoder implements MetadataDecoder {
} }
int nextFramePosition = id3Data.getPosition() + frameSize; int nextFramePosition = id3Data.getPosition() + frameSize;
if (nextFramePosition > id3Data.limit()) {
return null;
}
// Frame flags. // Frame flags.
boolean isCompressed = false; boolean isCompressed = false;
...@@ -166,12 +203,12 @@ public final class Id3Decoder implements MetadataDecoder { ...@@ -166,12 +203,12 @@ public final class Id3Decoder implements MetadataDecoder {
boolean isUnsynchronized = false; boolean isUnsynchronized = false;
boolean hasDataLength = false; boolean hasDataLength = false;
boolean hasGroupIdentifier = false; boolean hasGroupIdentifier = false;
if (id3Header.majorVersion == 3) { if (majorVersion == 3) {
isCompressed = (flags & 0x0080) != 0; isCompressed = (flags & 0x0080) != 0;
isEncrypted = (flags & 0x0040) != 0; isEncrypted = (flags & 0x0040) != 0;
hasGroupIdentifier = (flags & 0x0020) != 0; hasGroupIdentifier = (flags & 0x0020) != 0;
hasDataLength = isCompressed; hasDataLength = isCompressed;
} else if (id3Header.majorVersion == 4) { } else if (majorVersion == 4) {
hasGroupIdentifier = (flags & 0x0040) != 0; hasGroupIdentifier = (flags & 0x0040) != 0;
isCompressed = (flags & 0x0008) != 0; isCompressed = (flags & 0x0008) != 0;
isEncrypted = (flags & 0x0004) != 0; isEncrypted = (flags & 0x0004) != 0;
...@@ -199,26 +236,29 @@ public final class Id3Decoder implements MetadataDecoder { ...@@ -199,26 +236,29 @@ public final class Id3Decoder implements MetadataDecoder {
try { try {
Id3Frame frame; Id3Frame frame;
if (frameId0 == 'T' && frameId1 == 'X' && frameId2 == 'X' && frameId3 == 'X') { if (frameId0 == 'T' && frameId1 == 'X' && frameId2 == 'X'
&& (majorVersion == 2 || frameId3 == 'X')) {
frame = decodeTxxxFrame(id3Data, frameSize); frame = decodeTxxxFrame(id3Data, frameSize);
} else if (frameId0 == 'P' && frameId1 == 'R' && frameId2 == 'I' && frameId3 == 'V') { } else if (frameId0 == 'P' && frameId1 == 'R' && frameId2 == 'I' && frameId3 == 'V') {
frame = decodePrivFrame(id3Data, frameSize); frame = decodePrivFrame(id3Data, frameSize);
} else if (frameId0 == 'G' && frameId1 == 'E' && frameId2 == 'O' && frameId3 == 'B') { } else if (frameId0 == 'G' && frameId1 == 'E' && frameId2 == 'O'
&& (frameId3 == 'B' || majorVersion == 2)) {
frame = decodeGeobFrame(id3Data, frameSize); frame = decodeGeobFrame(id3Data, frameSize);
} else if (frameId0 == 'A' && frameId1 == 'P' && frameId2 == 'I' && frameId3 == 'C') { } else if (majorVersion == 2 ? (frameId0 == 'P' && frameId1 == 'I' && frameId2 == 'C')
frame = decodeApicFrame(id3Data, frameSize); : (frameId0 == 'A' && frameId1 == 'P' && frameId2 == 'I' && frameId3 == 'C')) {
frame = decodeApicFrame(id3Data, frameSize, majorVersion);
} else if (frameId0 == 'T') { } else if (frameId0 == 'T') {
String id = frameId3 != 0 ? String id = majorVersion == 2
String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3) : ? String.format(Locale.US, "%c%c%c", frameId0, frameId1, frameId2)
String.format(Locale.US, "%c%c%c", frameId0, frameId1, frameId2); : String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3);
frame = decodeTextInformationFrame(id3Data, frameSize, id); frame = decodeTextInformationFrame(id3Data, frameSize, id);
} else if (frameId0 == 'C' && frameId1 == 'O' && frameId2 == 'M' && } else if (frameId0 == 'C' && frameId1 == 'O' && frameId2 == 'M'
(frameId3 == 'M' || frameId3 == 0)) { && (frameId3 == 'M' || majorVersion == 2)) {
frame = decodeCommentFrame(id3Data, frameSize); frame = decodeCommentFrame(id3Data, frameSize);
} else { } else {
String id = frameId3 != 0 ? String id = majorVersion == 2
String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3) : ? String.format(Locale.US, "%c%c%c", frameId0, frameId1, frameId2)
String.format(Locale.US, "%c%c%c", frameId0, frameId1, frameId2); : String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3);
frame = decodeBinaryFrame(id3Data, frameSize, id); frame = decodeBinaryFrame(id3Data, frameSize, id);
} }
return frame; return frame;
...@@ -288,16 +328,29 @@ public final class Id3Decoder implements MetadataDecoder { ...@@ -288,16 +328,29 @@ public final class Id3Decoder implements MetadataDecoder {
return new GeobFrame(mimeType, filename, description, objectData); return new GeobFrame(mimeType, filename, description, objectData);
} }
private static ApicFrame decodeApicFrame(ParsableByteArray id3Data, int frameSize) private static ApicFrame decodeApicFrame(ParsableByteArray id3Data, int frameSize,
throws UnsupportedEncodingException { int majorVersion) throws UnsupportedEncodingException {
int encoding = id3Data.readUnsignedByte(); int encoding = id3Data.readUnsignedByte();
String charset = getCharsetName(encoding); String charset = getCharsetName(encoding);
byte[] data = new byte[frameSize - 1]; byte[] data = new byte[frameSize - 1];
id3Data.readBytes(data, 0, frameSize - 1); id3Data.readBytes(data, 0, frameSize - 1);
int mimeTypeEndIndex = indexOfZeroByte(data, 0); String mimeType;
String mimeType = new String(data, 0, mimeTypeEndIndex, "ISO-8859-1"); int mimeTypeEndIndex;
if (majorVersion == 2) {
mimeTypeEndIndex = 2;
mimeType = "image/" + new String(data, 0, 3, "ISO-8859-1").toLowerCase();
if (mimeType.equals("image/jpg")) {
mimeType = "image/jpeg";
}
} else {
mimeTypeEndIndex = indexOfZeroByte(data, 0);
mimeType = new String(data, 0, mimeTypeEndIndex, "ISO-8859-1").toLowerCase();
if (mimeType.indexOf('/') == -1) {
mimeType = "image/" + mimeType;
}
}
int pictureType = data[mimeTypeEndIndex + 1] & 0xFF; int pictureType = data[mimeTypeEndIndex + 1] & 0xFF;
...@@ -312,20 +365,6 @@ public final class Id3Decoder implements MetadataDecoder { ...@@ -312,20 +365,6 @@ public final class Id3Decoder implements MetadataDecoder {
return new ApicFrame(mimeType, description, pictureType, pictureData); return new ApicFrame(mimeType, description, pictureType, pictureData);
} }
private static TextInformationFrame decodeTextInformationFrame(ParsableByteArray id3Data,
int frameSize, String id) throws UnsupportedEncodingException {
int encoding = id3Data.readUnsignedByte();
String charset = getCharsetName(encoding);
byte[] data = new byte[frameSize - 1];
id3Data.readBytes(data, 0, frameSize - 1);
int descriptionEndIndex = indexOfEos(data, 0, encoding);
String description = new String(data, 0, descriptionEndIndex, charset);
return new TextInformationFrame(id, description);
}
private static CommentFrame decodeCommentFrame(ParsableByteArray id3Data, int frameSize) private static CommentFrame decodeCommentFrame(ParsableByteArray id3Data, int frameSize)
throws UnsupportedEncodingException { throws UnsupportedEncodingException {
int encoding = id3Data.readUnsignedByte(); int encoding = id3Data.readUnsignedByte();
...@@ -348,6 +387,20 @@ public final class Id3Decoder implements MetadataDecoder { ...@@ -348,6 +387,20 @@ public final class Id3Decoder implements MetadataDecoder {
return new CommentFrame(language, description, text); return new CommentFrame(language, description, text);
} }
private static TextInformationFrame decodeTextInformationFrame(ParsableByteArray id3Data,
int frameSize, String id) throws UnsupportedEncodingException {
int encoding = id3Data.readUnsignedByte();
String charset = getCharsetName(encoding);
byte[] data = new byte[frameSize - 1];
id3Data.readBytes(data, 0, frameSize - 1);
int descriptionEndIndex = indexOfEos(data, 0, encoding);
String description = new String(data, 0, descriptionEndIndex, charset);
return new TextInformationFrame(id, description);
}
private static BinaryFrame decodeBinaryFrame(ParsableByteArray id3Data, int frameSize, private static BinaryFrame decodeBinaryFrame(ParsableByteArray id3Data, int frameSize,
String id) { String id) {
byte[] frame = new byte[frameSize]; byte[] frame = new byte[frameSize];
...@@ -395,39 +448,6 @@ public final class Id3Decoder implements MetadataDecoder { ...@@ -395,39 +448,6 @@ 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;
}
private static final class Id3Header { private static final class Id3Header {
private final int majorVersion; private final int majorVersion;
......
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