/*
 * @(#)Juke.java	1.19	00/01/31
 *
 * Copyright (c) 1999 Sun Microsystems, Inc. All Rights Reserved.
 *
 * Sun grants you ("Licensee") a non-exclusive, royalty free, license to use,
 * modify and redistribute this software in source and binary code form,
 * provided that i) this copyright notice and license appear on all copies of
 * the software; and ii) Licensee does not utilize the software in a manner
 * which is disparaging to Sun.
 *
 * This software is provided "AS IS," without a warranty of any kind. ALL
 * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING ANY
 * IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE OR
 * NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN AND ITS LICENSORS SHALL NOT BE
 * LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING
 * OR DISTRIBUTING THE SOFTWARE OR ITS DERIVATIVES. IN NO EVENT WILL SUN OR ITS
 * LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR DIRECT,
 * INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER
 * CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF THE USE OF
 * OR INABILITY TO USE SOFTWARE, EVEN IF SUN HAS BEEN ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGES.
 *
 * This software is not designed or intended for use in on-line control of
 * aircraft, air traffic, aircraft navigation or aircraft communications; or in
 * the design, construction, operation or maintenance of any nuclear
 * facility. Licensee represents and warrants that it will not use or
 * redistribute the Software for such purposes.
 */


import java.awt.*;
import java.awt.font.*;
import java.awt.geom.*;
import java.awt.event.*;
import java.text.AttributedString;
import java.text.AttributedCharacterIterator;
import javax.swing.*;
import javax.swing.border.*;
import javax.swing.table.*;
import javax.swing.event.*;
import javax.sound.midi.*;
import javax.sound.sampled.*;
import java.io.File;
import java.io.InputStream;
import java.io.FileInputStream;
import java.io.BufferedInputStream;
import java.util.Vector;
import java.net.URL;



/**
 * A JukeBox for sampled and midi sound files.  Features duration progress, 
 * seek slider, pan and volume controls.
 *
 * @version @(#)Juke.java	1.19 00/01/31
 * @author Brian Lichtenwalter  
 */
public class Juke extends JPanel implements Runnable, LineListener, MetaEventListener, ControlContext {

    final int bufSize = 16384;
    PlaybackMonitor playbackMonitor = new PlaybackMonitor();

    Vector sounds = new Vector();
    Thread thread;
    Sequencer sequencer;
    boolean midiEOM, audioEOM;
    Synthesizer synthesizer;
    MidiChannel channels[]; 
    Object currentSound;
    String currentName;
    double duration;
    int num;
    boolean bump;
    boolean paused = false;
    JButton startB, pauseB, loopB, prevB, nextB;
    JTable table;
    JSlider panSlider, gainSlider;
    JSlider seekSlider;
    JukeTable jukeTable;
    Loading loading;
    Credits credits;
    String errStr;
    JukeControls controls;


    public Juke(String dirName) {
        setLayout(new BorderLayout());
        setBorder(new EmptyBorder(5,5,5,5));

        if (dirName != null) {
            loadJuke(dirName); 
        }

        JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, 
            jukeTable = new JukeTable(), controls = new JukeControls());
        splitPane.setContinuousLayout(true);
        add(splitPane);
    }


    public void open() {

        try {

            sequencer = MidiSystem.getSequencer();

			if (sequencer instanceof Synthesizer) {
				synthesizer = (Synthesizer)sequencer;
				channels = synthesizer.getChannels();
			} 

        } catch (Exception ex) { ex.printStackTrace(); return; }
        sequencer.addMetaEventListener(this);
        (credits = new Credits()).start();
    }


    public void close() {
        if (credits != null && credits.isAlive()) {
            credits.interrupt();
        }
        if (thread != null && startB != null) {
            startB.doClick(0);
        }
        if (jukeTable != null && jukeTable.frame != null) {
            jukeTable.frame.dispose();
            jukeTable.frame = null;
        }
        if (sequencer != null) {
            sequencer.close();
        }
    }


    public void loadJuke(String name) {
        try {
            File file = new File(name);
            if (file != null && file.isDirectory()) {
                String files[] = file.list();
                for (int i = 0; i < files.length; i++) {
                    File leafFile = new File(file.getAbsolutePath(), files[i]);
                    if (leafFile.isDirectory()) {
                        loadJuke(leafFile.getAbsolutePath());
                    } else {
                        addSound(leafFile);
                    }
                }
            } else if (file != null && file.exists()) {
                addSound(file);
            }
        } catch (SecurityException ex) {
            reportStatus(ex.toString());
            JavaSound.showInfoDialog();
        } catch (Exception ex) {
            reportStatus(ex.toString());
        }
    }


    private void addSound(File file) {
        String s = file.getName();
        if (s.endsWith(".au") || s.endsWith(".rmf") ||
            s.endsWith(".mid") || s.endsWith(".wav") ||
            s.endsWith(".aif") || s.endsWith(".aiff"))
        {
            sounds.add(file);
        }
    }


    public boolean loadSound(Object object) {

        duration = 0.0;
        (loading = new Loading()).start();

        if (object instanceof URL) {
            currentName = ((URL) object).getFile();
            playbackMonitor.repaint();
            try {
                currentSound = AudioSystem.getAudioInputStream((URL) object);
            } catch(Exception e) {
                try { 
                    currentSound = MidiSystem.getSequence((URL) object);
		} catch (InvalidMidiDataException imde) {
		    System.out.println("Unsupported audio file.");
		    return false;
                } catch (Exception ex) { 
                    ex.printStackTrace(); 
		    currentSound = null;
		    return false;
                }
            }
        } else if (object instanceof File) {
            currentName = ((File) object).getName();
            playbackMonitor.repaint();
            try {
                currentSound = AudioSystem.getAudioInputStream((File) object);
            } catch(Exception e1) {
                // load midi & rmf as inputstreams for now
                //try { 
                    //currentSound = MidiSystem.getSequence((File) object);
                //} catch (Exception e2) { 
                    try { 
                        FileInputStream is = new FileInputStream((File) object);
                        currentSound = new BufferedInputStream(is, 1024);
                    } catch (Exception e3) { 
                        e3.printStackTrace(); 
			currentSound = null;
			return false;
                    }
                //}
            }
        }


        loading.interrupt();

        // user pressed stop or changed tabs while loading
        if (sequencer == null) {
            currentSound = null;
            return false;
        } 

        if (currentSound instanceof AudioInputStream) {
           try {
                AudioInputStream stream = (AudioInputStream) currentSound;
                AudioFormat format = stream.getFormat();

                /**
                 * we can't yet open the device for ALAW/ULAW playback,
                 * convert ALAW/ULAW to PCM
                 */
                if ((format.getEncoding() == AudioFormat.Encoding.ULAW) ||
                    (format.getEncoding() == AudioFormat.Encoding.ALAW)) 
                {
                    AudioFormat tmp = new AudioFormat(
                                              AudioFormat.Encoding.PCM_SIGNED, 
                                              format.getSampleRate(),
                                              format.getSampleSizeInBits() * 2,
                                              format.getChannels(),
                                              format.getFrameSize() * 2,
                                              format.getFrameRate(),
                                              true);
                    stream = AudioSystem.getAudioInputStream(tmp, stream);
                    format = tmp;
                }
                DataLine.Info info = new DataLine.Info(
                                          Clip.class, 
                                          stream.getFormat(), 
                                          ((int) stream.getFrameLength() *
                                              format.getFrameSize()));

                Clip clip = (Clip) AudioSystem.getLine(info);
                clip.addLineListener(this);
                clip.open(stream);
                currentSound = clip;
                seekSlider.setMaximum((int) stream.getFrameLength());
            } catch (Exception ex) { 
		ex.printStackTrace(); 
		currentSound = null;
		return false;
	    }
        } else if (currentSound instanceof Sequence || currentSound instanceof BufferedInputStream) {
            try {
                sequencer.open();
                if (currentSound instanceof Sequence) {
                    sequencer.setSequence((Sequence) currentSound);
                } else {
                    sequencer.setSequence((BufferedInputStream) currentSound);
                }
				seekSlider.setMaximum((int)(sequencer.getMicrosecondLength() / 1000));

            } catch (InvalidMidiDataException imde) { 
		System.out.println("Unsupported audio file.");
		currentSound = null;
		return false;
            } catch (Exception ex) { 
		ex.printStackTrace(); 
		currentSound = null;
		return false;
	    }
        }

        seekSlider.setValue(0);
        
		// enable seek, pan, and gain sliders for sequences as well as clips
		seekSlider.setEnabled(true);
		panSlider.setEnabled(true);
        gainSlider.setEnabled(true);        
		
	duration = getDuration();

	return true;
    }


    public void playSound() {
        playbackMonitor.start();
        setGain();
        setPan();
        midiEOM = audioEOM = bump = false;
        if (currentSound instanceof Sequence || currentSound instanceof BufferedInputStream && thread != null) {
            sequencer.start();
            while (!midiEOM && thread != null && !bump) {
                try { thread.sleep(99); } catch (Exception e) {break;}
            }
            sequencer.stop();
            sequencer.close();
        } else if (currentSound instanceof Clip && thread != null) {
            Clip clip = (Clip) currentSound;
            clip.start();
            try { thread.sleep(99); } catch (Exception e) { }
            while ((paused || clip.isActive()) && thread != null && !bump) {
                try { thread.sleep(99); } catch (Exception e) {break;}
            }
            clip.stop();
            clip.close();
        }
        currentSound = null;
        playbackMonitor.stop();
    }


    public double getDuration() {
        double duration = 0.0;
        if (currentSound instanceof Sequence) {
            duration = ((Sequence) currentSound).getMicrosecondLength() / 1000000.0;
        }  else if (currentSound instanceof BufferedInputStream) {
			duration = sequencer.getMicrosecondLength() / 1000000.0;
		} else if (currentSound instanceof Clip) {
            Clip clip = (Clip) currentSound;
            duration = clip.getBufferSize() / 
                (clip.getFormat().getFrameSize() * clip.getFormat().getFrameRate());
        } 
        return duration;
    }


    public double getSeconds() {
        double seconds = 0.0;
        if (currentSound instanceof Clip) {
            Clip clip = (Clip) currentSound;
            seconds = clip.getFramePosition() / clip.getFormat().getFrameRate();
        } else if ( (currentSound instanceof Sequence) || (currentSound instanceof BufferedInputStream) ) {
            try {
                seconds = sequencer.getMicrosecondPosition() / 1000000.0;
            } catch (IllegalStateException e){
                System.out.println("TEMP: IllegalStateException "+
                    "on sequencer.getMicrosecondPosition(): " + e);
            }
        }
        return seconds;
    }


    public void update(LineEvent event) {
        if (event.getType() == LineEvent.Type.STOP && !paused) { 
            audioEOM = true;
        }
    }


    public void meta(MetaMessage message) {
        if (message.getType() == 47) {  // 47 is end of track
            midiEOM = true;
        }
    }


    private void reportStatus(String msg) {
        if ((errStr = msg) != null) {
            System.out.println(errStr);
            playbackMonitor.repaint();
        }
        if (credits != null && credits.isAlive()) {
            credits.interrupt();
        }
    }


    public Thread getThread() {
        return thread;
    }


    public void start() {
        thread = new Thread(this);
        thread.setName("Juke");
        thread.start();
    }


    public void stop() {
        if (thread != null) {
            thread.interrupt();
        }
        thread = null;
    }


    public void run() {
        do {
            table.scrollRectToVisible(new Rectangle(0,0,1,1));
            for (; num < sounds.size() && thread != null; num++) {
                table.scrollRectToVisible(new Rectangle(0,(num+2)*(table.getRowHeight()+table.getRowMargin()),1,1));
                table.setRowSelectionInterval(num, num);
                if( loadSound(sounds.get(num)) == true ) {
                    playSound();
		}
                // take a little break between sounds
                try { thread.sleep(222); } catch (Exception e) {break;}
            }
            num = 0;
        } while (loopB.isSelected() && thread != null);

        if (thread != null) {
            startB.doClick();
        }
        thread = null;
        currentName = null;
        currentSound = null;
        playbackMonitor.repaint();
    }


    public void setPan() {

        int value = panSlider.getValue();

        if (currentSound instanceof Clip) {
            try {
                Clip clip = (Clip) currentSound;
                FloatControl panControl = 
                    (FloatControl) clip.getControl(FloatControl.Type.PAN);
                panControl.setValue(value/100.0f);
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        } else if (currentSound instanceof Sequence || currentSound instanceof BufferedInputStream) {
            for (int i = 0; i < channels.length; i++) {                
				channels[i].controlChange(10, (int)(((double)value + 100.0) / 200.0 *  127.0));
            }										 
        }
    }


    public void setGain() {
        double value = gainSlider.getValue() / 100.0;

        if (currentSound instanceof Clip) {
            try {
                Clip clip = (Clip) currentSound;
                FloatControl gainControl = 
                  (FloatControl) clip.getControl(FloatControl.Type.MASTER_GAIN);
                float dB = (float) 
                  (Math.log(value==0.0?0.0001:value)/Math.log(10.0)*20.0);
                gainControl.setValue(dB);
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        } else if (currentSound instanceof Sequence || currentSound instanceof BufferedInputStream) {
            for (int i = 0; i < channels.length; i++) {                
				channels[i].controlChange(7, (int)(value * 127.0));

			}
        }
    }



    /**
     * GUI controls for start, stop, previous, next, pan and gain.
     */
    class JukeControls extends JPanel implements ActionListener, ChangeListener {

        public JukeControls() {
            setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));

            JPanel p1 = new JPanel();
            p1.setLayout(new BoxLayout(p1, BoxLayout.Y_AXIS));
            p1.setBorder(new EmptyBorder(10,0,5,0));
            JPanel p2 = new JPanel();
            startB = addButton("Start", p2, sounds.size() != 0);
            pauseB = addButton("Pause", p2, false);
            p1.add(p2);
            JPanel p3 = new JPanel();
            prevB = addButton("<<", p3, false);
            nextB = addButton(">>", p3, false);
            p1.add(p3);
            add(p1);
    
            JPanel p4 = new JPanel(new BorderLayout());
            EmptyBorder eb = new EmptyBorder(5,20,10,20);
            BevelBorder bb = new BevelBorder(BevelBorder.LOWERED);
            p4.setBorder(new CompoundBorder(eb,bb));
            p4.add(playbackMonitor);
            seekSlider = new JSlider(JSlider.HORIZONTAL, 0, 100, 0);
            seekSlider.setEnabled(false);
            seekSlider.addChangeListener(this);
            p4.add("South", seekSlider);
            add(p4);

            JPanel p5 = new JPanel();
            p5.setLayout(new BoxLayout(p5, BoxLayout.X_AXIS));
            p5.setBorder(new EmptyBorder(5,5,10,5));
            panSlider = new JSlider(-100, 100, 0);
            panSlider.addChangeListener(this);
            TitledBorder tb = new TitledBorder(new EtchedBorder());
            tb.setTitle("Pan = 0.0");
            panSlider.setBorder(tb);
            p5.add(panSlider);
            gainSlider = new JSlider(0, 100, 80);
            gainSlider.addChangeListener(this);
            tb = new TitledBorder(new EtchedBorder());
            tb.setTitle("Gain = 80");
            gainSlider.setBorder(tb);
            p5.add(gainSlider);
            add(p5);
        }

        private JButton addButton(String name, JPanel panel, boolean state) {
            JButton b = new JButton(name);
            b.addActionListener(this);
            b.setEnabled(state);
            panel.add(b);
            return b;
        }

        public void stateChanged(ChangeEvent e) {
            JSlider slider = (JSlider) e.getSource();
            int value = slider.getValue();
            if (slider.equals(seekSlider)) {
                if (currentSound instanceof Clip) {
                    ((Clip) currentSound).setFramePosition(value);
                } else if (currentSound instanceof Sequence) {
                    long dur = ((Sequence) currentSound).getMicrosecondLength();
					sequencer.setMicrosecondPosition(value * 1000);
                } else if (currentSound instanceof BufferedInputStream) {
                    long dur = sequencer.getMicrosecondLength();
					sequencer.setMicrosecondPosition(value * 1000);
                }
                playbackMonitor.repaint();
                return;
            }
            TitledBorder tb = (TitledBorder) slider.getBorder();
            String s = tb.getTitle();
            if (s.startsWith("Pan")) {
                s = s.substring(0, s.indexOf('=')+1) + s.valueOf(value/100.0);
                if (currentSound != null) {
                    setPan();
                }
            } else if (s.startsWith("Gain")) {
                s = s.substring(0, s.indexOf('=')+1) + s.valueOf(value);
                if (currentSound != null) {
                    setGain();
                }
            } 
            tb.setTitle(s);
            slider.repaint();
        }


        public void setComponentsEnabled(boolean state) {
            seekSlider.setEnabled(state);
            pauseB.setEnabled(state);
            prevB.setEnabled(state);
            nextB.setEnabled(state);
        }



        public void actionPerformed(ActionEvent e) {
            JButton button = (JButton) e.getSource();
            if (button.getText().equals("Start")) {
                if (credits != null) {
                    credits.interrupt();
                }
                paused = false;
                num = table.getSelectedRow();
                num = num == -1 ? 0 : num;
                start();
                button.setText("Stop");
                setComponentsEnabled(true);
            } else if (button.getText().equals("Stop")) {
                credits = new Credits();
                credits.start();
                paused = false;
                stop();
                button.setText("Start");
                pauseB.setText("Pause");
                setComponentsEnabled(false);
            } else if (button.getText().equals("Pause")) {
                paused = true;
                if (currentSound instanceof Clip) {
                    ((Clip) currentSound).stop();
                } else if (currentSound instanceof Sequence || currentSound instanceof BufferedInputStream) {
                    sequencer.stop();
                }
                playbackMonitor.stop();
                pauseB.setText("Resume");
            } else if (button.getText().equals("Resume")) {
                paused = false;
                if (currentSound instanceof Clip) {
                    ((Clip) currentSound).start();
                } else if (currentSound instanceof Sequence || currentSound instanceof BufferedInputStream) {
                    sequencer.start();
                }
                playbackMonitor.start();
                pauseB.setText("Pause");
            } else if (button.getText().equals("<<")) {
                paused = false;
                pauseB.setText("Pause");
                num = num-1 < 0 ? sounds.size()-1 : num-2;
                bump = true;
            } else if (button.getText().equals(">>")) {
                paused = false;
                pauseB.setText("Pause");
                num = num+1 == sounds.size() ? -1 : num;
                bump = true;
            }
        }
    }  // End JukeControls



    /**
     * Displays current sound and time elapsed.
     */
    public class PlaybackMonitor extends JPanel implements Runnable {
        
        String welcomeStr = "Welcome to Java Sound";
        Thread pbThread;
        Color black = new Color(20, 20, 20); 
        Color jfcBlue = new Color(204, 204, 255);
        Color jfcDarkBlue = jfcBlue.darker();
        Font font24 = new Font("serif", Font.BOLD, 24);
        Font font28 = new Font("serif", Font.BOLD, 28);
        Font font42 = new Font("serif", Font.BOLD, 42);
        FontMetrics fm28, fm42;

        public PlaybackMonitor() {
            fm28 = getFontMetrics(font28);
            fm42 = getFontMetrics(font42);
        }
    
        public void paint(Graphics g) {
            Graphics2D g2 = (Graphics2D) g;
            Dimension d = getSize();
            g2.setBackground(black);
            g2.clearRect(0, 0, d.width, d.height);
            g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
            g2.setColor(jfcBlue);

            if (errStr != null) {
                g2.setFont(new Font("serif", Font.BOLD, 18));
                g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
                g2.drawString("ERROR", 5, 20);
                AttributedString as = new AttributedString(errStr);
                Font font12 = new Font("serif", Font.PLAIN, 12);
                as.addAttribute(TextAttribute.FONT, font12, 0, errStr.length());
                AttributedCharacterIterator aci = as.getIterator();
                FontRenderContext frc = g2.getFontRenderContext();
                LineBreakMeasurer lbm = new LineBreakMeasurer(aci, frc);
                float x = 5, y = 25;
                lbm.setPosition(0);
                while (lbm.getPosition() < errStr.length()) {
                    TextLayout tl = lbm.nextLayout(d.width-x-5);
                    if (!tl.isLeftToRight()) {
                        x = d.width - tl.getAdvance();
                    }
                    tl.draw(g2, x, y += tl.getAscent());
                    y += tl.getDescent() + tl.getLeading();
                }
            } else if (currentName == null) {
                FontRenderContext frc = g2.getFontRenderContext();
                TextLayout tl = new TextLayout(welcomeStr, font28, frc);
                float x = (float) (d.width/2-tl.getBounds().getWidth()/2);
                tl.draw(g2, x, d.height/2);
                if (credits != null) {
                    credits.render(d, g2);
                }
            } else {
                g2.setFont(font24);
                g2.drawString(currentName, 5, fm28.getHeight()-5);
                if (duration <= 0.0) {
                    loading.render(d, g2);
                } else {
                    double seconds = getSeconds();
                    if (midiEOM || audioEOM) {
                        seconds = duration;
                    }
                    if (seconds > 0.0) {
                        g2.setFont(font42);
                        String s = String.valueOf(seconds);
                        s =  s.substring(0,s.indexOf('.')+2);
                        int strW = (int) fm42.getStringBounds(s, g2).getWidth();
                        g2.drawString(s, d.width-strW-9, fm42.getAscent());

                        int num = 30;
                        int progress = (int) (seconds / duration * num);
                        double ww = ((double) (d.width - 10) / (double) num);
                        double hh = (int) (d.height * 0.25);
                        double x = 0.0;
                        for ( ; x < progress; x+=1.0) {
                            g2.fill(new Rectangle2D.Double(x*ww+5, d.height-hh-5, ww-1, hh));
                        }
                        g2.setColor(jfcDarkBlue);
                        for ( ; x < num; x+=1.0) {
                            g2.fill(new Rectangle2D.Double(x*ww+5, d.height-hh-5, ww-1, hh));
                        }
                    }
                }
            }
        }
        
        public void start() {
            pbThread = new Thread(this);
            pbThread.setName("PlaybackMonitor");
            pbThread.start();
        }
        
        public void stop() {
            if (pbThread != null) {
                pbThread.interrupt();
            }
            pbThread = null;
        }
        
        public void run() {
            while (pbThread != null) {
                try {
                    pbThread.sleep(99);
                } catch (Exception e) { break; }
                repaint();
            }
            pbThread = null;
        }
    } // End PlaybackMonitor
            


    /**
     * Table to display the name of the sound.
     */
    class JukeTable extends JPanel implements ActionListener {

        TableModel dataModel;
        JFrame frame;
        JTextField textField;
        JButton applyB;

        public JukeTable() {
            setLayout(new BorderLayout());
            setPreferredSize(new Dimension(260,300));

            final String[] names = { "#", "Name" };
    
            dataModel = new AbstractTableModel() {
                public int getColumnCount() { return names.length; }
                public int getRowCount() { return sounds.size();}
                public Object getValueAt(int row, int col) { 
                    if (col == 0) {
                        return new Integer(row);
                    } else if (col == 1) {
                        Object object = sounds.get(row);
                        if (object instanceof File) {
                            return ((File) object).getName();
                        } else if (object instanceof URL) {
                            return ((URL) object).getFile();
                        }
                    } 
                    return null;
                }
                public String getColumnName(int col) {return names[col]; }
                public Class getColumnClass(int c) {
                    return getValueAt(0, c).getClass();
                }
                public boolean isCellEditable(int row, int col) {
                    return false;
                }
                public void setValueAt(Object aValue, int row, int col) {
                }
            };
    
            table = new JTable(dataModel);
            TableColumn col = table.getColumn("#");
            col.setMaxWidth(20);
            table.sizeColumnsToFit(0);
        
            JScrollPane scrollPane = new JScrollPane(table);
            EmptyBorder eb = new EmptyBorder(5,5,2,5);
            scrollPane.setBorder(new CompoundBorder(eb,new EtchedBorder()));
            add(scrollPane);

            JPanel p1 = new JPanel();
            JMenuBar menuBar = new JMenuBar();
            menuBar.setBorder(new BevelBorder(BevelBorder.RAISED));
            JMenu menu = (JMenu) menuBar.add(new JMenu("Add"));
            String items[] = { "File or Directory of Files", "URL" };
            for (int i = 0; i < items.length; i++) {
                JMenuItem item = menu.add(new JMenuItem(items[i]));
                item.addActionListener(this);
            } 
            p1.add(menuBar);

            menuBar = new JMenuBar();
            menuBar.setBorder(new BevelBorder(BevelBorder.RAISED));
            menu = (JMenu) menuBar.add(new JMenu("Remove"));
            JMenuItem item = menu.add(new JMenuItem("Selected"));
            item.addActionListener(this);
            item = menu.add(new JMenuItem("All"));
            item.addActionListener(this);
            p1.add(menuBar);

            loopB = addButton("loop", p1);
            loopB.setBackground(Color.gray);
            loopB.setSelected(true);

            add("South", p1);
        }

        
        private JButton addButton(String name, JPanel p) {
            JButton b = new JButton(name);
            b.addActionListener(this);
            p.add(b);
            return b;
        }

 
        private void doFrame(String titleName) {
            int w = 500;
            int h = 130;
            JPanel panel = new JPanel(new BorderLayout());
            JPanel p1 = new JPanel();
            if (titleName.endsWith("URL")) {
                p1.add(new JLabel("URL :"));
                textField = new JTextField("http://foo.bar.com/foo.wav");
                textField.addActionListener(this);
            } else {
                p1.add(new JLabel("File or Dir :"));
                String sep = String.valueOf(System.getProperty("file.separator").toCharArray()[0]);
                String text = null;
                try {
                    text = System.getProperty("user.dir") + sep;
                } catch (SecurityException ex) {
                    reportStatus(ex.toString());
                    JavaSound.showInfoDialog();
                    return;
                }
                textField = new JTextField(text);
                textField.setPreferredSize(new Dimension(w-100, 30));
                textField.addActionListener(this);
            }
            p1.add(textField);
            panel.add(p1);
            JPanel p2 = new JPanel();
            applyB = addButton("Apply", p2);
            addButton("Cancel", p2);
            panel.add("South", p2);
            frame = new JFrame(titleName);
            frame.getContentPane().add("Center", panel);
            frame.pack();
            Dimension d = Toolkit.getDefaultToolkit().getScreenSize();
            frame.setLocation(d.width/2 - w/2, d.height/2 - h/2);
            frame.setSize(w, h);
            frame.setVisible(true);
        }


        public void actionPerformed(ActionEvent e) {
            Object object = e.getSource();
            if (object instanceof JTextField) {
                applyB.doClick();
            } else if (object instanceof JMenuItem) {
                JMenuItem mi = (JMenuItem) object;
                if (mi.getText().startsWith("File")) {
                    doFrame("Add File or Directory");
                } else if (mi.getText().equals("URL")) {
                    doFrame("Add URL");
                } else if (mi.getText().equals("Selected")) {
                    int rows[] = table.getSelectedRows();
                    Vector tmp = new Vector();
                    for (int i = 0; i < rows.length;i++) {
                        tmp.add(sounds.get(rows[i]));
                    }
                    sounds.removeAll(tmp);
                    tableChanged();
                } else if (mi.getText().equals("All")) {
                    sounds.clear();
                    tableChanged();
                }
            } else if (object instanceof JButton) {
                JButton button = (JButton) e.getSource();
                if (button.getText().equals("Apply")) {
                    String name = textField.getText().trim();
                    if (name.startsWith("http") || name.startsWith("file")) {
                        try {
                            sounds.add(new URL(name));
                        } catch (Exception ex) { ex.printStackTrace(); };
                    } else {
                        loadJuke(name);
                    }
                    tableChanged();
                } else if (button.getText().equals("Cancel")) {
                    frame.dispose();
                    frame = null;
                    errStr = null;
                    playbackMonitor.repaint();
                } else if (button.getText().equals("loop")) {
                    loopB.setSelected(!loopB.isSelected());
                    loopB.setBackground(loopB.isSelected() ? Color.gray : Color.lightGray);
                }
                startB.setEnabled(sounds.size() != 0);
            }
        }

        public void tableChanged() {
            table.tableChanged(new TableModelEvent(dataModel));
        }
    }  // End JukeTable



    /**
     * Animation thread for when an audio file loads.
     */
    class Loading extends Thread {

        double extent; int incr;

        public void run() {
            extent = 360.0; incr = 10;
            while (true) {
                try { sleep(99); } catch (Exception ex) { break; }
                playbackMonitor.repaint();
            }
        }

        public void render(Dimension d, Graphics2D g2) { 
            if (isAlive()) {
                FontRenderContext frc = g2.getFontRenderContext();
                TextLayout tl = new TextLayout("Loading", g2.getFont(), frc);
                float sw = (float) tl.getBounds().getWidth();
                tl.draw(g2, d.width-sw-45, d.height-10);
                double x = d.width-33, y = d.height-30, ew = 25, eh = 25;
                g2.draw(new Ellipse2D.Double(x,y,ew,eh));
                g2.fill(new Arc2D.Double(x,y,ew,eh,90,extent,Arc2D.PIE));
                if ((extent -= incr) < 0) {
                    extent = 350.0;
                }
            }
        }
    }

            

    /**
     * Animation thread for the contributors of Java Sound.
     */
    class Credits extends Thread {

        int x;
        Font font16 = new Font("serif", Font.PLAIN, 16);
        String contributors = "Contributors : Kara Kytle, " + 
                              "Jan Borgersen, " + "Brian Lichtenwalter";
        int strWidth = getFontMetrics(font16).stringWidth(contributors);

        public void run() {
            x = -999; 
            while (!playbackMonitor.isShowing()) {
                try { sleep(999); } catch (Exception e) { return; }
            }
            for (int i = 0; i < 100; i++) {
                try { sleep(99); } catch (Exception e) { return; }
            }
            while (true) {
                if (--x < -strWidth) {
                    x = playbackMonitor.getSize().width;
                }
                playbackMonitor.repaint();
                try { sleep(99); } catch (Exception ex) { break; }
            }
        }

        public void render(Dimension d, Graphics2D g2) {
            if (isAlive()) {
                g2.setFont(font16);
                g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, 
                                    RenderingHints.VALUE_ANTIALIAS_OFF);
                g2.drawString(contributors, x, d.height-5);
            }
        }
    }
            

    public static void main(String args[]) {
        String media = "./audio";
        final Juke juke = new Juke(args.length == 0 ? media : args[0]);
        juke.open();
        JFrame f = new JFrame("Juke Box");
        f.addWindowListener(new WindowAdapter() {
            public void windowClosing(WindowEvent e) {System.exit(0);}
            public void windowIconified(WindowEvent e) { 
                juke.credits.interrupt();
            }
        });
        f.getContentPane().add("Center", juke);
        f.pack();
        Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
        int w = 750;
        int h = 340;
        f.setLocation(screenSize.width/2 - w/2, screenSize.height/2 - h/2);
        f.setSize(w, h);
        f.setVisible(true);
        if (args.length > 0) {
            File file = new File(args[0]);
            if (file == null && !file.isDirectory()) {
                System.out.println("usage: java Juke audioDirectory");
            } 
        }
    }
} 
