#!/usr/bin/perl -w # # DTMF decoder # # Given a sound file that contains a sequence of DTMF tones, one at # a time, separated by a lower-energy (quieter) period, prints out # the DTMF code sequence (e.g. numbers). It often doesn't quite get # the segmentation right, but it's pretty close. Note the tweakable # parameters near the top. # # Seth Golub http://www.aigeek.com/ # 30 Jul 2004 # # This program is hereby released into the public domain. use strict; use English; # You may need to tweak these parameters my $segment_step = 0.1; # in seconds my $segment_length = 0.4; my $file = $ARGV[0] or die "Usage: $0 filename\n"; die "$file not readable.\n" unless -r $file; my %map = ( "697 1209" => "1", "697 1336" => "2", "697 1477" => "3", "697 1633" => "A", "770 1209" => "4", "770 1336" => "5", "770 1477" => "6", "770 1633" => "B", "852 1209" => "7", "852 1336" => "8", "852 1477" => "9", "852 1633" => "C", "941 1209" => "*", "941 1336" => "0", "941 1477" => "#", "941 1633" => "D", "350 440" => "DIALTONE", "440 480" => "RING", "480 620" => "BUSY", ); my @centers = (350, 440, 480, 620, 697, 770, 852, 941, 1209, 1336, 1477, 1633); # Algorithm: # a short moving window across the audio # in each segment, measure the energy in each of several frequency bands # In a sample that contains a significant DTMF signal, two bands # should have significantly higher energy than all the others. # "hh:mm:ss.frac" sub time_format( $ ) { my ( $val ) = @ARG; my $frac = int(($val - int($val)) * 1000); my $seconds = int($val) % 60; my $minutes = int($val / 60) % 60; my $hours = int($val / 3600); return sprintf("%d:%02d:%02d.%03d", $hours, $minutes, $seconds, $frac); } sub get_bands( $$ ) { my ( $file, $segment_number ) = @ARG; my $start = time_format($segment_number * $segment_step); my @energies = (); foreach my $freq ( @centers ) { my $width = int(0.02 * $freq); # somewhat arbitrary my @output = `sox $file -e trim $start $segment_length band $freq $width stat 2>&1`; die "@output" if $CHILD_ERROR; @output = grep {/Max.* ampl/} @output; my ( $energy ) = $output[0] =~ /Maximum amplitude:.*?([0-9.]+)/; push(@energies, [$freq, $energy]); # print "$freq $energy\n"; # if anyone wants the raw data } @energies = sort { $b->[1] <=> $a->[1] } @energies; # highest energy first return wantarray ? @energies : \@energies; } sub get_code( $$ ) { my ( $file, $segment_number ) = @ARG; my @energies = get_bands($file, $segment_number); # If it's a real signal, the top two energies will be much higher # than the third highest. # TODO: test for this and return "" if there's no signal. my @top2 = map { $ARG->[0] } @energies[0..1]; # discard the energy values @top2 = sort { $a <=> $b } @top2; # sort them into a predictable order if ( defined($map{"@top2"}) ) { return $map{"@top2"}; } return ""; } sub num_segments( $ ) { my ( $file ) = @ARG; my @output = grep {/Length.*seconds/} `sox $file -e stat 2>&1`; die "Failed to get length of $file" unless scalar(@output) == 1; my ( $seconds ) = $output[0] =~ /:\s+(\d+\.?\d*)/; return int($seconds / $segment_step); } my $prev = ""; for my $segment_number ( 0 .. num_segments($file)-1 ) { my $code = get_code($file, $segment_number); if ( $code ne "" and $prev ne $code ) { print "$code\n"; } $prev = $code; }