Realtime dual-tone multi-frequency signaling (DTMF) decoder.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using NAudio.Wave; | |
using Frequency = GM.SP.Audio.SignalAnalysis.Frequency; | |
namespace GM.SP.Audio | |
{ | |
/// <summary> | |
/// https://en.wikipedia.org/wiki/Dual-tone_multi-frequency_signaling | |
/// </summary> | |
public class DTMFDecoder : IDisposable | |
{ | |
/// <summary> | |
/// Occurs when a new key is decoded. | |
/// </summary> | |
public event EventHandler<char> NewKey; | |
private WaveIn waveIn; | |
private List<float> buffer; | |
private char lastKey; | |
public void Dispose() | |
{ | |
StopListening(); | |
} | |
public void StartListening() | |
{ | |
StopListening(); | |
buffer = new List<float>(); | |
lastKey = ' '; | |
waveIn = new WaveIn(); | |
// 8000 sample rate is enough, because we will be analyzing 1024 samples at a time, which means that frequency resolution is 8000/1024 = 7.8125, which is enough for DTMF | |
waveIn.WaveFormat = new WaveFormat(8000, 16, 1); | |
waveIn.BufferMilliseconds = 100; | |
waveIn.DataAvailable += WaveIn_DataAvailable; | |
waveIn.StartRecording(); | |
} | |
public void StopListening() | |
{ | |
waveIn?.StopRecording(); | |
waveIn?.Dispose(); | |
} | |
private void WaveIn_DataAvailable(object sender, WaveInEventArgs e) | |
{ | |
byte[] bytes = e.Buffer; | |
int newSampleCount = e.BytesRecorded / 2; | |
// convert bytes to floats | |
for(int i = 0, j = 0; i < newSampleCount; ++i, j += 2) { | |
short valueShort = (short)((bytes[j + 1] << 8) | bytes[j]); | |
float sample = valueShort / ((valueShort > 0) ? 32767f : 32768f); | |
buffer.Add(sample); | |
} | |
// are there enough samples for analysis? | |
while(buffer.Count >= 1024) { | |
float[] samplesToAnalyze = buffer.Take(1024).ToArray(); | |
// move our window by 1024/8, for 8x larger time resolution | |
buffer.RemoveRange(0, 128); | |
Analyze(samplesToAnalyze); | |
} | |
} | |
private void Analyze(float[] samples) | |
{ | |
char key = ' '; | |
try { | |
// first, we find the biggest frequency below and above 1050 Hz | |
Frequency biggestBelow1050 = null; | |
Frequency biggestAbove1050=null; | |
{ | |
Frequency[] frequencies = SignalAnalysis.AnalyzeAudio(samples, 8000); | |
// only include frequencies between 50 and 2000 | |
frequencies = frequencies.Where(f => f.Hz > 50 && f.Hz<2000).ToArray(); | |
for(int i = frequencies.Length - 1; i > 1; --i) { | |
Frequency trenutna = frequencies[i]; | |
if(trenutna.Hz <= 1050) { | |
if(biggestBelow1050==null || trenutna.Amplitude > biggestBelow1050.Amplitude) | |
biggestBelow1050 = trenutna; | |
}else { | |
if(biggestAbove1050==null || trenutna.Amplitude > biggestAbove1050.Amplitude) | |
biggestAbove1050 = trenutna; | |
} | |
} | |
// average amplitude of both frequencies | |
float avgOfPeak = (biggestBelow1050.Amplitude + biggestAbove1050.Amplitude) * 0.5f; | |
if(avgOfPeak < 0.001) | |
// too silent | |
return; | |
// let's se if these 2 frequencies are at least 10x louder than the average of all others | |
float avgOfOthers = 0; | |
for(int i = frequencies.Length - 1; i >= 0; --i) { | |
Frequency f = frequencies[i]; | |
if(f.Hz == biggestBelow1050.Hz || f.Hz == biggestAbove1050.Hz) | |
continue; | |
avgOfOthers += f.Amplitude; | |
} | |
avgOfOthers /= (frequencies.Length - 2); | |
if(avgOfPeak < avgOfOthers * 10) | |
// they are not | |
return; | |
} | |
// DTMF analysis (https://en.wikipedia.org/wiki/Dual-tone_multi-frequency_signaling#Keypad) | |
int row; | |
int column; | |
{ | |
Frequency f1 = biggestBelow1050; | |
Frequency f2 = biggestAbove1050; | |
int threshold = 16; | |
// row | |
row = 0; | |
if(f1.Hz > 697 - threshold && f1.Hz < 697 + threshold) | |
row = 1; | |
else if(f1.Hz > 770 - threshold && f1.Hz < 770 + threshold) | |
row = 2; | |
else if(f1.Hz > 852 - threshold && f1.Hz < 852 + threshold) | |
row = 3; | |
else if(f1.Hz > 941 - threshold && f1.Hz < 941 + threshold) | |
row = 4; | |
if(row == 0) | |
// not close enough to any of the row DTMF frequencies | |
return; | |
// column | |
column = 0; | |
if(f2.Hz > 1209 - threshold && f2.Hz < 1209 + threshold) | |
column = 1; | |
else if(f2.Hz > 1336 - threshold && f2.Hz < 1336 + threshold) | |
column = 2; | |
else if(f2.Hz > 1477 - threshold && f2.Hz < 1477 + threshold) | |
column = 3; | |
else if(f2.Hz > 1633 - threshold && f2.Hz < 1633 + threshold) | |
column = 4; | |
if(column == 0) | |
// not close enough to any of the column DTMF frequencies | |
return; | |
} | |
key = Decode(row, column); | |
if(key == lastKey) | |
// do not invoke the same key multiple consecutive times | |
return; | |
NewKey?.Invoke(this, key); | |
} finally { | |
lastKey = key; | |
} | |
} | |
/// <summary> | |
/// Returns the key for the specified row and column. | |
/// </summary> | |
private char Decode(int row,int column) | |
{ | |
switch(row) { | |
case 1: | |
switch(column) { | |
case 1: | |
return '1'; | |
case 2: | |
return '2'; | |
case 3: | |
return '3'; | |
case 4: | |
return 'A'; | |
} | |
break; | |
case 2: | |
switch(column) { | |
case 1: | |
return '4'; | |
case 2: | |
return '5'; | |
case 3: | |
return '6'; | |
case 4: | |
return 'B'; | |
} | |
break; | |
case 3: | |
switch(column) { | |
case 1: | |
return '7'; | |
case 2: | |
return '8'; | |
case 3: | |
return '9'; | |
case 4: | |
return 'C'; | |
} | |
break; | |
case 4: | |
switch(column) { | |
case 1: | |
return '*'; | |
case 2: | |
return '0'; | |
case 3: | |
return '#'; | |
case 4: | |
return 'D'; | |
} | |
break; | |
} | |
throw new NotImplementedException("Wrong row and/or column."); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment