/*
* Copyright (C) 2008 JavaZOOM
* 2013 Trilarion
*
* 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 General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.vorbis.spi.sampled.file;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.HashMap;
import java.util.StringTokenizer;
import java.util.logging.Logger;
import javax.sound.sampled.AudioFileFormat;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.UnsupportedAudioFileException;
import org.sound.SoundException;
import org.sound.TDebug;
import org.sound.sampled.file.TAudioFileReader;
import org.vorbis.jcraft.jogg.Packet;
import org.vorbis.jcraft.jogg.Page;
import org.vorbis.jcraft.jogg.StreamState;
import org.vorbis.jcraft.jogg.SyncState;
import org.vorbis.jcraft.jorbis.Block;
import org.vorbis.jcraft.jorbis.Comment;
import org.vorbis.jcraft.jorbis.DspState;
import org.vorbis.jcraft.jorbis.Info;
import org.vorbis.jcraft.jorbis.VorbisFile;
/**
* This class implements the AudioFileReader class and provides an Ogg Vorbis
* file reader for use with the Java Sound Service Provider Interface.
*/
public class VorbisAudioFileReader extends TAudioFileReader {
private SyncState oggSyncState_ = null;
private StreamState oggStreamState_ = null;
private Page oggPage_ = null;
private Packet oggPacket_ = null;
private Info vorbisInfo = null;
private Comment vorbisComment = null;
private DspState vorbisDspState = null;
private Block vorbisBlock = null;
private int bufferMultiple_ = 4;
private int bufferSize_ = bufferMultiple_ * 256 * 2;
private byte[] buffer = null;
private int bytes = 0;
private int index = 0;
private InputStream oggBitStream_ = null;
private static final int INITAL_READ_LENGTH = 64000;
private static final int MARK_LIMIT = INITAL_READ_LENGTH + 1;
/**
*
*/
public VorbisAudioFileReader() {
super(MARK_LIMIT, true);
}
/**
* Return the AudioFileFormat from the given file.
*/
@Override
public AudioFileFormat getAudioFileFormat(File file) throws UnsupportedAudioFileException, IOException {
if (TDebug.TraceAudioFileReader) {
TDebug.out("getAudioFileFormat(File file)");
}
try (InputStream inputStream = new BufferedInputStream(new FileInputStream(file))) {
inputStream.mark(MARK_LIMIT);
AudioFileFormat aff = getAudioFileFormat(inputStream);
inputStream.reset();
// Get Vorbis file info such as length in seconds.
VorbisFile vf = new VorbisFile(file.getAbsolutePath());
return getAudioFileFormat(inputStream, (int) file.length(), (int) Math.round((vf.time_total(-1)) * 1000));
} catch (SoundException e) {
throw new IOException(e.getMessage());
}
}
/**
* Return the AudioFileFormat from the given URL.
*/
@Override
public AudioFileFormat getAudioFileFormat(URL url) throws UnsupportedAudioFileException, IOException {
if (TDebug.TraceAudioFileReader) {
TDebug.out("getAudioFileFormat(URL url)");
}
InputStream inputStream = url.openStream();
try {
return getAudioFileFormat(inputStream);
} finally {
if (inputStream != null) {
inputStream.close();
}
}
}
/**
* Return the AudioFileFormat from the given InputStream.
*/
@Override
public AudioFileFormat getAudioFileFormat(InputStream inputStream) throws UnsupportedAudioFileException, IOException {
if (TDebug.TraceAudioFileReader) {
TDebug.out("getAudioFileFormat(InputStream inputStream)");
}
try {
if (!inputStream.markSupported()) {
inputStream = new BufferedInputStream(inputStream);
}
inputStream.mark(MARK_LIMIT);
return getAudioFileFormat(inputStream, AudioSystem.NOT_SPECIFIED, AudioSystem.NOT_SPECIFIED);
} finally {
inputStream.reset();
}
}
/**
* Return the AudioFileFormat from the given InputStream and length in
* bytes.
*/
@Override
public AudioFileFormat getAudioFileFormat(InputStream inputStream, long medialength) throws UnsupportedAudioFileException, IOException {
return getAudioFileFormat(inputStream, (int) medialength, AudioSystem.NOT_SPECIFIED);
}
/**
* Return the AudioFileFormat from the given InputStream, length in bytes
* and length in milliseconds.
*/
protected AudioFileFormat getAudioFileFormat(InputStream bitStream, int mediaLength, int totalms) throws UnsupportedAudioFileException, IOException {
HashMap aff_properties = new HashMap();
HashMap af_properties = new HashMap();
if (totalms == AudioSystem.NOT_SPECIFIED) {
totalms = 0;
}
if (totalms <= 0) {
totalms = 0;
} else {
aff_properties.put("duration", totalms * 1000);
}
oggBitStream_ = bitStream;
init_jorbis();
index = 0;
try {
readHeaders(aff_properties, af_properties);
} catch (IOException ioe) {
if (TDebug.TraceAudioFileReader) {
TDebug.out(ioe.getMessage());
}
throw new UnsupportedAudioFileException(ioe.getMessage());
}
String dmp = vorbisInfo.toString();
if (TDebug.TraceAudioFileReader) {
TDebug.out(dmp);
}
int ind = dmp.lastIndexOf("bitrate:");
int minbitrate = -1;
int nominalbitrate = -1;
int maxbitrate = -1;
if (ind != -1) {
dmp = dmp.substring(ind + 8, dmp.length());
StringTokenizer st = new StringTokenizer(dmp, ",");
if (st.hasMoreTokens()) {
minbitrate = Integer.parseInt(st.nextToken());
}
if (st.hasMoreTokens()) {
nominalbitrate = Integer.parseInt(st.nextToken());
}
if (st.hasMoreTokens()) {
maxbitrate = Integer.parseInt(st.nextToken());
}
}
if (nominalbitrate > 0) {
af_properties.put("bitrate", nominalbitrate);
}
af_properties.put("vbr", true);
if (minbitrate > 0) {
aff_properties.put("ogg.bitrate.min.bps", minbitrate);
}
if (maxbitrate > 0) {
aff_properties.put("ogg.bitrate.max.bps", maxbitrate);
}
if (nominalbitrate > 0) {
aff_properties.put("ogg.bitrate.nominal.bps", nominalbitrate);
}
if (vorbisInfo.channels > 0) {
aff_properties.put("ogg.channels", vorbisInfo.channels);
}
if (vorbisInfo.rate > 0) {
aff_properties.put("ogg.frequency.hz", vorbisInfo.rate);
}
if (mediaLength > 0) {
aff_properties.put("ogg.length.bytes", mediaLength);
}
aff_properties.put("ogg.version", vorbisInfo.version);
//AudioFormat.Encoding encoding = VorbisEncoding.VORBISENC;
//AudioFormat format = new VorbisAudioFormat(encoding, vorbisInfo.rate, AudioSystem.NOT_SPECIFIED, vorbisInfo.channels, AudioSystem.NOT_SPECIFIED, AudioSystem.NOT_SPECIFIED, true,af_properties);
// Patch from MS to ensure more SPI compatibility ...
float frameRate = -1;
if (nominalbitrate > 0) {
frameRate = nominalbitrate / 8;
} else if (minbitrate > 0) {
frameRate = minbitrate / 8;
}
AudioFormat.Encoding encoding = VorbisEncoding.VORBISENC;
// New Patch from MS:
AudioFormat format = new VorbisAudioFormat(encoding, vorbisInfo.rate, AudioSystem.NOT_SPECIFIED, vorbisInfo.channels, 1, frameRate, false, af_properties);
// Patch end
return new VorbisAudioFileFormat(VorbisFileFormatType.OGG, format, AudioSystem.NOT_SPECIFIED, mediaLength, aff_properties);
}
/**
* Return the AudioInputStream from the given InputStream.
*/
@Override
public AudioInputStream getAudioInputStream(InputStream inputStream) throws UnsupportedAudioFileException, IOException {
if (TDebug.TraceAudioFileReader) {
TDebug.out("getAudioInputStream(InputStream inputStream)");
}
return getAudioInputStream(inputStream, AudioSystem.NOT_SPECIFIED, AudioSystem.NOT_SPECIFIED);
}
/**
* Return the AudioInputStream from the given InputStream.
*/
public AudioInputStream getAudioInputStream(InputStream inputStream, int medialength, int totalms) throws UnsupportedAudioFileException, IOException {
if (TDebug.TraceAudioFileReader) {
TDebug.out("getAudioInputStream(InputStream inputStreamint medialength, int totalms)");
}
try {
if (!inputStream.markSupported()) {
inputStream = new BufferedInputStream(inputStream);
}
inputStream.mark(MARK_LIMIT);
AudioFileFormat audioFileFormat = getAudioFileFormat(inputStream, medialength, totalms);
inputStream.reset();
return new AudioInputStream(inputStream, audioFileFormat.getFormat(), audioFileFormat.getFrameLength());
} catch (UnsupportedAudioFileException | IOException e) {
inputStream.reset();
throw e;
}
}
/**
* Return the AudioInputStream from the given File.
*/
@Override
public AudioInputStream getAudioInputStream(File file) throws UnsupportedAudioFileException, IOException {
if (TDebug.TraceAudioFileReader) {
TDebug.out("getAudioInputStream(File file)");
}
InputStream inputStream = new FileInputStream(file);
try {
return getAudioInputStream(inputStream);
} catch (UnsupportedAudioFileException | IOException e) {
if (inputStream != null) {
inputStream.close();
}
throw e;
}
}
/**
* Return the AudioInputStream from the given URL.
*/
@Override
public AudioInputStream getAudioInputStream(URL url) throws UnsupportedAudioFileException, IOException {
if (TDebug.TraceAudioFileReader) {
TDebug.out("getAudioInputStream(URL url)");
}
InputStream inputStream = url.openStream();
try {
return getAudioInputStream(inputStream);
} catch (UnsupportedAudioFileException | IOException e) {
if (inputStream != null) {
inputStream.close();
}
throw e;
}
}
/**
* Reads headers and comments.
*/
private void readHeaders(HashMap aff_properties, HashMap af_properties) throws IOException {
if (TDebug.TraceAudioConverter) {
TDebug.out("readHeaders(");
}
index = oggSyncState_.buffer(bufferSize_);
buffer = oggSyncState_.data;
bytes = readFromStream(buffer, index, bufferSize_);
if (bytes == -1) {
if (TDebug.TraceAudioConverter) {
TDebug.out("Cannot get any data from selected Ogg bitstream.");
}
throw new IOException("Cannot get any data from selected Ogg bitstream.");
}
oggSyncState_.wrote(bytes);
if (oggSyncState_.pageout(oggPage_) != 1) {
if (bytes < bufferSize_) {
throw new IOException("EOF");
}
if (TDebug.TraceAudioConverter) {
TDebug.out("Input does not appear to be an Ogg bitstream.");
}
throw new IOException("Input does not appear to be an Ogg bitstream.");
}
oggStreamState_.init(oggPage_.serialno());
vorbisInfo.init();
vorbisComment.init();
aff_properties.put("ogg.serial", oggPage_.serialno());
if (oggStreamState_.pagein(oggPage_) < 0) {
// error; stream version mismatch perhaps
if (TDebug.TraceAudioConverter) {
TDebug.out("Error reading first page of Ogg bitstream data.");
}
throw new IOException("Error reading first page of Ogg bitstream data.");
}
if (oggStreamState_.packetout(oggPacket_) != 1) {
// no page? must not be vorbis
if (TDebug.TraceAudioConverter) {
TDebug.out("Error reading initial header packet.");
}
throw new IOException("Error reading initial header packet.");
}
if (vorbisInfo.synthesis_headerin(vorbisComment, oggPacket_) < 0) {
// error case; not a vorbis header
if (TDebug.TraceAudioConverter) {
TDebug.out("This Ogg bitstream does not contain Vorbis audio data.");
}
throw new IOException("This Ogg bitstream does not contain Vorbis audio data.");
}
int i = 0;
while (i < 2) {
while (i < 2) {
int result = oggSyncState_.pageout(oggPage_);
if (result == 0) {
break;
} // Need more data
if (result == 1) {
oggStreamState_.pagein(oggPage_);
while (i < 2) {
result = oggStreamState_.packetout(oggPacket_);
if (result == 0) {
break;
}
if (result == -1) {
if (TDebug.TraceAudioConverter) {
TDebug.out("Corrupt secondary header. Exiting.");
}
throw new IOException("Corrupt secondary header. Exiting.");
}
vorbisInfo.synthesis_headerin(vorbisComment, oggPacket_);
i++;
}
}
}
index = oggSyncState_.buffer(bufferSize_);
buffer = oggSyncState_.data;
bytes = readFromStream(buffer, index, bufferSize_);
if (bytes == -1) {
break;
}
if (bytes == 0 && i < 2) {
if (TDebug.TraceAudioConverter) {
TDebug.out("End of file before finding all Vorbis headers!");
}
throw new IOException("End of file before finding all Vorbis headers!");
}
oggSyncState_.wrote(bytes);
}
// Read Ogg Vorbis comments.
byte[][] ptr = vorbisComment.user_comments;
String currComment;
int c = 0;
for (int j = 0; j < ptr.length; j++) {
if (ptr[j] == null) {
break;
}
currComment = (new String(ptr[j], 0, ptr[j].length - 1, "UTF-8")).trim();
if (TDebug.TraceAudioConverter) {
TDebug.out(currComment);
}
if (currComment.toLowerCase().startsWith("artist")) {
aff_properties.put("author", currComment.substring(7));
} else if (currComment.toLowerCase().startsWith("title")) {
aff_properties.put("title", currComment.substring(6));
} else if (currComment.toLowerCase().startsWith("album")) {
aff_properties.put("album", currComment.substring(6));
} else if (currComment.toLowerCase().startsWith("date")) {
aff_properties.put("date", currComment.substring(5));
} else if (currComment.toLowerCase().startsWith("copyright")) {
aff_properties.put("copyright", currComment.substring(10));
} else if (currComment.toLowerCase().startsWith("comment")) {
aff_properties.put("comment", currComment.substring(8));
} else if (currComment.toLowerCase().startsWith("genre")) {
aff_properties.put("ogg.comment.genre", currComment.substring(6));
} else if (currComment.toLowerCase().startsWith("tracknumber")) {
aff_properties.put("ogg.comment.track", currComment.substring(12));
} else {
c++;
aff_properties.put("ogg.comment.ext." + c, currComment);
}
aff_properties.put("ogg.comment.encodedby", new String(vorbisComment.vendor, 0, vorbisComment.vendor.length - 1));
}
}
/**
* Reads from the oggBitStream_ a specified number of Bytes(bufferSize_)
* worth starting at index and puts them in the specified buffer[].
*
* @return the number of bytes read or -1 if error.
*/
private int readFromStream(byte[] buffer, int index, int bufferSize_) {
int bytes;
try {
bytes = oggBitStream_.read(buffer, index, bufferSize_);
} catch (Exception e) {
if (TDebug.TraceAudioFileReader) {
TDebug.out("Cannot Read Selected Song");
}
bytes = -1;
}
return bytes;
}
/**
* Initializes all the jOrbis and jOgg vars that are used for song playback.
*/
private void init_jorbis() {
oggSyncState_ = new SyncState();
oggStreamState_ = new StreamState();
oggPage_ = new Page();
oggPacket_ = new Packet();
vorbisInfo = new Info();
vorbisComment = new Comment();
vorbisDspState = new DspState();
vorbisBlock = new Block(vorbisDspState);
buffer = null;
bytes = 0;
oggSyncState_.init();
}
private static final Logger LOG = Logger.getLogger(VorbisAudioFileReader.class.getName());
}