package org.ripple.power.sound; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; import javax.sound.sampled.AudioFormat; import javax.sound.sampled.AudioSystem; import javax.sound.sampled.DataLine; import javax.sound.sampled.FloatControl; import javax.sound.sampled.LineUnavailableException; import javax.sound.sampled.SourceDataLine; import com.jcraft.jogg.Packet; import com.jcraft.jogg.Page; import com.jcraft.jogg.StreamState; import com.jcraft.jogg.SyncState; import com.jcraft.jorbis.Block; import com.jcraft.jorbis.Comment; import com.jcraft.jorbis.DspState; import com.jcraft.jorbis.Info; public class JoggStreamer extends Thread { private static final int INITIAL_PACKET_COUNT = 3; public static final int BUFFER_SIZE = 8192; private static final float SHORT_RANGE = 32768 - 8192; private InputStream in; private SourceDataLine out; private Throwable failure; private Page oggPage; private SyncState oggSyncState; private Packet oggPacket; private StreamState oggStreamState; private Info vorbisInfo; private Comment vorbisComment; private DspState vorbisDspState; private Block vorbisBlock; private float[][][] mpcm; private int[] mindex; private byte[] sampleBuffer; private static int volume = 90; public JoggStreamer(InputStream in) { super("JoggStreamer"); this.in = in; } public static void setDefaultVolume(int percent) { volume = percent; } public boolean isRunning() { return in != null && isAlive(); } private static String convertField(byte[] field) { return field == null ? null : new String(field, 0, field.length - 1); } public int getChannelCount() { return vorbisInfo == null ? -1 : vorbisInfo.channels; } public int getRate() { return vorbisInfo == null ? -1 : vorbisInfo.rate; } public String getVendor() { return vorbisComment == null ? null : convertField(vorbisComment.vendor); } public int getCommentCount() { return vorbisComment == null ? 0 : vorbisComment.user_comments.length; } public String getComment(int index) { return vorbisComment == null ? null : convertField(vorbisComment.user_comments[index]); } public void run() { if (Thread.currentThread() != this) { throw new IllegalStateException("not this thread"); } try { SyncState syncState = this.oggSyncState = new SyncState(); while (in != null) { int off = syncState.buffer(BUFFER_SIZE); int n = in.read(syncState.data, off, BUFFER_SIZE); if (n > 0) { syncState.wrote(n); pageOut(); } else { break; } } } catch (EOFException e) { } catch (IOException e) { failure = e; e.printStackTrace(); } finally { try { if (in != null) { in.close(); in = null; } } catch (IOException e) { if (failure != null) { failure = e; } e.printStackTrace(); } if (out != null) { out.stop(); out.close(); } if (vorbisBlock != null) { vorbisBlock.clear(); vorbisBlock = null; } if (vorbisDspState != null) { vorbisDspState.clear(); vorbisDspState = null; } if (vorbisInfo != null) { vorbisInfo.clear(); vorbisInfo = null; } if (oggStreamState != null) { oggStreamState.clear(); oggStreamState = null; } if (oggSyncState != null) { oggSyncState.clear(); oggSyncState = null; } synchronized (this) { notifyAll(); } } } private void pageOut() throws IOException { if (oggPage == null) { oggPage = new Page(); } for (;;) { switch (oggSyncState.pageout(oggPage)) { case 0: return; case 1: if (oggStreamState == null) { oggStreamState = new StreamState(); oggStreamState.init(oggPage.serialno()); oggStreamState.reset(); } if (oggStreamState.pagein(oggPage) < 0) { throw new IOException("error reading ogg page"); } else { packetOut(); if (oggPage.eos() != 0) { throw new EOFException(); } } break; default: throw new IOException("ogg input format error"); } } } private void packetOut() throws IOException { if (oggPacket == null) { oggPacket = new Packet(); } for (;;) { switch (oggStreamState.packetout(oggPacket)) { case 0: return; case 1: if (oggPacket.packetno < INITIAL_PACKET_COUNT) { headerOut(); } else { pcmOut(); } break; default: if (oggPacket.packetno < INITIAL_PACKET_COUNT - 1) { throw new IOException("ogg input format error"); } else { synchronized (this) { notifyAll(); } } break; } } } private void headerOut() throws IOException { synchronized (this) { if (vorbisInfo == null) { vorbisInfo = new Info(); vorbisInfo.init(); vorbisComment = new Comment(); vorbisComment.init(); } } if (vorbisInfo.synthesis_headerin(vorbisComment, oggPacket) < 0) { throw new IOException("error reading vorbis header"); } } private void pcmOut() throws IOException { if (vorbisDspState == null) { mpcm = new float[1][][]; mindex = new int[vorbisInfo.channels]; vorbisDspState = new DspState(); vorbisDspState.synthesis_init(vorbisInfo); vorbisBlock = new Block(vorbisDspState); sampleBuffer = new byte[BUFFER_SIZE * 2]; } if (out == null) { openOutput(); } if (vorbisBlock.synthesis(oggPacket) == 0) { vorbisDspState.synthesis_blockin(vorbisBlock); } int n; int[] idx = mindex; int nch = vorbisInfo.channels; int max = sampleBuffer.length; while ((n = vorbisDspState.synthesis_pcmout(mpcm, idx)) > 0) { int len = (n < max ? n : max); float[][] pcm = mpcm[0]; int off = 0; for (int i = 0; i < len; i++) { for (int ch = 0; ch < nch; ch++) { int m = (int) (pcm[ch][idx[ch] + i] * SHORT_RANGE); if (m < Short.MIN_VALUE) { sampleBuffer[off++] = (byte) 0x00; sampleBuffer[off++] = (byte) 0x80; } else if (m > Short.MAX_VALUE) { sampleBuffer[off++] = (byte) 0xff; sampleBuffer[off++] = (byte) 0x7f; } else { short s = (short) m; sampleBuffer[off++] = (byte) s; sampleBuffer[off++] = (byte) (s >>> 8); } } } out.write(sampleBuffer, 0, 2 * nch * len); vorbisDspState.synthesis_read(len); } } private void openOutput() throws IOException { AudioFormat audioFormat = new AudioFormat((float) vorbisInfo.rate, 16, vorbisInfo.channels, true, false); DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat, AudioSystem.NOT_SPECIFIED); if (!AudioSystem.isLineSupported(info)) { throw new IOException("line format " + info + "not supported"); } try { out = (SourceDataLine) AudioSystem.getLine(info); out.open(audioFormat); } catch (LineUnavailableException e) { throw new IOException("audio unavailable: " + e.toString()); } out.start(); updateVolume(volume); } public void updateVolume(int percent) { if (out != null && out.isOpen()) { try { FloatControl c = (FloatControl) out .getControl(FloatControl.Type.MASTER_GAIN); float min = c.getMinimum(); float v = percent * (c.getMaximum() - min) / 100f + min; c.setValue(v); } catch (IllegalArgumentException e) { } } volume = percent; } public void interrupt() { in = null; } }