All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jitsi.service.neomedia.BasicVolumeControl Maven / Gradle / Ivy

/*
 * Copyright @ 2015 Atlassian Pty Ltd
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.jitsi.service.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.event.*;
import org.jitsi.utils.logging.*;

/**
 * Provides a basic implementation of VolumeControl which stores the
 * volume level/value set on it in the ConfigurationService.
 *
 * @author Damian Minkov
 * @author Lyubomir Marinov
 */
public class BasicVolumeControl
    implements VolumeControl,
               GainControl
{
    /**
     * The Logger used by the BasicVolumeControl class and its
     * instances for logging output.
     */
    private static final Logger logger
        = Logger.getLogger(BasicVolumeControl.class);

    /**
     * The maximum volume level accepted by AbstractVolumeControl.
     */
    protected static final float MAX_VOLUME_LEVEL = 1.0F;

    /**
     * The maximum volume level expressed in percent accepted by
     * AbstractVolumeControl.
     */
    public static final int MAX_VOLUME_PERCENT = 200;

    /**
     * The minimum volume level accepted by AbstractVolumeControl.
     */
    protected static final float MIN_VOLUME_LEVEL = 0.0F;

    /**
     * The minimum volume level expressed in percent accepted by
     * AbstractVolumeControl.
     */
    public static final int MIN_VOLUME_PERCENT = 0;

    /**
     * Applies the gain specified by gainControl to the signal defined
     * by the length number of samples given in buffer
     * starting at offset.
     *
     * @param gainControl the GainControl 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 buffer
     * @param length the number of samples of the signal given in
     * buffer
     */
    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);
                }
            }
        }
    }

    /**
     * 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 measured 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 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();
    }

    /**
     * Returns the measured 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 measured
     * power level.
     */
    private static float getPowerRatioFromDb(
            float gainInDb,
            float referencePowerLevel)
    {
        return (float) Math.pow(10, (gainInDb / 20)) * referencePowerLevel;
    }

    /**
     * Current level in db.
     */
    private float db;

    /**
     * Listeners interested in volume change inside FMJ/JMF.
     */
    private List gainChangeListeners;

    /**
     * The power level reference used to compute equivalents 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;

    /**
     * The VolumeChangeListeners interested in volume change events
     * through the VolumeControl interface.
     * 

* Because the instances of AbstractVolumeControl are global at the * time of this writing and, consequently, they cause the * VolumeChangeListeners to be leaked, the listeners are referenced * using WeakReferences. */ private final List> volumeChangeListeners = new ArrayList>(); /** * The current volume level. */ protected float volumeLevel; /** * The name of the configuration property which specifies the value of the * volume level of this AbstractVolumeControl. */ 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 BasicVolumeControl(String volumeLevelConfigurationPropertyName) { // Initializes default values. this.volumeLevel = getDefaultVolumeLevel(); this.gainReferenceLevel = getGainReferenceLevel(); this.volumeLevelConfigurationPropertyName = volumeLevelConfigurationPropertyName; // Read the initial volume level from the ConfigurationService. loadVolume(); } /** * Register for gain change update events. A GainChangeEvent is * posted when the state of the GainControl changes. * * @param listener The object to deliver events to. */ public void addGainChangeListener(GainChangeListener listener) { if(listener != null) { if(gainChangeListeners == null) gainChangeListeners = new ArrayList(); gainChangeListeners.add(listener); } } /** * Adds a VolumeChangeListener to be informed for any change * in the volume levels. * * @param listener volume change listener. */ public void addVolumeChangeListener(VolumeChangeListener listener) { synchronized (volumeChangeListeners) { Iterator> 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(listener)); } } /** * Fires a new GainChangeEvent to the GainChangeListeners * added to this instance to notify about a change in the level of this * GainControl. */ private void fireGainChange() { if(gainChangeListeners != null) { GainChangeEvent ev = new GainChangeEvent(this, mute, db, volumeLevel); for(GainChangeListener l : gainChangeListeners) l.gainChange(ev); } } /** * Fires a new VolumeChangeEvent to the * VolumeChangeListeners added to this instance to notify about a * change in the volume (level) of this VolumeControl. */ private void fireVolumeChange() { List ls; synchronized (volumeChangeListeners) { Iterator> i = volumeChangeListeners.iterator(); ls = new ArrayList( volumeChangeListeners.size()); while (i.hasNext()) { VolumeChangeListener l = i.next().get(); if (l == null) i.remove(); else ls.add(l); } } VolumeChangeEvent ev = new VolumeChangeEvent(this, volumeLevel, mute); for(VolumeChangeListener l : ls) l.volumeChange(ev); } /** * Not used. * * @return null */ public Component getControlComponent() { return null; } /** * Get the current gain set for this object in dB. * * @return The gain in dB. */ public float getDB() { return this.db; } /** * 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 maximum allowed volume value. * * @return the maximum allowed volume value. * @see org.jitsi.service.neomedia.VolumeControl */ public float getMaxValue() { return MAX_VOLUME_LEVEL; } /** * 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; } /** * Get mute state of sound. * * @return mute state of sound. */ public boolean getMute() { return mute; } /** * Current volume value. * * @return the current volume level. * * @see org.jitsi.service.neomedia.VolumeControl */ public float getVolume() { return volumeLevel; } /** * Reads the initial volume level from the system. */ protected void loadVolume() { try { ConfigurationService cfg = LibJitsi.getConfigurationService(); if (cfg != null) { String volumeLevelString = cfg.getString(volumeLevelConfigurationPropertyName); if (volumeLevelString != null) { volumeLevel = Float.parseFloat(volumeLevelString); if (logger.isDebugEnabled()) logger.debug("Restored volume: " + volumeLevel); } } } catch (Throwable t) { logger.warn("Failed to restore volume", t); } } /** * 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); } /** * Removes a VolumeChangeListener. * * @param listener the volume change listener to be removed. */ public void removeVolumeChangeListener(VolumeChangeListener listener) { synchronized (volumeChangeListeners) { Iterator> i = volumeChangeListeners.iterator(); while (i.hasNext()) { VolumeChangeListener l = i.next().get(); if ((l == null) || l.equals(listener)) i.remove(); } } } /** * 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; } /** * 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 GainControl 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); } /** * Mutes current sound. * * @param mute mutes/unmutes. */ public void setMute(boolean mute) { if (this.mute != mute) { this.mute = mute; fireVolumeChange(); fireGainChange(); } } /** * 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 setVolumeLevel(value); } /** * 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); fireGainChange(); return volumeLevel; } /** * Modifies the hardware microphone sensibility (hardware amplification). * This is a void function for AbstractVolumeControl since it does not have * any connection to hardware volume. But, this function must be redefined * by any extending class. */ protected void updateHardwareVolume() { /* * AbstractVolumeControl does not implement such functionality, the * method is defined and invoked in order to allow extenders to provide * such functionality. */ } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy