/*
* Copyright © Jon Kristensen, 2008.
* All rights reserved.
*
* This is version 1.0 of this source code, made to work with JOrbis 1.x. The
* last time this file was updated was the 15th of March, 2008.
*
* Version history:
*
* 1.0: Initial release.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of jonkri.com nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
import com.jcraft.jogg.*;
import com.jcraft.jorbis.*;
import java.io.InputStream;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.net.UnknownServiceException;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;
/**
* The ExamplePlayer
thread class will simply download and play
* OGG media. All you need to do is supply a valid URL as the first argument.
*
* @author Jon Kristensen
* @version 1.0
*/
public class ExamplePlayer extends Thread
{
// If you wish to debug this source, please set the variable below to true.
private final boolean debugMode = true;
/*
* URLConnection and InputStream objects so that we can open a connection to
* the media file.
*/
private URLConnection urlConnection = null;
private InputStream inputStream = null;
/*
* We need a buffer, it's size, a count to know how many bytes we have read
* and an index to keep track of where we are. This is standard networking
* stuff used with read().
*/
byte[] buffer = null;
int bufferSize = 2048;
int count = 0;
int index = 0;
/*
* JOgg and JOrbis require fields for the converted buffer. This is a buffer
* that is modified in regards to the number of audio channels. Naturally,
* it will also need a size.
*/
byte[] convertedBuffer;
int convertedBufferSize;
// The source data line onto which data can be written.
private SourceDataLine outputLine = null;
// A three-dimensional an array with PCM information.
private float[][][] pcmInfo;
// The index for the PCM information.
private int[] pcmIndex;
// Here are the four required JOgg objects...
private Packet joggPacket = new Packet();
private Page joggPage = new Page();
private StreamState joggStreamState = new StreamState();
private SyncState joggSyncState = new SyncState();
// ... followed by the four required JOrbis objects.
private DspState jorbisDspState = new DspState();
private Block jorbisBlock = new Block(jorbisDspState);
private Comment jorbisComment = new Comment();
private Info jorbisInfo = new Info();
/**
* The programs main()
method. Will read the first
* command-line argument and use it as URL, after which it will start the
* thread.
*
* @param args command-line arguments
*/
public static void main(String[] args)
{
// Set the URL as the first argument, if any.
String url = args.length > 0 ? url = args[0] : null;
/*
* If the url variable is set, start the thread. If not, give an error
* and die.
*/
if(url != null)
{
ExamplePlayer examplePlayer = new ExamplePlayer(url);
examplePlayer.start();
}
else
{
System.err.println("Please provide an argument with the file to "
+ "play.");
}
}
/**
* The constructor; will configure the InputStream
.
*
* @param pUrl the URL to be opened
*/
ExamplePlayer(String pUrl)
{
configureInputStream(getUrl(pUrl));
}
/**
* Given a string, getUrl()
will return an URL object.
*
* @param pUrl the URL to be opened
* @return the URL object
*/
public URL getUrl(String pUrl)
{
URL url = null;
try
{
url = new URL(pUrl);
}
catch(MalformedURLException exception)
{
System.err.println("Malformed \"url\" parameter: \"" + pUrl + "\"");
}
return url;
}
/**
* Sets the inputStream
object by taking an URL, opens a
* connection to it and get the InputStream
.
*
* @param pUrl the url to the media file
*/
private void configureInputStream(URL pUrl)
{
// Try to open a connection to the URL.
try
{
urlConnection = pUrl.openConnection();
}
catch(UnknownServiceException exception)
{
System.err.println("The protocol does not support input.");
}
catch(IOException exception)
{
System.err.println("An I/O error occoured while trying create the "
+ "URL connection.");
}
// If we have a connection, try to create an input stream.
if(urlConnection != null)
{
try
{
inputStream = urlConnection.getInputStream();
}
catch(IOException exception)
{
System.err
.println("An I/O error occoured while trying to get an "
+ "input stream from the URL.");
System.err.println(exception);
}
}
}
/**
* This method is probably easiest understood by looking at the body.
* However, it will - if no problems occur - call methods to initialize the
* JOgg JOrbis libraries, read the header, initialize the sound system, read
* the body of the stream and clean up.
*/
public void run()
{
// Check that we got an InputStream.
if(inputStream == null)
{
System.err.println("We don't have an input stream and therefor "
+ "cannot continue.");
return;
}
// Initialize JOrbis.
initializeJOrbis();
/*
* If we can read the header, we try to inialize the sound system. If we
* could initialize the sound system, we try to read the body.
*/
if(readHeader())
{
if(initializeSound())
{
readBody();
}
}
// Afterwards, we clean up.
cleanUp();
}
/**
* Initializes JOrbis. First, we initialize the SyncState
* object. After that, we prepare the SyncState
buffer. Then
* we "initialize" our buffer, taking the data in SyncState
.
*/
private void initializeJOrbis()
{
debugOutput("Initializing JOrbis.");
// Initialize SyncState
joggSyncState.init();
// Prepare the to SyncState internal buffer
joggSyncState.buffer(bufferSize);
/*
* Fill the buffer with the data from SyncState's internal buffer. Note
* how the size of this new buffer is different from bufferSize.
*/
buffer = joggSyncState.data;
debugOutput("Done initializing JOrbis.");
}
/**
* This method reads the header of the stream, which consists of three
* packets.
*
* @return true if the header was successfully read, false otherwise
*/
private boolean readHeader()
{
debugOutput("Starting to read the header.");
/*
* Variable used in loops below. While we need more data, we will
* continue to read from the InputStream.
*/
boolean needMoreData = true;
/*
* We will read the first three packets of the header. We start off by
* defining packet = 1 and increment that value whenever we have
* successfully read another packet.
*/
int packet = 1;
/*
* While we need more data (which we do until we have read the three
* header packets), this loop reads from the stream and has a big
* switch
statement which does what it's supposed to do in
* regards to the current packet.
*/
while(needMoreData)
{
// Read from the InputStream.
try
{
count = inputStream.read(buffer, index, bufferSize);
}
catch(IOException exception)
{
System.err.println("Could not read from the input stream.");
System.err.println(exception);
}
// We let SyncState know how many bytes we read.
joggSyncState.wrote(count);
/*
* We want to read the first three packets. For the first packet, we
* need to initialize the StreamState object and a couple of other
* things. For packet two and three, the procedure is the same: we
* take out a page, and then we take out the packet.
*/
switch(packet)
{
// The first packet.
case 1:
{
// We take out a page.
switch(joggSyncState.pageout(joggPage))
{
// If there is a hole in the data, we must exit.
case -1:
{
System.err.println("There is a hole in the first "
+ "packet data.");
return false;
}
// If we need more data, we break to get it.
case 0:
{
break;
}
/*
* We got where we wanted. We have successfully read the
* first packet, and we will now initialize and reset
* StreamState, and initialize the Info and Comment
* objects. Afterwards we will check that the page
* doesn't contain any errors, that the packet doesn't
* contain any errors and that it's Vorbis data.
*/
case 1:
{
// Initializes and resets StreamState.
joggStreamState.init(joggPage.serialno());
joggStreamState.reset();
// Initializes the Info and Comment objects.
jorbisInfo.init();
jorbisComment.init();
// Check the page (serial number and stuff).
if(joggStreamState.pagein(joggPage) == -1)
{
System.err.println("We got an error while "
+ "reading the first header page.");
return false;
}
/*
* Try to extract a packet. All other return values
* than "1" indicates there's something wrong.
*/
if(joggStreamState.packetout(joggPacket) != 1)
{
System.err.println("We got an error while "
+ "reading the first header packet.");
return false;
}
/*
* We give the packet to the Info object, so that it
* can extract the Comment-related information,
* among other things. If this fails, it's not
* Vorbis data.
*/
if(jorbisInfo.synthesis_headerin(jorbisComment,
joggPacket) < 0)
{
System.err.println("We got an error while "
+ "interpreting the first packet. "
+ "Apparantly, it's not Vorbis data.");
return false;
}
// We're done here, let's increment "packet".
packet++;
break;
}
}
/*
* Note how we are NOT breaking here if we have proceeded to
* the second packet. We don't want to read from the input
* stream again if it's not necessary.
*/
if(packet == 1) break;
}
// The code for the second and third packets follow.
case 2: case 3:
{
// Try to get a new page again.
switch(joggSyncState.pageout(joggPage))
{
// If there is a hole in the data, we must exit.
case -1:
{
System.err.println("There is a hole in the second "
+ "or third packet data.");
return false;
}
// If we need more data, we break to get it.
case 0:
{
break;
}
/*
* Here is where we take the page, extract a packet and
* and (if everything goes well) give the information to
* the Info and Comment objects like we did above.
*/
case 1:
{
// Share the page with the StreamState object.
joggStreamState.pagein(joggPage);
/*
* Just like the switch(...packetout...) lines
* above.
*/
switch(joggStreamState.packetout(joggPacket))
{
// If there is a hole in the data, we must exit.
case -1:
{
System.err
.println("There is a hole in the first"
+ "packet data.");
return false;
}
// If we need more data, we break to get it.
case 0:
{
break;
}
// We got a packet, let's process it.
case 1:
{
/*
* Like above, we give the packet to the
* Info and Comment objects.
*/
jorbisInfo.synthesis_headerin(
jorbisComment, joggPacket);
// Increment packet.
packet++;
if(packet == 4)
{
/*
* There is no fourth packet, so we will
* just end the loop here.
*/
needMoreData = false;
}
break;
}
}
break;
}
}
break;
}
}
// We get the new index and an updated buffer.
index = joggSyncState.buffer(bufferSize);
buffer = joggSyncState.data;
/*
* If we need more data but can't get it, the stream doesn't contain
* enough information.
*/
if(count == 0 && needMoreData)
{
System.err.println("Not enough header data was supplied.");
return false;
}
}
debugOutput("Finished reading the header.");
return true;
}
/**
* This method starts the sound system. It starts with initializing the
* DspState
object, after which it sets up the
* Block
object. Last but not least, it opens a line to the
* source data line.
*
* @return true if the sound system was successfully started, false
* otherwise
*/
private boolean initializeSound()
{
debugOutput("Initializing the sound system.");
// This buffer is used by the decoding method.
convertedBufferSize = bufferSize * 2;
convertedBuffer = new byte[convertedBufferSize];
// Initializes the DSP synthesis.
jorbisDspState.synthesis_init(jorbisInfo);
// Make the Block object aware of the DSP.
jorbisBlock.init(jorbisDspState);
// Wee need to know the channels and rate.
int channels = jorbisInfo.channels;
int rate = jorbisInfo.rate;
// Creates an AudioFormat object and a DataLine.Info object.
AudioFormat audioFormat = new AudioFormat((float) rate, 16, channels,
true, false);
DataLine.Info datalineInfo = new DataLine.Info(SourceDataLine.class,
audioFormat, AudioSystem.NOT_SPECIFIED);
// Check if the line is supported.
if(!AudioSystem.isLineSupported(datalineInfo))
{
System.err.println("Audio output line is not supported.");
return false;
}
/*
* Everything seems to be alright. Let's try to open a line with the
* specified format and start the source data line.
*/
try
{
outputLine = (SourceDataLine) AudioSystem.getLine(datalineInfo);
outputLine.open(audioFormat);
}
catch(LineUnavailableException exception)
{
System.out.println("The audio output line could not be opened due "
+ "to resource restrictions.");
System.err.println(exception);
return false;
}
catch(IllegalStateException exception)
{
System.out.println("The audio output line is already open.");
System.err.println(exception);
return false;
}
catch(SecurityException exception)
{
System.out.println("The audio output line could not be opened due "
+ "to security restrictions.");
System.err.println(exception);
return false;
}
// Start it.
outputLine.start();
/*
* We create the PCM variables. The index is an array with the same
* length as the number of audio channels.
*/
pcmInfo = new float[1][][];
pcmIndex = new int[jorbisInfo.channels];
debugOutput("Done initializing the sound system.");
return true;
}
/**
* This method reads the entire stream body. Whenever it extracts a packet,
* it will decode it by calling decodeCurrentPacket()
.
*/
private void readBody()
{
debugOutput("Reading the body.");
/*
* Variable used in loops below, like in readHeader(). While we need
* more data, we will continue to read from the InputStream.
*/
boolean needMoreData = true;
while(needMoreData)
{
switch(joggSyncState.pageout(joggPage))
{
// If there is a hole in the data, we just proceed.
case -1:
{
debugOutput("There is a hole in the data. We proceed.");
}
// If we need more data, we break to get it.
case 0:
{
break;
}
// If we have successfully checked out a page, we continue.
case 1:
{
// Give the page to the StreamState object.
joggStreamState.pagein(joggPage);
// If granulepos() returns "0", we don't need more data.
if(joggPage.granulepos() == 0)
{
needMoreData = false;
break;
}
// Here is where we process the packets.
processPackets: while(true)
{
switch(joggStreamState.packetout(joggPacket))
{
// Is it a hole in the data?
case -1:
{
debugOutput("There is a hole in the data, we "
+ "continue though.");
}
// If we need more data, we break to get it.
case 0:
{
break processPackets;
}
/*
* If we have the data we need, we decode the
* packet.
*/
case 1:
{
decodeCurrentPacket();
}
}
}
/*
* If the page is the end-of-stream, we don't need more
* data.
*/
if(joggPage.eos() != 0) needMoreData = false;
}
}
// If we need more data...
if(needMoreData)
{
// We get the new index and an updated buffer.
index = joggSyncState.buffer(bufferSize);
buffer = joggSyncState.data;
// Read from the InputStream.
try
{
count = inputStream.read(buffer, index, bufferSize);
}
catch(Exception e)
{
System.err.println(e);
return;
}
// We let SyncState know how many bytes we read.
joggSyncState.wrote(count);
// There's no more data in the stream.
if(count == 0) needMoreData = false;
}
}
debugOutput("Done reading the body.");
}
/**
* A clean-up method, called when everything is finished. Clears the
* JOgg/JOrbis objects and closes the InputStream
.
*/
private void cleanUp()
{
debugOutput("Cleaning up.");
// Clear the necessary JOgg/JOrbis objects.
joggStreamState.clear();
jorbisBlock.clear();
jorbisDspState.clear();
jorbisInfo.clear();
joggSyncState.clear();
// Closes the stream.
try
{
if(inputStream != null) inputStream.close();
}
catch(Exception e)
{
}
debugOutput("Done cleaning up.");
}
/**
* Decodes the current packet and sends it to the audio output line.
*/
private void decodeCurrentPacket()
{
int samples;
// Check that the packet is a audio data packet etc.
if(jorbisBlock.synthesis(joggPacket) == 0)
{
// Give the block to the DspState object.
jorbisDspState.synthesis_blockin(jorbisBlock);
}
// We need to know how many samples to process.
int range;
/*
* Get the PCM information and count the samples. And while these
* samples are more than zero...
*/
while((samples = jorbisDspState.synthesis_pcmout(pcmInfo, pcmIndex))
> 0)
{
// We need to know for how many samples we are going to process.
if(samples < convertedBufferSize)
{
range = samples;
}
else
{
range = convertedBufferSize;
}
// For each channel...
for(int i = 0; i < jorbisInfo.channels; i++)
{
int sampleIndex = i * 2;
// For every sample in our range...
for(int j = 0; j < range; j++)
{
/*
* Get the PCM value for the channel at the correct
* position.
*/
int value = (int) (pcmInfo[0][i][pcmIndex[i] + j] * 32767);
/*
* We make sure our value doesn't exceed or falls below
* +-32767.
*/
if(value > 32767)
{
value = 32767;
}
if(value < -32768)
{
value = -32768;
}
/*
* It the value is less than zero, we bitwise-or it with
* 32768 (which is 1000000000000000 = 10^15).
*/
if(value < 0) value = value | 32768;
/*
* Take our value and split it into two, one with the last
* byte and one with the first byte.
*/
convertedBuffer[sampleIndex] = (byte) (value);
convertedBuffer[sampleIndex + 1] = (byte) (value >>> 8);
/*
* Move the sample index forward by two (since that's how
* many values we get at once) times the number of channels.
*/
sampleIndex += 2 * (jorbisInfo.channels);
}
}
// Write the buffer to the audio output line.
outputLine.write(convertedBuffer, 0, 2 * jorbisInfo.channels
* range);
// Update the DspState object.
jorbisDspState.synthesis_read(range);
}
}
/**
* This method is being called internally to output debug information
* whenever that is wanted.
*
* @param output the debug output information
*/
private void debugOutput(String output)
{
if(debugMode) System.out.println("Debug: " + output);
}
}