blob: 28b7a71fe6e0671041f4b455d157c8190595e2b0 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.tika.parser.mp3;
import java.io.IOException;
import java.io.InputStream;
import java.io.PushbackInputStream;
import org.apache.commons.io.IOUtils;
/**
* <p>
* A specialized stream class which can be used to extract single frames of MPEG
* audio files.
* </p>
* <p>
* Instances of this class are constructed with an underlying stream which
* should point to an audio file. Read operations are possible in the usual way.
* However, there are special methods for searching and extracting headers of
* MPEG frames. Some meta information of frames can be queried.
* </p>
*/
class MpegStream extends PushbackInputStream {
/**
* Bit rate table for MPEG V1, layer 1.
*/
private static final int[] BIT_RATE_MPEG1_L1 =
{0, 32000, 64000, 96000, 128000, 160000, 192000, 224000, 256000, 288000, 320000, 352000,
384000, 416000, 448000};
/**
* Bit rate table for MPEG V1, layer 2.
*/
private static final int[] BIT_RATE_MPEG1_L2 =
{0, 32000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 160000, 192000, 224000,
256000, 320000, 384000};
/**
* Bit rate table for MPEG V1, layer 3.
*/
private static final int[] BIT_RATE_MPEG1_L3 =
{0, 32000, 40000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 160000, 192000,
224000, 256000, 320000};
/**
* Bit rate table for MPEG V2/V2.5, layer 1.
*/
private static final int[] BIT_RATE_MPEG2_L1 =
{0, 32000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 144000, 160000, 176000,
192000, 224000, 256000};
/**
* Bit rate table for MPEG V2/V2.5, layer 2 and 3.
*/
private static final int[] BIT_RATE_MPEG2_L2 =
{0, 8000, 16000, 24000, 32000, 40000, 48000, 56000, 64000, 80000, 96000, 112000, 128000,
144000, 160000};
/**
* Sample rate table for MPEG V1.
*/
private static final int[] SAMPLE_RATE_MPEG1 = {44100, 48000, 32000};
/**
* Sample rate table for MPEG V2.
*/
private static final int[] SAMPLE_RATE_MPEG2 = {22050, 24000, 16000};
/**
* Sample rate table for MPEG V2.5.
*/
private static final int[] SAMPLE_RATE_MPEG2_5 = {11025, 12000, 8000};
/**
* Sample rate table for all MPEG versions.
*/
private static final int[][] SAMPLE_RATE = createSampleRateTable();
/**
* Constant for the number of samples for a layer 1 frame.
*/
private static final int SAMPLE_COUNT_L1 = 384;
/**
* Constant for the number of samples for a layer 2 or 3 frame.
*/
private static final int SAMPLE_COUNT_L2 = 1152;
/**
* Constant for the size of an MPEG frame header in bytes.
*/
private static final int HEADER_SIZE = 4;
/**
* The current MPEG header.
*/
private AudioFrame currentHeader;
/**
* A flag whether the end of the stream is reached.
*/
private boolean endOfStream;
/**
* Creates a new instance of {@code MpegStream} and initializes it with the
* underlying stream.
*
* @param in the underlying audio stream
*/
public MpegStream(InputStream in) {
super(in, 2 * HEADER_SIZE);
}
/**
* Calculates the bit rate based on the given parameters.
*
* @param mpegVer the MPEG version
* @param layer the layer
* @param code the code for the bit rate
* @return the bit rate in bits per second
*/
private static int calculateBitRate(int mpegVer, int layer, int code) {
int[] arr = null;
if (mpegVer == AudioFrame.MPEG_V1) {
switch (layer) {
case AudioFrame.LAYER_1:
arr = BIT_RATE_MPEG1_L1;
break;
case AudioFrame.LAYER_2:
arr = BIT_RATE_MPEG1_L2;
break;
case AudioFrame.LAYER_3:
arr = BIT_RATE_MPEG1_L3;
break;
}
} else {
if (layer == AudioFrame.LAYER_1) {
arr = BIT_RATE_MPEG2_L1;
} else {
arr = BIT_RATE_MPEG2_L2;
}
}
return arr[code];
}
/**
* Calculates the sample rate based on the given parameters.
*
* @param mpegVer the MPEG version
* @param code the code for the sample rate
* @return the sample rate in samples per second
*/
private static int calculateSampleRate(int mpegVer, int code) {
return SAMPLE_RATE[mpegVer][code];
}
/**
* Calculates the length of an MPEG frame based on the given parameters.
*
* @param layer the layer
* @param bitRate the bit rate
* @param sampleRate the sample rate
* @param padding the padding flag
* @return the length of the frame in bytes
*/
private static int calculateFrameLength(int layer, int bitRate, int sampleRate, int padding) {
if (layer == AudioFrame.LAYER_1) {
return (12 * bitRate / sampleRate + padding) * 4;
} else {
return 144 * bitRate / sampleRate + padding;
}
}
/**
* Calculates the duration of a MPEG frame based on the given parameters.
*
* @param layer the layer
* @param sampleRate the sample rate
* @return the duration of this frame in milliseconds
*/
private static float calculateDuration(int layer, int sampleRate) {
int sampleCount = (layer == AudioFrame.LAYER_1) ? SAMPLE_COUNT_L1 : SAMPLE_COUNT_L2;
return (1000.0f / sampleRate) * sampleCount;
}
/**
* Calculates the number of channels based on the given parameters.
*
* @param chan the code for the channels
* @return the number of channels
*/
private static int calculateChannels(int chan) {
return chan < 3 ? 2 : 1;
}
/**
* Creates the complete array for the sample rate mapping.
*
* @return the table for the sample rates
*/
private static int[][] createSampleRateTable() {
int[][] arr = new int[4][];
arr[AudioFrame.MPEG_V1] = SAMPLE_RATE_MPEG1;
arr[AudioFrame.MPEG_V2] = SAMPLE_RATE_MPEG2;
arr[AudioFrame.MPEG_V2_5] = SAMPLE_RATE_MPEG2_5;
return arr;
}
/**
* Searches for the next MPEG frame header from the current stream position
* on. This method advances the underlying input stream until it finds a
* valid frame header or the end of the stream is reached. In the former
* case a corresponding {@code AudioFrame} object is created. In the latter
* case there are no more headers, so the end of the stream is probably
* reached.
*
* @return the next {@code AudioFrame} or <b>null</b>
* @throws IOException if an IO error occurs
*/
public AudioFrame nextFrame() throws IOException {
AudioFrame frame = null;
while (!endOfStream && frame == null) {
findFrameSyncByte();
if (!endOfStream) {
HeaderBitField headerField = createHeaderField();
if (!endOfStream) {
frame = createHeader(headerField);
if (frame == null) {
pushBack(headerField);
}
}
}
}
currentHeader = frame;
return frame;
}
/**
* Skips the current MPEG frame. This method can be called after a valid
* MPEG header has been retrieved using {@code nextFrame()}. In this case
* the underlying stream is advanced to the end of the associated MPEG
* frame or until the EOF is reached. The return value indicates
* whether the full frame could be skipped.
*
* @return <b>true</b> if a frame could be skipped, <b>false</b> otherwise, perhaps EOF?
* @throws IOException if an IO error occurs
*/
public boolean skipFrame() throws IOException {
if (currentHeader != null) {
long toSkip = currentHeader.getLength() - HEADER_SIZE;
long skipped = IOUtils.skip(in, toSkip);
currentHeader = null;
if (skipped < toSkip) {
return false;
}
return true;
}
return false;
}
/**
* Advances the underlying stream until the first byte of frame sync is
* found.
*
* @throws IOException if an error occurs
*/
private void findFrameSyncByte() throws IOException {
boolean found = false;
while (!found && !endOfStream) {
if (nextByte() == 0xFF) {
found = true;
}
}
}
/**
* Creates a bit field for the MPEG frame header.
*
* @return the bit field
* @throws IOException if an error occurs
*/
private HeaderBitField createHeaderField() throws IOException {
HeaderBitField field = new HeaderBitField();
field.add(nextByte());
field.add(nextByte());
field.add(nextByte());
return field;
}
/**
* Creates an {@code AudioFrame} object based on the given header field. If
* the header field contains invalid values, result is <b>null</b>.
*
* @param bits the header bit field
* @return the {@code AudioFrame}
*/
private AudioFrame createHeader(HeaderBitField bits) {
if (bits.get(21, 23) != 7) {
return null;
}
int mpegVer = bits.get(19, 20);
int layer = bits.get(17, 18);
int bitRateCode = bits.get(12, 15);
int sampleRateCode = bits.get(10, 11);
int padding = bits.get(9);
if (mpegVer == 1 || layer == 0 || bitRateCode == 0 || bitRateCode == 15 ||
sampleRateCode == 3) {
// invalid header values
return null;
}
int bitRate = calculateBitRate(mpegVer, layer, bitRateCode);
int sampleRate = calculateSampleRate(mpegVer, sampleRateCode);
int length = calculateFrameLength(layer, bitRate, sampleRate, padding);
float duration = calculateDuration(layer, sampleRate);
int channels = calculateChannels(bits.get(6, 7));
return new AudioFrame(mpegVer, layer, bitRate, sampleRate, channels, length, duration);
}
/**
* Reads the next byte.
*
* @return the next byte
* @throws IOException if an error occurs
*/
private int nextByte() throws IOException {
int result = 0;
if (!endOfStream) {
result = read();
if (result == -1) {
endOfStream = true;
}
}
return endOfStream ? 0 : result;
}
/**
* Pushes the given header field back in the stream so that the bytes are
* read again. This method is called if an invalid header was detected. Then
* search has to continue at the next byte after the frame sync byte.
*
* @param field the header bit field with the invalid frame header
* @throws IOException if an error occurs
*/
private void pushBack(HeaderBitField field) throws IOException {
unread(field.toArray());
}
/**
* A class representing the bit field of an MPEG header. It allows
* convenient access to specific bit groups.
*/
private static class HeaderBitField {
/**
* The internal value.
*/
private int value;
/**
* Adds a byte to this field.
*
* @param b the byte to be added
*/
public void add(int b) {
value <<= 8;
value |= b;
}
/**
* Returns the value of the bit group from the given start and end
* index. E.g. ''from'' = 0, ''to'' = 3 will return the value of the
* first 4 bits.
*
* @param from index
* @param to the to index
* @return the value of this group of bits
*/
public int get(int from, int to) {
int shiftVal = value >> from;
int mask = (1 << (to - from + 1)) - 1;
return shiftVal & mask;
}
/**
* Returns the value of the bit with the given index. The bit index is
* 0-based. Result is either 0 or 1, depending on the value of this bit.
*
* @param bit the bit index
* @return the value of this bit
*/
public int get(int bit) {
return get(bit, bit);
}
/**
* Returns the internal value of this field as an array. The array
* contains 3 bytes.
*
* @return the internal value of this field as int array
*/
public byte[] toArray() {
byte[] result = new byte[3];
result[0] = (byte) get(16, 23);
result[1] = (byte) get(8, 15);
result[2] = (byte) get(0, 7);
return result;
}
}
}