/*
* Copyright (C) 2014 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.exoplayer.extractor.mp3;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.util.MpegAudioHeader;
import com.google.android.exoplayer.util.ParsableByteArray;
import com.google.android.exoplayer.util.Util;
/**
* MP3 seeker that uses metadata from a XING header.
*/
/* package */ final class XingSeeker implements Mp3Extractor.Seeker {
/**
* Returns a {@link XingSeeker} for seeking in the stream, if required information is present.
* Returns {@code null} if not. On returning, {@code frame}'s position is not specified so the
* caller should reset it.
*
* @param mpegAudioHeader The MPEG audio header associated with the frame.
* @param frame The data in this audio frame, with its position set to immediately after the
* 'XING' or 'INFO' tag.
* @param position The position (byte offset) of the start of this frame in the stream.
* @param inputLength The length of the stream in bytes.
* @return A {@link XingSeeker} for seeking in the stream, or {@code null} if the required
* information is not present.
*/
public static XingSeeker create(MpegAudioHeader mpegAudioHeader, ParsableByteArray frame,
long position, long inputLength) {
int samplesPerFrame = mpegAudioHeader.samplesPerFrame;
int sampleRate = mpegAudioHeader.sampleRate;
long firstFramePosition = position + mpegAudioHeader.frameSize;
int flags = frame.readInt();
int frameCount;
if ((flags & 0x01) != 0x01 || (frameCount = frame.readUnsignedIntToInt()) == 0) {
// If the frame count is missing/invalid, the header can't be used to determine the duration.
return null;
}
long durationUs = Util.scaleLargeTimestamp(frameCount, samplesPerFrame * 1000000L, sampleRate);
if ((flags & 0x06) != 0x06) {
// If the size in bytes or table of contents is missing, the stream is not seekable.
return new XingSeeker(inputLength, firstFramePosition, durationUs);
}
long sizeBytes = frame.readUnsignedIntToInt();
frame.skipBytes(1);
long[] tableOfContents = new long[99];
for (int i = 0; i < 99; i++) {
tableOfContents[i] = frame.readUnsignedByte();
}
// TODO: Handle encoder delay and padding in 3 bytes offset by xingBase + 213 bytes:
// delay = (frame.readUnsignedByte() << 4) + (frame.readUnsignedByte() >> 4);
// padding = ((frame.readUnsignedByte() & 0x0F) << 8) + frame.readUnsignedByte();
return new XingSeeker(inputLength, firstFramePosition, durationUs, tableOfContents, sizeBytes);
}
/**
* Entries are in the range [0, 255], but are stored as long integers for convenience.
*/
private final long[] tableOfContents;
private final long firstFramePosition;
private final long sizeBytes;
private final long durationUs;
private final long inputLength;
private XingSeeker(long inputLength, long firstFramePosition, long durationUs) {
this(inputLength, firstFramePosition, durationUs, null, 0);
}
private XingSeeker(long inputLength, long firstFramePosition, long durationUs,
long[] tableOfContents, long sizeBytes) {
this.tableOfContents = tableOfContents;
this.firstFramePosition = firstFramePosition;
this.sizeBytes = sizeBytes;
this.durationUs = durationUs;
this.inputLength = inputLength;
}
@Override
public boolean isSeekable() {
return tableOfContents != null;
}
@Override
public long getPosition(long timeUs) {
if (!isSeekable()) {
return firstFramePosition;
}
float percent = timeUs * 100f / durationUs;
float fx;
if (percent <= 0f) {
fx = 0f;
} else if (percent >= 100f) {
fx = 256f;
} else {
int a = (int) percent;
float fa, fb;
if (a == 0) {
fa = 0f;
} else {
fa = tableOfContents[a - 1];
}
if (a < 99) {
fb = tableOfContents[a];
} else {
fb = 256f;
}
fx = fa + (fb - fa) * (percent - a);
}
long position = (long) ((1f / 256) * fx * sizeBytes) + firstFramePosition;
return inputLength != C.LENGTH_UNBOUNDED ? Math.min(position, inputLength - 1) : position;
}
@Override
public long getTimeUs(long position) {
if (!isSeekable()) {
return 0L;
}
long offsetByte = 256 * (position - firstFramePosition) / sizeBytes;
int previousIndex = Util.binarySearchFloor(tableOfContents, offsetByte, true, false);
long previousTime = getTimeUsForTocIndex(previousIndex);
if (previousIndex == 98) {
return previousTime;
}
// Linearly interpolate the time taking into account the next entry.
long previousByte = previousIndex == -1 ? 0 : tableOfContents[previousIndex];
long nextByte = tableOfContents[previousIndex + 1];
long nextTime = getTimeUsForTocIndex(previousIndex + 1);
long timeOffset =
(nextTime - previousTime) * (offsetByte - previousByte) / (nextByte - previousByte);
return previousTime + timeOffset;
}
@Override
public long getDurationUs() {
return durationUs;
}
/**
* Returns the time in microseconds corresponding to an index in the table of contents.
*/
private long getTimeUsForTocIndex(int tocIndex) {
return durationUs * (tocIndex + 1) / 100;
}
}