Java tutorial
/* * The MIT License (MIT) * * Copyright (c) 2015 Tinotenda Chemvura * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ package com.tino1b2be.dtmfdecoder; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.function.Consumer; import javax.sound.sampled.UnsupportedAudioFileException; import org.apache.commons.math3.complex.Complex; import org.apache.commons.math3.transform.DftNormalization; import org.apache.commons.math3.transform.FastFourierTransformer; import org.apache.commons.math3.transform.TransformType; import com.tino1b2be.audio.AudioFile; import com.tino1b2be.audio.AudioFileException; import com.tino1b2be.audio.TempAudio; import com.tino1b2be.audio.WavFileException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; /** * Class to decode DTMF signals in a supported audio file. * * @author Tinotenda Chemvura * */ public class DTMFUtil { private static final Logger logger = LogManager.getLogger(DTMFUtil.class); public static boolean debug = false; private long samplesReadSum = 0; double[][] framesBuffer; /** * True if decoder is to use the goertzel algorithm instead of the FFT False * by default */ public static boolean goertzel = false; private static final double CUT_OFF_POWER = 0.004; private double FftCutoffPowerNoiseRatio = 0.42;//0.46; private static final double FFT_FRAME_DURATION = 0.030; private static final double GOERTZEL_CUT_OFF_POWER_NOISE_RATIO = 0.87; private static final double GOERTZEL_FRAME_DURATION = 0.045; private boolean decoded; private enum LabelLen { DECODE_40, DECODE_60, DECODE_80, DECODE_100 }; private LabelLen labelLen = LabelLen.DECODE_40; private boolean decoder = false; private boolean generate = false; private String seq[] = { "", "" }; private AudioFile audio; private int frameSize; private int frameBufferSize; private int labelPauseDurr; private int symbolLength; private Consumer<String> onLabelAction; private long framesCount = 0; private static int[] freqIndicies; /** * The list of valid DTMF frequencies that are going to be processed and * searched for within the ITU-T recommendations . See the <a href= * "http://en.wikipedia.org/wiki/Dual-tone_multi-frequency_signaling" > * WikiPedia article on DTMF</a>. */ public static final int[] DTMF_FREQUENCIES_BIN = { 687, 697, 707, // 697 758, 770, 782, // 770 839, 852, 865, // 852 927, 941, 955, // 941 1191, 1209, 1227, // 1209 1316, 1336, 1356, // 1336 1455, 1477, 1499, // 1477 1609, 1633, 1647, 1657 // 1633 }; /** * The list of valid DTMF frequencies. See the <a href= * "http://en.wikipedia.org/wiki/Dual-tone_multi-frequency_signaling" > * WikiPedia article on DTMF</a>. */ public static final int[] DTMF_FREQUENCIES = { 697, 770, 852, 941, 1209, 1336, 1477, 1633 }; /** * The list of valid DTMF characters. See the <a href= * "http://en.wikipedia.org/wiki/Dual-tone_multi-frequency_signaling" > * WikiPedia article on DTMF</a>. */ public static final char[] DTMF_CHARACTERS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'A', 'B', 'C', 'D', '*', '#' }; // generation variables private double[] generatedSeq; private int outPauseDurr, outToneDurr; private double outFs; private File outFile; private char[] outChars; private boolean generated; /** * Constructor to decode an array of samples * * @param samples * array of samples (Mono channel) * @param Fs * Sampling Frequency * @throws AudioFileException * @throws DTMFDecoderException */ public DTMFUtil(double[] samples, int Fs) throws AudioFileException, DTMFDecoderException { // create an audio file object and export to a temp location // load the temp audio file and decode audio = new TempAudio(samples, Fs); this.init(); } /** * Constructor to decode an array of samples * * @param samples * array of samples (Stereo channel) * @param Fs * Sampling Frequency * @throws AudioFileException * @throws DTMFDecoderException */ public DTMFUtil(double[][] samples, int Fs) throws AudioFileException, DTMFDecoderException { // create an audio file object and export to a temp location // load the temp audio file and decode audio = new TempAudio(samples, Fs); this.init(); } /** * Constructor used to Decode an audio file * * @param data * AudioFile object to be processed. * @throws DTMFDecoderException */ public DTMFUtil(AudioFile data) throws DTMFDecoderException { this.audio = data; this.init(); } /** * Constructor used to Decode an audio file * * @param filename * Filename of the audio file to be processed * @throws Exception * @throws AudioFileException * @throws UnsupportedAudioFileException * @throws IOException * @throws WavFileException * @throws DTMFDecoderException */ public DTMFUtil(String filename) throws AudioFileException, IOException, DTMFDecoderException { this.audio = FileUtil.readAudioFile(filename); this.init(); } public DTMFUtil(InputStream stream) throws IOException, UnsupportedAudioFileException, DTMFDecoderException { this.audio = FileUtil.readMp3File(stream); this.init(); } /** * Constructor used to Decode an audio file * * @param file * File object for the audio file * @throws Exception * @throws AudioFileException * @throws UnsupportedAudioFileException * @throws IOException * @throws WavFileException * @throws DTMFDecoderException */ public DTMFUtil(File file) throws AudioFileException, DTMFDecoderException, IOException { this.audio = FileUtil.readAudioFile(file); this.init(); } private void init() throws DTMFDecoderException { this.decoder = true; setFrameSize(); if (!goertzel) setCentreIndicies(); this.decoded = false; } public DTMFUtil(File file, char[] chars, int fs, int toneDurr, int pauseDurr) throws DTMFDecoderException { this.generate = true; setChars(chars); if (fs < 8000) throw new DTMFDecoderException("Sampling frequency must be at least 8kHz."); this.outFs = fs; if (toneDurr < 40) throw new DTMFDecoderException("Tone duration should be greater than 40ms."); this.outToneDurr = toneDurr; if (toneDurr < 30) throw new DTMFDecoderException("Pause duration should be greater than 30ms."); this.outPauseDurr = pauseDurr; this.outFile = file; } public DTMFUtil(String filename, char[] chars, int fs, int toneDurr, int pauseDurr) throws DTMFDecoderException { this.generate = true; setChars(chars); this.outFs = fs; this.outToneDurr = toneDurr; this.outPauseDurr = pauseDurr; this.outFile = new File(filename); } /** * Check if the characters are valid and set the characters * * @param chars * Characters to be generated * @throws DTMFDecoderException */ private void setChars(char[] chars) throws DTMFDecoderException { outChars = new char[chars.length]; char[] cc = Arrays.copyOf(DTMF_CHARACTERS, DTMF_CHARACTERS.length); Arrays.sort(cc); for (int c = 0; c < chars.length; c++) { if (Arrays.binarySearch(cc, chars[c]) < 0) throw new DTMFDecoderException("The character \"" + chars[c] + "\" is not a DTMF character."); else outChars[c] = chars[c]; } } /** * Method to precalculate the indices to be used to locate the DTMF * frequencies in the power spectrum */ private void setCentreIndicies() { freqIndicies = new int[DTMF_FREQUENCIES_BIN.length]; for (int i = 0; i < freqIndicies.length; i++) { int ind = (int) Math.round(((DTMF_FREQUENCIES_BIN[i] * 1.0) / audio.getSampleRate()) * frameSize); freqIndicies[i] = ind; } } /** * Method to set the frame size for the decoding process. Framesize must be * a power of 2 * * @throws DTMFDecoderException * If Fs if less than 8kHz or loo large. */ private void setFrameSize() throws DTMFDecoderException { if (audio.getSampleRate() < 8000) throw new DTMFDecoderException("Sampling Rate cannot be less than 8kHz."); if (goertzel) { this.frameSize = (int) Math.floor(GOERTZEL_FRAME_DURATION * audio.getSampleRate()); this.frameBufferSize = (int) Math.ceil(frameSize / 3.0); } else { int size = 0; for (int i = 8; i <= 15; i++) { size = (int) Math.pow(2, i); if (size / (audio.getSampleRate() * 1.0) < FFT_FRAME_DURATION) continue; else { frameSize = size; this.frameBufferSize = (int) Math.ceil(frameSize / 3.0); logger.info("frameSize: " + frameSize); return; } } throw new DTMFDecoderException( "Sampling Frequency of the audio file is too high. Please use a file with a lower Sampling Frequency."); } } /** * Method to filter out the power spectrum information for the DTMF * frequencies given an array of power spectrum information from an FFT. * * @param frame * Frame with power spectrum information to be processed * @return an array with 8 doubles. Each representing the magnitude of the * corresponding dtmf frequency */ private static double[] filterFrame(double[] frame) { double[] out = new double[8]; // 687, 697, 707, // 697 0,1,2 // 758, 770, 782, // 770 3,4,5 // 839, 852, 865, // 852 6,7,8 // 927, 941, 955, // 941 9,10,11 // 1191, 1209, 1227, // 1209 12,13,14 // 1316, 1336, 1356, // 1336 15,16,17 // 1455, 1477, 1499, // 1477 18,19,20 // 1609, 1633, 1647, 1657 // 1633 21,22,23,24 // 687, 697, 707, // 697 0,1,2 out[0] = frame[freqIndicies[0]]; if (freqIndicies[0] != freqIndicies[1]) out[0] += frame[freqIndicies[1]]; if (freqIndicies[0] != freqIndicies[2] && freqIndicies[1] != freqIndicies[2]) out[0] += frame[freqIndicies[2]]; // 758, 770, 782, // 770 3,4,5 out[1] = frame[freqIndicies[3]]; if (freqIndicies[3] != freqIndicies[4]) out[1] += frame[freqIndicies[4]]; if (freqIndicies[3] != freqIndicies[5] && freqIndicies[4] != freqIndicies[5]) out[1] += frame[freqIndicies[5]]; // 839, 852, 865, // 852 6,7,8 out[2] = frame[freqIndicies[6]]; if (freqIndicies[6] != freqIndicies[7]) out[2] += frame[freqIndicies[7]]; if (freqIndicies[6] != freqIndicies[8] && freqIndicies[7] != freqIndicies[8]) out[2] += frame[freqIndicies[8]]; // 927, 941, 955, // 941 9,10,11 out[3] = frame[freqIndicies[9]]; if (freqIndicies[9] != freqIndicies[10]) out[3] += frame[freqIndicies[10]]; if (freqIndicies[9] != freqIndicies[11] && freqIndicies[10] != freqIndicies[11]) out[3] += frame[freqIndicies[11]]; // 1191, 1209, 1227, // 1209 12,13,14 out[4] = frame[freqIndicies[12]]; if (freqIndicies[12] != freqIndicies[13]) out[4] += frame[freqIndicies[13]]; if (freqIndicies[12] != freqIndicies[14] && freqIndicies[13] != freqIndicies[14]) out[5] += frame[freqIndicies[14]]; // 1316, 1336, 1356, // 1336 15,16,17 out[5] = frame[freqIndicies[15]]; if (freqIndicies[15] != freqIndicies[16]) out[5] += frame[freqIndicies[16]]; if (freqIndicies[15] != freqIndicies[17] && freqIndicies[16] != freqIndicies[17]) out[5] += frame[freqIndicies[17]]; // 1455, 1477, 1499, // 1477 18,19,20 out[6] = frame[freqIndicies[18]]; if (freqIndicies[18] != freqIndicies[19]) out[6] += frame[freqIndicies[19]]; if (freqIndicies[18] != freqIndicies[20] && freqIndicies[19] != freqIndicies[20]) out[6] += frame[freqIndicies[20]]; out[7] = frame[freqIndicies[21]]; if (frame[freqIndicies[22]] != frame[freqIndicies[21]]) out[7] += frame[freqIndicies[22]]; else out[7] += frame[freqIndicies[23]]; out[7] += frame[freqIndicies[24]]; return out; } /** * Method returns the DTMF sequence * * @return char array with the keys represented in the file * @throws DTMFDecoderException * Throws excepion when the file has not been decoded yet. */ public String[] getDecoded() throws DTMFDecoderException { if (!decoded) throw new DTMFDecoderException("File has not been decoded yet. Please run the method decode() first!"); return seq; } /** * Method to generate a frequency spectrum of the frame using FFT * * @param frame * Frame to be transformed * @param Fs * Sampling Frequency * @return an Array showing the realtive powers of all frequencies */ private static double[] transformFrameFFT(double[] frame, int Fs) { final FastFourierTransformer fft = new FastFourierTransformer(DftNormalization.STANDARD); final Complex[] spectrum = fft.transform(frame, TransformType.FORWARD); final double[] powerSpectrum = new double[frame.length / 2 + 1]; for (int i = 0; i < powerSpectrum.length; i++) { double abs = spectrum[i].abs(); powerSpectrum[i] = abs * abs; } return powerSpectrum; } /** * Method to generate a frequency spectrum of the frame using Goertzel * Algorithm * * @param frame * Frame to be transformed * @param Fs * Sampling Frequency * @return an Array showing the realtive powers of the DTMF frequencies * @throws DTMFDecoderException * If no samples have been provided for the goertze class to * transform */ private static double[] transformFrameG(double[] frame, int Fs) throws DTMFDecoderException { double[] out; GoertzelOptimised g = new GoertzelOptimised(Fs, frame, DTMF_FREQUENCIES_BIN); // 1. transform the frames using goertzel algorithm // 2. get the highest DTMF freq within the tolerance range and use that // magnitude to represet the corresponsing DTMF free if (g.compute()) { out = filterFrameG(g.getMagnitudeSquared()); return out; } else { throw new DTMFDecoderException("Decoding failed."); } } /** * Method to get the highest DTMF freq within the tolerance range and use * that magnitude to represet the corresponsing DTMF freq * * @param frame * Frame with 274 magnitudes to be processed * @return an array with 8 magnitudes. Each representing the magnitude of * each frequency */ private static double[] filterFrameG(double[] frame) { double[] out = new double[8]; out[0] = DecoderUtil.max(Arrays.copyOfRange(frame, 0, 3)); out[1] = DecoderUtil.max(Arrays.copyOfRange(frame, 3, 6)); out[2] = DecoderUtil.max(Arrays.copyOfRange(frame, 6, 9)); out[3] = DecoderUtil.max(Arrays.copyOfRange(frame, 9, 12)); out[4] = DecoderUtil.max(Arrays.copyOfRange(frame, 12, 15)); out[5] = DecoderUtil.max(Arrays.copyOfRange(frame, 15, 18)); out[6] = DecoderUtil.max(Arrays.copyOfRange(frame, 18, 21)); out[7] = DecoderUtil.max(Arrays.copyOfRange(frame, 21, 25)); return out; } /** * Method to detect whether a frame is too noisy for detection * * @param dft_data * Frequency spectrum magnitudes for the DTMF frequencies * @param power_spectrum * @return true is noisy or false if it is acceptable */ private boolean isNoisy(double[] dft_data, double[] power_spectrum) { if (power_spectrum == null) return true; // sum the powers of all frequencies = sum // find ratio of the (sum of two highest peaks) : sum double[] temp1 = Arrays.copyOfRange(dft_data, 0, 4); double[] temp2 = Arrays.copyOfRange(dft_data, 4, 8); Arrays.sort(temp1); Arrays.sort(temp2); // ratio = (max(lower freqs) + max(higher freqs))/sum(all freqs in // spectrum) return ((temp1[temp1.length - 1] + temp2[temp2.length - 1]) / DecoderUtil.sumArray(power_spectrum)) < FftCutoffPowerNoiseRatio; } private boolean isNoisyG(double[] dft_data) { // sum the powers of all frequencies = sum // find ratio of the (sum of two highest peaks) : sum double[] temp1 = Arrays.copyOfRange(dft_data, 0, 4); double[] temp2 = Arrays.copyOfRange(dft_data, 4, 8); Arrays.sort(temp1); Arrays.sort(temp2); double one = temp1[temp1.length - 1]; double two = temp2[temp2.length - 1]; double sum = DecoderUtil.sumArray(dft_data); return ((one + two) / sum) < GOERTZEL_CUT_OFF_POWER_NOISE_RATIO; } /** * Method to decode a frame given the frequency spectrum information of the * frame * * @param dft_data * Frequency spectrum information showing the relative magnitudes * of the power of each DTMF frequency * @return DTMF charatcter represented by the frame * @throws DTMFDecoderException */ private static char getRawChar(double[] dft_data) throws DTMFDecoderException { char out = 0; int low, hi; double[] lower = Arrays.copyOfRange(dft_data, 0, 4); double[] higher = Arrays.copyOfRange(dft_data, 4, 8); low = DecoderUtil.maxIndex(lower); hi = DecoderUtil.maxIndex(higher); if (low == 0) { // low = 697 if (hi == 0) { // High = 1209 out = '1'; } else if (hi == 1) { // high = 1336 out = '2'; } else if (hi == 2) { // high = 1477 out = '3'; } else if (hi == 3) { // high = 1633 out = 'A'; } else throw new DTMFDecoderException("Something went terribly wrong!"); } else if (low == 1) { // low = 770 if (hi == 0) { // high = 1209 out = '4'; } else if (hi == 1) { // high = 1336 out = '5'; } else if (hi == 2) { // high = 1477 out = '6'; } else if (hi == 3) { // high = 1633 out = 'B'; } else throw new DTMFDecoderException("Something went terribly wrong!"); } else if (low == 2) { // low = 852 if (hi == 0) { // high = 1209 out = '7'; } else if (hi == 1) { // high = 1336 out = '8'; } else if (hi == 2) { // high = 1477 out = '9'; } else if (hi == 3) { // high = 1633 out = 'C'; } else throw new DTMFDecoderException("Something went terribly wrong!"); } else if (low == 3) { // low = 941 if (hi == 0) { // high = 1209 out = '*'; } else if (hi == 1) { // high = 1336 out = '0'; } else if (hi == 2) { // high = 1477 out = '#'; } else if (hi == 3) { // high = 1633 out = 'D'; } else throw new DTMFDecoderException("Something went terribly wrong!"); } else throw new DTMFDecoderException("Something went terribly wrong!"); return out; } /** * Method to decode the wav file. * * @return String representation of the sequence of DTMF tones represented * in the wav file * @throws IOException * @throws AudioFileException * @throws WavFileException */ private void decodeMono40() throws IOException, AudioFileException { char prev = '_'; char prev2 = '_'; String seq2 = ""; String seq22 = ""; int count = 0; do { char curr; try { curr = decodeNextFrameMono(); } catch (DTMFDecoderException e) { break; } if (debug) System.out.print(curr); // System.out.print(curr); if (curr != '_') { if (curr == prev) { // eliminate false positives if (curr != prev2) { if (debug) { seq22 = seq22.substring(0, seq22.length() - 1); seq22 += curr + "."; count = 0; seq2 += curr; } else { seq22 += curr + "."; seq2 += curr; } } } } if (count % 50 == 0) { seq22 += '_'; } count++; prev2 = prev; prev = curr; } while (true); seq[0] = seq2; } /** * Method to decode the wav file. * * @return String representation of the sequence of DTMF tones represented * in the wav file * @throws IOException * @throws AudioFileException * @throws WavFileException */ private void decodeMono60() throws IOException, AudioFileException { char prev = '_'; char prev2 = '_'; char prev3 = '_'; String seq2 = ""; String seq22 = ""; int count = 0; do { char curr; try { curr = decodeNextFrameMono(); } catch (DTMFDecoderException e) { break; } if (debug) System.out.print(curr); // System.out.print(curr); if (curr != '_') { if (curr == prev && curr == prev2) { // eliminate false // positives if (curr != prev3) { if (debug) { seq22 = seq22.substring(0, seq22.length() - 1); seq22 += curr + "."; count = 0; seq2 += curr; } else { seq22 += curr + "."; seq2 += curr; } } } } if (count % 30 == 0) { seq22 += '_'; } count++; prev3 = prev2; prev2 = prev; prev = curr; } while (true); seq[0] = seq2; } /** * Method to decode the wav file. * * @return String representation of the sequence of DTMF tones represented * in the wav file * @throws IOException * @throws AudioFileException * @throws WavFileException */ private void decodeMono80() throws IOException, AudioFileException { char prev = '_'; char prev2 = '_'; char prev3 = '_'; char prev4 = '_'; String seq2 = ""; String seq22 = ""; int count = 0; do { char curr; try { curr = decodeNextFrameMono(); } catch (DTMFDecoderException e) { break; } if (debug) System.out.print(curr); // System.out.print(curr); if (curr != '_') { if (curr == prev && curr == prev2 && curr == prev3) { // eliminate // false // positives if (curr != prev4) { if (debug) { seq22 = seq22.substring(0, seq22.length() - 1); seq22 += curr + "."; count = 0; seq2 += curr; } else { seq22 += curr + "."; seq2 += curr; } } } } if (count % 100 == 0) { seq22 += '_'; } count++; prev4 = prev3; prev3 = prev2; prev2 = prev; prev = curr; } while (true); seq[0] = seq2; } /** * Method to decode the wav file. * * @return String representation of the sequence of DTMF tones represented * in the wav file * @throws IOException * @throws AudioFileException * @throws WavFileException */ private void decodeMono100() throws IOException, AudioFileException { char prev = '_'; char prev2 = '_'; char prev3 = '_'; char prev4 = '_'; char prev5 = '_'; String seq2 = ""; String seq22 = ""; int count = 0; do { char curr; try { curr = decodeNextFrameMono(); if (onLabelReact(seq2)) { seq2 = ""; } } catch (DTMFDecoderException e) { break; } if (debug) System.out.print(curr); // System.out.print(curr); if (curr != '_') { if (curr == prev && curr == prev2 && curr == prev3 && curr == prev4) { // eliminate // false // positives if (curr != prev5) { if (debug) { seq22 = seq22.substring(0, seq22.length() - 1); seq22 += curr + "."; count = 0; seq2 += curr; } else { seq22 += curr + "."; seq2 += curr; } } } } if (count % 100 == 0) { seq22 += '_'; } count++; prev5 = prev4; prev4 = prev3; prev3 = prev2; prev2 = prev; prev = curr; } while (true); seq[0] = seq2; } /** * Method to decode the wav file. * * @return String representation of the sequence of DTMF tones represented * in the wav file * @throws IOException * @throws WavFileException */ private void decodeStereo40() throws IOException, AudioFileException { char curr[]; char[] prev = { '_', '_' }; char[] prev2 = { '_', '_' }; String[] seq2 = { "", "" }; do { try { if (onLabelReact(seq2)) { seq2 = new String[] { "", "" }; } curr = decodeNextFrameStereo(); } catch (DTMFDecoderException e) { break; } // decode channel 1 if (curr[0] != '_') { if (curr[0] == prev[0]) { // eliminate false positives if (curr[0] != prev2[0]) { seq2[0] += curr[0]; } } } prev2[0] = prev[0]; prev[0] = curr[0]; // decode channel 2 if (curr[1] != '_') { if (curr[1] == prev[1]) { // eliminate false positives if (curr[1] != prev2[1]) { seq2[1] += curr[1]; } } } prev2[1] = prev[1]; prev[1] = curr[1]; } while (true); seq = seq2; } /** * Method to decode the wav file. * * @return String representation of the sequence of DTMF tones represented * in the wav file * @throws IOException * @throws IOException, AudioFileException */ private void decodeStereo60() throws IOException, AudioFileException { char curr[]; char[] prev = { '_', '_' }; char[] prev2 = { '_', '_' }; char[] prev3 = { '_', '_' }; String[] seq2 = { "", "" }; do { try { if (onLabelReact(seq2)) { seq2 = new String[] { "", "" }; } curr = decodeNextFrameStereo(); } catch (DTMFDecoderException e) { break; } //++framesCount; // decode channel 1 if (curr[0] != '_') { if (curr[0] == prev[0] && curr[0] == prev2[0]) { // eliminate // false // positives if (curr[0] != prev3[0]) { seq2[0] += curr[0]; framesCount = 0; // zero count on change } } } prev3[0] = prev2[0]; prev2[0] = prev[0]; prev[0] = curr[0]; // decode channel 2 if (curr[1] != '_') { if (curr[1] == prev[1] && curr[1] == prev2[1]) { // eliminate // false // positives if (curr[1] != prev3[1]) { seq2[1] += curr[1]; //System.out.println(seq2[1] + " " + framesCount); framesCount = 0; // zero count on change } } } prev3[1] = prev2[1]; prev2[1] = prev[1]; prev[1] = curr[1]; } while (true); seq = seq2; } /** * Method to decode the wav file. * * @return String representation of the sequence of DTMF tones represented * in the wav file * @throws IOException * @throws WavFileException */ private void decodeStereo80() throws IOException, AudioFileException { char curr[]; char[] prev = { '_', '_' }; char[] prev2 = { '_', '_' }; char[] prev3 = { '_', '_' }; char[] prev4 = { '_', '_' }; String[] seq2 = { "", "" }; do { try { if (onLabelReact(seq2)) { seq2 = new String[] { "", "" }; } curr = decodeNextFrameStereo(); } catch (DTMFDecoderException e) { break; } // decode channel 1 if (curr[0] != '_') { if (curr[0] == prev[0] && curr[0] == prev2[0] && curr[0] == prev3[0]) { // eliminate // false // positives if (curr[0] != prev4[0]) { seq2[0] += curr[0]; } } } prev4[0] = prev3[0]; prev3[0] = prev2[0]; prev2[0] = prev[0]; prev[0] = curr[0]; // decode channel 2 if (curr[1] != '_') { if (curr[1] == prev[1] && curr[1] == prev2[1] && curr[1] == prev3[1]) { // eliminate // false // positives if (curr[1] != prev4[1]) { seq2[1] += curr[1]; } } } prev4[1] = prev3[1]; prev3[1] = prev2[1]; prev2[1] = prev[1]; prev[1] = curr[1]; } while (true); seq = seq2; } /** * Method to decode the wav file. * * @return String representation of the sequence of DTMF tones represented * in the wav file * @throws IOException * @throws WavFileException */ private void decodeStereo100() throws IOException, AudioFileException { char curr[]; char[] prev = { '_', '_' }; char[] prev2 = { '_', '_' }; char[] prev3 = { '_', '_' }; char[] prev4 = { '_', '_' }; char[] prev5 = { '_', '_' }; String[] dtmfLabels = { "", "" }; boolean changed = false; do { try { if (onLabelReact(dtmfLabels)) { dtmfLabels = new String[] { "", "" }; } curr = decodeNextFrameStereo(); } catch (DTMFDecoderException e) { break; } // decode channel 1 if (curr[0] != '_') { if (curr[0] == prev[0] && curr[0] == prev2[0] && curr[0] == prev3[0] && curr[0] == prev4[0]) { // eliminate // false // positives if (curr[0] != prev5[0]) { dtmfLabels[0] += curr[0]; } } } prev5[0] = prev4[0]; prev4[0] = prev3[0]; prev3[0] = prev2[0]; prev2[0] = prev[0]; prev[0] = curr[0]; // decode channel 2 if (curr[1] != '_') { if (curr[1] == prev[1] && curr[1] == prev2[1] && curr[1] == prev3[1] && curr[1] == prev4[1]) { // eliminate // false // positives if (curr[1] != prev5[1]) { dtmfLabels[1] += curr[1]; } } } prev5[1] = prev4[1]; prev4[1] = prev3[1]; prev3[1] = prev2[1]; prev2[1] = prev[1]; prev[1] = curr[1]; } while (true); seq = dtmfLabels; } /** * Method to decode the next frame in a buffer of a mono channeled wav file * * @return the decoded DTMF character * @throws AudioFileException * @throws IOException * @throws WavFileException * @throws DTMFDecoderException */ private char decodeNextFrameMono() throws AudioFileException, DTMFDecoderException, IOException { double[] buffer = new double[frameBufferSize]; double[] tempBuffer11 = new double[frameBufferSize]; double[] tempBuffer21 = new double[frameBufferSize]; int framesRead = audio.read(buffer); if (framesRead < frameBufferSize) { audio.close(); throw new DTMFDecoderException("Out of frames"); } double[] frame; if (goertzel) { frame = DecoderUtil.concatenateAll(tempBuffer21, tempBuffer11, buffer); tempBuffer21 = tempBuffer11; tempBuffer11 = buffer; } else { // slice off the extra bit to make the framesize a power of 2 int slice = buffer.length + tempBuffer11.length + tempBuffer21.length - frameSize; double[] sliced = Arrays.copyOfRange(buffer, 0, buffer.length - slice); frame = DecoderUtil.concatenateAll(tempBuffer21, tempBuffer11, sliced); } char out; // check if the power of the signal is high enough to be accepted. if (DecoderUtil.signalPower(frame) < CUT_OFF_POWER) return '_'; if (goertzel) { // transform frame and return frequency spectrum information double[] dft_data = DTMFUtil.transformFrameG(frame, (int) audio.getSampleRate()); // check if the frame has too much noise if (isNoisyG(dft_data)) return '_'; out = DTMFUtil.getRawChar(dft_data); return out; } else { // transform frame and return frequency spectrum information double[] power_spectrum = DTMFUtil.transformFrameFFT(frame, (int) audio.getSampleRate()); // filter out the 8 DTMF frequencies from the power spectrum double[] dft_data = filterFrame(power_spectrum); // check if the frame has too much noise if (isNoisy(dft_data, power_spectrum)) return '_'; out = DTMFUtil.getRawChar(dft_data); return out; } } /** * Method to decode the next frame in a buffer of a stereo wav file * * @return the decoded DTMF character * @throws IOException * @throws WavFileException * @throws DTMFDecoderException */ private char[] decodeNextFrameStereo() throws IOException, AudioFileException, DTMFDecoderException { if (framesBuffer == null) framesBuffer = new double[2][frameBufferSize]; else { Arrays.fill(framesBuffer[0], 0.0); Arrays.fill(framesBuffer[1], 0.0); } double[] tempBuffer11 = new double[frameBufferSize]; double[] tempBuffer21 = new double[frameBufferSize]; double[] tempBuffer12 = new double[frameBufferSize]; double[] tempBuffer22 = new double[frameBufferSize]; int samplesRead; samplesRead = audio.read(framesBuffer); samplesReadSum += 1; if (samplesRead < frameBufferSize) { audio.close(); throw new DTMFDecoderException("Out of samples"); } double[] frame1, frame2; if (goertzel) { frame1 = DecoderUtil.concatenateAll(tempBuffer21, tempBuffer11, framesBuffer[0]); frame2 = DecoderUtil.concatenateAll(tempBuffer22, tempBuffer12, framesBuffer[1]); tempBuffer21 = tempBuffer11; tempBuffer11 = framesBuffer[0]; tempBuffer22 = tempBuffer12; tempBuffer12 = framesBuffer[1]; } else { int slice = framesBuffer.length + tempBuffer11.length + tempBuffer21.length - frameSize; double[] sliced1 = Arrays.copyOfRange(framesBuffer[0], 0, framesBuffer.length - slice); double[] sliced2 = Arrays.copyOfRange(framesBuffer[1], 0, framesBuffer.length - slice); frame1 = DecoderUtil.concatenateAll(tempBuffer21, tempBuffer11, sliced1); frame2 = DecoderUtil.concatenateAll(tempBuffer22, tempBuffer12, sliced2); } char[] outArr = { 'T', 'T' }; // check if the power of the signal is high enough to be accepted. if (DecoderUtil.noiseSignalCut(frame1, CUT_OFF_POWER)) { outArr[0] = '_'; } if (DecoderUtil.noiseSignalCut(frame2, CUT_OFF_POWER)) { outArr[1] = '_'; } if (outArr[0] == '_' && outArr[1] == '_') { logger.trace("cut off noise signal"); return outArr; } if (goertzel) { // transform frame double[] dft_data1 = transformFrameG(frame1, (int) audio.getSampleRate()); double[] dft_data2 = transformFrameG(frame2, (int) audio.getSampleRate()); // check if the frame has too much noise if (isNoisyG(dft_data1)) { outArr[0] = '_'; } if (isNoisyG(dft_data2)) { outArr[1] = '_'; } if (outArr[0] == '_' && outArr[1] == '_') { return outArr; } try { if (outArr[0] != '_') { outArr[0] = getRawChar(dft_data1); logger.debug("framesRead: " + samplesReadSum + " outArr[0] " + outArr[0]); } if (outArr[1] != '_') { outArr[1] = getRawChar(dft_data2); logger.debug("framesRead: " + samplesReadSum + " outArr[1] " + outArr[1]); } } catch (DTMFDecoderException e) { e.printStackTrace(); throw new DTMFDecoderException("Something went wrong."); } return outArr; } else { // transform frames double[] power_spectrum1, power_spectrum2; // transform channel 1 if (outArr[0] != '_') { power_spectrum1 = DTMFUtil.transformFrameFFT(frame1, audio.getSampleRate()); } else { power_spectrum1 = null; } // transform channel 2 if (outArr[1] != '_') { power_spectrum2 = DTMFUtil.transformFrameFFT(frame2, audio.getSampleRate()); } else { power_spectrum2 = null; } // filter frame 1 double[] dft_data1, dft_data2; if (power_spectrum1 != null) { dft_data1 = filterFrame(power_spectrum1); } else { dft_data1 = null; } // filter frame 2 if (power_spectrum2 != null) { dft_data2 = filterFrame(power_spectrum2); } else { dft_data2 = null; } // check if the frame 1 has too much noise if (isNoisy(dft_data1, power_spectrum1)) { outArr[0] = '_'; logger.trace("noisy signal detected, skip " + FftCutoffPowerNoiseRatio); } if (isNoisy(dft_data2, power_spectrum2)) { outArr[1] = '_'; logger.trace("noisy signal detected, skip"); } if (outArr[0] == '_' && outArr[1] == '_') { return outArr; } try { if (outArr[0] != '_') { outArr[0] = DTMFUtil.getRawChar(dft_data1); System.out.println("framesRead: " + samplesReadSum + " outArr[0] " + outArr[0]); } if (outArr[1] != '_') { outArr[1] = DTMFUtil.getRawChar(dft_data2); System.out.println("framesRead: " + samplesReadSum + " outArr[1] " + outArr[1]); } } catch (DTMFDecoderException e) { logger.error(e); throw new DTMFDecoderException("Something went wrong."); } return outArr; } } /** * Method to decode the wav file and return the sequence of DTMF tones * represented. * * @return True if decoding process was successful * @throws IOException * @throws WavFileException * @throws DTMFDecoderException */ public boolean decode() throws IOException, AudioFileException, DTMFDecoderException { if (!decoder) throw new DTMFDecoderException( "The object was not instantiated in decoding mode. Please use the correct constructor."); if (decoded) { return true; } int numChannels = audio.getNumChannels(); if (numChannels > 2 || numChannels <= 0) throw new DTMFDecoderException("Can only decode mono and stereo files."); logger.info("LabelPauseDurr: {}", getLabelPauseDurr()); logger.info("getMillisecondsPerFrame: {}", getMillisecondsPerFrame()); switch (labelLen) { case DECODE_40: if (numChannels == 1) decodeMono40(); else decodeStereo40(); break; case DECODE_60: if (numChannels == 1) decodeMono60(); else decodeStereo60(); break; case DECODE_80: if (numChannels == 1) decodeMono80(); else decodeStereo80(); break; case DECODE_100: if (numChannels == 1) decodeMono100(); else decodeStereo100(); break; default: decodeMono100(); decoded = true; } return true; } /** * Method to set the minimum duration of the DTMF tones to be detected * * @param duration * minimum duration of a tone. 0 or negative to use the default * ITU-T recommended value (40ms) * @throws DTMFDecoderException * Throws an exception if the duration is less than 40ms */ public void setMinToneDuration(int duration) throws DTMFDecoderException { if (duration <= 0) // use default duration of 40ms return; else if (duration < 40) throw new DTMFDecoderException( "Minimum tone duration must be greater than 40ms or, use 0 or a negative number to use the default ITU-T value."); else if (duration < 80) labelLen = LabelLen.DECODE_60; else if (duration < 100) labelLen = LabelLen.DECODE_80; else if (duration > 100) labelLen = LabelLen.DECODE_100; } /** * Method to check if the given audio file has been decoded. */ public boolean isDecoded() { return decoded; } /** * Method to get the number of channels in the audio files being decoded * * @return */ public int getChannelCount() { return audio.getNumChannels(); } /** * Method to generate the DTMF tone. * * @return True if generation was successful * @throws DTMFDecoderException */ public boolean generate() throws DTMFDecoderException { if (!generate) throw new DTMFDecoderException( "The object was not instantiated in the generation mode. Plese use the correct constructor."); ArrayList<Double> outSamples = new ArrayList<Double>(); // calculate length (number of samples) of the tones and pauses int toneLen = (int) Math.floor((outToneDurr * outFs) / 1000.0); int pauseLen = (int) Math.floor((outPauseDurr * outFs) / 1000.0); // Add a pause at beginning of the file addPause(outSamples, pauseLen); // add the tones for (int i = 0; i < outChars.length; i++) { // add tone samples addTone(outSamples, outChars[i], toneLen); // add pause samples addPause(outSamples, pauseLen); } // Add a pause at the end of the file addPause(outSamples, pauseLen); generatedSeq = new double[outSamples.size()]; for (int i = 0; i < generatedSeq.length; i++) generatedSeq[i] = outSamples.get(i); generated = true; return true; } /** * Method to generate samples representing a dtmf tone * * @param samples * array of samples to add the generated samples to. * @param c * DTMF character to generate. * @param toneLen * Number of samples to generate. * @throws DTMFDecoderException * If the given character is not a dtmf character. */ private void addTone(ArrayList<Double> samples, char c, int toneLen) throws DTMFDecoderException { double[] f = getFreqs(c); for (double s = 0; s < toneLen; s++) { double lo = Math.sin(2.0 * Math.PI * f[0] * s / outFs); double hi = Math.sin(2.0 * Math.PI * f[1] * s / outFs); samples.add((hi + lo) / 2.0); // samples.add(hi); } } /** * Method get the DTMF lower and upper frequencies. * * @param c * DTMF character * @return DTMF Frequencies to use to generate the tone. * @throws DTMFDecoderException * If the given character is not a DTMF character. */ private double[] getFreqs(char c) throws DTMFDecoderException { double[] out = new double[2]; if (c == '0') { out[0] = 941; out[1] = 1336; } else if (c == '1') { out[0] = 697; out[1] = 1209; } else if (c == '2') { out[0] = 697; out[1] = 1336; } else if (c == '3') { out[0] = 697; out[1] = 1477; } else if (c == '4') { out[0] = 770; out[1] = 1209; } else if (c == '5') { out[0] = 770; out[1] = 1336; } else if (c == '6') { out[0] = 770; out[1] = 1477; } else if (c == '7') { out[0] = 852; out[1] = 1209; } else if (c == '8') { out[0] = 852; out[1] = 1336; } else if (c == '9') { out[0] = 852; out[1] = 1477; } else if (c == 'A' || c == 'a') { out[0] = 697; out[1] = 1633; } else if (c == 'B' || c == 'b') { out[0] = 770; out[1] = 1633; } else if (c == 'C' || c == 'c') { out[0] = 852; out[1] = 1633; } else if (c == 'D' || c == 'd') { out[0] = 941; out[1] = 1633; } else throw new DTMFDecoderException("\"" + c + "\" is not a DTMF Character."); return out; } /** * Method to add samples that represent a pause to the output * * @param samples * Array of samples to add to. * @param pauseLen * Number of samples to add. */ private void addPause(ArrayList<Double> samples, int pauseLen) { for (int s = 0; s < pauseLen; s++) samples.add(0.0); } /** * Write the generated sequenec to a wav file. * * @throws WavFileException * @throws IOException */ public void export() throws IOException, WavFileException { FileUtil.writeWavFile(outFile, generatedSeq, outFs); } /** * Get the samples array of the DTMF sequence of tones. * * @return array with the samples of the dtmf sequence that has been * generated. * @throws DTMFDecoderException * If the samples have no been generated yet. */ public double[] getGeneratedSequence() throws DTMFDecoderException { if (generated) return generatedSeq; else throw new DTMFDecoderException("Samples have not been generated yet. Please run generate() first."); } public void setFftCutoffPowerNoiseRatio(double val) { FftCutoffPowerNoiseRatio = val; } private void labelReact(String label) { this.onLabelAction.accept(label); } private int getMillisecondsPerFrame() { return this.frameBufferSize * 1000 / audio.getSampleRate(); } public int getLabelPauseDurr() { return labelPauseDurr; } public void setLabelPauseDurr(int labelPauseDurr) { this.labelPauseDurr = labelPauseDurr; } public int getSymbolLength() { return symbolLength; } public void setSymbolLength(int symbolLength) { this.symbolLength = symbolLength; } public Consumer<String> getOnLabelAction() { return onLabelAction; } public void setOnLabelAction(Consumer<String> onLabelAction) { this.onLabelAction = onLabelAction; } private boolean onLabelReact(String[] seq2) { if (!seq2[1].isEmpty() || !seq2[0].isEmpty()) { ++framesCount; int len = Math.max(seq2[0].length(), seq2[1].length()); //logger.debug("onLabelReact " + len + " " + framesCount + " " + framesCount * getMillisecondsPerFrame()); int stuff_len = 30; // additional latency label_len(100) + pause_len(50) == 170 sometimes if ((framesCount * getMillisecondsPerFrame()) > (getLabelPauseDurr() + getSymbolLength() + stuff_len) * len) { String label; if (seq2[0].isEmpty()) { label = seq2[1]; } else { label = seq2[0]; } framesCount = 0; labelReact(label); return true; } } return false; } private boolean onLabelReact(String seq2) { if (!seq2.isEmpty()) { ++framesCount; int stuff_len = 30; // additional latency label_len(100) + pause_len(50) == 170 sometimes if (framesCount * getMillisecondsPerFrame() > (getLabelPauseDurr() + getSymbolLength() + stuff_len) * seq2.length()) { framesCount = 0; labelReact(seq2); return true; } } return false; } }