/* * Jitsi, the OpenSource Java VoIP and Instant Messaging client. * * Distributable under LGPL license. * See terms of license at gnu.org. */ package org.jitsi.impl.neomedia; import java.awt.*; import java.lang.ref.*; import java.util.*; import java.util.List; import javax.media.*; import org.jitsi.service.configuration.*; import org.jitsi.service.libjitsi.*; import org.jitsi.service.neomedia.*; import org.jitsi.service.neomedia.event.*; import org.jitsi.util.*; /** * Controls media service volume input or output. If a playback volume level * is set we change it on all current players, as we synchronize volume * on all players. Implements interface exposed from media service, also * implements the interface used in the Renderer part of JMF and merges the two * functionalities to work as one. * * @author Damian Minkov * @author Lyubomir Marinov */ public class AbstractVolumeControl implements VolumeControl, GainControl { /** * The <tt>Logger</tt> used by the <tt>VolumeControlImpl</tt> class and * its instances for logging output. */ private static final Logger logger = Logger.getLogger(AbstractVolumeControl.class); /** * The minimum volume level accepted by <tt>AbstractVolumeControl</tt>. */ protected static final float MIN_VOLUME_LEVEL = 0.0F; /** * The minimum volume level expressed in percent accepted by * <tt>AbstractVolumeControl</tt>. */ public static final int MIN_VOLUME_PERCENT = 0; /** * The maximum volume level accepted by <tt>AbstractVolumeControl</tt>. */ protected static final float MAX_VOLUME_LEVEL = 1.0F; /** * The maximum volume level expressed in percent accepted by * <tt>AbstractVolumeControl</tt>. */ public static final int MAX_VOLUME_PERCENT = 200; /** * The <tt>VolumeChangeListener</tt>s interested in volume change events * through the <tt>VolumeControl</tt> interface. * <p> * Because the instances of <tt>AbstractVolumeControl</tt> are global at the * time of this writing and, consequently, they cause the * <tt>VolumeChangeListener</tt>s to be leaked, the listeners are referenced * using <tt>WeakReference</tt>s. * </p> */ private final List<WeakReference<VolumeChangeListener>> volumeChangeListeners = new ArrayList<WeakReference<VolumeChangeListener>>(); /** * Listeners interested in volume change inside FMJ/JMF. */ private List<GainChangeListener> gainChangeListeners; /** * The current volume level. */ protected float volumeLevel; /** * The power level reference used to compute equivelents between the volume * power level and the gain in decibels. */ private float gainReferenceLevel; /** * Current mute state, by default we start unmuted. */ private boolean mute = false; /** * Current level in db. */ private float db; /** * The name of the configuration property which specifies the value of the * volume level of this <tt>AbstractVolumeControl</tt>. */ private final String volumeLevelConfigurationPropertyName; /** * Creates volume control instance and initializes initial level value * if stored in the configuration service. * * @param volumeLevelConfigurationPropertyName the name of the configuration * property which specifies the value of the volume level of the new * instance */ public AbstractVolumeControl( String volumeLevelConfigurationPropertyName) { // Initializes default values. this.volumeLevel = getDefaultVolumeLevel(); this.gainReferenceLevel = getGainReferenceLevel(); this.volumeLevelConfigurationPropertyName = volumeLevelConfigurationPropertyName; // Read the initial volume level from the ConfigurationService. this.loadVolume(); } /** * Applies the gain specified by <tt>gainControl</tt> to the signal defined * by the <tt>length</tt> number of samples given in <tt>buffer</tt> * starting at <tt>offset</tt>. * * @param gainControl the <tt>GainControl</tt> which specifies the gain to * apply * @param buffer the samples of the signal to apply the gain to * @param offset the start of the samples of the signal in <tt>buffer</tt> * @param length the number of samples of the signal given in * <tt>buffer</tt> */ public static void applyGain( GainControl gainControl, byte[] buffer, int offset, int length) { if (gainControl.getMute()) Arrays.fill(buffer, offset, offset + length, (byte) 0); else { // Assign a maximum of MAX_VOLUME_PERCENT to the volume scale. float level = gainControl.getLevel() * (MAX_VOLUME_PERCENT / 100); if (level != 1) { for (int i = offset, toIndex = offset + length; i < toIndex; i += 2) { int i1 = i + 1; short s = (short) ((buffer[i] & 0xff) | (buffer[i1] << 8)); /* Clip, don't wrap. */ int si = s; si = (int) (si * level); if (si > Short.MAX_VALUE) s = Short.MAX_VALUE; else if (si < Short.MIN_VALUE) s = Short.MIN_VALUE; else s = (short) si; buffer[i] = (byte) s; buffer[i1] = (byte) (s >> 8); } } } } /** * Current volume value. * * @return the current volume level. * * @see org.jitsi.service.neomedia.VolumeControl */ public float getVolume() { return volumeLevel; } /** * Get the current gain set for this * object as a value between 0.0 and 1.0 * * @return The gain in the level scale (0.0-1.0). * * @see javax.media.GainControl */ public float getLevel() { return volumeLevel; } /** * Returns the minimum allowed volume value. * * @return the minimum allowed volume value. * * @see org.jitsi.service.neomedia.VolumeControl */ public float getMinValue() { return MIN_VOLUME_LEVEL; } /** * Returns the maximum allowed volume value. * * @return the maximum allowed volume value. * * @see org.jitsi.service.neomedia.VolumeControl */ public float getMaxValue() { return MAX_VOLUME_LEVEL; } /** * Changes volume level. * * @param value the new level to set. * @return the actual level which was set. * * @see org.jitsi.service.neomedia.VolumeControl */ public float setVolume(float value) { return this.setVolumeLevel(value); } /** * Set the gain using a floating point scale * with values between 0.0 and 1.0. * 0.0 is silence; 1.0 is the loudest * useful level that this <code>GainControl</code> supports. * * @param level The new gain value specified in the level scale. * @return The level that was actually set. * * @see javax.media.GainControl */ public float setLevel(float level) { return this.setVolumeLevel(level); } /** * Internal implementation combining setting level from JMF * and from outside Media Service. * * @param value the new value, changed if different from current * volume settings. * @return the value that was changed or just the current one if * the same. */ private float setVolumeLevel(float value) { if (value < MIN_VOLUME_LEVEL) value = MIN_VOLUME_LEVEL; else if (value > MAX_VOLUME_LEVEL) value = MAX_VOLUME_LEVEL; if (volumeLevel == value) return value; volumeLevel = value; updateHardwareVolume(); fireVolumeChange(); /* * Save the current volume level in the ConfigurationService so that we * can restore it on the next application run. */ ConfigurationService cfg = LibJitsi.getConfigurationService(); if (cfg != null) { cfg.setProperty( this.volumeLevelConfigurationPropertyName, String.valueOf(volumeLevel)); } db = getDbFromPowerRatio(value, this.gainReferenceLevel); fireGainEvents(); return volumeLevel; } /** * Mutes current sound. * * @param mute mutes/unmutes. */ public void setMute(boolean mute) { if (this.mute != mute) { this.mute = mute; fireVolumeChange(); fireGainEvents(); } } /** * Get mute state of sound. * * @return mute state of sound. */ public boolean getMute() { return mute; } /** * Set the gain in decibels. * Setting the gain to 0.0 (the default) implies that the audio * signal is neither amplified nor attenuated. * Positive values amplify the audio signal and negative values attenuate * the signal. * * @param gain The new gain in dB. * @return The gain that was actually set. * * @see javax.media.GainControl */ public float setDB(float gain) { if(this.db != gain) { this.db = gain; float volumeLevel = getPowerRatioFromDb(gain, gainReferenceLevel); setVolumeLevel(volumeLevel); } return this.db; } /** * Get the current gain set for this object in dB. * @return The gain in dB. */ public float getDB() { return this.db; } /** * Register for gain change update events. * A <code>GainChangeEvent</code> is posted when the state * of the <code>GainControl</code> changes. * * @param listener The object to deliver events to. */ public void addGainChangeListener(GainChangeListener listener) { if(listener != null) { if(gainChangeListeners == null) gainChangeListeners = new ArrayList<GainChangeListener>(); gainChangeListeners.add(listener); } } /** * Remove interest in gain change update events. * * @param listener The object that has been receiving events. */ public void removeGainChangeListener(GainChangeListener listener) { if(listener != null && gainChangeListeners != null) gainChangeListeners.remove(listener); } /** * Adds a <tt>VolumeChangeListener</tt> to be informed for any change * in the volume levels. * * @param listener volume change listener. */ public void addVolumeChangeListener(VolumeChangeListener listener) { synchronized (volumeChangeListeners) { Iterator<WeakReference<VolumeChangeListener>> i = volumeChangeListeners.iterator(); boolean contains = false; while (i.hasNext()) { VolumeChangeListener l = i.next().get(); if (l == null) i.remove(); else if (l.equals(listener)) contains = true; } if(!contains) volumeChangeListeners.add( new WeakReference<VolumeChangeListener>(listener)); } } /** * Removes a <tt>VolumeChangeListener</tt>. * * @param listener the volume change listener to be removed. */ public void removeVolumeChangeListener(VolumeChangeListener listener) { synchronized (volumeChangeListeners) { Iterator<WeakReference<VolumeChangeListener>> i = volumeChangeListeners.iterator(); while (i.hasNext()) { VolumeChangeListener l = i.next().get(); if ((l == null) || l.equals(listener)) i.remove(); } } } /** * Fire a change in volume to interested listeners. */ private void fireVolumeChange() { List<VolumeChangeListener> ls; synchronized (volumeChangeListeners) { Iterator<WeakReference<VolumeChangeListener>> i = volumeChangeListeners.iterator(); ls = new ArrayList<VolumeChangeListener>( volumeChangeListeners.size()); while (i.hasNext()) { VolumeChangeListener l = i.next().get(); if (l == null) i.remove(); else ls.add(l); } } VolumeChangeEvent changeEvent = new VolumeChangeEvent(this, volumeLevel, mute); for(VolumeChangeListener l : ls) l.volumeChange(changeEvent); } /** * Fire events informing for our current state. */ private void fireGainEvents() { if(gainChangeListeners != null) { GainChangeEvent gainchangeevent = new GainChangeEvent(this, mute, db, volumeLevel); for(GainChangeListener gainchangelistener : gainChangeListeners) gainchangelistener.gainChange(gainchangeevent); } } /** * Not used. * @return null */ public Component getControlComponent() { return null; } /** * Returns the decibel value for a ratio between a power level required and * the reference power level. * * @param powerLevelRequired The power level wished for the signal * (corresponds to the mesured power level). * @param referencePowerLevel The reference power level. * * @return The gain in Db. */ private static float getDbFromPowerRatio( float powerLevelRequired, float referencePowerLevel) { float powerRatio = powerLevelRequired / referencePowerLevel; // Limits the lowest power ratio to be 0.0001. float minPowerRatio = 0.0001F; float flooredPowerRatio = Math.max(powerRatio, minPowerRatio); return (float) (20.0 * Math.log10(flooredPowerRatio)); } /** * Returns the mesured power level corresponding to a gain in decibel and * compared to the reference power level. * * @param gainInDb The gain in Db. * @param referencePowerLevel The reference power level. * * @return The power level the signal, which corresponds to the mesured * power level. */ private static float getPowerRatioFromDb( float gainInDb, float referencePowerLevel) { return (float) Math.pow(10, (gainInDb / 20)) * referencePowerLevel; } /** * Returns the default volume level. * * @return The default volume level. */ protected static float getDefaultVolumeLevel() { return MIN_VOLUME_LEVEL + (MAX_VOLUME_LEVEL - MIN_VOLUME_LEVEL) / ((MAX_VOLUME_PERCENT - MIN_VOLUME_PERCENT) / 100); } /** * Returns the reference volume level for computing the gain. * * @return The reference volume level for computing the gain. */ protected static float getGainReferenceLevel() { return getDefaultVolumeLevel(); } /** * Modifies the hardware microphone sensibility (hardaware amplification). * This is a void function for AbstractVolumeControl sincei it does not have * any connection to hardware volume. But, this function must be redefined * by any extending class. */ protected void updateHardwareVolume() { // Nothing to do. This AbstractVolumeControl only modifies the gain. } /** * Reads the initial volume level from the system. */ protected void loadVolume() { try { ConfigurationService cfg = LibJitsi.getConfigurationService(); if (cfg != null) { String volumeLevelString = cfg.getString(this.volumeLevelConfigurationPropertyName); if (volumeLevelString != null) { this.volumeLevel = Float.parseFloat(volumeLevelString); if(logger.isDebugEnabled()) { logger.debug("Restored volume: " + volumeLevelString); } } } } catch (Throwable t) { logger.warn("Error restoring volume", t); } } }