/*
* Mobicents, Communications Middleware
*
* Copyright (c) 2008, Red Hat Middleware LLC or third-party
* contributors as
* indicated by the @author tags or express copyright attribution
* statements applied by the authors. All third-party contributions are
* distributed under license by Red Hat Middleware LLC.
*
* This copyrighted material is made available to anyone wishing to use, modify,
* copy, or redistribute it subject to the terms and conditions of the GNU
* Lesser General Public License, as published by the Free Software Foundation.
*
* This program 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 distribution; if not, write to:
* Free Software Foundation, Inc.
* 51 Franklin Street, Fifth Floor
*
* Boston, MA 02110-1301 USA
*/
package org.mobicents.media.server.impl.resource.cnf;
import java.util.ArrayList;
import java.util.Collection;
import org.mobicents.media.Buffer;
import org.mobicents.media.Format;
import org.mobicents.media.MediaSink;
import org.mobicents.media.MediaSource;
import org.mobicents.media.Outlet;
import org.mobicents.media.format.AudioFormat;
import org.mobicents.media.server.impl.AbstractSink;
import org.mobicents.media.server.impl.AbstractSinkSet;
import org.mobicents.media.server.spi.Connection;
import org.mobicents.media.server.spi.Endpoint;
import org.mobicents.media.server.spi.Timer;
/**
*
* @author Oleg Kulikov
*/
public class AudioMixer extends AbstractSinkSet implements Outlet {
protected final static AudioFormat LINEAR = new AudioFormat(
AudioFormat.LINEAR, 8000, 16, 1,
AudioFormat.LITTLE_ENDIAN,
AudioFormat.SIGNED);
protected final static Format[] formats = new Format[]{LINEAR};
private int packetSize;
private int packetPeriod = 20;
private int jitter = 60;
private MixerOutput mixerOutput;
private double targetGain = 1;
private double currentGain = 1;
private static double maxStepDown = 1. / 22; // 51 samples transition
// from gain 1 to gain 0
private static double maxStepUp = 1. / 4000; // 3000 samples transition
// from gain 1 to gain 0
/**
* Creates a new instance of AudioMixer.
*
* @param packetPeriod
* packetization period in milliseconds.
* @param fmt
* format of the output stream.
*/
public AudioMixer(String name, Timer timer) {
super(name);
mixerOutput = new MixerOutput(this);
mixerOutput.setSyncSource(timer);
init();
}
/**
* Initializes audio mixer.
*
* @throws javax.media.format.UnsupportedFormatException
*/
private void init() {
this.packetSize = 16 * packetPeriod;
}
/**
* (Non Java-doc.)
*
* @see org.mobicents.media.Outlet#getOutput().
*/
public MediaSource getOutput() {
return mixerOutput;
}
@Override
public void start() {
mixerOutput.start();
}
@Override
public void stop() {
mixerOutput.stop();
}
@Override
public void setEndpoint(Endpoint endpoint) {
super.setEndpoint(endpoint);
mixerOutput.setEndpoint(endpoint);
System.out.println("Set endpoint " + endpoint);
Collection<AbstractSink> streams = getStreams();
for (AbstractSink stream : streams) {
stream.setEndpoint(endpoint);
}
}
@Override
public void setConnection(Connection connection) {
super.setConnection(connection);
mixerOutput.setConnection(connection);
Collection<AbstractSink> streams = getStreams();
for (AbstractSink stream : streams) {
stream.setConnection(connection);
}
}
/**
* Converts inner byte representation of the signal into
* 16bit per sample array
*
* @param input the array where sample takes two elements.
* @return array where sample takes one element.
*/
public short[] byteToShortArray(byte[] input) {
short[] output = new short[input.length >> 1];
for (int q = 0; q < input.length; q += 2) {
short f = (short) (((input[q + 1]) << 8) | (input[q] & 0xff));
output[q >> 1] = f;
}
return output;
}
/**
* Mixes input packets.
*
* @param input collection of arras of samples of same length
* @return array of result array of samples.
*/
public byte[] mix(ArrayList<byte[]> input) {
int numSamples = packetSize >> 1;
short[][] streams = new short[input.size()][];
for (int q = 0; q < input.size(); q++) {
streams[q] = byteToShortArray(input.get(q));
}
int[] mixed = new int[numSamples];
for (int q = 0; q < numSamples; q++) {
for (int w = 0; w < input.size(); w++) {
mixed[q] += streams[w][q];
}
}
int numExceeding = 0;
int maxExcess = 0;
for (int q = 0; q < numSamples; q++) {
int excess = 0;
int overflow = mixed[q] - Short.MAX_VALUE;
int underflow = mixed[q] - Short.MIN_VALUE;
if (overflow > 0) {
excess = overflow;
} else if (underflow < 0) {
excess = -underflow;
}
if (excess > 0) {
numExceeding++;
}
maxExcess = Math.max(maxExcess, excess);
}
if (numExceeding > numSamples >> 5) {
targetGain = (float) (Short.MAX_VALUE) / (float) (Short.MAX_VALUE + maxExcess + 2000);
} else {
targetGain = 1;
}
byte[] data = new byte[packetSize];
int l = 0;
for (int q = 0; q < numSamples; q++) {
mixed[q] *= currentGain;
if (targetGain - currentGain >= maxStepUp) {
currentGain += maxStepUp;
} else if (currentGain - targetGain > maxStepDown) {
currentGain -= maxStepDown;
}
short s = (short) (mixed[q]);
data[l++] = (byte) (s);
data[l++] = (byte) (s >> 8);
}
return data;
}
/**
* (Non Java-doc).
*
* @see org.mobicents.media.server.impl.AbstractSource#evolve(org.mobicents.media.Buffer, long).
*/
public void evolve(Buffer buffer, long timestamp, long seq) {
Collection<AbstractSink> streams = getStreams();
ArrayList<byte[]> frames = new ArrayList();
for (AbstractSink stream : streams) {
MixerInputStream input = (MixerInputStream) stream;
if (input.isReady()) {
frames.add(input.read(packetPeriod));
}
}
byte[] data = mix(frames);
buffer.setData(data);
buffer.setOffset(0);
buffer.setLength(data.length);
buffer.setDuration(packetPeriod);
buffer.setTimeStamp(timestamp);
buffer.setSequenceNumber(seq);
buffer.setFormat(LINEAR);
buffer.setEOM(false);
buffer.setDiscard(false);
}
/**
* (Non Java-doc.)
*
* @see org.mobicents.media.MediaSink#isAcceptable(org.mobicents.media.Format)
*/
public boolean isAcceptable(Format fmt) {
return fmt.matches(LINEAR);
}
/**
* (Non Java-doc.)
* @see org.mobicents.media.server.impl.AbstractSink#onMediaTransfer(org.mobicents.media.Buffer)
*/
public void onMediaTransfer(Buffer buffer) {
throw new UnsupportedOperationException();
}
/**
* (Non Java-doc.)
*
* @see org.mobicents.media.MediaSink#getFormats()
*/
public Format[] getFormats() {
return formats;
}
public void connect(MediaSink sink) {
getOutput().connect(sink);
}
public void disconnect(MediaSink sink) {
getOutput().disconnect(sink);
}
@Override
public AbstractSink createSink(MediaSource otherParty) {
MixerInputStream input = new MixerInputStream(this, jitter);
return input;
}
@Override
public void destroySink(AbstractSink sink) {
((MixerInputStream) sink).mixer = null;
}
}