diff --git a/build.xml b/build.xml index da3e1745f..94ee922bd 100644 --- a/build.xml +++ b/build.xml @@ -1062,6 +1062,8 @@ prefix="net/java/sip/communicator/impl/media/protocol"/> + @@ -1095,6 +1097,8 @@ prefix="net/java/sip/communicator/impl/media/protocol"/> + @@ -1127,6 +1131,8 @@ prefix="net/java/sip/communicator/impl/media/protocol"/> + diff --git a/src/net/java/sip/communicator/impl/neomedia/NeomediaActivator.java b/src/net/java/sip/communicator/impl/neomedia/NeomediaActivator.java index a21dc6fed..419372979 100644 --- a/src/net/java/sip/communicator/impl/neomedia/NeomediaActivator.java +++ b/src/net/java/sip/communicator/impl/neomedia/NeomediaActivator.java @@ -6,6 +6,7 @@ */ package net.java.sip.communicator.impl.neomedia; +import net.java.sip.communicator.service.audionotifier.*; import net.java.sip.communicator.service.configuration.*; import net.java.sip.communicator.service.fileaccess.*; import net.java.sip.communicator.service.gui.*; @@ -14,6 +15,8 @@ import net.java.sip.communicator.service.resources.*; import net.java.sip.communicator.util.*; +import net.java.sip.communicator.impl.neomedia.notify.*; + import org.osgi.framework.*; /** @@ -122,6 +125,24 @@ public void start(BundleContext bundleContext) //following property to make sure that it would accept java generated //IPv6 addresses that contain address scope zones. System.setProperty("gov.nist.core.STRIP_ADDR_SCOPES", "true"); + + // AudioNotify Service + AudioNotifierServiceImpl audioNotifier = new AudioNotifierServiceImpl( + mediaServiceImpl.getDeviceConfiguration()); + + audioNotifier.setMute( + !getConfigurationService() + .getBoolean( + "net.java.sip.communicator.impl.sound.isSoundEnabled", + true)); + + getBundleContext() + .registerService( + AudioNotifierService.class.getName(), + audioNotifier, + null); + + logger.info("Audio Notifier Service ...[REGISTERED]"); } /** diff --git a/src/net/java/sip/communicator/impl/neomedia/neomedia.manifest.mf b/src/net/java/sip/communicator/impl/neomedia/neomedia.manifest.mf index a2741606c..1c2f9cfef 100644 --- a/src/net/java/sip/communicator/impl/neomedia/neomedia.manifest.mf +++ b/src/net/java/sip/communicator/impl/neomedia/neomedia.manifest.mf @@ -38,6 +38,7 @@ Import-Package: org.osgi.framework, gnu.java.zrtp.utils, gnu.java.zrtp.zidfile Export-Package: net.java.sip.communicator.service.neomedia, + net.java.sip.communicator.service.audionotifier, net.java.sip.communicator.service.neomedia.device, net.java.sip.communicator.service.neomedia.event, net.java.sip.communicator.service.neomedia.format diff --git a/src/net/java/sip/communicator/impl/neomedia/notify/AudioNotifierServiceImpl.java b/src/net/java/sip/communicator/impl/neomedia/notify/AudioNotifierServiceImpl.java new file mode 100644 index 000000000..e44327d0a --- /dev/null +++ b/src/net/java/sip/communicator/impl/neomedia/notify/AudioNotifierServiceImpl.java @@ -0,0 +1,175 @@ +/* + * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client. + * + * Distributable under LGPL license. + * See terms of license at gnu.org. + */ +package net.java.sip.communicator.impl.neomedia.notify; + +import java.net.*; +import java.util.*; + +import net.java.sip.communicator.impl.neomedia.*; +import net.java.sip.communicator.impl.neomedia.device.*; +import net.java.sip.communicator.service.audionotifier.*; +import net.java.sip.communicator.util.*; + +/** + * The implementation of the AudioNotifierService. + * + * @author Yana Stamcheva + */ +public class AudioNotifierServiceImpl + implements AudioNotifierService, + PropertyChangeListener +{ + private static final Map audioClips = + new HashMap(); + + private boolean isMute; + + /** + * Device config to look for notify device. + */ + private DeviceConfiguration deviceConfiguration; + + /** + * Creates audio notify service. + * @param deviceConfiguration the device configuration. + */ + public AudioNotifierServiceImpl(DeviceConfiguration deviceConfiguration) + { + this.deviceConfiguration = deviceConfiguration; + deviceConfiguration.addPropertyChangeListener(this); + } + + /** + * Creates an SCAudioClip from the given URI and adds it to the list of + * available audio-s. + * + * @param uri the path where the audio file could be found + */ + public SCAudioClipImpl createAudio(String uri) + { + SCAudioClipImpl audioClip; + + synchronized (audioClips) + { + if(audioClips.containsKey(uri)) + { + audioClip = audioClips.get(uri); + } + else + { + URL url = + NeomediaActivator.getResources().getSoundURLForPath(uri); + + if (url == null) + { + // Not found by the class loader. Perhaps it's a local file. + try + { + url = new URL(uri); + } + catch (MalformedURLException e) + { + //logger.error("The given uri could not be parsed.", e); + return null; + } + } + + try + { + if(getDeviceConfiguration().getAudioSystem().equals( + DeviceConfiguration.AUDIO_SYSTEM_JAVASOUND)) + { + audioClip = new JMFAudioClipImpl(url, this); + } + else if(getDeviceConfiguration().getAudioSystem().equals( + DeviceConfiguration.AUDIO_SYSTEM_PORTAUDIO)) + { + audioClip = new PortAudioClipImpl(url, this); + } + else + return null; + } + catch (Throwable e) + { + // Cannot create audio to play + return null; + } + + audioClips.put(uri, audioClip); + } + } + + return audioClip; + } + + /** + * Removes the given audio from the list of available audio clips. + * + * @param audioClip the audio to destroy + */ + public void destroyAudio(SCAudioClip audioClip) + { + synchronized (audioClips) { + audioClips.remove(audioClip); + } + } + + /** + * Enables or disables the sound in the application. If FALSE, we try to + * restore all looping sounds if any. + * + * @param isMute when TRUE disables the sound, otherwise enables the sound. + */ + public void setMute(boolean isMute) + { + this.isMute = isMute; + + for (SCAudioClipImpl audioClip : audioClips.values()) + { + if (isMute) + { + audioClip.internalStop(); + } + else if (audioClip.isLooping()) + { + audioClip.playInLoop(audioClip.getLoopInterval()); + } + } + } + + /** + * Returns TRUE if the sound is currently disabled, FALSE otherwise. + * @return TRUE if the sound is currently disabled, FALSE otherwise + */ + public boolean isMute() + { + return isMute; + } + + /** + * The device configuration. + * + * @return the deviceConfiguration + */ + public DeviceConfiguration getDeviceConfiguration() + { + return deviceConfiguration; + } + + /** + * Listens for changes in notify device + * @param evt the event that notify device has changed. + */ + public void propertyChange(PropertyChangeEvent evt) + { + if(evt.getPropertyName().equals( + DeviceConfiguration.AUDIO_NOTIFY_DEVICE)) + { + audioClips.clear(); + } + } +} diff --git a/src/net/java/sip/communicator/impl/neomedia/notify/JMFAudioClipImpl.java b/src/net/java/sip/communicator/impl/neomedia/notify/JMFAudioClipImpl.java new file mode 100644 index 000000000..d6422d58d --- /dev/null +++ b/src/net/java/sip/communicator/impl/neomedia/notify/JMFAudioClipImpl.java @@ -0,0 +1,201 @@ +/* + * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client. + * + * Distributable under LGPL license. + * See terms of license at gnu.org. + */ +package net.java.sip.communicator.impl.neomedia.notify; + +import java.applet.*; +import java.awt.event.*; +import java.io.*; +import java.lang.reflect.*; +import java.net.*; +import java.security.*; + +import javax.swing.*; + +import net.java.sip.communicator.service.audionotifier.*; + +/** + * Implementation of SCAudioClip. + * + * @author Yana Stamcheva + */ +public class JMFAudioClipImpl + extends SCAudioClipImpl + implements ActionListener + +{ + private static Constructor acConstructor = null; + + private final Timer playAudioTimer = new Timer(1000, null); + + private final AudioClip audioClip; + + private final AudioNotifierService audioNotifier; + + /** + * Creates the audio clip and initialize the listener used from the + * loop timer. + * + * @param url the url pointing to the audio file + * @param audioNotifier the audio notify service + * @throws IOException cannot audio clip with supplied url. + */ + public JMFAudioClipImpl(URL url, AudioNotifierService audioNotifier) + throws IOException + { + this.audioClip = createAppletAudioClip(url.openStream()); + this.audioNotifier = audioNotifier; + + this.playAudioTimer.addActionListener(this); + } + + /** + * Plays this audio. + */ + public void play() + { + if ((audioClip != null) && !audioNotifier.isMute()) + audioClip.play(); + } + + /** + * Plays this audio in loop. + * + * @param interval the loop interval + */ + public void playInLoop(int interval) + { + if(!audioNotifier.isMute()) + { + if(interval == 0) + audioClip.loop(); + else + { + //first play the audio and then start the timer and wait + audioClip.play(); + playAudioTimer.setDelay(interval); + playAudioTimer.setRepeats(true); + + playAudioTimer.start(); + } + } + + setLoopInterval(interval); + setIsLooping(true); + } + + /** + * Stops this audio. + */ + public void stop() + { + if (audioClip != null) + audioClip.stop(); + + if (isLooping()) + { + playAudioTimer.stop(); + setIsLooping(false); + } + } + + /** + * Stops this audio without setting the isLooping property in the case of + * a looping audio. The AudioNotifier uses this method to stop the audio + * when setMute(true) is invoked. This allows us to restore all looping + * audios when the sound is restored by calling setMute(false). + */ + public void internalStop() + { + if (audioClip != null) + audioClip.stop(); + + if (isLooping()) + playAudioTimer.stop(); + } + + /** + * Creates an AppletAudioClip. + * + * @param inputstream the audio input stream + * @throws IOException + */ + private static AudioClip createAppletAudioClip(InputStream inputstream) + throws IOException + { + if (acConstructor == null) + { + try + { + acConstructor + = AccessController.doPrivileged( + new PrivilegedExceptionAction>() + { + public Constructor run() + throws ClassNotFoundException, + NoSuchMethodException, + SecurityException + { + return createAcConstructor(); + } + }); + } + catch (PrivilegedActionException paex) + { + throw + new IOException( + "Failed to get AudioClip constructor: " + + paex.getException()); + } + } + + try + { + return acConstructor.newInstance(inputstream); + } + catch (Exception ex) + { + throw new IOException("Failed to construct the AudioClip: " + ex); + } + } + + @SuppressWarnings("unchecked") + private static Constructor createAcConstructor() + throws ClassNotFoundException, + NoSuchMethodException, + SecurityException + { + Class class1; + try + { + class1 + = Class.forName( + "com.sun.media.sound.JavaSoundAudioClip", + true, + ClassLoader.getSystemClassLoader()); + } + catch (ClassNotFoundException cnfex) + { + class1 + = Class.forName("sun.audio.SunAudioClip", true, null); + } + return + (Constructor) class1.getConstructor(InputStream.class); + } + + /** + * Plays an audio clip. Used in the playAudioTimer to play an audio in loop. + * @param e the event. + */ + public void actionPerformed(ActionEvent e) + { + if (audioClip != null) + { + audioClip.stop(); + audioClip.play(); + } + } +} diff --git a/src/net/java/sip/communicator/impl/neomedia/notify/PortAudioClipImpl.java b/src/net/java/sip/communicator/impl/neomedia/notify/PortAudioClipImpl.java new file mode 100644 index 000000000..47576eae9 --- /dev/null +++ b/src/net/java/sip/communicator/impl/neomedia/notify/PortAudioClipImpl.java @@ -0,0 +1,170 @@ +/* + * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client. + * + * Distributable under LGPL license. + * See terms of license at gnu.org. + */ +package net.java.sip.communicator.impl.neomedia.notify; + +import java.io.*; +import java.net.*; + +import javax.sound.sampled.*; + +import net.java.sip.communicator.impl.media.protocol.portaudio.*; +import net.java.sip.communicator.impl.media.protocol.portaudio.streams.*; +import net.java.sip.communicator.util.*; + +/** + * Implementation of SCAudioClip using PortAudio. + * + * @author Damian Minkov + */ +public class PortAudioClipImpl + extends SCAudioClipImpl +{ + private static final Logger logger + = Logger.getLogger(PortAudioClipImpl.class); + + private final AudioNotifierServiceImpl audioNotifier; + + private boolean started = false; + + private final URL url; + + /** + * Creates the audio clip and initialize the listener used from the + * loop timer. + * + * @param url the url pointing to the audio file + * @param audioNotifier the audio notify service + * @throws IOException cannot audio clip with supplied url. + */ + public PortAudioClipImpl(URL url, AudioNotifierServiceImpl audioNotifier) + throws IOException + { + this.audioNotifier = audioNotifier; + this.url = url; + } + + /** + * Plays this audio. + */ + public void play() + { + if ((url != null) && !audioNotifier.isMute()) + { + started = true; + new Thread(new PlayThread()).start(); + } + } + + /** + * Plays this audio in loop. + * + * @param interval the loop interval + */ + public void playInLoop(int interval) + { + setLoopInterval(interval); + setIsLooping(true); + + play(); + } + + /** + * Stops this audio. + */ + public void stop() + { + internalStop(); + setIsLooping(false); + } + + /** + * Stops this audio without setting the isLooping property in the case of + * a looping audio. The AudioNotifier uses this method to stop the audio + * when setMute(true) is invoked. This allows us to restore all looping + * audios when the sound is restored by calling setMute(false). + */ + public void internalStop() + { + if (url != null) + started = false; + } + + private class PlayThread + implements Runnable + { + byte[] buffer = new byte[1024]; + private OutputPortAudioStream portAudioStream = null; + + public void run() + { + try + { + while(true) + { + AudioInputStream audioStream = + AudioSystem.getAudioInputStream(url); + AudioFormat audioStreamFormat = audioStream.getFormat(); + + if (portAudioStream == null) + { + int deviceIndex = + PortAudioUtils.getDeviceIndexFromLocator( + audioNotifier.getDeviceConfiguration(). + getAudioNotifyDevice().getLocator()); + + portAudioStream = PortAudioManager.getInstance(). + getOutputStream( + deviceIndex, + audioStreamFormat.getSampleRate(), + audioStreamFormat.getChannels(), + PortAudioUtils.getPortAudioSampleFormat( + audioStreamFormat.getSampleSizeInBits())); + portAudioStream.start(); + } + + if(!started) + { + portAudioStream.stop(); + return; + } + + while(audioStream.read(buffer) != -1) + { + portAudioStream.write(buffer); + } + + if(!isLooping()) + { + portAudioStream.stop(); + break; + } + else + { + Thread.sleep(getLoopInterval()); + } + } + } + catch (PortAudioException e) + { + logger.error( + "Cannot open portaudio device for notifications", e); + } + catch (IOException e) + { + logger.error("Error reading from audio resource", e); + } + catch (InterruptedException e) + { + logger.error("Cannot wait the interval between plays", e); + } + catch (UnsupportedAudioFileException e) + { + logger.error("Unknown file format", e); + } + } + } +} diff --git a/src/net/java/sip/communicator/impl/neomedia/notify/SCAudioClipImpl.java b/src/net/java/sip/communicator/impl/neomedia/notify/SCAudioClipImpl.java new file mode 100644 index 000000000..745b3d288 --- /dev/null +++ b/src/net/java/sip/communicator/impl/neomedia/notify/SCAudioClipImpl.java @@ -0,0 +1,94 @@ +/* + * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client. + * + * Distributable under LGPL license. + * See terms of license at gnu.org. + */ +package net.java.sip.communicator.impl.neomedia.notify; + +import net.java.sip.communicator.service.audionotifier.*; + +/** + * Common properties impl for SCAudioClip. + * + * @author Damian Minkov + */ +public abstract class SCAudioClipImpl + implements SCAudioClip +{ + private boolean isLooping; + + private int loopInterval; + + private boolean isInvalid; + + /** + * Returns TRUE if this audio is invalid, FALSE otherwise. + * + * @return TRUE if this audio is invalid, FALSE otherwise + */ + public boolean isInvalid() + { + return isInvalid; + } + + /** + * Marks this audio as invalid or not. + * + * @param isInvalid TRUE to mark this audio as invalid, FALSE otherwise + */ + public void setInvalid(boolean isInvalid) + { + this.setIsInvalid(isInvalid); + } + + /** + * Returns TRUE if this audio is currently playing in loop, FALSE otherwise. + * @return TRUE if this audio is currently playing in loop, FALSE otherwise. + */ + public boolean isLooping() + { + return isLooping; + } + + /** + * Returns the loop interval if this audio is looping. + * @return the loop interval if this audio is looping + */ + public int getLoopInterval() + { + return loopInterval; + } + + /** + * @param isLooping the isLooping to set + */ + public void setIsLooping(boolean isLooping) + { + this.isLooping = isLooping; + } + + /** + * @param loopInterval the loopInterval to set + */ + public void setLoopInterval(int loopInterval) + { + this.loopInterval = loopInterval; + } + + /** + * @param isInvalid the isInvalid to set + */ + public void setIsInvalid(boolean isInvalid) + { + this.isInvalid = isInvalid; + } + + /** + * Stops this audio without setting the isLooping property in the case of + * a looping audio. The AudioNotifier uses this method to stop the audio + * when setMute(true) is invoked. This allows us to restore all looping + * audios when the sound is restored by calling setMute(false). + */ + public abstract void internalStop(); +}