/* * ALMA - Atacama Large Millimiter Array (c) European Southern Observatory, 2010 * * This library is free software; you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License as published by the Free * Software Foundation; either version 2.1 of the License, or (at your option) * any later version. * * This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more * details. * * You should have received a copy of the GNU Lesser General Public License * along with this library; if not, write to the Free Software Foundation, Inc., * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * */ package alma.acsplugins.alarmsystem.gui.sound; import java.io.IOException; import java.net.URL; import java.util.Map; import java.util.Set; import java.util.Timer; import java.util.TimerTask; import javax.sound.sampled.AudioFileFormat; import javax.sound.sampled.AudioFormat; import javax.sound.sampled.AudioInputStream; import javax.sound.sampled.AudioSystem; import javax.sound.sampled.DataLine; import javax.sound.sampled.LineUnavailableException; import javax.sound.sampled.Mixer; import javax.sound.sampled.SourceDataLine; import javax.sound.sampled.UnsupportedAudioFileException; import alma.acsplugins.alarmsystem.gui.table.AlarmTableModel; /** * A class to play sounds for alarms. * <P> * The class emits a sound every <code>TIME_INTERVAL</code> seconds * if the table contains alarms not seen by the user. * The sound depends on the highest priority of the alarms * not seen i.e. every priority has its own specific sound. * <P> * It is possible to inhibit the sound for a given period of * time, <code>INHIBIT_TIME</code>. * After <code>INHIBIT_TIME</code> seconds, the sounds will be * re-enabled. * <P> * In case of error playing a sound, objects of this class do * nothing because we do not want to trigger a failure in the * application neither to present a dialog to the operator. * Apart of that, if the audio system is in use by another * application then it might happen that it is not possible * to reserve a new line and play the sound.; * <P> * <I>Note</I>: * after testing in tf-ohg I have found that the STE sound environment * is quite problematic. for example in my account there were 4 mixers * but I have tried another one that add only one and in that case it was * not possible to find a line to play the sound. * With my account, even with 4 mixers available the process was able * to play the sound but not in the speakers. * <BR> * To limit the risk that the sound is played in the wrong device or * that the line obtained by default is not available because locked * by another application and so n, I decided to play the sound * in all available devices with the risk to play it twice. * * @author acaproni * @since ACS 8.1.0 */ public class AlarmSound extends TimerTask { /** * The table model */ private final AlarmTableModel tableModel; /** * The size of the buffer */ private final int EXTERNAL_BUFFER_SIZE = 524288; // 128Kb /** * A sound is emitted every TIME_INTERVAL seconds */ private static final int TIME_INTERVAL=60; /** * Inhibit the sound for the given amount of seconds. * <P> * The <code>INHIBIT_TIME</code> is a multiple of * <code>TIME_INTERVAL</code>. */ private static final int INHIBIT_TIME=15; /** * Objects of this class plays a sound when the priority of * an unseen alarm is equal or less then soundLevel appears in * the table. * <P> * It is not possible to inhibit alarms having priority 0 or 1 * therefore this property can only have values in range [1-3] */ private int soundLevel=3; /** * The counter to re-enable the level * <P> * It is reset to 0 when a new level is selected or after * <code>INHIBIT_CONTER</code> seconds. */ private int inhibitCounter=0; /** * The timer to emit the sound */ private final Timer timer = new Timer("AlarmSound",true); /** * <code>true</code> if the object has been close. */ private boolean closed=false; /** * A listener for sound events. * <P> * This class supports only one listener; if more then * one listener is added then only the last one will be notified. */ private volatile AlarmSoundListener listener=null; /** * The URLs of the sounds to play. * <P> * The index of the array correspond to the level of the * alarm i.e. for alarms with level=0 the sound to * play is <code>soundURLs[0]</code>. * */ private static final URL[] soundURLs = { AlarmSound.class.getResource("/alma/acsplugins/alarmsystem/gui/sound/resources/level0.wav"), AlarmSound.class.getResource("/alma/acsplugins/alarmsystem/gui/sound/resources/level1.wav"), AlarmSound.class.getResource("/alma/acsplugins/alarmsystem/gui/sound/resources/level2.wav"), AlarmSound.class.getResource("/alma/acsplugins/alarmsystem/gui/sound/resources/level3.wav") }; /** * Constructor * * @param model The table model */ public AlarmSound(AlarmTableModel model) { if (model==null) { throw new IllegalArgumentException("The AlarmTableModel can't be null"); } tableModel=model; // Start the timer timer.schedule(this, TIME_INTERVAL*1000, TIME_INTERVAL*1000); //dumpAudioInformation(); } /** * Play the sound for the given priority * * @param priority The priority of the alarm */ private void play(int priority) throws Exception { if (priority<0 || priority>3) { throw new IllegalStateException("Invalid alarm priority "+priority); } URL url=soundURLs[priority]; AudioInputStream audioInputStream=null; try { audioInputStream=AudioSystem.getAudioInputStream(url); } catch (Throwable t) { // If there is an error then the panel does nothing // It might happen for example if another application // is locking the audio. System.err.println(t.getMessage()); t.printStackTrace(); return; } // Obtain the information about the AudioInputStream AudioFormat audioFormat = audioInputStream.getFormat(); SourceDataLine line=null; DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat); // Get the list of available mixers Mixer.Info[] mixersInfo = AudioSystem.getMixerInfo(); // Try to get and open a line in one of the mixers until // one is available is found for (int i=0; i<mixersInfo.length && line==null; i++) { Mixer.Info mi=mixersInfo[i]; try { Mixer mixer = AudioSystem.getMixer(mi); line=(SourceDataLine)mixer.getLine(info); } catch (LineUnavailableException lue) { System.err.println("Line unavailable "+lue.getMessage()); line=null; continue; } catch (Throwable t) { System.err.println("Exception getting the line "+t.getMessage()); line=null; continue; } try { line.open(audioFormat, EXTERNAL_BUFFER_SIZE); } catch (Throwable t) { System.err.println("Error opeining the line: "+t.getMessage()); line=null; continue; } try { line.start(); } catch (Throwable t) { System.err.println("Error starting the line: "+t.getMessage()); line=null; continue; } try { playOnLine(line,audioInputStream); } catch (Throwable t) { System.err.println("Error playing: "+t.getMessage()); line=null; continue; } // plays what's left and and closes the audioChannel line.drain(); line.close(); } } /** * Play the sound using the passed line. * * @param line * @param audioIStream */ private void playOnLine(SourceDataLine line,AudioInputStream audioIStream) { if (line==null) { return; } int readBytes = 0; byte[] audioBuffer = new byte[EXTERNAL_BUFFER_SIZE]; try { while (readBytes != -1) { readBytes = audioIStream.read(audioBuffer, 0,audioBuffer.length); if (readBytes >= 0){ int ret=line.write(audioBuffer, 0, readBytes); } } } catch (IOException e1) { System.err.println("Exception caught: "+e1.getMessage()); } } /** * Set the level of the alarm to which a sound has to be played. * * @param level The new level of the sound to play; * level must be in the same range of {@link AlarmSound#soundLevel}. * @see {@link AlarmSound#soundLevel} */ public void inhibit(int level) { if (level<1 || level>3) { throw new IllegalArgumentException("Level "+level+" not in [1,3]"); } soundLevel=level; inhibitCounter=0; } /** * Return the inhibit sound level * * @return thethe inhibit sound level * @see {@link AlarmSound#soundLevel} */ public int getSoundLevel() { return soundLevel; } /** * Close the timer. */ public void close() { listener=null; closed=true; timer.cancel(); } /** * The task to emit the sound. */ @Override public void run() { // Should the inhibit level be cleared? if (++inhibitCounter>=INHIBIT_TIME) { inhibitCounter=0; soundLevel=3; if (listener!=null) { listener.reset(); } } final int pri= tableModel.hasNotAckAlarms(); // Audibles inhibited for the lowest 2 priorities till // the alarm system is better is better configured // // TODO: remove the following link to re-enable sounds for // lowest priorities if (pri>1) { return; } if (pri<0 || pri>soundLevel) { // No unseen alarms in table // or alarm inhibited return; } if (listener!=null) { listener.playing(pri); } // Emit the sound in a separate thread Thread soundPlayerThread = new Thread(new Runnable() { public void run() { try { play(pri); } catch (Throwable t) { // Write a message in stderr System.err.println("Error emitting sound for priority "+pri); t.printStackTrace(System.err); } finally { if (listener!=null) { listener.played(); } } } }, "SoundPlayer"); soundPlayerThread.setDaemon(true); soundPlayerThread.start(); } /** * Add or remove a listener * * @param listener If not <code>null</code> add the listener; * otherwise remove the listener */ public void addSoundListener(AlarmSoundListener listener) { this.listener=listener; } /** * Dump info about supported audio, file types and so on... * <P> * This method is useful while updating the audio files. */ private void dumpAudioInformation() { // Java supported file types AudioFileFormat.Type[] fileTypes= AudioSystem.getAudioFileTypes(); if (fileTypes==null || fileTypes.length==0) { System.out.println("No audio file types supported."); } else { for (AudioFileFormat.Type type: fileTypes) { System.out.println(type.toString()+", extension "+type.getExtension()); } } Mixer.Info[] mixerInfos=AudioSystem.getMixerInfo(); System.out.println("Mixers found: "+mixerInfos.length); for (Mixer.Info mi: mixerInfos) { System.out.println("\tMixer "+mi.getName()+": "+mi.getVendor()+", "+mi.getDescription()); } // Dump info about the alarm files for (URL url: soundURLs) { AudioFileFormat format=null; try { format=AudioSystem.getAudioFileFormat(url); } catch (IOException ioe) { System.err.println("Error "+ioe.getMessage()+" accessing URL "+url.toString()); continue; } catch (UnsupportedAudioFileException ue) { System.err.println("Unsupported audio format for "+url+" ("+ue.getMessage()+")"); } System.out.println("Properties of "+url); System.out.println("\tAudio file type "+format.getType().toString()); System.out.println("\tIs file type supported: "+AudioSystem.isFileTypeSupported(format.getType())); System.out.println("\tLength in byes "+format.getByteLength()); Map<String,Object> props=format.properties(); Set<String> keys=props.keySet(); for (String str: keys) { System.out.println("\t["+str+", "+props.get(str).toString()+"]"); } AudioFormat aFormat=format.getFormat(); System.out.println("\tEncoding "+aFormat.getEncoding().toString()); System.out.print("\tByte order "); if (aFormat.isBigEndian()) { System.out.println("big endian"); } else { System.out.println("little endian"); } System.out.println("\tSample rate: "+aFormat.getSampleRate()); System.out.println("\tNum. of bits of a sample: "+aFormat.getSampleSizeInBits()); System.out.println("\tNum. of channels: "+aFormat.getChannels()); } } }