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