/*
 * @(#)SoundUtilities.java    1.1  23 August 2001
 *
 * Copyright 2004
 * College of Computer and Information Science
 * Northeastern University
 * Boston, MA  02115
 *
 * The Java Power Tools software may be used for educational
 * purposes as long as this copyright notice is retained intact
 * at the top of all source files.
 *
 * To discuss possible commercial use of this software, 
 * contact Richard Rasala at Northeastern University, 
 * College of Computer and Information Science,
 * 617-373-2462 or rasala@ccs.neu.edu.
 *
 * The Java Power Tools software has been designed and built
 * in collaboration with Viera Proulx and Jeff Raab.
 *
 * Should this software be modified, the words "Modified from 
 * Original" must be included as a comment below this notice.
 *
 * All publication rights are retained.  This software or its 
 * documentation may not be published in any media either
 * in whole or in part without explicit permission.
 *
 * This software was created with support from Northeastern 
 * University and from NSF grant DUE-9950829.
 */

package edu.neu.ccs.util;

import java.io.*;
import java.net.URL;
import java.util.Vector;
import javax.sound.midi.*;
import javax.sound.sampled.*;

/**
 * <P>Provides static utility methods for playback of sound.</P>
 *
 * @author  Jeff Raab
 * @version 2.2
 * @since   1.1
 */
public class SoundUtilities {

    /** Lines currently running. */
    private static Vector currentLines = new Vector();
    
    /** Gain for all lines created through this interface. */
    private static float gain = 1.0f;
    
    /** Whether or not lines created through this interface are muted. */
    private static boolean muted = false;
    
    /** Pan for all lines created through this interface. */
    private static float pan = 0.0f;
    
    ////////////////
    // Public API //
    ////////////////
    
    /**
     * Sets the gain for all lines created through this interface
     * to the given gain in dB.
     *
     * @param dB the desired gain level
     * @see #getGain()
     */
    public static void setGain(float dB) {
        gain = dB;
    
        Line[] lines = (Line[])currentLines.toArray(new Line[0]);
        for (int i = 0; i < lines.length; i++)
            SoundUtilities.applyGain(gain, lines[i]);
    }
    
    /** 
     * Returns the gain for all lines created through this interface. 
     *
     * @see #setGain(float)
     */
    public static float getGain() {
        return gain;
    }
    
    /**
     * Sets whether or not sound is muted for all lines
     * created through this interface.
     *
     * @param isMuted whether or not sound is muted
     */
    public static void setMuted(boolean isMuted) {
        if (isMuted == muted)
            return;
            
        muted = isMuted;

        setGain(gain);
    }
    
    /** 
     * Returns whether or not sound is muted for all lines
     * created through this interface.
     *
     * @see #setMuted(boolean)
     */
    public static boolean isMuted() {
        return muted;
    }
    
    /**
     * Sets the pan for all lines created through this interface
     * to the given pan.
     *
     * @param p the desired pan level
     * @see #getPan()
     */
    public static void setPan(float p) {
        pan = p;
    
        Line[] lines = (Line[])currentLines.toArray(new Line[0]);
        for (int i = 0; i < lines.length; i++)
            SoundUtilities.applyPan(pan, lines[i]);
    }
    
    /** 
     * Returns the pan for all lines created through this interface. 
     *
     * @see #setPan(float)
     */
    public static float getPan() {
        return pan;
    }
    
    /**
     * Plays the sound file with the given file name.
     *
     * If a file with the given name cannot be found, opened or read,
     * this playback operation is cancelled.
     *
     * If the audio system for the host machine is unavailable,
     * this playback operation is cancelled.
     *
     * @param soundFileName the file name for the sound to be played
     */
    public static SourceDataLine playSound(String soundFileName) {
        return SoundUtilities.playSound(new File(soundFileName));
    }
    
    /*
     * Plays the given sound file.
     *
     * If the given file cannot be found, opened, or read,
     * this playback operation is cancelled.
     *
     * If the audio system for the host machine is unavailable,
     * this playback operation is cancelled.
     *
     * @param soundFile the audio file to be played
     */
    public static SourceDataLine playSound(File soundFile) {
        try {
            return SoundUtilities.playImpl(
                AudioSystem.getAudioInputStream(soundFile));
        }
        catch (IOException ex) {}
        catch (UnsupportedAudioFileException ex2) {}
        
        return null;
    }
    
    /*
     * Plays the sound file at the given URL.
     *
     * If the file at the given URL cannot be found, opened, or read,
     * this playback operation is cancelled.
     *
     * If the audio system for the host machine is unavailable,
     * this playback operation is cancelled.
     *
     * @param soundFile the audio file to be played
     */
    public static SourceDataLine playSound(URL soundURL) {
        try {
            return SoundUtilities.playImpl(
                AudioSystem.getAudioInputStream(soundURL));
        }
        catch (IOException ex) {}
        catch (UnsupportedAudioFileException ex2) {}
        
        return null;
    }
    
    /**
     * Plays the MIDI sequence stored in a file with the given name.
     *
     * If a file with the given name cannot be found, opened, or read,
     * this playback operation is cancelled.
     *
     * If the MIDI system for the host machine is unavailable,
     * this playback operation is cancelled.
     *
     * @param midiFileName the file name for the desired sequence
     */
    public static Thread playMidi(String midiFileName) {
        try {
            return SoundUtilities.playImpl(
                MidiSystem.getSequence(new File(midiFileName)));
        }
        catch (IOException ex) {}
        catch (InvalidMidiDataException ex2) {}
        
        return null;
    }
    
    /**
     * Plays the MIDI sequence stored in the given file.
     *
     * If the given file cannot be found, opened, or read,
     * this playback operation is cancelled.
     *
     * If the MIDI system for the host machine is unavailable,
     * this playback operation is cancelled.
     *
     * @param midiFile the file containing the desired sequence
     */
    public static Thread playMidi(File midiFile) {
        try {
            return SoundUtilities.playImpl(
            MidiSystem.getSequence(midiFile));
        }
        catch (IOException ex) {}
        catch (InvalidMidiDataException ex2) {}
        
        return null;
    }
    
    /**
     * Plays the MIDI sequence stored in the file at the given URL.
     *
     * If the file at the given URL cannot be found, opened, or read,
     * this playback operation is cancelled.
     *
     * If the MIDI system for the host machine is unavailable,
     * this playback operation is cancelled.
     *
     * @param midiFile the file containing the desired sequence
     */
    public static Thread playMidi(URL midiURL) {
        try {
            SoundUtilities.playImpl(MidiSystem.getSequence(midiURL));
        }
        catch (IOException ex) {}
        catch (InvalidMidiDataException ex2) {}
        
        return null;
    }
    
    /////////////////////
    // Private methods //
    /////////////////////

    /** Plays the given audio input stream. */
    private static SourceDataLine playImpl(
        final AudioInputStream stream) 
    {
        final AudioFormat soundFormat = stream.getFormat();

        // define the requirements for a playback line
        DataLine.Info info = new DataLine.Info(
            SourceDataLine.class, soundFormat);

        // quit if a compatible line can't be found
        if (!AudioSystem.isLineSupported(info))
            return null;

        // get and open the line for playback
        final SourceDataLine line;
        try {
            line = (SourceDataLine)AudioSystem.getLine(info);
            line.open(soundFormat, 16384);
        } catch (LineUnavailableException ex) { 
            return null;
        }

        // apply sound environment
        SoundUtilities.applyGain(gain, line);
        SoundUtilities.applyPan(pan, line);
        
        // add line to list of current lines
        currentLines.add(line);

        // make a thread to play the audio data
        Thread playThread = new Thread() {
            public void run() {
                try {
                    int size = line.getBufferSize() / 8 * 
                               soundFormat.getFrameSize();
                    byte[] buffer = new byte[size];
                    int count;

                    while (true) {
                        count = stream.read(buffer);

                        if (count == -1)
                            break;

                        while (count > 0)
                            count -= line.write(buffer, 0, count);
                    }
                    
                    line.stop();
                    line.close();

                // quit if anything goes wrong
                } catch (Exception e) {}
                
                // clean up line
                SoundUtilities.removeLine(line);
            }
        };
        
        // start the source data line and the playback thread
        line.start();
        playThread.start();
        
        // return the sound output line
        return line;
    }

    /** Plays the given MIDI sequence. */
    private static Thread playImpl(Sequence midiSong) {
        final Sequencer midiSequencer;
        try {
            midiSequencer = MidiSystem.getSequencer();

            if (!midiSequencer.isOpen())
                midiSequencer.open();
        }
        catch (MidiUnavailableException ex) {
            return null;
        }

        try {
            midiSequencer.setSequence(midiSong);
        }
        catch (InvalidMidiDataException ex) {
            return null;
        }
        
        Thread playThread = new Thread() {
            public void run() {
                midiSequencer.start();

                while (midiSequencer.isRunning())
                    JPTUtilities.pauseThread(50);
                    
                midiSequencer.stop();
            }
        };
        
        playThread.start();
        
        return playThread;
    }
    
    /** Removes the given line from the list of current lines. */
    private static void removeLine(Line l) {
        currentLines.remove(l);
    }

    /** Sets the gain for the given line to the given gain. */
    private static void applyGain(float gain, Line l) {
        try {
            if (muted)
                gain = -Float.MAX_VALUE;
    
            FloatControl gainControl = (FloatControl)l.getControl(
                FloatControl.Type.MASTER_GAIN);

            gain = (float)Math.max(gain, gainControl.getMinimum());
            gain = (float)Math.min(gain, gainControl.getMaximum());

            gainControl.setValue(gain);
        }
        catch (IllegalArgumentException ex) {}
    }

    /** Sets the pan for the given line to the given pan. */
    private static void applyPan(float pan, Line l) {
        try {
            FloatControl panControl = (FloatControl)l.getControl(
                FloatControl.Type.PAN);

            pan = (float)Math.max(pan, panControl.getMinimum());
            pan = (float)Math.min(pan, panControl.getMaximum());

            panControl.setValue(pan);
        }
        catch (IllegalArgumentException ex) {}
    }
}
