001    /*
002     Copyright (C) 2003 Adam Olsen
003    
004     This program is free software; you can redistribute it and/or modify
005     it under the terms of the GNU General Public License as published by
006     the Free Software Foundation; either version 1, or (at your option)
007     any later version.
008    
009     This program is distributed in the hope that it will be useful,
010     but WITHOUT ANY WARRANTY; without even the implied warranty of
011     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
012     GNU General Public License for more details.
013    
014     You should have received a copy of the GNU General Public License
015     along with this program; if not, write to the Free Software
016     Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
017     */
018    
019    package com.valhalla.jbother.sound;
020    
021    import java.awt.Toolkit;
022    import java.io.File;
023    import java.io.FileOutputStream;
024    import java.io.IOException;
025    import java.io.InputStream;
026    
027    import javax.sound.sampled.AudioFormat;
028    import javax.sound.sampled.AudioInputStream;
029    import javax.sound.sampled.AudioSystem;
030    import javax.sound.sampled.DataLine;
031    import javax.sound.sampled.LineUnavailableException;
032    import javax.sound.sampled.SourceDataLine;
033    
034    import com.valhalla.jbother.BuddyList;
035    import com.valhalla.jbother.JBother;
036    import com.valhalla.settings.Settings;
037    
038    /**
039     * Plays sounds with different methods. Available methods are: with Java Sound
040     * System, a system command, or a pc speaker beep
041     *
042     * @author Adam Olsen
043     * @version 1.0
044     */
045    public class SoundPlayer {
046        private Thread thread;
047    
048        private static SoundPlayer instance;
049    
050        private static boolean running = false;
051        protected static javax.swing.Timer timer = new javax.swing.Timer( 500, new java.awt.event.ActionListener()
052            {
053                public void actionPerformed( java.awt.event.ActionEvent e )
054                {
055                    timer.stop();
056                }
057            } );
058    
059        /**
060         * Constructor is private, because this is a Singleton
061         */
062        private SoundPlayer() {
063        }
064    
065        /**
066         * Plays a sound using the method in the settings
067         *
068         * @param settingName
069         *            the setting name to play
070         */
071        public static void play(String settingName) {
072            if( Settings.getInstance().getBoolean("noSound")) return;
073            if( BuddyList.getInstance().getCurrentPresenceMode() == org.jivesoftware.smack.packet.Presence.Mode.DO_NOT_DISTURB ) return;
074    
075            if (!Settings.getInstance().getBoolean(settingName + "Play"))
076                return;
077    
078    
079            if(timer.isRunning()) {
080                return;
081            }
082            else timer.start();
083    
084            String method = Settings.getInstance().getProperty("soundMethod");
085            if (method == null)
086                method = "";
087    
088            if (method.equals("Console Beep")) {
089                Toolkit.getDefaultToolkit().beep();
090                return;
091            }
092    
093            String strFilename = Settings.getInstance().getProperty(settingName);
094    
095            if (strFilename == null || strFilename.equals("")) {
096                com.valhalla.Logger.debug("no file to play");
097                return;
098            }
099    
100            if (strFilename.equals("(default)")) {
101                strFilename = loadDefault(settingName);
102                if (strFilename == null)
103                    return;
104            }
105    
106            if (method.equals("Command")) {
107                    if( running ) return;
108                    else running = true;
109                String c = Settings.getInstance().getProperty("soundCommand");
110                if (c.indexOf("%s") > -1)
111                    c = c.replaceAll("%s", strFilename);
112                else
113                    c = c + " " + strFilename;
114    
115                try {
116                    Runtime.getRuntime().exec(c);
117                } catch (java.io.IOException e) {
118                }
119                running = false;
120    
121                return;
122            }
123    
124            if (instance == null)
125                instance = new SoundPlayer();
126            if (instance.running)
127                return;
128    
129            instance.running = true;
130            instance.thread = new Thread(new SoundPlayerThread(instance,
131                    strFilename));
132            try {
133                instance.thread.start();
134            } catch (Exception ex) {
135                instance.running = false;
136            }
137        }
138    
139        public static boolean playSoundFile(String file, String method,
140                String soundCommand) {
141    
142            if(timer.isRunning()) return true;
143            else timer.start();
144    
145            if (method.equals("Console Beep")) {
146                Toolkit.getDefaultToolkit().beep();
147                return true;
148            }
149    
150            if (method.equals("Command")) {
151                String c = soundCommand;
152                if (c.indexOf("%s") > -1)
153                    c = c.replaceAll("%s", file);
154                else
155                    c = c + " " + file;
156    
157                try {
158                    Runtime.getRuntime().exec(c);
159                } catch (java.io.IOException e) {
160                }
161                return true;
162            }
163    
164            File f = new File(file);
165            if (!f.exists())
166                return false;
167            if (instance == null)
168                instance = new SoundPlayer();
169            if (instance.running)
170                return true;
171    
172            instance.running = true;
173            instance.thread = new Thread(new SoundPlayerThread(instance, file));
174            try {
175                instance.thread.start();
176            } catch (Exception ex) {
177                instance.running = false;
178                return false;
179            }
180            return true;
181        }
182    
183        /**
184         * Set the "running" state to false
185         */
186        protected void nullIt() {
187            running = false;
188        }
189    
190        /**
191         * Loads a default sound from the running jar file and puts it into a cache
192         *
193         * @param settingName
194         *            the setting to load
195         */
196        public static String loadDefault(String settingName) {
197            String defaultDir = Settings.getInstance().getProperty( "defaultSoundSet", "default" );
198    
199            try {
200                File cacheDir = new File(JBother.settingsDir + File.separatorChar
201                        + "soundcache" + File.separatorChar + defaultDir );
202                if (!cacheDir.isDirectory() && !cacheDir.mkdirs()) {
203                    com.valhalla.Logger
204                            .debug("Could not create sound cache directory.");
205                    return null;
206                }
207    
208                File outPutFile = new File(cacheDir.getPath() + File.separatorChar
209                        + settingName + ".wav");
210                if (outPutFile.exists())
211                    return outPutFile.getPath();
212    
213                InputStream file = BuddyList.getInstance().getClass()
214                        .getClassLoader().getResourceAsStream(
215                                "sounds/" + defaultDir + "/" + settingName + ".wav");
216                if (file == null) {
217                    com.valhalla.Logger
218                            .debug("Could not find default sound file in resources for "
219                                    + settingName);
220                    return null;
221                }
222    
223                FileOutputStream out = new FileOutputStream(outPutFile);
224    
225                byte data[] = new byte[1024];
226                while (file.available() > 0) {
227                    int size = file.read(data);
228                    out.write(data, 0, size);
229                }
230    
231                file.close();
232                out.close();
233    
234                return outPutFile.getPath();
235            } catch (IOException ex) {
236                com.valhalla.Logger.debug(ex.getMessage());
237            }
238    
239            return null;
240        }
241    
242        /*public static void clearCache()
243        {
244            try {
245                File cacheDir = new File(JBother.profileDir + File.separatorChar
246                    + "soundcache");
247                if (!cacheDir.isDirectory() && !cacheDir.mkdirs()) {
248                    com.valhalla.Logger
249                            .debug("Could not create sound cache directory.");
250                    return;
251                }
252    
253                File files[] = cacheDir.listFiles();
254                for( int i = 0; i < files.length; i++ )
255                {
256                    File file = files[i];
257                    if( file.getName().endsWith( ".wav" ) )
258                    {
259                        file.delete();
260                    }
261                }
262            }
263            catch( Exception e ) { com.valhalla.Logger.logException( e ); }
264        }*/
265    }
266    
267    /**
268     * Plays a sound using the Java Sound System
269     *
270     * @author jresources.org
271     * @version ?
272     */
273    
274    class SoundPlayerThread implements Runnable {
275        private static final int EXTERNAL_BUFFER_SIZE = 128000;
276    
277        private String strFilename;
278    
279        private SoundPlayer player;
280    
281        /**
282         * Sets up the thread with a specified sound
283         *
284         * @param player
285         *            the calling player
286         * @param file
287         *            the .wav file to play
288         */
289        public SoundPlayerThread(SoundPlayer player, String file) {
290            this.player = player;
291            this.strFilename = file;
292        }
293    
294        public void run() {
295            File soundFile = new File(strFilename);
296    
297            //this code taken from jseresources.org. Thanks!
298    
299            /*
300             * We have to read in the sound file.
301             */
302            AudioInputStream audioInputStream = null;
303            try {
304                audioInputStream = AudioSystem.getAudioInputStream(soundFile);
305            } catch (Exception e) {
306                /*
307                 * In case of an exception, we dump the exception including the
308                 * stack trace to the console output. Then, we exit the program.
309                 */
310                com.valhalla.Logger.logException(e);
311            }
312    
313            /*
314             * From the AudioInputStream, i.e. from the sound file, we fetch
315             * information about the format of the audio data. These information
316             * include the sampling frequency, the number of channels and the size
317             * of the samples. These information are needed to ask Java Sound for a
318             * suitable output line for this audio file.
319             */
320            AudioFormat audioFormat = audioInputStream.getFormat();
321    
322            /*
323             * Asking for a line is a rather tricky thing. We have to construct an
324             * Info object that specifies the desired properties for the line.
325             * First, we have to say which kind of line we want. The possibilities
326             * are: SourceDataLine (for playback), Clip (for repeated playback) and
327             * TargetDataLine (for recording). Here, we want to do normal playback,
328             * so we ask for a SourceDataLine. Then, we have to pass an AudioFormat
329             * object, so that the Line knows which format the data passed to it
330             * will have. Furthermore, we can give Java Sound a hint about how big
331             * the internal buffer for the line should be. This isn't used here,
332             * signaling that we don't care about the exact size. Java Sound will
333             * use some default value for the buffer size.
334             */
335            SourceDataLine line = null;
336            DataLine.Info info = new DataLine.Info(SourceDataLine.class,
337                    audioFormat);
338            try {
339                line = (SourceDataLine) AudioSystem.getLine(info);
340    
341                /*
342                 * The line is there, but it is not yet ready to receive audio data.
343                 * We have to open the line.
344                 */
345                line.open(audioFormat);
346            } catch (LineUnavailableException e) {
347                com.valhalla.Logger.logException(e);
348            } catch (Exception e) {
349                com.valhalla.Logger.logException(e);
350            }
351    
352            if (line == null) {
353                player.nullIt();
354    
355                return;
356            }
357    
358            /*
359             * Still not enough. The line now can receive data, but will not pass
360             * them on to the audio output device (which means to your sound card).
361             * This has to be activated.
362             */
363            line.start();
364    
365            /*
366             * Ok, finally the line is prepared. Now comes the real job: we have to
367             * write data to the line. We do this in a loop. First, we read data
368             * from the AudioInputStream to a buffer. Then, we write from this
369             * buffer to the Line. This is done until the end of the file is
370             * reached, which is detected by a return value of -1 from the read
371             * method of the AudioInputStream.
372             */
373            int nBytesRead = 0;
374            byte[] abData = new byte[EXTERNAL_BUFFER_SIZE];
375            while (nBytesRead != -1) {
376                try {
377                    nBytesRead = audioInputStream.read(abData, 0, abData.length);
378                } catch (IOException e) {
379                    com.valhalla.Logger.logException(e);
380                }
381                if (nBytesRead >= 0) {
382                    int nBytesWritten = line.write(abData, 0, nBytesRead);
383                }
384            }
385    
386            /*
387             * Wait until all data are played. This is only necessary because of the
388             * bug noted below. (If we do not wait, we would interrupt the playback
389             * by prematurely closing the line and exiting the VM.)
390             *
391             * Thanks to Margie Fitch for bringing me on the right path to this
392             * solution.
393             */
394            line.drain();
395    
396            /*
397             * All data are played. We can close the shop.
398             */
399            line.close();
400    
401            player.nullIt();
402    
403            try {
404                Thread.sleep(200);
405            } catch (InterruptedException e) {
406            }
407    
408            return;
409        }
410    }