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.*;
023    
024    import javax.sound.sampled.*;
025    import com.valhalla.jbother.*;
026    
027    import com.valhalla.settings.Settings;
028    
029    /**
030     * Plays sounds with different methods.
031     * Available methods are:  with Java Sound System, a system command, or a pc speaker beep
032     *
033     * @author Adam Olsen
034     * @version 1.0
035    */
036    public class SoundPlayer
037    {
038            private Thread thread;
039            private static SoundPlayer instance;
040            private boolean running = false;
041    
042            /**
043             * Constructor is private, because this is a Singleton
044            */
045            private SoundPlayer() { }
046    
047            /**
048             * Plays a sound using the method in the settings
049             * @param settingName the setting name to play
050            */
051            public static void play( String settingName )
052            {
053                    if( !Settings.getInstance().getBoolean( settingName + "Play" ) ) return;
054                    String method = Settings.getInstance().getProperty( "soundMethod" );
055                    if( method == null ) method = "";
056    
057                    if( method.equals( "Console Beep" ) )
058                    {
059                            Toolkit.getDefaultToolkit().beep();
060                            return;
061                    }
062    
063                    String strFilename = Settings.getInstance().getProperty( settingName );
064    
065                    if( strFilename == null || strFilename.equals( "" ) )
066                    {
067                            com.valhalla.Logger.debug( "no file to play" );
068                            return;
069                    }
070    
071                    if( strFilename.equals( "(default)" ) )
072                    {
073                            strFilename = loadDefault( settingName );
074                            if( strFilename == null ) return;
075                    }
076    
077                    if( method.equals( "Command" ) )
078                    {
079                            try {
080                                    Runtime.getRuntime().exec( Settings.getInstance().getProperty( "soundCommand" ) + " " + strFilename );
081                            }
082                            catch( java.io.IOException e ) { }
083    
084                            return;
085                    }
086    
087                    if( instance == null ) instance = new SoundPlayer();
088                    if( instance.running ) return;
089    
090                    instance.running = true;
091                    instance.thread = new Thread( new SoundPlayerThread( instance, strFilename ) );
092                    try {
093                            instance.thread.start();
094                    }
095                    catch( Exception ex ) { instance.running = false; }
096            }
097    
098            /**
099             * Set the "running" state to false
100            */
101            protected void nullIt()
102            {
103                    running = false;
104            }
105    
106            /**
107             * Loads a default sound from the running jar file and puts it into a cache
108             * @param settingName the setting to load
109            */
110            private static String loadDefault( String settingName )
111            {
112                    try {
113                            File cacheDir = new File( Settings.getInstance().getSettingsDir().getPath() + File.separatorChar + "soundcache" );
114                            if( !cacheDir.isDirectory() && !cacheDir.mkdirs() )
115                            {
116                                    com.valhalla.Logger.debug( "Could not create sound cache directory." );
117                                    return null;
118                            }
119    
120                            File outPutFile = new File( cacheDir.getPath() + File.separatorChar + settingName + ".wav" );
121                            if( outPutFile.exists() ) return outPutFile.getPath();
122    
123                            InputStream file =
124                                    BuddyList.getInstance().getClass().getClassLoader().getResourceAsStream( "sounds/default/" + settingName + ".wav" );
125                            if( file == null )
126                            {
127                                    com.valhalla.Logger.debug( "Could not find default sound file in resources for " + settingName );
128                                    return null;
129                            }
130    
131                            FileOutputStream out = new FileOutputStream( outPutFile );
132    
133                            byte data[] = new byte[1024];
134                            while( file.available() > 0 )
135                            {
136                                    int size = file.read( data );
137                                    out.write( data, 0, size );
138                            }
139    
140                            file.close();
141                            out.close();
142    
143                            return outPutFile.getPath();
144                    }
145                    catch( IOException ex )
146                    {
147                            com.valhalla.Logger.debug( ex.getMessage() );
148                    }
149    
150                    return null;
151            }
152    }
153    
154    /**
155     * Plays a sound using the Java Sound System
156     * @author jresources.org
157     * @version ?
158    */
159    class SoundPlayerThread implements Runnable
160    {
161            private static final int        EXTERNAL_BUFFER_SIZE = 128000;
162    
163            private String strFilename;
164            private SoundPlayer player;
165    
166            /**
167             * Sets up the thread with a specified sound
168             * @param player the calling player
169             * @param file the .wav file to play
170            */
171            public SoundPlayerThread( SoundPlayer player, String file )
172            {
173                    this.player = player;
174                    this.strFilename = file;
175            }
176    
177            public void run()
178            {
179                    File soundFile = new File(strFilename);
180    
181    
182                    //this code taken from jseresources.org.  Thanks!
183    
184                    /*
185                      We have to read in the sound file.
186                    */
187                    AudioInputStream        audioInputStream = null;
188                    try
189                    {
190                            audioInputStream = AudioSystem.getAudioInputStream(soundFile);
191                    }
192                    catch (Exception e)
193                    {
194                            /*
195                              In case of an exception, we dump the exception
196                              including the stack trace to the console output.
197                              Then, we exit the program.
198                            */
199                            com.valhalla.Logger.logException( e );
200                    }
201    
202                    /*
203                      From the AudioInputStream, i.e. from the sound file,
204                      we fetch information about the format of the
205                      audio data.
206                      These information include the sampling frequency,
207                      the number of
208                      channels and the size of the samples.
209                      These information
210                      are needed to ask Java Sound for a suitable output line
211                      for this audio file.
212                    */
213                    AudioFormat     audioFormat = audioInputStream.getFormat();
214    
215                    /*
216                      Asking for a line is a rather tricky thing.
217                      We have to construct an Info object that specifies
218                      the desired properties for the line.
219                      First, we have to say which kind of line we want. The
220                      possibilities are: SourceDataLine (for playback), Clip
221                      (for repeated playback)       and TargetDataLine (for
222                      recording).
223                      Here, we want to do normal playback, so we ask for
224                      a SourceDataLine.
225                      Then, we have to pass an AudioFormat object, so that
226                      the Line knows which format the data passed to it
227                      will have.
228                      Furthermore, we can give Java Sound a hint about how
229                      big the internal buffer for the line should be. This
230                      isn't used here, signaling that we
231                      don't care about the exact size. Java Sound will use
232                      some default value for the buffer size.
233                    */
234                    SourceDataLine  line = null;
235                    DataLine.Info   info = new DataLine.Info(SourceDataLine.class,
236                                                             audioFormat);
237                    try
238                    {
239                            line = (SourceDataLine) AudioSystem.getLine(info);
240    
241                            /*
242                              The line is there, but it is not yet ready to
243                              receive audio data. We have to open the line.
244                            */
245                            line.open(audioFormat);
246                    }
247                    catch (LineUnavailableException e)
248                    {
249                            com.valhalla.Logger.logException( e );
250                    }
251                    catch (Exception e)
252                    {
253                            com.valhalla.Logger.logException( e );
254                    }
255    
256                    if( line == null )
257                    {
258                            player.nullIt();
259    
260                            return;
261                    }
262    
263                    /*
264                      Still not enough. The line now can receive data,
265                      but will not pass them on to the audio output device
266                      (which means to your sound card). This has to be
267                      activated.
268                    */
269                    line.start();
270    
271                    /*
272                      Ok, finally the line is prepared. Now comes the real
273                      job: we have to write data to the line. We do this
274                      in a loop. First, we read data from the
275                      AudioInputStream to a buffer. Then, we write from
276                      this buffer to the Line. This is done until the end
277                      of the file is reached, which is detected by a
278                      return value of -1 from the read method of the
279                      AudioInputStream.
280                    */
281                    int     nBytesRead = 0;
282                    byte[]  abData = new byte[EXTERNAL_BUFFER_SIZE];
283                    while (nBytesRead != -1)
284                    {
285                            try
286                            {
287                                    nBytesRead = audioInputStream.read(abData, 0, abData.length);
288                            }
289                            catch (IOException e)
290                            {
291                                    com.valhalla.Logger.logException( e );
292                            }
293                            if (nBytesRead >= 0)
294                            {
295                                    int     nBytesWritten = line.write(abData, 0, nBytesRead);
296                            }
297                    }
298    
299                    /*
300                      Wait until all data are played.
301                      This is only necessary because of the bug noted below.
302                      (If we do not wait, we would interrupt the playback by
303                      prematurely closing the line and exiting the VM.)
304    
305                      Thanks to Margie Fitch for bringing me on the right
306                      path to this solution.
307                    */
308                    line.drain();
309    
310                    /*
311                      All data are played. We can close the shop.
312                    */
313                    line.close();
314    
315                    player.nullIt();
316    
317                    try {
318                            Thread.sleep( 200 );
319                    }
320                    catch( InterruptedException e ) { }
321    
322                    return;
323            }
324    }