package ddf.minim; import java.io.IOException; import java.util.Map; import javax.sound.sampled.AudioFormat; import javax.sound.sampled.AudioInputStream; import javax.sound.sampled.Clip; import javax.sound.sampled.DataLine; import javax.sound.sampled.LineUnavailableException; import javax.sound.sampled.SourceDataLine; import javazoom.spi.vorbis.sampled.convert.DecodedVorbisAudioInputStream; import org.tritonus.share.sampled.FloatSampleBuffer; import processing.core.PApplet; class VorbisOggFile extends Thread implements AudioRecording { private SignalSplitter splitter; private EffectsChain effects; // file reading stuff private String fileName; private Map properties; private long bytesRead; private long startedWritingBytes = -1; private long lengthInMillis; private boolean play; private boolean loop; private int numLoops; private DecodedVorbisAudioInputStream ais; private byte[] rawBytes,rawBytes2,byteBufferConverted; // line writing stuff private AudioFormat format; private SourceDataLine line; private FloatSampleBuffer buffer; private boolean finished; //Sources are invalid now private boolean STEREO_OUTPUT = true; //Bypass stereoline for faster samples to listeners private boolean atEOF; //Symbolizes whether the "cursor" is at the end of the file. VorbisOggFile(String fn, Map props, AudioInputStream stream, SourceDataLine sdl, int bufferSize) { format = sdl.getFormat(); line = sdl; buffer = new FloatSampleBuffer(format.getChannels(), bufferSize, format.getSampleRate()); Minim.debug("FloatSampleBuffer has " + buffer.getSampleCount() + " samples."); finished = false; fileName = fn; properties = props; play = loop = false; numLoops = 0; ais = (DecodedVorbisAudioInputStream)stream; rawBytes = new byte[buffer.getByteArrayBufferSize(format)]; rawBytes2 = new byte[rawBytes.length]; byteBufferConverted = new byte[rawBytes.length]; splitter = new SignalSplitter(format, bufferSize); effects = new EffectsChain(); } public void run() { try { line.open(format, bufferSize() * format.getFrameSize() * 4); } catch (LineUnavailableException e) { Minim.error("Error opening SourceDataLine: " + e.getMessage()); } line.start(); while ( !finished ) { byte[] temp = rawBytes2; rawBytes2 = rawBytes; rawBytes = temp; atEOF = false; try { // read bytes if we're playing if (play) { int toRead = rawBytes.length; int bytesRead = 0; while ( bytesRead < toRead ) { int actualRead = ais.read(rawBytes, bytesRead, toRead - bytesRead); // -1 means end of file if ( actualRead == -1 ) { if ( loop ) { // reset the stream if (numLoops == Clip.LOOP_CONTINUOUSLY) { rewind(); } // reset the stream, decrement loop count else if (numLoops > 0) { rewind(); numLoops--; } // otherwise just stop playing else { loop = false; play = false; atEOF = true; } } else { atEOF = true; } } // if bytesRead == -1 else { atEOF = false; bytesRead += actualRead; } }// while bytesRead < toRead } // if play } catch (IOException e) { Minim.error("AudioPlayer: error reading from the file - " + e.getMessage()); } // convert the bytes to floating point samples int frameCount = rawBytes.length / format.getFrameSize(); buffer.setSamplesFromBytes(rawBytes, 0, format, 0, frameCount); //Rawbytes now free! // process the samples and broadcast them to our listeners if ( type() == Minim.MONO ) { float[] samp = buffer.getChannel(0); if ( effects.hasEnabled() ) { effects.process(samp); } splitter.samples(samp); } else { float [] sampL = buffer.getChannel(0); float [] sampR = buffer.getChannel(1); if ( effects.hasEnabled() ) { effects.process(sampL, sampR); } splitter.samples(sampL,sampR); } // finally convert them back to bytes and write to our line int wrote = buffer.convertToByteArray(byteBufferConverted, 0, format); if (STEREO_OUTPUT) line.write(byteBufferConverted, 0, byteBufferConverted.length); } line.drain(); line.stop(); line.close(); line = null; try { ais.close(); } catch (IOException e) { Minim.error("Couldn't close the stream"); } ais = null; } public void play() { play = true; loop = false; } public void play(int millis) { cue(millis); play(); } public boolean isPlaying() { return play; } public void pause() { play = false; } public void rewind() { Minim.debug("Rewinding..."); try { ais.close(); } catch (IOException e) { Minim.error("Couldn't close the stream."); } AudioInputStream encIn = Minim.getAudioInputStream(fileName); // converts the stream to PCM audio from mp3 audio ais = (DecodedVorbisAudioInputStream)Minim.getAudioInputStream(format, encIn); } public void loop() { loop(Clip.LOOP_CONTINUOUSLY); } public void loop(int n) { loop = true; numLoops = n; play = true; } /** * May or may not be available until you start playing. */ public int length() { if ( properties != null && properties.containsKey("ogg.length.bytes") ) { lengthInMillis = VobisBytes2Millis((Integer)properties.get("ogg.length.bytes")); System.out.println("GOT LENGTH: "+lengthInMillis); } else { lengthInMillis = -1; } return (int)lengthInMillis; } public int position() { Map prop = ais.properties(); long getBytesRead = (Long)prop.get("ogg.position.byte"); long getBytesLastUpdated = (Long)prop.get("ogg.position.byte.update"); long getByteUpdateSize = (Long)prop.get("ogg.position.byte.updateSize"); if (isPlaying()){ int expectedSize = (int) VobisBytes2Millis(getByteUpdateSize); int diff = (int)((System.nanoTime()-getBytesLastUpdated)/1e6); diff = (int) (VobisBytes2Millis(getBytesRead)+PApplet.constrain(diff, 0, expectedSize)); //System.out.println((int)(System.nanoTime()/1e6)+" "+diff); return diff; } else { return (int)VobisBytes2Millis(getBytesRead); } } public long VobisBytes2Millis(long bytes){ long res = (long)(.5f*1000*bytes/(double)format.getSampleRate()*format.getChannels()); return res; } public long VobisMillis2Bytes(long millis){ long res = (long)(2*millis*(double)format.getSampleRate()/1000/format.getChannels()); return res; } public void cue(int millis) { if ( millis < 0 ) { rewind(); return; } /** * The length comes seemingly late; we should be able to safely seek before then. Hopefully~ */ if ( millis > length() && length() >= 0) millis = length(); if ( millis > position() ) { Minim.debug("Skipping for cue to "+millis+" from " + position()); skip(millis - position()); } else { Minim.debug("Rewind and skip for cue."); rewind(); skip(millis); } } public void skip(int millis) { if ( millis > 0 ) { if (false){ //HACKED WAY: boolean playstate = play; play = false; long toSkip = position()-millis; try { ais.skip(-toSkip); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } play = playstate; return; } Minim.debug("Skipping forward by " + millis + " milliseconds."); // if it puts us past the end of the file, only skip what's left if ( position() + millis > length() && length()>=0 ) { millis = length() - position(); } int targetMs = millis+position(); // don't want the io thread to read while we're skipping boolean playstate = play; play = false; long toSkip = VobisMillis2Bytes(millis); Minim.debug("Skipping " + millis + " millis " + toSkip+" bytes at a time."); byte[] skipBytes = new byte[(int)toSkip]; int totalSkipped = 0; try { // it's only able to read about 2 seconds at a time // so we've got to loop until we've skipped the requested amount while (position()<targetMs) { totalSkipped += ais.read(skipBytes, 0, skipBytes.length); } } catch (IOException e) { Minim.error("Unable to skip due to read error: " + e.getMessage()); } Minim.debug("Total actually skipped was " + totalSkipped + ", which is " + VobisBytes2Millis(totalSkipped) + " milliseconds."); // restore the play state play = playstate; } else if ( millis < 0 ) { Minim.debug("Skipping backwards by " + (-millis) + " milliseconds."); // to skip backwards we need to rewind // and then cue to the new position // remember that millis is negative, that's why we add if ( position() > 0 ) { int pos = position() + millis; rewind(); if ( pos > 0 ) { cue(pos); } } } } public boolean isLooping() { return loop; } public int type() { return format.getChannels(); } public void open() { start(); } public void close() { finished = true; } public void addEffect(AudioEffect effect) { effects.add(effect); } public void clearEffects() { effects.clear(); } public void disableEffect(int i) { effects.disable(i); } public void disableEffect(AudioEffect effect) { effects.disable(effect); } public int effectCount() { return effects.size(); } public void effects() { effects.enableAll(); } public boolean hasEffect(AudioEffect e) { return effects.contains(e); } public void enableEffect(int i) { effects.enable(i); } public void enableEffect(AudioEffect effect) { effects.enable(effect); } public AudioEffect getEffect(int i) { return effects.get(i); } public boolean isEffected() { return effects.hasEnabled(); } public boolean isEnabled(AudioEffect effect) { return effects.isEnabled(effect); } public void noEffects() { effects.disableAll(); } public void removeEffect(AudioEffect effect) { effects.remove(effect); } public AudioEffect removeEffect(int i) { return effects.remove(i); } private boolean hasListeners = false; public void addListener(AudioListener listener) { hasListeners = true; splitter.addListener(listener); } public int bufferSize() { return splitter.bufferSize(); } public AudioFormat getFormat() { return format; } public void removeListener(AudioListener listener) { splitter.removeListener(listener); } public DataLine getDataLine() { return line; } public float sampleRate() { return splitter.sampleRate(); } public Map getProperties() { return properties; } public AudioInputStream getAudioInputStream() { return ais; } public void hearNot(boolean ByPassSpeakerSystem) { STEREO_OUTPUT = !ByPassSpeakerSystem; } public boolean isAtEndOfStream(){ return atEOF; } }