/* * 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 com.sun.voip.Logger; import com.sun.voip.Util; import com.sun.voip.SpatialAudio; public class SunSpatialAudio implements SpatialAudio { private static final double MAX_DELAY = .63; private static double falloff = .94; private static double minVolume = .7; private static double echoDelay = 0; // .5 seems to be a reasonable value; private static double echoVolume = .35; private static double behindVolume = .9; private static int maxExp; private double msPerSample; private static double newEchoDelay = echoDelay; private String conferenceId; private String callId; private int sampleRate; private int channels; private int samplesPerPacket; private int packetLength; public SunSpatialAudio() { } public void initialize(String conferenceId, String callId, int sampleRate, int channels, int samplesPerPacket) { this.conferenceId = conferenceId; this.callId = callId; this.sampleRate = sampleRate; this.channels = channels; this.samplesPerPacket = samplesPerPacket; packetLength = samplesPerPacket * channels; msPerSample = 1000. / sampleRate; if (Logger.logLevel >= Logger.LOG_INFO) { Logger.println(toString() + ":" + conferenceId + " " + callId + "::" + " sample rate " + sampleRate + ", channels " + channels + ", milliseconds per sample " + msPerSample); } setMaxExp(); } public static void setSpatialBehindVolume(double behindVolume) { Logger.println("Spatial behind volume set to " + behindVolume); SunSpatialAudio.behindVolume = behindVolume; } public static double getSpatialBehindVolume() { return behindVolume; } public static void setSpatialEchoDelay(double echoDelay) { Logger.println("Echo delay set to " + echoDelay); SunSpatialAudio.newEchoDelay = echoDelay; } public static double getSpatialEchoDelay() { return newEchoDelay; } public static void setSpatialEchoVolume(double echoVolume) { Logger.println("Echo volume set to " + echoVolume); SunSpatialAudio.echoVolume = echoVolume; } public static double getSpatialEchoVolume() { return echoVolume; } public static void setSpatialFalloff(double falloff) { SunSpatialAudio.falloff = falloff; setMaxExp(); } public static double getSpatialFalloff() { return falloff; } public static void setSpatialMinVolume(double minVolume) { SunSpatialAudio.minVolume = minVolume; if (minVolume < 0) { SunSpatialAudio.minVolume = 0; } else if (minVolume > 1) { SunSpatialAudio.minVolume = 1; } setMaxExp(); } public static double getSpatialMinVolume() { return minVolume; } private static void setMaxExp() { /* * Calculate the exponent by which falloff must be raised to * in order to get a value near minVolume. * * falloff**maxExp = minVolume which is equivalent to * * maxExp * log(falloff) = log(minVolume) which means * * maxExp = log(minVolume) / log(falloff); */ if (minVolume >= falloff) { maxExp = 0; } else { maxExp = (int) ((Math.log(minVolume) / Math.log(falloff))); } if (Logger.logLevel >= Logger.LOG_INFO) { Logger.println("minVolume " + minVolume + " falloff " + falloff + " maxExp set to " + maxExp); } } int count = 0; /* * Generate spatial audio. */ public int[] generateSpatialAudio(String sourceId, int[] previousContribution, int[] currentContribution, double[] spatialValues) { if (channels == 1) { return currentContribution; // no support for 1 channel conference } if (previousContribution == null && currentContribution == null) { return null; // nothing here to do } echoDelay = newEchoDelay; // set echo delay in case it changed double frontBack = spatialValues[0]; if (echoVolume == 0 || echoDelay == 0) { frontBack = 0; } double leftRight = spatialValues[1]; double volume = spatialValues[3]; double delay = MAX_DELAY * leftRight; /* * Calculate the number of ints for the delay. * There are always 2 channels so we have to multiply by 2. */ int delayLength = (int) Math.round(delay / msPerSample) * 2; if (Logger.logLevel >= Logger.LOG_INFO) { if ((count++ % 200) == 0) { Logger.println("Delay " + (Math.round(delay * 1000) / 1000.) + " delay length " + delayLength); } } int[] newContribution; double nonDominantChannelVolume = getAttenuatedVolume(leftRight, volume); if (nonDominantChannelVolume < minVolume) { nonDominantChannelVolume = minVolume; } if (delayLength == 0) { /* * Sound is in the center. */ if (volume == 1 && frontBack >= 0) { /* * There are no adjustments to be made. Just return * the current contribution. */ if (Logger.logLevel == -88) { Util.dump("Current contribution", currentContribution, 0, 16); } return currentContribution; } newContribution = new int[packetLength]; if (Logger.logLevel == -88) { Logger.println("need to make new contribution"); } if (currentContribution != null) { int copyLength = Math.min(packetLength, currentContribution.length); System.arraycopy(currentContribution, 0, newContribution, 0, copyLength); } } else { if (Logger.logLevel == -88) { Logger.println("do leftRight " + delayLength); } newContribution = doLeftRight(previousContribution, currentContribution, delayLength, nonDominantChannelVolume); } if (frontBack < 0) { if (Logger.logLevel == -88) { Logger.println("do frontBack " + echoDelay); } doFrontBack(previousContribution, newContribution, frontBack, delayLength, nonDominantChannelVolume); volume *= getAttenuatedVolume(frontBack, behindVolume); } if (volume == 1) { return newContribution; } if (Logger.logLevel == -78) { Logger.println("Adjust volumes to " + (Math.round(volume * 1000) / 1000.)); } return adjustVolumes(newContribution, volume); } private int[] doLeftRight(int[] previousContribution, int[] currentContribution, int delayLength, double nonDominantChannelVolume) { int channelOffset; if (delayLength < 0) { channelOffset = 1; // delay right channel delayLength = -delayLength; } else { channelOffset = 0; // delay left channel } int[] newContribution = new int[packetLength]; if (currentContribution == null) { /* * There is no current contribution but there is a * previous contribution. Just copy the previous * contribution to our zero filled new contribution. */ int inIx = previousContribution.length - delayLength + channelOffset; int outIx = channelOffset; for (int i = 0; i < delayLength; i += 2) { newContribution[outIx] = previousContribution[inIx]; inIx += 2; outIx += 2; } return newContribution; } /* * There is a current contribution. There may or may not * be a previous contribution. We must not modify the * current contribution so we make a copy. */ System.arraycopy(currentContribution, 0, newContribution, 0, packetLength); //Util.dump("new contrib", newContribution, 0, newContribution.length); /* * Now shift the newContribution up by delayLength, then * copy the previousContribution samples to the beginning * of our newContribution. * * First copy to an intermediate buffer so we don't overwrite * good data. Otherwise, we'd have to start the copy at the * end of the buffer and move downward. */ int[] c = new int[newContribution.length - delayLength]; for (int i = channelOffset; i < c.length; i += 2) { c[i] = newContribution[i]; } for (int i = channelOffset; i < c.length; i += 2) { newContribution[i + delayLength] = (int) (c[i] * nonDominantChannelVolume); } if (previousContribution != null) { int inIx = packetLength - delayLength + channelOffset; int outIx = channelOffset; for (int i = 0; i < delayLength; i += 2) { newContribution[outIx] = (int) (previousContribution[inIx] * nonDominantChannelVolume); inIx += 2; outIx += 2; } } else { //Logger.println("current but no prev"); for (int i = channelOffset; i < delayLength; i += 2) { newContribution[i] = 0; } } return newContribution; } private int count1 = 0; private void doFrontBack(int[] previousContribution, int[] newContribution, double frontBack, int delayLength, double nonDominantChannelVolume) { //Util.dump("result before, p 1, c 3", newContribution, 0, // newContribution.length); //dump(newContribution); int echoDelayLength = (int) Math.round(echoDelay / msPerSample); echoDelayLength *= Math.abs(frontBack); echoDelayLength *= 2; // 2 samples for stereo if (echoDelayLength <= 0) { return; } int channelOffset; if (delayLength < 0) { channelOffset = 1; // delay right channel delayLength = -delayLength; } else { channelOffset = 0; // delay left channel } if (Logger.logLevel >= Logger.LOG_INFO) { if ((count1 % 200) == 0) { Logger.println("adding echo" + " delayLength " + delayLength + " c off " + channelOffset + " edl " + echoDelayLength + " echoDelay " + echoDelay + " msps " + (Math.round(msPerSample * 1000) / 1000.)); } } /* * Copy newContribution */ int[] c = new int[packetLength]; System.arraycopy(newContribution, 0, c, 0, c.length); int inIx = 0; int outIx = echoDelayLength; int length = c.length - echoDelayLength; if (Logger.logLevel >= Logger.LOG_INFO) { if ((count1 % 200) == 0) { Logger.println("inIx " + inIx + " outIx " + outIx + " length " + length); } } for (int i = 0; i < length; i++) { newContribution[outIx] = clip((int) (c[outIx] + (c[inIx] * echoVolume))); inIx++; outIx++; } if (previousContribution == null) { count1++; return; } /* * Add echo from previousContribution */ if (delayLength == 0) { inIx = packetLength - echoDelayLength; outIx = 0; length = echoDelayLength; for (int i = 0; i < length; i++) { newContribution[outIx] = clip((int) (c[outIx] + (previousContribution[inIx] * echoVolume))); inIx++; outIx++; } count1++; return; } inIx = packetLength - delayLength - echoDelayLength + channelOffset; outIx = channelOffset; length = echoDelayLength; if ((count1 % 200) == 0) { Logger.println("inIx " + inIx + " outIx " + outIx + " length " + length); } for (int i = 0; i < length; i += 2) { newContribution[outIx] = clip((int) (c[outIx] + (previousContribution[inIx] * echoVolume * nonDominantChannelVolume))); inIx += 2; outIx += 2; } /* * Add echo from the other channel */ if (channelOffset == 0) { outIx = 1; } else { outIx = 0; } inIx = outIx + packetLength - echoDelayLength; length = echoDelayLength - outIx; if ((count1 % 200) == 0) { Logger.println("inIx " + inIx + " outIx " + outIx + " length " + length); } int i = 0; for (i = 0; i < length; i += 2) { newContribution[outIx] = clip((int) (c[outIx] + (previousContribution[inIx] * echoVolume))); inIx += 2; outIx += 2; } count1++; } private int[] adjustVolumes(int[] contribution, double volume) { /* * Adjust the volume */ int[] c = new int[contribution.length]; for (int i = 0; i < contribution.length; i++) { c[i] = clip((int) (contribution[i] * volume)); } return c; } private int clip(int sample) { if (sample > 32767) { if (Logger.logLevel == -79) { Logger.println("clipping " + sample + " to 32767"); } return 32767; } if (sample < -32768) { if (Logger.logLevel == -79) { Logger.println("clipping " + sample + " to -32768"); } return -32768; } return sample; } private double getAttenuatedVolume(double offset, double volume) { if (offset == 0) { return 1; } int exp = (int) (Math.abs(offset) * maxExp); return volume * Math.pow(falloff, exp); } public String toString() { return "SunSpatialAudio"; } public static void main(String[] args) { new SunSpatialAudio().test(); } private void test() { initialize("Test", "Test", 44100, 2, 44100 / 50); echoDelay = .1; double[] spatialValues = new double[4]; spatialValues[0] = -1; spatialValues[1] = .5; spatialValues[2] = 0; spatialValues[3] = 1; int[] p = new int[64]; int[] c = new int[64]; int[] result; fill(p, 1); fill(c, 3); //Util.dump("c before", c, 0, c.length); result = generateSpatialAudio("Test", p, c, spatialValues); Util.dump("result, p 1, c 3", result, 0, result.length); dump(result); if (false) { p = c; c = new int[64]; fill(c); Util.dump("c before 2", c, 0, c.length); result = generateSpatialAudio("Test", p, c, spatialValues); dump(result); Util.dump("p set, c set", result, 0, result.length); c = new int[64]; fill(c); Util.dump("c before 3", c, 0, c.length); result = generateSpatialAudio("Test", p, c, spatialValues); Util.dump("c null", result, 0, result.length); dump(result); c = new int[64]; fill(c, 4); Util.dump("c echo", c, 0, c.length); spatialValues[0] = -1; spatialValues[1] = 1; spatialValues[2] = 0; spatialValues[3] = 1; result = generateSpatialAudio("Test", p, c, spatialValues); Util.dump("after adding echo", result, 0, result.length); dump(result); System.out.println("===================="); Util.dump("c echo", c, 0, c.length); result = generateSpatialAudio("Test", p, c, spatialValues); Util.dump("after adding echo", result, 0, result.length); dump(result); } } private void fill(int[] buf, int v) { for (int i = 0; i < buf.length; i += 2) { buf[i] = v; buf[i + 1] = v; } } private int v = 0; private void fill(int[] buf) { for (int i = 0; i < buf.length; i += 2) { buf[i] = v++; buf[i + 1] = v++; } } private void dump(int[] c) { System.out.println("\nleft " + c.length); for (int i = 0; i < c.length; i += 2) { System.out.print(Integer.toHexString(c[i] & 0xff) + " "); } System.out.println("\n\nright " + c.length); for (int i = 1; i < c.length; i += 2) { System.out.print(Integer.toHexString(c[i] & 0xff) + " "); } System.out.println("\n"); } }