File Coverage

blib/lib/Audio/Analyzer/ToneDetect.pm
Criterion Covered Total %
statement 92 94 97.8
branch 35 38 92.1
condition 18 33 54.5
subroutine 15 15 100.0
pod 5 5 100.0
total 165 185 89.1


line stmt bran cond sub pod time code
1             package Audio::Analyzer::ToneDetect;
2              
3 1     1   20237 use strict;
  1         2  
  1         38  
4 1     1   5 use warnings;
  1         2  
  1         25  
5 1     1   19 use 5.010;
  1         7  
  1         50  
6              
7             our $VERSION = '0.04';
8              
9 1     1   788 use Audio::Analyzer;
  1         9766  
  1         26  
10 1     1   8 use Carp;
  1         2  
  1         58  
11 1     1   1068 use Sort::Key::Top 'rnkeytop';
  1         1768  
  1         1318  
12              
13             sub new {
14 3     3 1 3499 my ( $class, %args ) = @_;
15              
16 3         12 my $self = bless {}, $class;
17              
18 3   50     23 $self->{source} = delete $args{source} || \*STDIN;
19 3   50     19 $self->{sample_rate} = delete $args{sample_rate} || 16000;
20 3   50     17 $self->{chunk_size} = delete $args{chunk_size} || 1024;
21 3   50     14 $self->{chunk_max} = delete $args{chunk_max} || 70;
22 3   50     17 $self->{min_tone_length} = delete $args{min_tone_length} || 0.5;
23 3         7 $self->{valid_tones} = delete $args{valid_tones};
24 3         9 $self->{valid_error_cb} = delete $args{valid_error_cb};
25 3   50     20 $self->{rejected_freqs} = delete $args{rejected_freqs} || [];
26              
27 3 100 66     18 if ( $self->{valid_tones} && $self->{valid_tones} eq 'builtin' ) {
28 2         8 $self->{valid_tones} = _get_builtin_tones();
29             }
30              
31             croak(
32 3 50       13 "Invalid chunk_size ($self->{chunk_size}). chunk_size must be power of 2"
33             ) unless _is_pow_of_two( $self->{chunk_size} );
34              
35 3         16 $self->{chunks_required}
36             = int(
37             $self->{min_tone_length} * $self->{sample_rate} / $self->{chunk_size} );
38              
39 3         21 $self->{analyzer} = Audio::Analyzer->new(
40             file => $self->{source},
41             sample_rate => $self->{sample_rate},
42             dft_size => $self->{chunk_size},
43             channels => 1,
44             %args
45             );
46              
47 3         12935 $self->{freqs} = $self->{analyzer}->freqs;
48              
49 3         35 return $self;
50             }
51              
52             sub valid_tones {
53 3     3 1 915 my ( $self, $new_tone_map ) = @_;
54 3 100       16 $self->{valid_tones} = [ sort @$new_tone_map ] if $new_tone_map;
55 3         7 return $self->{valid_tones};
56             }
57              
58             sub get_next_tone {
59 10     10 1 1469 my $self = shift;
60 10         19 my $min_tone_length = shift;
61              
62 10         13 state $last_detected = 0;
63 10         15 state @buff;
64              
65 10         12 my $chunks_required;
66 10 50       29 if ($min_tone_length) {
67 0         0 $chunks_required = int(
68             $min_tone_length * $self->{sample_rate} / $self->{chunk_size} );
69             }
70 10   33     57 $chunks_required ||= $self->{chunks_required};
71              
72 10         17 my $chunk_count = 0;
73 10         31 while ( $chunk_count < $self->{chunk_max} ) {
74 282         375 $chunk_count++;
75 282         1264 my $chunk = $self->{analyzer}->next;
76 282         1071217 my $fft = $chunk->fft;
77 282     144384   655792 my $top = rnkeytop { $fft->[0][$_] } 1 => 0 .. $#{ $fft->[0] };
  144384         230139  
  282         6849  
78 282         4643 my $detected_freq = $self->{freqs}[$top];
79 282 100       6784 next if $detected_freq == $last_detected;
80 100 50       137 next if grep { $_ == $detected_freq } @{ $self->{rejected_freqs} };
  0         0  
  100         328  
81              
82 100         193 push @buff, $detected_freq;
83 100 100       285 shift @buff if @buff > $chunks_required;
84 100 100 100     661 next unless @buff == $chunks_required && _all_match( \@buff );
85 10         20 $last_detected = $detected_freq;
86              
87 10 100       189 return $detected_freq unless $self->{valid_tones};
88              
89 6         29 my ( $valid_tone, $delta ) = $self->find_closest_valid($detected_freq);
90 6 100       51 next unless $valid_tone;
91 5 100       323 return wantarray ? ( $valid_tone, $delta ) : $valid_tone;
92             }
93 1         9 return;
94             }
95              
96             sub get_next_two_tones {
97 3     3 1 551 my $self = shift;
98 3         6 my ( $tone_a_length, $tone_b_length ) = @_;
99              
100 3   50     9 my $tone_a = $self->get_next_tone($tone_a_length) || return;
101 3   50     15 my $tone_b = $self->get_next_tone($tone_b_length) || return;
102 3 100       58 return wantarray ? ( $tone_a, $tone_b ) : "$tone_a $tone_b";
103             }
104              
105             sub find_closest_valid {
106 6     6 1 12 my ( $self, $freq ) = @_;
107 6         12 my $lower = 0;
108 6         10 my $upper;
109              
110 6         11 for my $possibility ( @{ $self->{valid_tones} } ) {
  6         19  
111 842 100       1290 last if $upper;
112 836 100       1533 $lower = $possibility if $possibility <= $freq;
113 836 100       1328 $upper = $possibility if $possibility > $freq;
114             }
115 6   33     21 $upper ||= $lower;
116 6 100       25 my $valid_tone
117             = ( $freq - $lower ) < ( $upper - $freq )
118             ? $lower
119             : $upper;
120              
121 6 100       24 if ( $self->{valid_error_cb} ) {
122 3         21 my $cb_result = $self->{valid_error_cb}
123             ->( $valid_tone, $freq, $freq - $valid_tone );
124 3 100       21 if ( defined $cb_result ) {
125 2 100       7 return if $cb_result == 0;
126 1         2 $valid_tone = $cb_result;
127             }
128             }
129              
130 5         20 return ( $valid_tone, $freq - $valid_tone );
131             }
132              
133 94   100 94   167 sub _all_match { my $l = shift; $_ == $l->[0] || return 0 for @$l; return 1 }
  94         3629  
  10         48  
134              
135             sub _is_pow_of_two {
136              
137             # if pow of 2 exactly 1 bit is set all others unset and n - 1 will have
138             # that bit unset and all lower bits set thus binary AND of n & n -1 will
139             # result in 0
140 3   33 3   22 return $_[0] != 0 && ( $_[0] & ( $_[0] - 1 ) ) == 0;
141             }
142              
143             sub _get_builtin_tones {
144              
145             # via http://sourceforge.net/projects/tonedetect/ not sure complete/accurate
146             # want better list
147              
148 2     2   48 return [ ( qw (
149             282.2 288.5 294.7 296.5 304.7 307.8 313.0 321.4 321.7
150             330.5 335.6 339.6 346.7 349.0 350.5 358.6 358.9 366.0
151             368.5 371.5 378.6 382.3 384.6 389.0 398.1 399.2 399.8
152             410.8 412.1 416.9 422.1 426.6 433.7 435.3 441.6 445.7
153             454.6 457.1 457.9 470.5 473.2 474.8 483.5 489.8 495.8
154             496.8 507.0 510.5 517.5 517.8 524.6 524.8 532.5 539.0
155             540.7 543.3 547.5 553.9 562.3 562.5 564.7 569.1 577.5
156             582.1 584.8 589.7 592.5 600.9 602.6 604.2 607.5 615.8
157             617.4 622.5 623.7 631.5 634.5 637.5 640.6 643.0 645.7
158             651.9 652.6 662.3 667.5 668.3 669.9 672.0 682.5 688.3
159             691.8 693.0 697.5 701.0 707.3 712.5 716.7 726.8 727.1
160             727.5 732.0 741.3 746.8 757.5 761.3 765.0 767.4 767.4
161             772.5 787.5 788.5 794.3 795.4 799.0 802.5 810.2 817.5
162             822.2 832.5 832.5 832.9 834.0 847.5 851.1 855.5 862.5
163             870.5 871.0 877.5 879.0 881.0 892.5 903.2 907.9 910.0
164             911.5 912.0 922.5 928.1 937.5 944.1 950.0 952.4 952.5
165             953.7 967.5 977.2 979.9 984.4 992.0 996.8 1006.9 1011.6
166             1034.7 1036.0 1041.2 1047.1 1063.2 1082.0 1084.0 1089.0 1092.4
167             1122.1 1122.5 1130.0 1140.2 1153.4 1161.4 1180.0 1185.2 1191.4
168             1217.8 1232.0 1246.0 1251.4 1285.8 1287.0 1304.0 1321.2 1344.0
169             1357.6 1362.1 1395.0 1403.0 1423.5 1433.4 1465.0 1488.4 1530.0
170             1556.7 1598.0 1628.3 1642.0 1669.0 1717.1 1743.0 1795.6 1820.0
171             1877.5 1901.0 1985.0 2051.6 2073.0 2143.8 2164.0 2260.0 2341.8
172             2361.0 2447.6 2465.0 2556.9 2575.0 2672.9 2688.0 2792.4 2807.0
173             2932.0 3062.0 3197.0 3339.0 3487.0 )
174             ) ];
175             }
176              
177             1;
178             __END__