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 }