File Coverage

blib/lib/Net/SIP/DTMF.pm
Criterion Covered Total %
statement 191 201 95.0
branch 72 94 76.6
condition 23 39 58.9
subroutine 20 20 100.0
pod 2 2 100.0
total 308 356 86.5


line stmt bran cond sub pod time code
1             ###########################################################################
2             # Net::SIP::DTMF
3             # implements DTMF handling (audio and rfc2833)
4             ###########################################################################
5              
6 43     43   250 use strict;
  43         83  
  43         1032  
7 43     43   178 use warnings;
  43         71  
  43         1324  
8             package Net::SIP::DTMF;
9 43     43   204 use base 'Exporter';
  43         93  
  43         5413  
10             our @EXPORT = qw(dtmf_generator dtmf_extractor);
11              
12 43     43   288 use Net::SIP::Debug;
  43         83  
  43         233  
13 43     43   241 use Time::HiRes 'gettimeofday';
  43         111  
  43         382  
14 43     43   3694 use Carp 'croak';
  43         97  
  43         41537  
15              
16             ###########################################################################
17             # sub dtmf_generator returns a sub, which is used to generate RTP packet
18             # for DTMF events
19             # Args: ($event,$duration,%args)
20             # $event: DTMF event ([0-9A-D*#]), anything else will be pause
21             # $duration: duration in ms
22             # %args:
23             # rfc2833_type => $rtptype: if defined will generate RFC2833 RTP events
24             # audio_type => $rtptype: if defined will generate audio
25             # volume => volume for rfc2833 events (default 10)
26             # Returns: $sub
27             # $sub: sub which returns @rtp_packets when called with
28             # $sub->($seq,$timestamp,$srcid)
29             # if $sub returns () the DTMF event is finished (>duration)
30             # if $sub returns ('') no data are produced (pause between events)
31             # usually sub will return just one packet, but for RTP event ends it
32             # will return 3 to make sure that at least one gets received
33             #
34             ###########################################################################
35             sub dtmf_generator {
36 84     84 1 580 my ($event,$duration,%pargs) = @_;
37              
38             # empty or invalid stuff will cause pause/silence
39 84 100 66     717 $event = '' if ! defined $event or $event !~ m{[\dA-D\*\#]}i;
40              
41 84 100       456 if ( defined( my $type = $pargs{rfc2833_type} )) {
    50          
42             # create RFC2833 payload
43 42         221 return _dtmf_gen_rtpevent($event,$type,$duration,%pargs);
44             } elsif ( defined($type = $pargs{audio_type})) {
45             # create audio payload
46 42         206 return _dtmf_gen_audio($event,$type,$duration,%pargs);
47             } else {
48 0         0 croak "neither rfc2833 nor audio RTP type defined"
49             }
50             }
51              
52             ###########################################################################
53             # sub dtmf_extractor creates sub to extract DTMF from RTP
54             # Args: (%pargs)
55             # %pargs: rfc2833_type, audio_type like in dtmf_generator
56             # will try to extract DTMF from RTP packets for any type set, e.g.
57             # RFC2833 and audio can be done in parallel
58             # Returns: $sub
59             # $sub: should be called with ($packet,[$time]), if $time not
60             # given current time will be used. The $sub itself will return () if no
61             # event (end) was found and ($event,$duration,$type) if event was detected.
62             # $event is [0-9A-D*#], $type rfc2833|audio
63             # Comment: FIXME - maybe disable audio detection if a rfc2833 event was
64             # received. In this case the peer obviously uses rfc2833
65             ###########################################################################
66             sub dtmf_extractor {
67 9     9 1 50 my %pargs = @_;
68 9         17 my %sub;
69 9 50       42 if ( defined( my $type = delete $pargs{rfc2833_type} )) {
70             # extract from RFC2833 payload
71 9         71 $sub{$type} = _dtmf_xtc_rtpevent(%pargs);
72             }
73 9 50       53 if ( defined( my $type = delete $pargs{audio_type})) {
74             # extract from audio payload
75 9         176 $sub{$type} = _dtmf_xtc_audio(%pargs);
76             }
77 9 50       57 croak "neither rfc2833 nor audio RTP type defined" if ! %sub;
78              
79 9         30 my $lastseq;
80             return sub {
81 4207     4207   11135 my ($pkt,$time) = @_;
82 4207         29447 my ($ver,$type,$seq,$tstamp,$srcid,$payload) = unpack('CCnNNa*',$pkt);
83 4207 50       13020 $ver == 0b10000000 or return;
84 4207         7920 my $marker;
85 4207 100       12460 if ($type & 0b10000000) {
86 36         72 $marker = 1;
87 36         90 $type &= 0b01111111;
88             }
89              
90 4207         7312 my $seqdiff;
91 4207 100       12236 if (defined $lastseq) {
92 4198         12047 $seqdiff = (2**16 + $seq - $lastseq) & 0xffff;
93 4198 50       15843 if (!$seqdiff) {
    50          
94 0 0       0 $DEBUG && DEBUG(20,"dropping duplicate RTP");
95 0         0 return;
96             } elsif ($seqdiff>2**15) {
97 0 0       0 $DEBUG && DEBUG(20,"dropping out of order RTP");
98 0         0 return;
99             } else {
100 4198 50 33     14572 $DEBUG && $seqdiff>1 && DEBUG(30,'lost %d packets (%d-%d)',
101             $seqdiff-1,$lastseq+1,$seq-1);
102             }
103             }
104 4207         6502 $lastseq = $seq;
105              
106 4207 50       15614 my $sub = $sub{$type} or return;
107 4207 100       12556 my ($event,$duration,$media) = $sub->($payload,$time,$marker,$seqdiff)
108             or return;
109 72         706 return ($event, int(1000*$duration),$media);
110 9         273 };
111             }
112              
113              
114             ###########################################################################
115             # END OF PUBLIC INTERFACE
116             ###########################################################################
117              
118             ###########################################################################
119             #
120             # RTP DTMF events
121             #
122             ###########################################################################
123             # mapping between event string and integer for RTP events
124             my %event2i;
125             { my $i=0; %event2i = map { $_ => $i++ } split('','0123456789*#ABCD'); }
126             my %i2event = reverse %event2i;
127              
128              
129             ###########################################################################
130             # generate DTMF RTP events according to rfc2833
131             # Args: $event,$duration,%args
132             # %args: volume => v will be used to set volume of RTP event, default 10
133             # Returns: $sub for $event
134             # Comment: the sub should then be called with $sub->($seq,$timstamp,$srcid)
135             # This will generate the RTP packet.
136             # If $event is no DTMF event it will return '' to indicate pause
137             ###########################################################################
138             sub _dtmf_gen_rtpevent {
139 42     42   249 my ($event,$type,$duration,%args) = @_;
140 42   50     300 my $volume = $args{volume} || 10;
141              
142 42         112 $duration/=1000; # ms ->s
143 42         152 my $start = gettimeofday();
144 42         84 my $end = 0;
145 42         80 my $first = 1;
146 42         79 my $initial_timestamp;
147              
148             return sub {
149 777     777   2216 my ($seq,$timestamp,$srcid) = @_;
150              
151             # all packets get timestamp from start of event
152 777 100       2236 if ( ! $initial_timestamp ) {
153 42         92 $initial_timestamp = $timestamp;
154 42         192 return ''; # need another call to get duration
155             }
156              
157 735 100       4277 if ( gettimeofday() - $start > $duration ) {
158 84 100       300 return if $end; # end already sent
159 42         87 $end = 1;
160             }
161              
162 693 100       2871 return '' if $event eq '';
163              
164 639         1393 my $pt = $type;
165 639 100       1699 if ( $first ) {
166 24         44 $first = 0;
167 24         69 $pt |= 0b10000000; # marker bit set on first packet of event
168             }
169             return pack('CCnNNCCn',
170             0b10000000,
171             $pt,
172             $seq,
173             $initial_timestamp,
174             $srcid,
175 639 50       8221 $event2i{$event},
176             ($end<<7) | $volume,
177             $timestamp > $initial_timestamp
178             ? $timestamp - $initial_timestamp
179             : 0x10000 - $initial_timestamp + $timestamp,
180             );
181             }
182 42         539 }
183              
184             ###########################################################################
185             # returns sub to extract DTMF events from RTP telephone-event/8000 payload
186             # Args: NONE
187             # Returns: $sub
188             # $sub - will be called with ($rtp_payload,[$time],$marker)
189             # will return ($event,$duration) if DTMF event was found
190             ###########################################################################
191             sub _dtmf_xtc_rtpevent {
192 9     9   20 my $current_event;
193             return sub {
194 956     956   2548 my ($payload,$time,$marker) = @_;
195 956         4029 my ($event,$volume,$duration) = unpack('CCn',$payload);
196 956         2850 $event = $i2event{$event};
197 956         1427 my $end;
198 956 100       2844 if ( $volume & 0b10000000 ) {
199 36         80 $end = 1;
200 36         81 $volume &= 0b01111111
201             }
202 956 100       3839 if ( ! $current_event ) {
    50          
203 36 50       78 return if $end; # probably repeated send of end
204             # we don't look at the marker for initial packet, because maybe
205             # the initial packet got lost
206 36   33     351 $current_event = [ $event,$time||gettimeofday(),$volume ];
207             } elsif ( $event eq $current_event->[0] ) {
208 920 100       2588 if ( $end ) {
209             # explicit end of event
210 36         91 my $ce = $current_event;
211 36         91 $current_event = undef;
212 36   33     335 $time ||= gettimeofday();
213 36         371 return ($ce->[0],$time - $ce->[1],'rfc2833');
214             }
215             } else {
216             # implicit end because we got another event
217 0         0 my $ce = $current_event;
218 0   0     0 $time||= gettimeofday();
219 0         0 $current_event = [ $event,$time,$volume ];
220 0 0       0 return if ! $ce->[2]; # volume == 0
221 0         0 return ($ce->[0],$time - $ce->[1],'rfc2833');
222             }
223 920         5963 return;
224 9         248 };
225             }
226              
227             ###########################################################################
228             #
229             # RTP DTMF audio
230             #
231             ###########################################################################
232              
233             # mapping between frequence and key for audio
234             my @freq1 = (697,770,852,941);
235             my @freq2 = (1209,1336,1477,1633);
236             my @keys = '123A 456B 789C *0#D' =~m{(\S)}g;
237              
238             my (%event2f,@f2event);
239             for( my $i=0;$i<@keys;$i++ ) {
240             my $freq1 = $freq1[ $i/4 ];
241             my $freq2 = $freq2[ $i%4 ];
242             $event2f{$keys[$i]} = [$freq1,$freq2];
243             $f2event[$freq1][$freq2] = $keys[$i];
244             }
245              
246             # basic paramter, PCMU/8000 160 samples per RTP packet
247             my $volume = 100;
248             my $samples4s = 8000;
249             my $samples4pkt = 160;
250              
251 43     43   331 use constant PI => 3.14159265358979323846;
  43         87  
  43         48952  
252              
253             # tables for audio processing get computed on first use
254             # cosinus is precomputed. How exakt a cos will be depends on
255             # the size of the table $tabsize
256             my $tabsize = 256;
257             my @costab;
258              
259             # tables for PCMU u-law compression
260             my @ulaw_expandtab;
261             my @ulaw_compresstab;
262              
263             # Goertzel algorithm
264             my $gzpkts = 3; # 3 RTP packets = 60ms
265             my %coeff;
266             my @blackman; # exact blackman
267              
268             # precompute stuff into tables for faster operation
269             sub _init_audio_processing {
270              
271             # audio generation
272 4 50   4   16 @costab and return;
273 4         15 for(my $i=0;$i<$tabsize;$i++) {
274 1024         3055 $costab[$i] = $volume/100*16383*cos(2*PI*$i/$tabsize);
275             }
276              
277             # PCMU/8000 u-law (de)compression
278 4         19 for( my $i=0;$i<128;$i++) {
279 512         2281 $ulaw_expandtab[$i] = int( (256**($i/127) - 1) / 255 * 32767 );
280             }
281 4         12 my $j = 0;
282 4         16 for( my $i=0;$i<32768;$i++ ) {
283 131072         145548 $ulaw_compresstab[$i] = $j;
284 131072 100 100     319597 $j++ if $j<127 and $ulaw_expandtab[$j+1] - $i < $i - $ulaw_expandtab[$j];
285             }
286              
287 4         46 for my $freq (@freq1,@freq2) {
288 32         68 my $k = int(0.5+$samples4pkt*$freq/$samples4s);
289 32         45 my $w = 2*PI/$samples4pkt*$k;
290 32         118 $coeff{$freq} = 2*cos($w);
291             }
292              
293 4         12 my $n = $samples4pkt*$gzpkts;
294 4         28 for( my $i=0;$i<$n;$i++) {
295 1920         3532 $blackman[$i] = 0.426591 - 0.496561*cos(2*PI*$i/$n) +0.076848*cos(4*PI*$i/$n)
296             }
297             }
298              
299              
300             ###########################################################################
301             # sub _dtmf_gen_audio returns a sub to generate audio/silence for DTMF in
302             # any duration
303             # Args: $event,$duration
304             # Returns: $sub for $event
305             # Comment: the sub should then be called with $sub->($seq,$timstamp,$srcid)
306             # This will generate the RTP packet.
307             # If $event is no DTMF event it will return a sub which gives silence.
308             # Data returned from the subs are PCMU/8000, 160 samples per packet
309             ###########################################################################
310             sub _dtmf_gen_audio {
311 42     42   164 my ($event,$type,$duration) = @_;
312              
313 42         172 $duration/=1000; # ms ->s
314 42         155 my $start = gettimeofday();
315              
316 42         138 my $f = $event2f{$event};
317 42 100       129 if ( ! $f ) {
318             # generate silence
319             return sub {
320 73     73   192 my ($seq,$timestamp,$srcid) = @_;
321 73 100       388 return if gettimeofday() - $start > $duration; # done
322 55         798 return pack('CCnNNa*',
323             0b10000000,
324             $type,
325             $seq,
326             $timestamp,
327             $srcid,
328             pack('C',128) x $samples4pkt,
329             );
330             }
331 18         266 }
332              
333 24 50       104 _init_audio_processing() if !@costab;
334              
335 24         77 my ($f1,$f2) = @$f;
336 24         43 $f1*= $tabsize;
337 24         40 $f2*= $tabsize;
338 24         74 my $d1 = int($f1/$samples4s);
339 24         62 my $d2 = int($f2/$samples4s);
340 24         58 my $g1 = $f1 % $samples4s;
341 24         79 my $g2 = $f2 % $samples4s;
342 24         70 my $e1 = int($samples4s/2);
343 24         52 my $e2 = int($samples4s/2);
344 24         40 my $i1 = my $i2 = 0;
345              
346             return sub {
347 635     635   1926 my ($seq,$timestamp,$srcid) = @_;
348 635 100       2939 return if gettimeofday() - $start > $duration; # done
349              
350 611         1481 my $samples = $samples4pkt;
351 611         1546 my $buf = '';
352 611         2098 while ( $samples-- > 0 ) {
353 97760         139298 my $val = $costab[$i1]+$costab[$i2];
354 97760 100       192399 my $c = $val>=0 ? 255-$ulaw_compresstab[$val] : 127-$ulaw_compresstab[-$val];
355 97760         168007 $buf .= pack('C',$c);
356              
357 97760 100       151550 $e1+= $samples4s, $i1++ if $e1<0;
358 97760         121826 $i1 = ($i1+$d1) % $tabsize;
359 97760         115721 $e1-= $g1;
360              
361 97760 100       154526 $e2+= $samples4s, $i2++ if $e2<0;
362 97760         121618 $i2 = ($i2+$d2) % $tabsize;
363 97760         156146 $e2-= $g2;
364             }
365 611         5042 return pack('CCnNNa*',
366             0b10000000,
367             $type,
368             $seq,
369             $timestamp,
370             $srcid,
371             $buf,
372             );
373             }
374 24         420 }
375              
376              
377              
378             ###########################################################################
379             # returns sub to extract DTMF events from RTP PCMU/8000 payload
380             # Args: NONE
381             # Returns: $sub
382             # $sub - will be called with ($rtp_payload,[$time])
383             # will return ($event,$duration) if DTMF event was found, event being 0..15
384             ###########################################################################
385             sub _dtmf_xtc_audio {
386 9 100   9   76 _init_audio_processing() if !@costab;
387 9         41 my (%d1,%d2,@time,@lastev);
388             return sub {
389 3251     3251   7299 my ($payload,$time) = @_;
390 3251   33     21305 $time ||= gettimeofday();
391             my @samples = map {
392 3251 100       37914 ( $_<128 ? -$ulaw_expandtab[127-$_] : $ulaw_expandtab[255-$_] )/32768
  520160         894636  
393             } unpack('C*',$payload);
394 3251 50       21467 @samples == $samples4pkt or return; # unexpected sample size
395              
396 3251         9231 unshift @time, $time;
397              
398 3251         8196 for my $f (@freq1,@freq2) {
399 26008         50968 my $coeff = $coeff{$f};
400              
401 26008   100     57215 my $da1 = $d1{$f} ||= [];
402 26008   100     51743 my $da2 = $d2{$f} ||= [];
403 26008         43170 unshift @$da1,0;
404 26008         35353 unshift @$da2,0;
405              
406 26008         60137 for(my $gzi=0;$gzi<@$da1;$gzi++) {
407 77808         106697 my $d1 = $da1->[$gzi];
408 77808         97719 my $d2 = $da2->[$gzi];
409 77808         102072 my $o = $gzi*$samples4pkt;
410 77808         130626 for( my $i=0;$i<@samples;$i++) {
411 12449280         24791705 ($d2,$d1) = ($d1, $samples[$i]*$blackman[$i+$o] + $coeff*$d1 - $d2);
412             }
413 77808         109008 $da1->[$gzi] = $d1;
414 77808         167950 $da2->[$gzi] = $d2;
415             }
416             }
417              
418 3251 100       10639 return if @time < $gzpkts;
419              
420 3233         6886 $time = pop @time;
421 3233         5740 my @r;
422 3233         8563 for my $f (@freq1,@freq2) {
423 25864         31070 my $d1 = pop(@{$d1{$f}});
  25864         43483  
424 25864         31249 my $d2 = pop(@{$d2{$f}});
  25864         37511  
425 25864         78419 push @r, [ $f, $d1*$d1+$d2*$d2-$d1*$d2*$coeff{$f} ];
426             }
427              
428              
429             # the highest two freq should be significantly higher then rest
430 3233         19051 @r = sort { $b->[1] <=> $a->[1] } @r; # sort by magnitude, largest first
  44240         67947  
431 3233         6953 my $event;
432 3233 100 66     24180 if ( @r and ! $r[2][1] || $r[1][1]/$r[2][1]> 5 ) {
      66        
433 927         3322 $event = $f2event[ $r[0][0] ][ $r[1][0] ];
434 927 100       3335 $event = $f2event[ $r[1][0] ][ $r[0][0] ] if ! defined $event;
435             }
436              
437 3233 100       12535 $event = '' if ! defined $event;
438 3233         11322 push @lastev,[$event,$time];
439             # remove pause from start of lastev
440 3233   100     22970 shift(@lastev) while (@lastev && $lastev[0][0] eq '');
441              
442             # if last event same as first wait for more
443 3233 100       19464 if ( ! @lastev ) {
    100          
444             # return; # no events detected
445             } elsif ( $event eq $lastev[0][0] ) {
446 927         13049 return; # event not finished
447             } else {
448 36         125 my @ev = shift(@lastev);
449 36   66     300 while (@lastev and $lastev[0][0] eq $ev[0][0]) {
450 891         2625 push @ev,shift(@lastev);
451             }
452             # get the event at least 2 times
453 36 50       118 return if @ev == 1;
454 36         586 return ($ev[0][0],$ev[-1][1]-$ev[0][1],'audio'); # event,duration
455             }
456              
457 2270         29757 return;
458 9         340 };
459             }
460              
461             1;