/*
* Copyright 2007 Sun Microsystems, Inc.
*
* This file is part of jVoiceBridge.
*
* jVoiceBridge is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation and distributed hereunder
* to you.
*
* jVoiceBridge 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* Sun designates this particular file as subject to the "Classpath"
* exception as provided by Sun in the License file that accompanied this
* code.
*/
package com.sun.voip.server;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import com.sun.voip.AudioConversion;
import com.sun.voip.Logger;
import com.sun.voip.SpatialAudio;
import com.sun.voip.MixDataSource;
import com.sun.voip.RtpPacket;
public class MixManager {
private static final int MAX_VOLUME = 16;
private ArrayList mixDescriptors = new ArrayList();
private ConferenceMember member;
private int conferenceSamplesPerPacket;
private int channels;
private boolean useFastMix = false;
private SpatialAudio sa;
public MixManager(ConferenceMember member,
int conferenceSamplesPerPacket, int channels) {
this.member = member;
this.conferenceSamplesPerPacket = conferenceSamplesPerPacket;
this.channels = channels;
/*
* Calculate the sample rate.
* Each packet has 20ms of data (50 packets per second).
*/
int sampleRate = 50 * (conferenceSamplesPerPacket / channels);
sa = getSpatialAudio();
if (Logger.logLevel >= Logger.LOG_INFO) {
Logger.println("Using spatial audio module: " + sa);
}
sa.initialize(member.getConferenceManager().getId(),
member.getCallParticipant().getCallId(), sampleRate,
channels, conferenceSamplesPerPacket / channels);
}
private SpatialAudio getSpatialAudio() {
String s = System.getProperty("com.sun.voip.server.SPATIAL_AUDIO");
if (s != null) {
try {
Class micClass = Class.forName(s);
Class[] params = new Class[] { };
Constructor constructor = micClass.getConstructor(params);
if (constructor != null) {
Object[] args = new Object[] { };
return (SpatialAudio) constructor.newInstance(args);
}
Logger.println("constructor not found for: " + s);
} catch (Exception e) {
Logger.println("Error loading '" + s + "': "
+ e.getMessage());
}
}
return new SunSpatialAudio();
}
public void addMix(MixDescriptor mixDescriptor) {
MixDataSource mixDataSource = mixDescriptor.getMixDataSource();
if (mixDataSource instanceof WhisperGroup) {
WhisperGroup wg = (WhisperGroup) mixDataSource;
if (wg.hasCommonMix() == false) {
if (Logger.logLevel >= Logger.LOG_INFO) {
Logger.println("No common mix, not adding " + wg);
}
return;
}
}
MixDescriptor md = findMixDescriptor(mixDataSource);
if (md != null) {
removeMix(md);
}
mixDescriptors.add(mixDescriptor);
setUseFastMix();
}
public void addMix(MixDataSource mixDataSource, double attenuation) {
MixDescriptor mixDescriptor = findMixDescriptor(mixDataSource);
if (attenuation == 0) {
if (mixDescriptor != null) {
if (Logger.logLevel >= Logger.LOG_MOREINFO) {
Logger.println("Call " + member
+ " Remove mix, volume 0 " + " mixDataSource "
+ mixDataSource);
}
removeMix(mixDescriptor);
} else {
if (Logger.logLevel >= Logger.LOG_MOREINFO) {
Logger.println("Call " + member
+ " no need to add " + mixDataSource + " volume 0");
}
}
setUseFastMix();
return;
}
if (mixDescriptor == null) {
mixDescriptor = new MixDescriptor(mixDataSource, attenuation);
mixDescriptors.add(mixDescriptor);
if (Logger.logLevel >= Logger.LOG_MOREINFO) {
Logger.println("created new mix for " + mixDataSource
+ " " + attenuation);
}
setUseFastMix();
return;
}
mixDescriptor.setAttenuation(attenuation);
setUseFastMix();
}
public void removeMix(MixDataSource mixDataSource) {
MixDescriptor mixDescriptor = findMixDescriptor(mixDataSource);
if (mixDescriptor == null) {
if (Logger.logLevel >= Logger.LOG_MOREINFO) {
Logger.println("Didn't find MixDescriptor for "
+ mixDataSource);
}
return;
}
removeMix(mixDescriptor);
}
public void removeMix(MixDescriptor mixDescriptor) {
mixDescriptors.remove(mixDescriptor);
if (Logger.logLevel >= Logger.LOG_MOREINFO) {
Logger.println("Call " + member
+ " removeMix removed " + mixDescriptor);
}
setUseFastMix();
}
public void setAttenuation(MixDescriptor md, double attenuation) {
md.setAttenuation(attenuation);
setUseFastMix();
}
public void setMuted(MixDescriptor md, boolean isMuted) {
md.setMuted(isMuted);
setUseFastMix();
}
public MixDescriptor findMixDescriptor(MixDataSource mixDataSource) {
for (int i = 0; i < mixDescriptors.size(); i++) {
MixDescriptor mixDescriptor = (MixDescriptor) mixDescriptors.get(i);
if (mixDescriptor.getMixDataSource() == mixDataSource) {
return mixDescriptor;
}
}
if (forcePrivateMix) {
double[] volume = new double[4];
volume[0] = .5D;
volume[1] = .5D;
volume[2] = .5D;
volume[3] = .5D;
return new MixDescriptor(mixDataSource, 1.0, volume);
}
return null;
}
private void setUseFastMix() {
useFastMix = false;
if (mixDescriptors.size() != 2) {
if (Logger.logLevel >= Logger.LOG_MOREDETAIL) {
Logger.println("Call " + member +
" Can't use fastMix, must have exactly 2 descriptors");
}
return;
}
for (int i = 0; i < 2; i++) {
MixDescriptor mixDescriptor = (MixDescriptor)
mixDescriptors.get(i);
if (mixDescriptor.isMuted()) {
if (Logger.logLevel >= Logger.LOG_MOREDETAIL) {
Logger.println("Call " + member
+ " Can't use fastMix, md muted " + mixDescriptor);
}
return;
}
MixDataSource mixDataSource = mixDescriptor.getMixDataSource();
if (mixDataSource instanceof MemberReceiver) {
if (mixDataSource != member.getMemberReceiver()) {
if (Logger.logLevel >= Logger.LOG_MOREDETAIL) {
Logger.println("Call " + member
+ " Can't use fastMix, have private mix");
}
return;
}
if (mixDescriptor.getEffectiveVolume() != -1.0) {
if (Logger.logLevel >= Logger.LOG_MOREDETAIL) {
Logger.println("Call " + member
+ " Can't use fastMix, no mix minus");
}
return;
}
continue;
} else if (mixDataSource instanceof WhisperGroup == false) {
if (Logger.logLevel >= Logger.LOG_MOREDETAIL) {
Logger.println("Call " + member
+ " Can't use fastMix, not simple mix");
}
return;
}
if (mixDescriptor.isNop() == false) {
if (Logger.logLevel >= Logger.LOG_MOREDETAIL) {
Logger.println(" Call " + member
+ "Can't use fastMix, not spatially neutral");
}
return;
}
}
MixDescriptor mixDescriptor = (MixDescriptor) mixDescriptors.get(0);
if (mixDescriptor.getMixDataSource() instanceof MemberReceiver) {
/*
* Swap the two descriptors so that the conference mix is first
*/
mixDescriptors.add(mixDescriptor);
mixDescriptors.remove(0);
}
if (Logger.logLevel >= Logger.LOG_MOREDETAIL) {
Logger.println("Using fastMix");
}
useFastMix = true;
}
public void showDescriptors() {
Logger.println("Call " + member + " descriptors "
+ mixDescriptors.size());
for (int i = 0; i < mixDescriptors.size(); i++) {
MixDescriptor mixDescriptor = (MixDescriptor) mixDescriptors.get(i);
Logger.println(mixDescriptor.toString());
}
}
public ArrayList getMixDescriptors() {
return mixDescriptors;
}
/*
* For load testing
*/
private static boolean forcePrivateMix = false;
public static void setForcePrivateMix(boolean forcePrivateMix) {
MixManager.forcePrivateMix = forcePrivateMix;
}
public static boolean getForcePrivateMix() {
return forcePrivateMix;
}
public MixDescriptor setPrivateMix(MixDataSource mixDataSource,
double[] spatialValues) {
MixDescriptor mixDescriptor;
mixDescriptor = findMixDescriptor(mixDataSource);
if (Logger.logLevel >= Logger.LOG_MOREINFO) {
Logger.println("Private mix " + mixDescriptor);
}
if (mixDescriptor == null) {
mixDescriptor = new MixDescriptor(mixDataSource, 1.0,
spatialValues);
mixDescriptors.add(mixDescriptor);
Logger.println("Call " + member
+ " creating new private mix for " + mixDataSource + " "
+ mixDescriptor + " vol " + spatialValues[3]);
} else {
if (mixDescriptor.equals(mixDataSource, spatialValues)) {
return null; // same as before
}
mixDescriptor.setSpatialValues(spatialValues);
}
if (Logger.logLevel >= Logger.LOG_MOREINFO) {
Logger.println("MixManager: Setting private mix "
+ mixDescriptor);
}
setUseFastMix();
return mixDescriptor;
}
public int[] mix() {
int[] outData = null;
if (mixDescriptors.size() == 0) {
return null;
}
if (useFastMix) {
if (Logger.logLevel >= Logger.LOG_INFO) {
setUseFastMix(); // reset
if (useFastMix == false) {
Logger.println("Call " + member
+ " useFastMix should have been false! "
+ "resetting...");
Logger.println("Call " + member + " md size "
+ mixDescriptors.size());
Logger.println(toAbbreviatedString());
}
}
if (useFastMix) {
return fastMix();
}
}
outData = new int[conferenceSamplesPerPacket];
//Logger.println("Call " + member + " MixManager mixing "
// + mixDescriptors.size());
boolean needToSend = false;
for (int i = 0; i < mixDescriptors.size(); i++) {
MixDescriptor mixDescriptor = (MixDescriptor) mixDescriptors.get(i);
if (mixDescriptor.isMuted()) {
continue;
}
MixDataSource mixDataSource = mixDescriptor.getMixDataSource();
int[] contribution = mixDataSource.getCurrentContribution();
if (mixDescriptor.isPrivateMix() == true) {
double[] spatialValues = mixDescriptor.getSpatialValues();
if (MixDescriptor.isSpatiallyNeutral(spatialValues) &&
spatialValues[3] != 0) {
/*
* Since only the volume needs to be adjusted,
* rather than subtracting out the contribution
* and then adding in the volume we can
* set the volume to volume - 1 and add that in.
*/
if (mixDataSource.contributionIsInCommonMix()) {
double[] sv = new double[4];
sv[0] = spatialValues[0];
sv[1] = spatialValues[1];
sv[2] = spatialValues[2];
sv[3] = spatialValues[3] - 1;
spatialValues = sv;
if (Logger.logLevel == -69) {
Logger.println("Call " + member + " pm for "
+ mixDataSource.toAbbreviatedString()
+ " s3 " + spatialValues[3]);
}
}
} else {
/*
* Subtract the current contribution from the mix
*/
if (contribution != null &&
mixDataSource.contributionIsInCommonMix()) {
WhisperGroup.mixData(contribution, outData, false);
if (spatialValues[3] == 0) {
if (Logger.logLevel == -44) {
Logger.println("subtracted out "
+ mixDescriptor);
}
continue; // we've already subtracted it out
}
}
}
contribution = sa.generateSpatialAudio(
mixDataSource.getSourceId(),
mixDataSource.getPreviousContribution(),
contribution, spatialValues);
}
if (contribution != null) {
/*
* Mix into an int[] so that we can clip once after
* we're done mixing.
*/
boolean add = mixDescriptor.getEffectiveVolume() != -1;
WhisperGroup.mixData(contribution, outData, add);
needToSend = true;
}
}
if (needToSend == false) {
return null;
}
AudioConversion.clip(outData);
if (Logger.logLevel == -39) {
checkData(outData, false);
}
return outData;
}
/*
* We know there are two MixDescriptors and the first one is
* the conference Mix and the second one is for subtracting
* out the member's own data.
*/
private int[] fastMix() {
MixDescriptor conferenceMixDescriptor = (MixDescriptor)
mixDescriptors.get(0);
int[] conferenceMixContribution =
conferenceMixDescriptor.getMixDataSource().getCurrentContribution();
if (conferenceMixContribution == null) {
return null;
}
int[] outData = new int[conferenceSamplesPerPacket];
MixDescriptor memberMixDescriptor = (MixDescriptor)
mixDescriptors.get(1);
int[] memberContribution =
memberMixDescriptor.getMixDataSource().getCurrentContribution();
if (memberContribution == null) {
if (outData.length <= conferenceMixContribution.length)
System.arraycopy(conferenceMixContribution, 0, outData, 0, outData.length);
else
System.arraycopy(conferenceMixContribution, 0, outData, 0, conferenceMixContribution.length);
if (Logger.logLevel == -39) {
checkData(outData, useFastMix);
}
AudioConversion.clip(outData);
return outData;
}
WhisperGroup.mixData(conferenceMixContribution, memberContribution, outData);
if (Logger.logLevel == -39) {
checkData(outData, useFastMix);
}
AudioConversion.clip(outData);
return outData;
}
private void checkData(int[] data, boolean useFastMix) {
for (int i = 0; i < data.length; i++) {
if (data[i] != 0) {
Logger.println("Call " + member + " Non-zero data at " + i);
Logger.println("Call " + member
+ " useFastMix " + useFastMix);
Logger.println("Call " + member + " "
+ toAbbreviatedString());
if (mixDescriptors.size() != 2 && useFastMix == true) {
Logger.println("useFastMix should be false!!!");
}
break;
}
}
}
//public void adjustVolume(MixDescriptor md, double volume) {
// md.adjustSpatialVolume(volume);
//}
public void adjustVolume(int[] data, double volume) {
if (volume == 1) {
return;
}
if (volume == 0) {
for (int i = 0; i < data.length; i++) {
data[i] = 0; // optimize when volume is 0
}
return;
}
for (int i = 0; i < data.length; i++) {
data[i] = AudioConversion.clip((int)(data[i] * volume));
}
}
public String toString() {
String s = "";
synchronized (this) {
for (int i = 0; i < mixDescriptors.size(); i++) {
MixDescriptor mixDescriptor = (MixDescriptor)
mixDescriptors.get(i);
s += mixDescriptor.toString();
s += "\n";
}
}
return s;
}
public String toAbbreviatedString() {
String s = "";
synchronized (this) {
for (int i = 0; i < mixDescriptors.size(); i++) {
MixDescriptor mixDescriptor = (MixDescriptor)
mixDescriptors.get(i);
s += " " + mixDescriptor.toAbbreviatedString();
if (member.getWhisperGroup() ==
mixDescriptor.getMixDataSource()) {
s += " + ";
}
s += "\n";
}
}
return s;
}
}