org.restcomm.media.resource.dtmf.DetectorImpl Maven / Gradle / Ivy
/*
* JBoss, Home of Professional Open Source
* Copyright 2011, Red Hat, Inc. and individual contributors
* by the @authors tag. See the copyright.txt in the distribution for a
* full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software 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 software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.restcomm.media.resource.dtmf;
import java.io.IOException;
import java.util.Collection;
import java.util.Iterator;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import org.apache.log4j.Logger;
import org.restcomm.media.ComponentType;
import org.restcomm.media.component.AbstractSink;
import org.restcomm.media.component.audio.AudioOutput;
import org.restcomm.media.component.audio.GoertzelFilter;
import org.restcomm.media.component.oob.OOBOutput;
import org.restcomm.media.scheduler.PriorityQueueScheduler;
import org.restcomm.media.scheduler.Task;
import org.restcomm.media.spi.dtmf.DtmfDetector;
import org.restcomm.media.spi.dtmf.DtmfDetectorListener;
import org.restcomm.media.spi.format.Format;
import org.restcomm.media.spi.format.FormatFactory;
import org.restcomm.media.spi.format.Formats;
import org.restcomm.media.spi.listener.Listeners;
import org.restcomm.media.spi.listener.TooManyListenersException;
import org.restcomm.media.spi.memory.Frame;
import org.restcomm.media.spi.pooling.PooledObject;
/**
* Implements inband DTMF detector.
*
* Inband means that DTMF is transmitted within the audio of the phone conversation, i.e. it is audible to the conversation
* partners. Therefore only uncompressed codecs like g711 alaw or ulaw can carry inband DTMF reliably. Female voice are known to
* once in a while trigger the recognition of a DTMF tone. For analog lines inband is the only possible means to transmit DTMF.
*
* Though Inband DTMF detection may work for other codecs like SPEEX, GSM, G729 as DtmfDetector is using DSP in front of
* InbandDetector there is no guarantee that it will always work. In future MMS may not have DSP in front of InbandDetector and
* hence Inband detection for codecs like SPEEX, GSM, G729 may completely stop
*
* @author yulian oifa
* @author amit bhayani
* @author Henrique Rosa ([email protected])
*/
public class DetectorImpl extends AbstractSink implements DtmfDetector, PooledObject {
private static final long serialVersionUID = 450306501541827622L;
private final static Format linear = FormatFactory.createAudioFormat("linear", 8000, 16, 1);
private final static Formats formats = new Formats();
static {
formats.add(linear);
}
/**
* The default duration of the DTMF tone.
*/
private final static int TONE_DURATION = 80;
public final static String[][] events = new String[][] { { "1", "2", "3", "A" }, { "4", "5", "6", "B" }, { "7", "8", "9", "C" }, { "*", "0", "#", "D" } };
// private final static String[] evtID = new String[]{"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "#", "*"};
private final static String[] oobEvtID = new String[] { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "*", "#", "A", "B", "C", "D" };
private final static int[] lowFreq = new int[] { 697, 770, 852, 941 };
private final static int[] highFreq = new int[] { 1209, 1336, 1477, 1633 };
private GoertzelFilter[] lowFreqFilters = new GoertzelFilter[4];
private GoertzelFilter[] highFreqFilters = new GoertzelFilter[4];
private double threshold = 0;
private int level;
private int offset = 0;
private int toneDuration = TONE_DURATION;
private int N = 8 * toneDuration;
private double scale = (double) toneDuration / (double) 1000;
private double p[] = new double[4];
private double P[] = new double[4];
private double[] signal;
private double maxAmpl;
private DtmfBuffer dtmfBuffer;
private Listeners listeners = new Listeners();
private EventSender eventSender;
private PriorityQueueScheduler scheduler;
private AudioOutput output;
private OOBOutput oobOutput;
private OOBDetector oobDetector;
private static final Logger logger = Logger.getLogger(DetectorImpl.class);
public DetectorImpl(String name, PriorityQueueScheduler scheduler) {
super(name);
this.scheduler = scheduler;
dtmfBuffer = new DtmfBuffer(this);
eventSender = new EventSender();
signal = new double[N];
for (int i = 0; i < 4; i++) {
lowFreqFilters[i] = new GoertzelFilter(lowFreq[i], N, scale);
highFreqFilters[i] = new GoertzelFilter(highFreq[i], N, scale);
}
this.level = DEFAULT_SIGNAL_LEVEL;
output = new AudioOutput(scheduler, ComponentType.DTMF_DETECTOR.getType());
oobOutput = new OOBOutput(scheduler, ComponentType.DTMF_DETECTOR.getType());
output.join(this);
oobDetector = new OOBDetector();
oobOutput.join(oobDetector);
}
public AudioOutput getAudioOutput() {
return this.output;
}
public OOBOutput getOOBOutput() {
return this.oobOutput;
}
@Override
public void activate() {
this.offset = 0;
this.maxAmpl = 0;
this.dtmfBuffer.clear();
output.start();
oobOutput.start();
}
@Override
public void deactivate() {
output.stop();
oobOutput.stop();
}
public void setDuration(int duartion) {
this.toneDuration = duartion;
}
public int getDuration() {
return this.toneDuration;
}
@Override
public void setVolume(int level) {
this.level = level;
threshold = Math.pow(Math.pow(10, level), 0.1) * Short.MAX_VALUE;
}
public void setLasy(boolean isLazy) {
}
@Override
public int getVolume() {
return level;
}
@Override
public void onMediaTransfer(Frame buffer) throws IOException {
byte[] data = buffer.getData();
int M = buffer.getLength();
int k = 0;
while (k < M) {
while (offset < N && k < M - 1) {
double s = ((data[k++] & 0xff) | (data[k++] << 8));
double sa = Math.abs(s);
if (sa > maxAmpl) {
maxAmpl = sa;
}
signal[offset++] = s;
}
// if dtmf buffer full check signal
if (offset == N) {
offset = 0;
// and if max amplitude of signal is greater theshold
// try to detect tone.
if (maxAmpl >= threshold) {
maxAmpl = 0;
getPower(lowFreqFilters, signal, 0, p);
getPower(highFreqFilters, signal, 0, P);
String tone = getTone(p, P);
if (tone != null)
dtmfBuffer.push(tone);
}
}
}
}
private void getPower(GoertzelFilter[] filters, double[] data, int offset, double[] power) {
for (int i = 0; i < 4; i++) {
// power[i] = filter.getPower(freq[i], data, 0, data.length, (double) TONE_DURATION / (double) 1000);
power[i] = filters[i].getPower(data, offset);
}
}
/**
* Searches maximum value in the specified array.
*
* @param data[] input data.
* @return the index of the maximum value in the data array.
*/
private int getMax(double data[]) {
int idx = 0;
double max = data[0];
for (int i = 1; i < data.length; i++) {
if (max < data[i]) {
max = data[i];
idx = i;
}
}
return idx;
}
/**
* Searches DTMF tone.
*
* @param f the low frequency array
* @param F the high frequency array.
* @return DTMF tone.
*/
private String getTone(double f[], double F[]) {
int fm = getMax(f);
boolean fd = true;
for (int i = 0; i < f.length; i++) {
if (fm == i) {
continue;
}
double r = f[fm] / (f[i] + 1E-15);
if (r < threshold) {
fd = false;
break;
}
}
if (!fd) {
return null;
}
int Fm = getMax(F);
boolean Fd = true;
for (int i = 0; i < F.length; i++) {
if (Fm == i) {
continue;
}
double r = F[Fm] / (F[i] + 1E-15);
if (r < threshold) {
Fd = false;
break;
}
}
if (!Fd) {
return null;
}
return events[fm][Fm];
}
public Formats getNativeFormats() {
return formats;
}
public String getMask() {
return null;
}
public void setMask(String mask) {
}
@Override
public void setInterdigitInterval(int interval) {
dtmfBuffer.setInterdigitInterval(interval);
}
@Override
public int getInterdigitInterval() {
return dtmfBuffer.getInterdigitInterval();
}
protected void fireEvent(String tone) {
eventSender.events.add(new DtmfEventImpl(this, tone, 0));
// schedule event delivery
scheduler.submit(eventSender, PriorityQueueScheduler.INPUT_QUEUE);
}
protected void fireEvent(DtmfEventImpl evt) {
eventSender.events.add(evt);
// schedule event delivery
scheduler.submit(eventSender, PriorityQueueScheduler.INPUT_QUEUE);
}
protected void fireEvent(Collection evts) {
eventSender.events.addAll(evts);
// schedule event delivery
scheduler.submit(eventSender, PriorityQueueScheduler.INPUT_QUEUE);
}
@Override
public void flushBuffer() {
dtmfBuffer.flush();
}
public void clearBuffer() {
dtmfBuffer.clear();
}
@Override
public void addListener(DtmfDetectorListener listener) throws TooManyListenersException {
listeners.add(listener);
}
@Override
public void removeListener(DtmfDetectorListener listener) {
listeners.remove(listener);
}
@Override
public void clearAllListeners() {
listeners.clear();
}
@Override
public void clearDigits() {
dtmfBuffer.clear();
}
public class EventSender extends Task {
private final Queue events = new ConcurrentLinkedQueue<>();
public EventSender() {
super();
}
@Override
public int getQueueNumber() {
return PriorityQueueScheduler.INPUT_QUEUE;
}
@Override
public long perform() {
final Iterator iterator = this.events.iterator();
while (iterator.hasNext()) {
DtmfEventImpl evt = iterator.next();
// try to deliver or queue to buffer if not delivered
if (!listeners.dispatch(evt)) {
dtmfBuffer.queue(evt);
if (logger.isInfoEnabled()) {
logger.info(String.format("(%s) Buffered '%s' tone", getName(), evt.getTone()));
}
} else {
if (logger.isInfoEnabled()) {
logger.info(String.format("(%s) Delivered '%s' tone", getName(), evt.getTone()));
}
}
// Remove event from collection
iterator.remove();
}
return 0;
}
}
private class OOBDetector extends AbstractSink {
private static final long serialVersionUID = -1065228790095547415L;
private byte currTone = (byte) 0xFF;
private long latestSeq = 0;
private boolean hasEndOfEvent = false;
private long endSeq = 0;
public OOBDetector() {
super("oob detector");
}
@Override
public void onMediaTransfer(Frame buffer) throws IOException {
byte[] data = buffer.getData();
if (data.length != 4)
return;
boolean endOfEvent = false;
endOfEvent = (data[1] & 0X80) != 0;
// lets ignore end of event packets
if (endOfEvent) {
hasEndOfEvent = true;
endSeq = buffer.getSequenceNumber();
return;
}
// lets update sync data , allowing same tone come after 160ms from previous tone , not including end of tone
if (currTone == data[0]) {
if (hasEndOfEvent) {
if (buffer.getSequenceNumber() <= endSeq && buffer.getSequenceNumber() > (endSeq - 8)) {
// out of order , belongs to same event
// if comes after end of event then its new one
return;
}
} else if ((buffer.getSequenceNumber() < (latestSeq + 8)) && buffer.getSequenceNumber() > (latestSeq - 8)) {
if (buffer.getSequenceNumber() > latestSeq)
latestSeq = buffer.getSequenceNumber();
return;
}
}
hasEndOfEvent = false;
endSeq = 0;
latestSeq = buffer.getSequenceNumber();
currTone = data[0];
dtmfBuffer.push(oobEvtID[currTone]);
}
@Override
public void activate() {
}
@Override
public void deactivate() {
}
}
@Override
public void checkIn() {
// TODO Auto-generated method stub
}
@Override
public void checkOut() {
// TODO Auto-generated method stub
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy