import java.util.*; import javaforce.*; import javaforce.media.*; import javaforce.voip.*; /** Handles all aspects of audio processing (recording, playback, ringing sounds, conference mixing, etc.) */ public class Audio { //audio data private short silence[] = new short[882]; private short silence8[] = new short[160]; private short silence16[] = new short[320]; private short mixed[] = new short[882]; private short recording[] = new short[882]; private short indata8[] = new short[160]; private short indata16[] = new short[320]; private short outdata[] = new short[882]; //read from mic private short ringing[] = new short[882]; private short callWaiting[] = new short[882]; private short data8[] = new short[160]; private short data16[] = new short[320]; private AudioOutput output; private AudioInput input; private Timer timer; private Player player; private Reader reader; private Thread restart; private boolean primeOutput; private PhoneLine lines[]; private int line = -1; private boolean inRinging = false, outRinging = false; private MeterController mc; private int volPlay = 100, volRec = 100; private boolean mute = false; private DTMF dtmf = new DTMF(44100); private boolean active = false; private Object activeLock = new Object(); private int deactivateDelay; private static final int deactivateDelayInit = 50 * 5; //5 seconds private int underBufferCount; private javaforce.voip.Wav inWav, outWav; private int speakerDelay = 0; private int sampleRate, sampleRate50, sampleRate50x2; /** Init audio system. Audio needs access to the lines and the MeterController to send audio levels back to the panel. */ public boolean init(PhoneLine lines[], MeterController mc) { this.lines = lines; this.mc = mc; sampleRate = 44100; sampleRate50 = sampleRate / 50; sampleRate50x2 = sampleRate50 * 2; //setup inbound ring tone loadRingTones(); if (!start()) return false; timer = new Timer(); timer.scheduleAtFixedRate(new TimerTask() { public void run() { process(); } }, 0, 20); return true; } private void loadRingTones() { if (Settings.current.inRingtone.startsWith("*")) { if (Settings.current.inRingtone.equals("*RING")) { inWav = new javaforce.voip.Wav(); inWav.load(getClass().getResourceAsStream("ringing.wav")); } else if (Settings.current.inRingtone.equals("*NA")) { initInRinging(440, 480, 2000, 4000, 2000, 4000); //north america } else if (Settings.current.inRingtone.equals("*UK")) { initInRinging(400, 450, 400, 200, 400, 2000); //UK } else { inWav = new javaforce.voip.Wav(); inWav.load(getClass().getResourceAsStream("ringing.wav")); } } else { inWav = new javaforce.voip.Wav(); if (!inWav.load(Settings.current.inRingtone)) { JFLog.log("Failed to load : " + Settings.current.inRingtone); inWav = new javaforce.voip.Wav(); inWav.load(getClass().getResourceAsStream("ringing.wav")); } } //setup outbound ringback tone if (Settings.current.outRingtone.startsWith("*")) { if (Settings.current.outRingtone.equals("*RING")) { outWav = new javaforce.voip.Wav(); outWav.load(getClass().getResourceAsStream("ringing.wav")); } else if (Settings.current.outRingtone.equals("*NA")) { initOutRinging(440, 480, 2000, 4000, 2000, 4000); //north america } else if (Settings.current.outRingtone.equals("*UK")) { initOutRinging(400, 450, 400, 200, 400, 2000); //UK } else { initOutRinging(440, 480, 2000, 4000, 2000, 4000); //north america } } else { outWav = new javaforce.voip.Wav(); if (!outWav.load(Settings.current.outRingtone)) { JFLog.log("Failed to load : " + Settings.current.outRingtone); initOutRinging(440, 480, 2000, 4000, 2000, 4000); //north america } } } private boolean start() { output = new AudioOutput(); input = new AudioInput(); JFLog.log("output=" + output + ",input=" + input); output.listDevices(); input.listDevices(); deactivateDelay = deactivateDelayInit; underBufferCount = 0; if (!input.start(1, sampleRate, 16, sampleRate50x2, Settings.current.audioInput)) { JFLog.log("Input.start() failed"); return false; } if (!output.start(1, sampleRate, 16, sampleRate50x2, Settings.current.audioOutput)) { JFLog.log("Output.start() failed"); return false; } primeOutput = true; synchronized(activeLock) { active = true; } player = new Player(); player.start(); reader = new Reader(); reader.start(); return true; } int snd_id_play = -1, snd_id_record = -1; private void stop() { if (player != null) { player.cancel(); player = null; } if (reader != null) { reader.cancel(); reader = null; } if (active) { synchronized(activeLock) { active = false; } output.stop(); input.stop(); } mc.setMeterPlay(0); mc.setMeterRec(0); } /** Frees resources. */ public void uninit() { if (timer != null) { timer.cancel(); timer = null; JF.sleep(25); //wait for any pending timer events } if (record != null) { record.close(); record = null; } stop(); inWav = null; outWav = null; } /** Returns if software volume control on recording. */ public boolean isSWVolRec() { return true; } /** Returns if software volume control on playing. */ public boolean isSWVolPlay() { return true; } /** Changes which line user wants to listen to. */ public void selectLine(int line) { this.line = line; } /** Changes software/hardware playback volume level. */ public void setVolPlay(int lvl) { volPlay = lvl; Settings.current.volPlaySW = volPlay; // Settings.saveSettings(); } /** Changes software/hardware recording volume level. */ public void setVolRec(int lvl) { volRec = lvl; Settings.current.volRecSW = volRec; // Settings.saveSettings(); } /** Sets mute state. */ public void setMute(boolean state) { mute = state; } /** Scales samples to a software volume control. */ private void scaleBufferVolume(short buf[], int len, int scale) { float fscale; if (scale == 0) { for (int a = 0; a < len; a++) { buf[a] = 0; } } else { if (scale <= 75) { fscale = 1.0f - ((75-scale) * 0.014f); for (int a = 0; a < len; a++) { buf[a] = (short) (buf[a] * fscale); } } else { fscale = 1.0f + ((scale-75) * 0.04f); float value; for (int a = 0; a < len; a++) { value = buf[a] * fscale; if (value < Short.MIN_VALUE) buf[a] = Short.MIN_VALUE; else if (value > Short.MAX_VALUE) buf[a] = Short.MAX_VALUE; else buf[a] = (short)value; } } } } /** Writes data to the audio system (output to speakers). */ private void write(short buf[]) { if (player == null || !active) return; scaleBufferVolume(buf, 882, volPlay); if (primeOutput) { player.add(silence); primeOutput = false; } player.add(buf); synchronized(player.lock) { player.lock.notify(); } int lvl = 0; for (int a = 0; a < 882; a++) { if (Math.abs(buf[a]) > lvl) lvl = Math.abs(buf[a]); } mc.setMeterPlay(lvl * 100 / 32768); if ((Settings.current.speakerMode) && (lvl >= Settings.current.speakerThreshold)) { if (speakerDelay <= 0) { mc.setSpeakerStatus(false); } speakerDelay = Settings.current.speakerDelay; } } /** Reads data from the audio system (input from mic). */ private boolean read(short buf[]) { if (reader == null || !active) return false; if (!reader.read(buf)) return false; scaleBufferVolume(buf, buf.length, volRec); int lvl = 0; for (int a = 0; a < buf.length; a++) { if (Math.abs(buf[a]) > lvl) lvl = Math.abs(buf[a]); } mc.setMeterRec(lvl * 100 / 32768); if (speakerDelay > 0) { speakerDelay -= 20; System.arraycopy(silence, 0, buf, 0, 882); if (speakerDelay <= 0) { mc.setSpeakerStatus(true); } } return true; } /** Timer event that is triggered every 20ms. Processes playback / recording. */ public void process() { //20ms timer if (timer == null) return; long start = System.nanoTime(); try { //do playback int cc = 0; //conf count byte encoded[]; if (!active) { for (int a = 0; a < 6; a++) { if ((lines[a].talking) || (lines[a].ringing) || (lines[a].dtmf != 'x')) { if (restart == null) { restart = new Thread() { public void run() { Audio.this.start(); restart = null; } }; restart.start(); } break; } } } else { if (!Settings.current.keepAudioOpen) { int iuc = 0; //in use count for (int a = 0; a < 6; a++) { if ((lines[a].talking) || (lines[a].ringing) || (lines[a].dtmf != 'x')) { iuc++; } } if (iuc == 0) { deactivateDelay--; if (deactivateDelay <= 0) { JFLog.log("Audio inactive"); stop(); } } else { deactivateDelay = deactivateDelayInit; } } } for (int a = 0; a < 6; a++) { if (lines[a].talking) { if ((lines[a].cnf) && (!lines[a].hld)) cc++; } } for (int a = 0; a < 6; a++) { if (lines[a].ringing && !lines[a].ringback && !lines[a].incoming) { if (!outRinging) { if (outWav != null) { outWav.reset(); } else { startRinging(); } outRinging = true; } break; } if (a == 5) { outRinging = false; } } for (int a = 0; a < 6; a++) { if (lines[a].incoming) { if (!inRinging) { if (inWav != null) { inWav.reset(); } else { startRinging(); } inRinging = true; } break; } if (a == 5) { inRinging = false; } } if ((cc > 1) && (line != -1) && (lines[line].cnf)) { //conference mode System.arraycopy(silence, 0, mixed, 0, 882); for (int a = 0; a < 6; a++) { if (lines[a].talking) { lines[a].samples = getSamples(a); if ((lines[a].samples != null) && (lines[a].cnf) && (!lines[a].hld)) { mix(mixed, lines[a].samples, 0 + a); } } } if (inRinging) mix(mixed, getCallWaiting(), 6); if (lines[line].dtmf != 'x') mix(mixed, dtmf.getSamples(lines[line].dtmf), 7); write(mixed); } else { //single mode System.arraycopy(silence, 0, mixed, 0, 882); if (line != -1) { if (lines[line].dtmf != 'x') mix(mixed, dtmf.getSamples(lines[line].dtmf), 8); } if ((line != -1) && (lines[line].talking) && (!lines[line].hld)) { RTPChannel channel = lines[line].audioRTP.getDefaultChannel(); int rate = channel.coder.getSampleRate(); switch (rate) { case 8000: //G729, G711, GSM if (channel.getSamples(indata8)) { mix(mixed, indata8, 9); } break; case 16000: //G722 if (channel.getSamples(indata16)) { mix(mixed, indata16, 9); } break; } if (inRinging) mix(mixed, getCallWaiting(), 10); write(mixed); } else { if (inRinging || outRinging) mix(mixed, getRinging(), 11); if (active) write(mixed); } } if (record != null) System.arraycopy(mixed, 0, recording, 0, 882); //do recording if (!active) return; if (!read(outdata)) { underBufferCount++; if (underBufferCount > 10) { //a few is normal JFLog.log("Audio:mic underbuffer"); } System.arraycopy(silence, 0, outdata, 0, 882); } if (mute) { System.arraycopy(silence, 0, outdata, 0, 882); } for (int a = 0; a < 6; a++) { if ((lines[a].talking) && (!lines[a].hld)) { if (lines[a].ringback) { //send only silence during ringback encoded = encodeSilence(lines[a].audioRTP.getDefaultChannel().coder); } else { if ((lines[a].cnf) && (cc > 1)) { //conference mode (mix = outdata + all other cnf lines except this one) System.arraycopy(outdata, 0, mixed, 0, 882); for (int b = 0; b < 6; b++) { if (b == a) continue; if ((lines[b].talking) && (lines[b].cnf) && (!lines[b].hld) && (lines[b].samples != null)) mix(mixed, lines[b].samples, 12 + b); } encoded = encode(lines[a].audioRTP.getDefaultChannel().coder, mixed, 36 + a); if (record != null && line == a) mix(recording, mixed, 18 + a); } else { //single mode if (line == a) { encoded = encode(lines[a].audioRTP.getDefaultChannel().coder, outdata, 42 + a); if (record != null) mix(recording, outdata, 24 + a); } else { encoded = encodeSilence(lines[a].audioRTP.getDefaultChannel().coder); } } } if (lines[a].dtmfend) { lines[a].audioRTP.getDefaultChannel().writeDTMF(lines[a].dtmf, true); } else if (lines[a].dtmf != 'x') { lines[a].audioRTP.getDefaultChannel().writeDTMF(lines[a].dtmf, false); } else { lines[a].audioRTP.getDefaultChannel().writeRTP(encoded,0,encoded.length); } } if (lines[a].dtmfend) { lines[a].dtmfend = false; lines[a].dtmf = 'x'; } } if (record != null) record.add(recording); long stop = System.nanoTime(); long diff = (stop - start) / 1000; if (diff > 20000) { JFLog.log("process took:" + diff); } } catch (Exception e) { JFLog.log(e); } } /** Gets samples from RTPChannel for a line. */ private short[] getSamples(int idx) { RTPChannel channel = lines[idx].audioRTP.getDefaultChannel(); switch (channel.coder.getSampleRate()) { case 8000: if (!channel.getSamples(lines[idx].samples8)) return null; return lines[idx].samples8; case 16000: if (!channel.getSamples(lines[idx].samples16)) return null; return lines[idx].samples16; } return null; } private byte[] encode(Coder coder, short in[], int bufIdx) { byte encoded[] = null; int rate = coder.getSampleRate(); switch (rate) { case 8000: interpolate(data8, in, bufIdx); encoded = coder.encode(data8); break; case 16000: interpolate(data16, in, bufIdx); encoded = coder.encode(data16); break; } return encoded; } private byte[] encodeSilence(Coder coder) { byte encoded[] = null; int rate = coder.getSampleRate(); switch (rate) { case 8000: encoded = coder.encode(silence8); break; case 16000: encoded = coder.encode(silence16); break; } return encoded; } /** These samples hold the last sample of a buffer used to interpolate the next * block of samples. */ private short lastSamples[] = new short[48]; /** Mixes 'in' samples into 'out' samples. * Uses linear interpolation if out.length != in.length * * bufIdx : array index into lastSamples to store last sample used in interpolation * */ public void mix(short out[], short in[], int bufIdx) { int outLength = out.length; int inLength = in.length; if (outLength == inLength) { //no interpolation for (int a = 0; a < outLength; a++) { out[a] += in[a]; } } else { //linear interpolation //there is a one sample delay due to interpolation float d = ((float)inLength) / ((float)outLength); float p1 = 1.0f; float p2 = 0.0f; short s1 = lastSamples[bufIdx]; short s2 = in[0]; int inIdx = 1; int outIdx = 0; while (true) { out[outIdx++] += (s1 * p1 + s2 * p2); if (outIdx == outLength) break; p1 -= d; p2 += d; while (p1 < 0.0f) { s1 = s2; s2 = in[inIdx++]; p1 += 1.0f; p2 -= 1.0f; } } lastSamples[bufIdx] = s2; } } /** Interpolate 'in' onto 'out' (does not mix) */ public void interpolate(short out[], short in[], int bufIdx) { int outLength = out.length; int inLength = in.length; if (outLength == inLength) { //no interpolation for (int a = 0; a < outLength; a++) { out[a] = in[a]; } } else { //linear interpolation //there is a one sample delay due to interpolation float d = ((float)inLength) / ((float)outLength); float p1 = 1.0f; float p2 = 0.0f; short s1 = lastSamples[bufIdx]; short s2 = in[0]; int inIdx = 1; int outIdx = 0; while (true) { out[outIdx++] = (short)(s1 * p1 + s2 * p2); if (outIdx == outLength) break; p1 -= d; p2 += d; while (p1 < 0.0f) { s1 = s2; s2 = in[inIdx++]; p1 += 1.0f; p2 -= 1.0f; } } lastSamples[bufIdx] = s2; } } /** Starts a generated ringing phone sound. */ private void startRinging() { outRingFreqCount = 0; outRingCycle = 0; outRingCount = 0; outRingFreq1Pos = 0.0; outRingFreq2Pos = 0.0; inRingFreqCount = 0; inRingCycle = 0; inRingCount = 0; inRingFreq1Pos = 0.0; inRingFreq2Pos = 0.0; waitCount = 0; waitCycle = 0; } private final double ringVol = 9000.0; private int outRingFreq1, outRingFreq2; private int outRingFreqCount; private double outRingFreq1Pos, outRingFreq2Pos; private int outRingCycle; private int outRingCount; private int outRingTimes[] = new int[4]; private int inRingFreq1, inRingFreq2; private int inRingFreqCount; private double inRingFreq1Pos, inRingFreq2Pos; private int inRingCycle; private int inRingCount; private int inRingTimes[] = new int[4]; private void initOutRinging(int freq1, int freq2, int o1, int p1, int o2, int p2) { outRingFreq1 = freq1; outRingFreq2 = freq2; outRingTimes[0] = o1; outRingTimes[1] = p1; outRingTimes[2] = o2; outRingTimes[3] = p2; } private void initInRinging(int freq1, int freq2, int o1, int p1, int o2, int p2) { inRingFreq1 = freq1; inRingFreq2 = freq2; inRingTimes[0] = o1; inRingTimes[1] = p1; inRingTimes[2] = o2; inRingTimes[3] = p2; } private static final double pi2 = Math.PI * 2.0; /** Returns next 20ms of ringing phone. */ public short[] getRinging() { if (outRinging && outWav != null) { return outWav.getSamples(); } if (inRinging && inWav != null) { return inWav.getSamples(); } if (outRinging) { outRingCount += 20; if (outRingCount == outRingTimes[outRingCycle]) { outRingCount = 0; outRingCycle++; if (outRingCycle == 4) outRingCycle = 0; } if (outRingCycle % 2 == 1) { outRingFreqCount = 0; outRingFreq1Pos = 0.0; outRingFreq2Pos = 0.0; return silence; } //freq1 double theta1 = pi2 * outRingFreq1 / 44100.0; for (int a = 0; a < 882; a++) { ringing[a] = (short) (Math.sin(outRingFreq1Pos) * ringVol); outRingFreq1Pos += theta1; } //freq2 double theta2 = pi2 * outRingFreq2 / 44100.0; for (int a = 0; a < 882; a++) { ringing[a] += (short) (Math.sin(outRingFreq2Pos) * ringVol); outRingFreq2Pos += theta2; } outRingFreqCount += 882; if (outRingFreqCount == 44100) { outRingFreqCount = 0; outRingFreq1Pos = 0.0; outRingFreq2Pos = 0.0; } return ringing; } if (inRinging) { inRingCount += 20; if (inRingCount == inRingTimes[inRingCycle]) { inRingCount = 0; inRingCycle++; if (inRingCycle == 4) inRingCycle = 0; } if (inRingCycle % 2 == 1) { inRingFreqCount = 0; inRingFreq1Pos = 0.0; inRingFreq2Pos = 0.0; return silence; } //freq1 double theta1 = pi2 * inRingFreq1 / 44100.0; for (int a = 0; a < 882; a++) { ringing[a] = (short) (Math.sin(inRingFreq1Pos) * ringVol); inRingFreq1Pos += theta1; } //freq2 double theta2 = pi2 * inRingFreq2 / 44100.0; for (int a = 0; a < 882; a++) { ringing[a] = (short) (Math.sin(inRingFreq2Pos) * ringVol); inRingFreq2Pos += theta2; } inRingFreqCount += 882; if (inRingFreqCount == 44100) { inRingFreqCount = 0; inRingFreq1Pos = 0.0; inRingFreq2Pos = 0.0; } return ringing; } return silence; //does not happen } private int waitCount; private double waitPos; private static final double waitTheta = pi2 * 440.0 / 44100.0; private int waitCycle; /** Returns next 20ms of a generated call waiting sound (beep beep). */ public short[] getCallWaiting() { //440 (2 bursts for 0.3 seconds) //2on 2off 2on 200off[4sec] waitCycle++; if (waitCycle == 206) waitCycle = 0; if ((waitCycle > 6) || (waitCycle == 2) || (waitCycle == 3)) { waitCount = 0; waitPos = 0.0; return silence; } //440 for (int a = 0; a < 882; a++) { callWaiting[a] = (short) (Math.sin(waitPos) * ringVol); waitPos += waitTheta; } waitCount += 882; if (waitCount == 44100) { waitCount = 0; waitPos = 0.0; } return callWaiting; } public Record record; private class Player extends Thread { private volatile boolean playerActive = true; private volatile boolean done = false; private AudioBuffer buffer = new AudioBuffer(44100, 1, 2); //freq, chs, seconds public final Object lock = new Object(); public void run() { short buf[] = new short[882]; while (playerActive) { synchronized(lock) { if (buffer.size() < 882) { try { lock.wait(); } catch (Exception e) {} } if (buffer.size() < 882) continue; } buffer.get(buf, 0, 882); synchronized(activeLock) { if (active) { output.write(buf); } } } done = true; } public void add(short buf[]) { buffer.add(buf, 0, 882); } public void flush() { buffer.clear(); } public void cancel() { playerActive = false; while (!done) { synchronized(lock) { lock.notify(); } JF.sleep(10); } } } private class Reader extends Thread { private volatile boolean readerActive = true; private volatile boolean done = false; private AudioBuffer buffer = new AudioBuffer(44100, 1, 2); //freq, chs, seconds public void run() { short buf[] = new short[882]; while (readerActive) { synchronized(activeLock) { if (active) { if (input.read(buf)) { buffer.add(buf, 0, 882); continue; } } } JF.sleep(10); } done = true; } public void flush() { buffer.clear(); } public void cancel() { readerActive = false; while (!done) { JF.sleep(10); } } public boolean read(short buf[]) { if (buffer.size() < buf.length) return false; buffer.get(buf, 0, buf.length); return true; } } }