File Coverage

blib/lib/Audio/Analyzer.pm
Criterion Covered Total %
statement 243 328 74.0
branch 32 74 43.2
condition 2 9 22.2
subroutine 47 56 83.9
pod 5 12 41.6
total 329 479 68.6


line stmt bran cond sub pod time code
1             package Audio::Analyzer;
2              
3             our $VERSION = '0.22';
4              
5 1     1   667 use strict;
  1         3  
  1         30  
6 1     1   5 use warnings;
  1         1  
  1         26  
7              
8 1     1   7 use Carp qw(croak);
  1         4  
  1         68  
9              
10 1     1   937 use Math::FFT;
  1         5515  
  1         37  
11              
12 1     1   11 use constant DEFAULT_DFT_SIZE => 2 ** 11;
  1         2  
  1         86  
13 1     1   6 use constant DEFAULT_SAMPLE_RATE => 44100;
  1         2  
  1         46  
14 1     1   6 use constant DEFAULT_CHANNELS => 2;
  1         3  
  1         39  
15 1     1   5 use constant DEFAULT_BITS_PER_SAMPLE => 16;
  1         3  
  1         41  
16              
17             #value can be undef if fileless operation is being used
18 1     1   5 use constant INPUT => 0;
  1         2  
  1         39  
19 1     1   5 use constant FILE_NAME => 1;
  1         2  
  1         45  
20 1     1   5 use constant DFT_SIZE => 2;
  1         2  
  1         48  
21 1     1   4 use constant SEEK_STEP => 3;
  1         2  
  1         43  
22 1     1   5 use constant READ_SIZE => 4;
  1         1  
  1         41  
23 1     1   12 use constant CHANNELS => 5;
  1         2  
  1         46  
24 1     1   5 use constant BYTES_PER_SAMPLE => 6;
  1         1  
  1         51  
25 1     1   5 use constant SAMPLE_RATE => 7;
  1         2  
  1         39  
26 1     1   5 use constant FFT => 8;
  1         2  
  1         49  
27 1     1   5 use constant FREQ_CACHE => 9;
  1         1  
  1         38  
28 1     1   4 use constant SCALER => 10;
  1         2  
  1         43  
29 1     1   5 use constant BYTES => 11;
  1         2  
  1         45  
30 1     1   5 use constant EOF_FOUND => 12;
  1         2  
  1         53  
31 1     1   4 use constant FILELESS_BUFFER => 13;
  1         2  
  1         2158  
32              
33             sub new {
34 1     1 1 17 my ($class, %opts) = @_;
35              
36 1         2 my $self = [];
37              
38 1         2 bless($self, $class);
39              
40 1         6 $self->init(%opts);
41              
42 1         5 return $self;
43             }
44              
45             sub add_samples {
46 0     0 0 0 my ($self, @samples) = @_;
47 0         0 push(@{$self->[FILELESS_BUFFER]}, @samples);
  0         0  
48 0         0 return;
49             }
50              
51             sub sample_buffer_size {
52 0     0 0 0 my ($self) = @_;
53 0         0 return scalar(@{$self->[FILELESS_BUFFER]});
  0         0  
54             }
55              
56             sub next {
57 33     33 1 523533 my ($self) = @_;
58 33         73 my @samples;
59             my $chunk;
60            
61 33 50       129 if (defined($self->[INPUT])) {
62 33         133 my $pcm = $self->read_pcm;
63            
64 33 100       375 if (! defined($pcm)) {
65 1         4 return undef;
66             }
67            
68 32         140 @samples = $self->convert_pcm($pcm);
69            
70             } else {
71 0         0 @samples = $self->get_fileless_samples;
72 0 0       0 return undef unless @samples;
73             }
74              
75              
76 32         177 my $channels = $self->split_channels(@samples);
77              
78 32         192 $chunk = Audio::Analyzer::Chunk->new($self, $channels);
79              
80 32         223 return $chunk;
81             }
82              
83             sub progress {
84 0     0 1 0 my ($self) = @_;
85 0         0 my $bytes = $self->[BYTES];
86 0         0 my $input = $self->[INPUT];
87 0         0 my $size = (stat($input))[7];
88              
89 0         0 return int($bytes / $size * 100);
90             }
91              
92             sub freqs {
93 2     2 1 494 my ($self) = @_;
94 2         5 my $sample_rate = $self->[SAMPLE_RATE];
95 2         2 my $dft_size = $self->[DFT_SIZE];
96 2         3 my $freq_cache = $self->[FREQ_CACHE];
97 2         2 my @freqs;
98              
99 2 100       5 if (defined($freq_cache)) {
100 1         6 return $freq_cache;
101             }
102              
103 1         5 for(my $i = 0; $i < $dft_size / 2; $i++) {
104 32         78 $freqs[$i] = $i / $dft_size * $sample_rate;
105             }
106              
107 1         2 $self->[FREQ_CACHE] = \@freqs;
108              
109 1         4 return $self->[FREQ_CACHE];
110             }
111              
112             #private interface starts here
113              
114             sub init {
115 1     1 0 3 my ($self, %opts) = @_;
116 1         2 my $file;
117             my $dft_size;
118 0         0 my $seek_step;
119 0         0 my $channels;
120 0         0 my $bits_per_sample;
121 0         0 my $sample_rate;
122 0         0 my $read_size;
123 0         0 my $scaler;
124 0         0 my $fps;
125              
126 1 50       4 if (exists($opts{'file'})) {
    0          
127 1 50       4 if (! defined($file = $opts{'file'})) {
128 0         0 croak "if the file option is given it must have a value";
129             }
130             } elsif (! defined $opts{fileless}) {
131 0         0 croak "no file was specified and fileless operation is not configured";
132             }
133              
134 1 50       4 if (! defined($dft_size = $opts{'dft_size'})) {
135 0         0 $dft_size = DEFAULT_DFT_SIZE;
136             }
137              
138 1 50       3 if (! defined($sample_rate = $opts{'sample_rate'})) {
139 0         0 $sample_rate = DEFAULT_SAMPLE_RATE;
140             }
141              
142 1 50       4 if (! defined($channels = $opts{'channels'})) {
143 0         0 $channels = DEFAULT_CHANNELS;
144             }
145              
146 1 50       3 if (defined($bits_per_sample = $opts{'bits_per_sample'})) {
147 0 0 0     0 if ($bits_per_sample != 8 && $bits_per_sample != 16) {
148 0         0 croak("bits_per_sample must be 8 or 16");
149             }
150             } else {
151 1         2 $bits_per_sample = DEFAULT_BITS_PER_SAMPLE;
152             }
153              
154 1         3 $read_size = $dft_size * $channels * $bits_per_sample / 8;
155              
156 1 50 33     8 if (defined($fps = $opts{'fps'})) {
    50          
    50          
157 0 0       0 croak "unable to use audio/visual sync with fileless operation"
158             unless defined $file;
159 0         0 $seek_step = $sample_rate / $fps * $bits_per_sample / 8 * $channels;
160             } elsif (defined $seek_step && ! defined $file) {
161 0         0 croak "unable to use seek_step with fileless operation";
162             } elsif (! defined($seek_step = $opts{'seek_step'})) {
163 1         2 $seek_step = $read_size;
164             }
165              
166 1 50       3 if (defined($file)) {
167 1 50       2 if (ref($file) eq 'GLOB') {
168 0         0 $self->[INPUT] = $file;
169 0         0 $self->[FILE_NAME] = scalar($file);
170             } else {
171 1 50       41 croak "could not open $file: $!" unless open(PCM, $file);
172            
173 1         6 $self->[INPUT] = \*PCM;
174 1         3 $self->[FILE_NAME] = $file;
175             }
176             }
177              
178 1         3 $self->[BYTES_PER_SAMPLE] = $bits_per_sample / 8;
179 1         1 $self->[CHANNELS] = $channels;
180 1         2 $self->[SAMPLE_RATE] = $sample_rate;
181 1         2 $self->[DFT_SIZE] = $dft_size;
182 1         1 $self->[SEEK_STEP] = $seek_step;
183 1         1 $self->[READ_SIZE] = $read_size;
184 1         3 $self->[BYTES] = 0;
185 1         2 $self->[EOF_FOUND] = 0;
186            
187 1 50       3 unless(defined($file)) {
188 0         0 $self->[FILELESS_BUFFER] = [];
189             }
190              
191 1 50       41 if (! exists($opts{scaler})) {
    0          
192 1         8 $scaler = Audio::Analyzer::ACurve->new($self);
193             } elsif(defined($opts{scaler})) {
194 0         0 my $requested = $opts{scaler};
195              
196 0         0 $scaler = $requested->new($self);
197             }
198              
199 1         3 $self->[SCALER] = $scaler;
200              
201 1         3 return $self;
202             }
203              
204             sub split_channels {
205 32     32 0 226 my ($self, @samples) = @_;
206 32         168 my $channels = $self->[CHANNELS];
207 32         52 my @split;
208 32         47 my $size = scalar(@samples);
209              
210 32         349 for(my $i = 0; $i < $size; $i++) {
211 2048         2540 my $chan = int($i % $channels);
212 2048         1951 push(@{$split[$chan]}, $samples[$i]);
  2048         5125  
213             }
214              
215 32         173 return \@split;
216             }
217              
218              
219             #converts PCM into floating point representation
220             sub convert_pcm {
221 32     32 0 58 my ($self, $pcm) = @_;
222 32         67 my $bytes_per_sample = $self->[BYTES_PER_SAMPLE];
223 32         41 my @samples;
224              
225 32 50       67 if ($bytes_per_sample == 2) {
226 32         106 while(length($pcm) >= 2) {
227 2048         3515 my $sample = unpack('s<', substr($pcm, 0, 2, ''));
228 2048         5118 push(@samples, $sample);
229             }
230             } else {
231 0         0 die "8 bit PCM isn't implemented yet";
232             }
233              
234 32         538 return @samples;
235             }
236              
237             sub get_fileless_samples {
238 0     0 0 0 my ($self) = @_;
239 0         0 my $input = $self->[INPUT];
240 0         0 my $read_size = $self->[READ_SIZE];
241 0         0 my $fileless_buffer = $self->[FILELESS_BUFFER];
242 0         0 my $samples_needed = $read_size / $self->[BYTES_PER_SAMPLE];
243            
244 0 0       0 if (defined $input) {
245 0         0 die "read_buffer() was called for fileless operation but there was an input file ref?";
246             }
247              
248 0 0       0 return () unless scalar(@$fileless_buffer) >= $samples_needed;
249 0         0 return splice(@$fileless_buffer, 0, $samples_needed);
250             }
251              
252             sub read_pcm {
253 34     34 0 53 my ($self) = @_;
254 34         66 my $input = $self->[INPUT];
255 34         70 my $read_size = $self->[READ_SIZE];
256 34         63 my $seek_step = $self->[SEEK_STEP];
257 34         66 my $bytes = $self->[BYTES];
258 34         50 my $EOF_found = $self->[EOF_FOUND];
259 34         45 my $buf;
260             my $ret;
261 0         0 my $rewind;
262            
263 34 50       129 die "there was no input filehandle ref" unless defined $input;
264            
265 34         224 $ret = read($input, $buf, $read_size);
266              
267 34 50       180 if (! defined($ret)) {
    100          
    100          
268 0         0 die "could not read: $!";
269             } elsif ($ret == 0) {
270 1         4 return undef;
271             } elsif ($ret < $read_size) {
272             #hit the end and did not get enough data for the FFT - seek
273             #backwards a whole read_size and finish the last reading
274             #as best as possible
275 1         30 my $size = (stat($input))[7];
276            
277 1         3 $self->[EOF_FOUND] = 1;
278              
279 1 50       11 seek($input, $size - $read_size, 0) or die "could not seek: $!";
280              
281 1         6 return $self->read_pcm;
282             }
283              
284 32         80 $bytes += $seek_step;
285              
286 32         59 $rewind = $read_size - $seek_step;
287              
288 32 50 33     87 if ($rewind && ! $EOF_found) {
289 0 0       0 seek($input, $rewind * -1, 1) or die "could not seek: $!";
290             }
291              
292 32         53 $self->[BYTES] = $bytes;
293              
294 32         326 return $buf;
295             }
296              
297             sub scaler {
298 32     32 1 41 my ($self) = @_;
299            
300 32         92 return $self->[SCALER];
301             }
302              
303             package Audio::Analyzer::Chunk;
304              
305             our $VERSION = '0.02';
306              
307 1     1   8 use strict;
  1         1  
  1         42  
308 1     1   6 use warnings;
  1         2  
  1         690  
309              
310             sub new {
311 32     32   56 my ($class, $analyzer, $channels) = @_;
312 32         76 my $self = {};
313              
314 32         114 $self->{analyzer} = $analyzer;
315 32         82 $self->{channels} = $channels;
316              
317 32         142 bless($self, $class);
318              
319 32         72 return $self;
320             }
321              
322             sub pcm {
323 0     0   0 my ($self) = @_;
324              
325 0         0 return $self->{channels};
326             }
327              
328             sub fft {
329 32     32   15086 my ($self) = @_;
330 32         75 my $channels = $self->{channels};
331 32         45 my @mags;
332              
333 32         133 for(my $i = 0; $i < scalar(@$channels); $i++) {
334 32         110 $mags[$i] = $self->do_fft($channels->[$i]);
335             }
336              
337 32         93 return \@mags;
338             }
339              
340             sub rms {
341 0     0   0 my $self = shift(@_);
342 0         0 my $size = scalar(@_);
343 0         0 my $sum;
344              
345 0         0 for(my $i = 0; $i < $size; $i++) {
346 0         0 $sum += $_[$i] ** 2;
347             }
348              
349 0         0 $sum /= $size;
350              
351 0         0 return sqrt($sum);
352             }
353              
354             sub combine_fft {
355 0     0   0 my ($self, $channels) = @_;
356 0         0 my $num_channels = scalar(@$channels);
357 0         0 my $length = scalar(@{$channels->[0]});
  0         0  
358 0         0 my @new;
359              
360 0         0 for(my $i = 0; $i < $length; $i++) {
361 0         0 my @row;
362              
363 0         0 for(my $j = 0; $j < $num_channels; $j++) {
364 0         0 push(@row, $channels->[$j][$i]);
365             }
366              
367 0         0 $new[$i] = $self->rms(@row);
368             }
369              
370 0         0 return \@new;
371             }
372              
373             sub analyzer {
374 32     32   55 my ($self) = @_;
375              
376 32         124 return $self->{analyzer};
377             }
378              
379             #private methods
380              
381             sub do_fft {
382 32     32   54 my ($self, $samples) = @_;
383 32         206 my $fft = Math::FFT->new($samples);
384 32         1345 my $coeff = $fft->rdft;
385 32         1810 my $size = scalar(@$coeff);
386 32         40 my $k = 0;
387 32         47 my @mag;
388              
389 32         127 $mag[$k] = sqrt($coeff->[$k*2]**2);
390              
391 32         127 for($k = 1; $k < $size / 2; $k++) {
392 992         3691 $mag[$k] = sqrt(($coeff->[$k * 2] ** 2) + ($coeff->[$k * 2 + 1] ** 2));
393             }
394              
395 32         99 $self->scale(\@mag);
396              
397 32         353 return \@mag;
398             }
399              
400             sub scale {
401 32     32   52 my ($self, $mags) = @_;
402 32         106 my $scaler = $self->analyzer->scaler;
403            
404 32 50       146 if (defined($scaler)) {
405 32         115 $scaler->scale($mags);
406             }
407             }
408              
409             package Audio::Analyzer::ACurve;
410              
411             our $VERSION = '0.02';
412              
413 1     1   6 use strict;
  1         1  
  1         27  
414 1     1   5 use warnings;
  1         3  
  1         31  
415              
416 1     1   6 use Carp;
  1         2  
  1         89  
417            
418 1     1   5 use constant SCALE => 5000000; #tested by running some Prodigy
  1         1  
  1         485  
419             #through the system
420              
421             sub new {
422 1     1   2 my ($class, $analyzer) = @_;
423 1         2 my $self = {};
424              
425 1         3 $self->{analyzer} = $analyzer;
426              
427 1 50       3 if (! defined($analyzer)) {
428 0         0 croak "I need an analyzer";
429             }
430              
431 1         3 bless($self, $class);
432              
433 1         3 $self->init;
434              
435 1         3 return $self;
436             }
437              
438             sub init {
439 1     1   2 my ($self) = @_;
440 1         5 my $analyzer = $self->{analyzer};
441 1         1 my @correction;
442 1         5 my $freqs = $analyzer->freqs;
443              
444 1         4 for(my $i = 0; $i < scalar(@$freqs); $i++) {
445 32         35 my $freq = $freqs->[$i];
446            
447 32 50       49 if ($freq < 10000) {
448 32         50 $correction[$i] = $self->solve_one_A($freq);
449             } else {
450 0         0 $correction[$i] = 1;
451             }
452              
453             }
454              
455 1         13 $self->{correction} = \@correction;
456             }
457              
458             sub solve_one_A {
459 32     32   32 my ($self, $freq) = @_;
460 32         35 my $term_1 = ($freq ** 2) + (20.6 ** 2);
461 32         36 my $term_2 = ($freq ** 2) + (12200 ** 2);
462 32         38 my $term_3 = sqrt(($freq ** 2) + (107.7 ** 2));
463 32         35 my $term_4 = sqrt(($freq ** 2) + (737.9 ** 2));
464            
465 32         107 return (12200 ** 2) * ($freq ** 4) / ($term_1 * $term_2 * $term_3 * $term_4);
466             }
467              
468             sub scale {
469 32     32   46 my ($self, $mags) = @_;
470 32         68 my $correction = $self->{correction};
471 32         49 my $size = scalar(@$mags);
472              
473 32         114 for(my $i = 0; $i < $size; $i++) {
474 1024         1382 $mags->[$i] *= $correction->[$i];
475 1024         1337 $mags->[$i] /= SCALE;
476              
477 1024 50       2866 if ($mags->[$i] > 1) {
478 0           $mags->[$i] = 1;
479             }
480             }
481             }
482              
483             package Audio::Analyzer::AutoScaler;
484              
485             our $VERSION = '0.02';
486              
487 1     1   6 use strict;
  1         2  
  1         33  
488 1     1   5 use warnings;
  1         1  
  1         185  
489              
490             sub new {
491 0     0     my ($class) = @_;
492 0           my $self = {};
493              
494 0           $self->{peak} = 0;
495              
496 0           bless($self, $class);
497              
498 0           return $self;
499             }
500              
501             sub scale {
502 0     0     my ($self, $readings) = @_;
503 0           my $size = scalar(@$readings);
504              
505 0           for(my $i = 0; $i < $size; $i++) {
506 0           my $one = $readings->[$i];
507              
508 0 0         if ($one > $self->{peak}) {
509 0           $self->{peak} = $one;
510             }
511              
512 0           $one /= $self->{peak};
513              
514 0           $readings->[$i] = $one;
515             }
516             }
517              
518             1;
519              
520             __END__