/*
* Copyright (c) 2008, 2009, 2010, 2011 Denis Tulskiy
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* version 3 along with this work. If not, see <http://www.gnu.org/licenses/>.
*/
package com.tulskiy.musique.audio.formats.mp3;
import com.tulskiy.musique.audio.IcyInputStream;
import com.tulskiy.musique.playlist.Track;
import com.tulskiy.musique.playlist.TrackData;
import com.tulskiy.musique.util.AudioMath;
import javazoom.jl.decoder.*;
import javax.sound.sampled.AudioFormat;
import java.io.*;
import java.net.URI;
import java.net.URLDecoder;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* @Author: Denis Tulskiy
* @Date: 12.06.2009
*/
public class MP3Decoder implements com.tulskiy.musique.audio.Decoder {
private static final int DECODE_AFTER_SEEK = 9;
private LinkedHashMap<File, SeekTable> seekTableCache = new LinkedHashMap<File, SeekTable>(10, 0.7f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<File, SeekTable> eldest) {
return size() > 10;
}
};
private Bitstream bitstream;
private javazoom.jl.decoder.Decoder decoder;
private AudioFormat audioFormat;
private Header readFrame;
private Track track;
private long totalSamples;
private long streamSize;
private int samplesPerFrame;
private int sampleOffset = 0;
private int encDelay;
private long currentSample;
private boolean streaming = false;
private int oldBitrate;
private Header skipFrame() throws BitstreamException {
readFrame = bitstream.readFrame();
if (readFrame == null) {
return null;
}
bitstream.closeFrame();
return readFrame;
}
private int samplesToMinutes(long samples) {
return (int) (samples / track.getTrackData().getSampleRate() / 60f);
}
@SuppressWarnings({"ResultOfMethodCallIgnored"})
private boolean createBitstream(long targetSample) {
if (bitstream != null)
bitstream.close();
bitstream = null;
try {
File file = track.getTrackData().getFile();
FileInputStream fis = new FileInputStream(file);
//so we compute target frame first
targetSample += encDelay;
int targetFrame = (int) ((double) targetSample / samplesPerFrame);
sampleOffset = (int) (targetSample - targetFrame * samplesPerFrame) * audioFormat.getFrameSize();
//then we get the seek table or create it if needed
SeekTable seekTable = seekTableCache.get(file);
if (seekTable == null &&
samplesToMinutes(totalSamples) > 10) {
seekTable = new SeekTable();
seekTableCache.put(file, seekTable);
}
int currentFrame = 0;
//if we have a point, use it
if (seekTable != null) {
SeekTable.SeekPoint seekPoint = seekTable.get(targetFrame - DECODE_AFTER_SEEK);
fis.skip(seekPoint.offset);
currentFrame = seekPoint.frame;
}
//then we create the bitstream
bitstream = new Bitstream(fis);
decoder = new javazoom.jl.decoder.Decoder();
readFrame = null;
for (int i = currentFrame; i < targetFrame - DECODE_AFTER_SEEK; i++) {
skipFrame();
//store frame's position
if (seekTable != null && i % 10000 == 0) {
seekTable.add(i, streamSize - bitstream.getPosition());
}
}
//decode some frames to warm up the decoder
int framesToDecode = targetFrame < DECODE_AFTER_SEEK ? targetFrame : DECODE_AFTER_SEEK;
for (int i = 0; i < framesToDecode; i++) {
readFrame = bitstream.readFrame();
if (readFrame != null)
decoder.decodeFrame(readFrame, bitstream);
bitstream.closeFrame();
}
return true;
} catch (IOException ex) {
ex.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
public boolean open(final Track track) {
if (track == null)
return false;
this.track = track;
TrackData trackData = track.getTrackData();
try {
URI location = trackData.getLocation();
InputStream fis;
if (trackData.isFile()) {
logger.fine("Opening file: " + trackData.getFile());
streaming = false;
fis = new FileInputStream(trackData.getFile());
streamSize = trackData.getFile().length();
} else {
trackData.setCodec("MP3 Stream");
logger.fine("Opening stream: " + URLDecoder.decode(location.toString(), "utf8"));
streaming = true;
fis = IcyInputStream.create(track);
decoder = new Decoder();
}
bitstream = new Bitstream(fis);
Header header = bitstream.readFrame();
encDelay = header.getEncDelay();
int encPadding = header.getEncPadding();
int sampleRate = header.frequency();
int channels = header.mode() == Header.SINGLE_CHANNEL ? 1 : 2;
trackData.setSampleRate(sampleRate);
trackData.setChannels(channels);
oldBitrate = trackData.getBitrate();
samplesPerFrame = (int) (header.ms_per_frame() * header.frequency() / 1000);
audioFormat = new AudioFormat(sampleRate, 16, channels, true, false);
if (!streaming) {
totalSamples = samplesPerFrame * (header.max_number_of_frames(streamSize) + header.min_number_of_frames(streamSize)) / 2;
if (encPadding < totalSamples) {
totalSamples -= encPadding;
}
totalSamples -= encDelay;
bitstream.close();
fis.close();
createBitstream(0);
}
currentSample = 0;
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
public AudioFormat getAudioFormat() {
return audioFormat;
}
public void seekSample(long targetSample) {
currentSample = targetSample;
createBitstream(targetSample);
}
public int decode(byte[] buf) {
try {
readFrame = bitstream.readFrame();
if (readFrame == null) {
return -1;
}
if (readFrame.bitrate_instant() > 0)
track.getTrackData().setBitrate(readFrame.bitrate_instant() / 1000);
if (!streaming && currentSample >= totalSamples)
return -1;
SampleBuffer output = (SampleBuffer) decoder.decodeFrame(readFrame, bitstream);
bitstream.closeFrame();
int dataLen = output.getBufferLength() * 2;
int len = dataLen - sampleOffset;
if (dataLen == 0) {
return 0;
}
currentSample += AudioMath.bytesToSamples(len, audioFormat.getFrameSize());
if (!streaming && currentSample > totalSamples) {
len -= AudioMath.samplesToBytes(currentSample - totalSamples, audioFormat.getFrameSize());
}
toByteArray(output.getBuffer(), sampleOffset / 2, len / 2, buf);
sampleOffset = 0;
readFrame = null;
return len;
} catch (BitstreamException e) {
e.printStackTrace();
} catch (DecoderException e) {
e.printStackTrace();
}
return -1;
}
public void close() {
if (bitstream != null)
bitstream.close();
track.getTrackData().setBitrate(oldBitrate);
readFrame = null;
}
private void toByteArray(short[] samples, int offs, int len, byte[] dest) {
int idx = 0;
short s;
while (len-- > 0) {
s = samples[offs++];
dest[idx++] = (byte) s;
dest[idx++] = (byte) (s >>> 8);
}
}
}