// $Id: Looper.java,v 1.5 2001/05/04 21:13:09 mito Exp $ import java.io.*; import javax.sound.sampled.*; public class Looper implements Runnable { // Data written to this is played by the soundcard private SourceDataLine sdl; // Write this many bytes per inner loop execution static private final int innerLoopWriteSize = 512; // new sounds to be played are placed on this queue private Queue incoming = new Queue(); // lock to wait on when waiting for a sound to play private Object soundLock = new Object(); // assume a standard sampling rate static final public int sampRate = 44100; // the latency through the low-level sound system // this must be tuned for each system static private final double sysLatencyTime = 0.695; // the sound latency expressed in samples static private final int sysLatency = (int)(sysLatencyTime*sampRate); // a standard audio format for playing audio static public AudioFormat stdFormat = new AudioFormat( AudioFormat.Encoding.PCM_SIGNED, 44100.0f, 16, 1, 2, 44100.0f, false ); // The construtor opens a playback audio line // and creates a background thread for streaming data public Looper() { // Open the playback line sdl = getOutputLine(); // Create a background thread for streaming audio Thread t = new Thread( this, "looper" ); t.start(); } // Tell the looper to play the next sound // set to null to turn sound off public void setSound( byte raw[] ) { synchronized( soundLock ) { // place the new sound on the queue incoming.put( raw ); // tell the thread that there's a new sound soundLock.notifyAll(); } } // Background streaming thread public void run() { // The currently playing sound byte currentRaw[] = null; // The position within the currently playing sound int cursor = 0; // open the output line for playing try { // open it with our standard audio format sdl.open( stdFormat ); // start it playing sdl.start(); } catch( LineUnavailableException lue ) { throw new RuntimeException( lue.toString() ); } while (true) { synchronized (soundLock) { // Grab the next sound, if one is waiting // If mulitple sounds are waiting, we'll just // zip through them to the latest one while (incoming.numWaiting()>0) { // Copy of the reference to the current sound byte[] crcopy = currentRaw; // Get the next sound currentRaw = (byte[])incoming.get(); if (currentRaw != null) { // We have to flush the buffer, and thus have to // back the cursor up a certain amount to keep the // looping seamless int backlog = sdl.getBufferSize()-sdl.available(); cursor -= backlog; // Back up by a bit more, because of the latency // through the low-level sound system cursor -= sysLatency*2; // 16 bit, don'tcha know // If we backup past the beginning of the // sound, wrap around to the end if (cursor < 0) { cursor += currentRaw.length; // If we're still before the beginning, just // don't bother -- start the sound from the top if (cursor<0) cursor = 0; } // Flush the data buffer sdl.flush(); } // In case the current or previous sounds are // null, or if they are different lengths, // don't bother trying to do the seamless looping -- // start the sound from the top if (currentRaw == null || crcopy == null || currentRaw.length != crcopy.length ) { cursor = 0; } } // If there is no sound playing, wait for one if (currentRaw==null) { try { soundLock.wait(); continue; } catch( InterruptedException ie ) {} } // Otherwise, play a chunk of sound int bytesLeft = currentRaw.length-cursor; // If we've reached the end, start from the top // of the sound if (bytesLeft<=0) { // restart sound cursor = 0; bytesLeft = currentRaw.length; } // Don't write more than 'innerLoopWriteSize' int towrite = innerLoopWriteSize; if (towrite > bytesLeft) towrite = bytesLeft; // Write a chunk int r = sdl.write( currentRaw, cursor, towrite ); if (r==-1) throw new RuntimeException( "Can't write to line!" ); // Remember how much we wrote, by advancing the cursor // to the next chunk of sound cursor += r; } } } // Utility class -- open an output line with our standard // audio format public SourceDataLine getOutputLine() { try { DataLine.Info info = new DataLine.Info( SourceDataLine.class, stdFormat ); SourceDataLine sdl = (SourceDataLine)AudioSystem.getLine( info ); return sdl; } catch( LineUnavailableException lue ) { throw new RuntimeException( "Can't get output line" ); } } // Load an audio file static protected byte [] loadSound( File file ) throws IOException { try { // Grab an audio stream connected to the file AudioInputStream ain = AudioSystem.getAudioInputStream( file ); // How long, in *samples*, is the file? long len = ain.getFrameLength(); // 16-bit audio means 2 bytes per sample, so we need // a byte array twice as long as the number of samples byte bs[] = new byte[(int)len*2]; // Read everything, and make sure we got it all int r = ain.read( bs ); if (r != len*2) throw new RuntimeException( "Read only "+r+" of "+file+" out of "+ (len*2)+" sound bytes" ); return bs; } catch( UnsupportedAudioFileException uafe ) { throw new IOException( uafe.toString() ); } } // wait for the user to hit [ENTER]. If they type // 'q' or 'Q', then quit static public void waitForEnter() throws IOException { int c; while (true) { c = System.in.read(); if (c=='q' || c=='Q') System.exit( 0 ); if (c=='\n' || c=='\r') return; } } // Cycle through the audio files specified on the command-line, // waiting for the user to press [ENTER] before moving on to // the next one static public void main( String args[] ) throws Exception { Looper looper = new Looper(); int a=0; while (true) { String filename = args[a]; File file = new File( filename ); byte sound[] = loadSound( file ); System.out.println( "Playing "+filename ); looper.setSound( sound ); waitForEnter(); a = (a+1)%args.length; } } }