File Coverage

blib/lib/Audio/FindChunks.pm
Criterion Covered Total %
statement 76 194 39.1
branch 12 90 13.3
condition 9 70 12.8
subroutine 14 23 60.8
pod 8 17 47.0
total 119 394 30.2


line stmt bran cond sub pod time code
1             package Audio::FindChunks;
2              
3 1     1   18710 use 5.00503;
  1         5  
  1         46  
4 1     1   5 use strict;
  1         2  
  1         37  
5              
6 1     1   981 use Data::Flow qw(0.09);
  1         10071  
  1         204  
7              
8             BEGIN {
9 1     1   6 require DynaLoader;
10 1     1   9 use vars qw($VERSION @ISA);
  1         2  
  1         188  
11              
12 1         19 @ISA = qw(DynaLoader);
13              
14 1         2 $VERSION = '2.00';
15              
16 1         698 bootstrap Audio::FindChunks $VERSION;
17 1         7 my $do_dbg = !!$ENV{FIND_CHUNKS_DEBUG}; # Convert to logical
18 1     0 0 8225 eval "sub do_dbg () {$do_dbg}";
  0         0  
19             }
20              
21             die "Version 1.00 of Data::Flow is defective" if $Data::Flow::VERSION eq '1.00';
22              
23             # Preloaded methods go here.
24              
25 0 0   0 0 0 sub default ($$$) {my ($o, $k, $v) = @_; $o->{$k} = $v unless defined $o->{$k}}
  0         0  
26              
27             my $le_short_size = length pack 'v', 0;
28             my $short_size = length pack 's', 0;
29             my $int_size = length pack 'i', 0;
30             my $long_post = ($] >= 5.006 ? '!' : '');
31             my $long = "l$long_post";
32             my $long_size = length pack $long, 0;
33             my $double_size = length pack 'd', 0;
34             my $pointer_size = length pack 'p', 0;
35             my $pointer_unpack = (($pointer_size == $int_size) ? 'I' : "L$long_post");
36             my $long_min = unpack $long, pack $long, -1e100;
37             my $long_max = -$long_min-1;
38             my $do_dbg = $ENV{FIND_CHUNKS_DEBUG};
39              
40             sub le_short_sample_multichannel ($$$$$$) {
41 0     0 0 0 my ($totstride, $stride, $channels, $out, $chunksize) =
42             (shift,shift,shift,shift,shift);
43 0         0 my $size = length $_[0];
44 0         0 my $bufaddr = unpack $pointer_unpack, pack 'p', $_[0];
45 0 0       0 die "Size of buffer not multiple of total stride" if $size % $totstride;
46             # Do in multiples of 7K (to falicitate lcd 8K Level I cache)
47 0 0       0 $chunksize = $totstride * int((7*(1<<10))/$totstride) unless defined $chunksize;
48 0         0 my $processed = 0;
49 0         0 while ($size > 0) {
50 0 0       0 $chunksize = $size if $chunksize > $size;
51 0         0 $size -= $chunksize;
52 0         0 my $samples = $chunksize / $totstride;
53 0         0 $processed += $samples;
54 0         0 for my $c (0..$channels-1) {
55 0 0       0 warn sprintf "Ch %d: Samples %d %d %d %d ..., totstride %d, %d samples\n",
56             $c, unpack('s4', unpack 'P8', pack $pointer_unpack, $bufaddr + $stride * $c), $totstride, $samples
57             if do_dbg();
58             # void le_short_sample_stats(char *buf, int stride, long samples, array_stats_t *stat)
59 0         0 le_short_sample_stats($bufaddr + $stride * $c, $totstride, $samples,
60             $out->[$c]);
61 0 0       0 warn sprintf " => %d\n", unpack 'd', $out->[$c] if do_dbg();
62             }
63 0         0 $bufaddr += $chunksize;
64             }
65 0         0 return $processed;
66             }
67              
68 34     34 0 180 sub rnd ($) {sprintf '%.0f', shift}
69              
70             my $wav_header = <
71             a4 # header: 'RIFF'
72             V # size: Size of what follows
73             a4 # type: 'WAVE'
74              
75             a4 # type1: 'fmt ' subchunk
76             V # size1: Size of the rest of subchunk
77             v # format: 1 for pcm
78             v # channels: 2 stereo 1 mono
79             V # frequency
80             V # bytes_per_sec
81             v # bytes_per_sample
82             v # bits_per_sample_channel
83              
84             a4 # type2: 'data' subchunk
85             V # sizedata: Size of the rest of subchunk
86             EOH
87              
88             my @wav_fields = ($wav_header =~ /^\s*\w+\s*#\s*(\w+)/mg);
89              
90             $wav_header =~ s/#.*//g; # For v5.005
91              
92             my $header_size = length pack $wav_header, (0) x 20;
93             sub MY_INF () {1e200}
94              
95             sub wav_eat_header ($) {
96 0     0 0 0 my $fh = shift;
97 0         0 my $in;
98 0 0       0 my $read = sysread $fh, $in, $header_size or die "can't read the header";
99 0 0       0 return {buf => $in} unless $read == $header_size;
100 0         0 my %vals;
101 0 0       0 @vals{@wav_fields} = unpack $wav_header, $in or return {buf => $in};
102 0 0       0 return {buf => $in} unless $vals{header} eq 'RIFF';
103 0 0 0     0 die "Unexpected RIFF format"
      0        
      0        
      0        
      0        
      0        
104             unless $vals{type} eq 'WAVE' and $vals{type1} eq 'fmt '
105             and $vals{size1} == 0x10 and $vals{format} == 1
106             and $vals{bits_per_sample_channel} == 16 and $vals{format} == 1
107             and $vals{type2} eq 'data';
108 0         0 $vals{buf} = $in;
109 0         0 return \%vals;
110             }
111              
112             sub SOUND () {2} # Constants... Rarely promoted or demoted
113             sub SIGNAL () {1} # May be promoted or demoted
114             sub NOISE () {0} # Likewise
115             sub SILENCE () {-1} # Rarely promoted or demoted
116              
117             sub merge_blocks ($) { # array ref: 0: type, 1: start, 2: len
118 8     8 0 11 my $blocks = shift;
119 8         8 my $c = 0;
120 8         10 my @new;
121 8         13 for my $b (@$blocks) {
122 500 100 100     2077 push(@new, [@$b]), next if not @new or $b->[0] != $new[-1][0];
123 444         634 $new[-1][2] += $b->[2];
124             }
125             \@new
126 8         104 }
127              
128             my %defaults = (
129             # For getting PCM flow (and if averaging data is read from cache)
130             frequency => 44100,
131             bytes_per_sample => 4,
132             channels => 2,
133             sizedata => MY_INF,
134             out_fh => \*STDOUT,
135             preprocess => {mp3 => [[qw(lame --silent --decode)], [], ['-']]}, # Second contains extra args to read stdin
136             # For getting RMS info
137             sec_per_chunk => 0.1,
138             # RMS cache
139             rms_extension => '.rms',
140             # For threshold calculation
141             threshold_in_sorted_min_rel => 0,
142             threshold_in_sorted_min_sec => 1,
143             threshold_in_sorted_max_rel => 0.5,
144             threshold_in_sorted_max_sec => 0,
145             threshold_ratio => 0.15,
146             threshold_factor_min => 1,
147             threshold_factor_max => 1,
148             # Chunkification: smoothification
149             above_thres_window => 11,
150             above_thres_window_rel => 0.25,
151             # Chunkification
152             max_tracks => 9999,
153             min_signal_sec => 5,
154             min_silence_sec => 2,
155             ignore_signal_sec => 1,
156             # Final enlargement
157             local_level_ignore_pre_sec => 0.3,
158             local_level_ignore_post_sec => 0.3,
159             local_level_ignore_pre_rel => 0.02,
160             local_level_ignore_post_rel => 0.02,
161             local_threshold_factor => 1.05,
162             extend_track_end_sec => 0.5,
163             extend_track_begin_sec => 0.3,
164             min_boundary_silence_sec => 0.2,
165             );
166              
167             my %mirror_from = ( # May be set separately, otherwise are synonims
168             min_actual_silence_sec => 'min_silence_sec',
169             min_start_silence_sec => 'min_boundary_silence_sec',
170             min_end_silence_sec => 'min_boundary_silence_sec',
171             cache_rms_write => 'cache_rms',
172             cache_rms_read => 'cache_rms',
173             min_silence_chunks_merge => 'min_silence_chunks',
174             );
175              
176             my %chunk_times =
177             map { (my $n = $_) =~ s/_sec/_chunks/;
178             ($n => {'filter'
179             => [sub {rnd(shift()/shift)}, $_, 'sec_per_chunk']}) }
180             grep /_sec$/, keys %defaults, keys %mirror_from;
181              
182             my @recognized = # these default to undef, but accessing them is not fatal
183             qw(filename stem_strip_extension filter raw_pcm rms_filename close_fh
184             override_header_info cache_rms subchunk_size skip_medians);
185              
186             my %filters = (
187             # For getting RMS info
188             filestem => [sub { my $f = shift;
189             return 'filehandle' unless defined $f;
190             $f =~ s/\.(\w+)$// if shift;
191             $f }, 'filename', 'stem_strip_extension'],
192             input_type => [sub { return unless defined (my $f = shift);
193             return unless $f =~ /\.(\w+)$/;
194             my $h = shift;
195             return lc $1 if not $h->{$1} and $h->{lc $1};
196             $1 }, 'filename', 'preprocess'],
197             preprocess_a => [sub {return unless defined $_[0];
198             $_[1]->{$_[0]} }, 'input_type', 'preprocess'],
199             preprocess_input => [sub { my ($cmd, $f) = @_; return unless $cmd;
200             return [@{$cmd->[0]}, $f, @{$cmd->[2]}]
201             if defined $f;
202             return [@{$cmd->[0]}, @{$cmd->[1]}, @{$cmd->[2]}];
203             }, 'preprocess_a', 'filename'],
204             fh_bin => [sub { my $fh = shift; binmode $fh; $fh }, 'fh'],
205             out_fh_bin => [sub { return unless shift;
206             my $fh = shift; binmode $fh; $fh
207             }, 'filter', 'out_fh'],
208             rms_filename_default => [sub {shift() . shift}, 'filestem', 'rms_extension'],
209             read_from_rms_file => [sub { return if shift; # Need output stream, not only RMS
210             shift or defined shift
211             }, 'filter', 'cache_rms_read', 'rms_filename'],
212             write_to_rms_file => [sub {shift or defined shift},
213             'cache_rms_write', 'rms_filename'],
214             rms_filename_actual => [sub {my $f = shift; return $f if defined $f; shift},
215             'rms_filename', 'rms_filename_default'],
216             samples_per_chunk => [sub {rnd(shift()*shift)}, 'sec_per_chunk', 'frequency'],
217             bytes_per_chunk => [sub {shift()*shift}, 'samples_per_chunk', 'bytes_per_sample'],
218             rms_data_arr_f => [sub {return unless shift;
219             local *RMS; open RMS, '< ' . shift or return; # No file is OK
220             binmode *RMS;
221             my $c = -s \*RMS;
222             my @in;
223             26 == sysread RMS, $in[0], 26 or die "Short read on RMS";
224             $in[0] =~ /^GramoFile Binary RMS Data\n/i
225             or die "Unknown format of RMS file";
226             $c - 26 == sysread RMS, $in[0], $c - 26 or die "Short read on RMS";
227             push @in, unpack "${long}2", substr $in[0], 0, 2*$long_size;
228             substr($in[0], 0, 2*$long_size) = '';
229             die "Malformed length of RMS file" # sam/chunk, chunks
230             unless $in[2] * $double_size == length $in[0];
231             my $sam = shift;
232             die "Samples per chunk mismatch: RMSfile => $in[1], expected => $sam" # sam/chunk, chunks
233             unless $in[1] == $sam;
234             \@in }, 'read_from_rms_file', 'rms_filename_actual',
235             'samples_per_chunk'],
236             # For threshold calculation
237             medians => [sub { my $av = shift; my @r = $av->[0]; # Allocate the buffer
238             double_median3($av->[0], $r[0], shift) unless shift;
239             \@r }, 'rms_data', 'skip_medians', 'chunks'],
240             sorted => [sub { my $av = shift; my @r = $av->[0]; # Allocate the buffer
241             double_sort($av->[0], $r[0], shift);
242             \@r }, 'medians', 'chunks'],
243             map(("threshold_in_sorted_$_" =>
244             [sub { my ($c, $r) = shift; $r = $c*shift() + shift() - 1;
245             $r = $c - 1 unless $r < $c - 1;
246             $r = 0 unless $r > 0; $r
247             }, 'chunks', "threshold_in_sorted_${_}_rel", "threshold_in_sorted_${_}_chunks"],
248             "threshold_$_" =>
249             [sub { shift() *
250             sqrt unpack 'd',
251             substr shift->[0], $double_size * rnd(shift), $double_size
252             }, "threshold_factor_$_", 'sorted', "threshold_in_sorted_$_"]),
253             'max', 'min'),
254             threshold => [sub { my $min = shift; shift() * (shift()-$min) + $min
255             }, 'threshold_min', 'threshold_ratio', 'threshold_max'],
256             # Chunkification: smoothification
257             above_thres => [sub { my $c = shift; my @r = 'x' x ($int_size * $c); # Reserve space
258             double_find_above(shift->[0], $r[0], $c, shift()**2);
259             \@r }, 'chunks', 'rms_data', 'threshold'],
260             above_thres_in_window => [sub { my $a = shift; my @r = $a->[0]; # Reserve space
261             int_sum_window($a->[0], $r[0], shift, shift);
262             \@r}, 'above_thres', 'chunks', 'above_thres_window'],
263             above_thres_window_abs => [sub {shift()*shift},
264             'above_thres_window_rel', 'above_thres_window'],
265             maybe_signal => [sub { my $a = shift; my @r = $a->[0]; # Reserve space
266             int_find_above($a->[0], $r[0], shift, shift); \@r
267             }, 'above_thres_in_window', 'chunks', 'above_thres_window_abs'],
268             # Chunkification
269             maybe_trk_pk => [sub { my $max = shift; my @r = 'x' x (3*$long_size*$max); # Reserve space
270             my $c = bool_find_runs(shift->[0], $r[0], shift, $max);
271             die "Max count $max of track candidates exceeded"
272             unless $c >= 0;
273             substr($r[0], 3*$long_size*$c) = ''; # Truncate
274             \@r }, 'max_tracks', 'maybe_signal', 'chunks'],
275             # Unpack
276             b0 => [sub { my ($c, @b) = -1; my $tracks = shift->[0];
277             my $cnt = length($tracks)/(3*$long_size);
278             my @bl = unpack $long.(3*$cnt), $tracks;
279             while (++$c < $cnt) { # [SIGNAL/NOISE, start, len]
280             push @b, [@bl[3*$c, 3*$c + 1, 3*$c + 2]];
281             } return [@b] }, 'maybe_trk_pk'],
282             # "Force" long enough blocks
283             b1 => [sub { my @b = map [@$_], @{shift()}; # Deep copy
284             my ($min_sign, $min_sil) = (shift, shift);
285             for my $t (@b) {
286             $t->[0] = SOUND, next
287             if $t->[0] == SIGNAL and $t->[2] >= $min_sign;
288             $t->[0] = SILENCE, next
289             if $t->[0] == NOISE and $t->[2] >= $min_sil;
290             }
291             # Force silence if it happens at boundary:
292             $b[$_]->[0] == NOISE and $b[$_]->[0] = SILENCE
293             for 0, -1;
294             \@b }, 'b0', 'min_signal_chunks', 'min_silence_chunks'],
295             # Ignore short bursts of signals (may be reversed later)
296             b2 => [sub { my @b = map [@$_], @{shift()}; # Deep copy
297             my ($c, $ign_sign) = (0, shift);
298             while (++$c < @b - 1) { # XXXX What about those with SILENCE?
299             $b[$c]->[0] = NOISE
300             if $b[$c]->[0] == SIGNAL and $b[$c]->[2] <= $ign_sign
301             and $b[$c-1]->[0] == NOISE and $b[$c+1]->[0] == NOISE
302             } # After ignoring, need to merge similar blocks
303             merge_blocks \@b }, 'b1', 'ignore_signal_chunks'],
304             # Long enough silence block could appear after b1 ==> b2...
305             b3 => [sub { my @b = map [@$_], @{shift()}; # Deep copy
306             my $min_sil_mrg = shift;
307             for my $t (@b) {
308             $t->[0] = SILENCE, next
309             if $t->[0] == NOISE and $t->[2] >= $min_sil_mrg;
310             } # Need to merge similar blocks???
311             merge_blocks \@b }, 'b2', 'min_silence_chunks_merge'],
312             # All undecided are signal unless between two silence intervals
313             b4 => [sub { my @b = map [@$_], @{shift()}; # Deep copy
314             my ($left, $c) = (SILENCE, -1);
315             while (++$c < @b) {
316             my $this = $b[$c][0];
317             $left = $this, next if $this == SILENCE or $this == SOUND;
318             # Found undecided, force to SOUND unless between two SILENCE
319             $b[$c][0] = SOUND, next if $left == SOUND;
320             # $left is SILENCE, need to check the right one...
321             my ($right, $cr) = (SILENCE, $c);
322             while (++$cr < @b) {
323             my $r = $b[$cr][0];
324             $right = $r, last if $r == SILENCE or $r == SOUND;
325             }
326             $b[$c++][0] = $right while $c < $cr;
327             $left = $right;
328             } # After ignoring, need to merge similar blocks
329             merge_blocks \@b }, 'b3'],
330             # Final enlargement of signal
331             b => [sub { my @b = map [@$_], @{shift()}; # Deep copy
332             my ($ign_pre, $ign_pre_rel, $ign_post, $ign_post_rel) = (shift, shift, shift, shift);
333             my ($meds, $thres_factor) = (shift, shift);
334             my ($ext_beg, $ext_end) = (shift, shift);
335             my ($min_silence, $min_silence_s, $min_silence_e) = (shift, shift, shift);
336             my $c = -1;
337             for my $b (@b) {
338             ++$c;
339             next unless $b->[0] == SILENCE;
340             my $pre = rnd($ign_pre + $ign_pre_rel * $b->[2]);
341             my $post = rnd($ign_post + $ign_post_rel * $b->[2]);
342             my $ilen = $pre + $post;
343             next unless $b->[2] > $ilen;
344             my $s = $b->[1];
345             my $av = double_sum( $meds->[0], $s + $pre, $b->[2] - $ilen ) / ($b->[2] - $ilen);
346             $av *= $thres_factor*$thres_factor;
347              
348             my $e = $s + $b->[2];
349             if ($c) { # Not for the "leading gap"
350             while ($s < $e) {
351             my $lev = unpack 'd',
352             substr $meds->[0], $s*$double_size, $double_size;
353             last if $lev <= $av;
354             $s++;
355             }
356             my $add = $e - $s;
357             $add = $ext_end if $add > $ext_end;
358             $s += $add;
359             $b[$c-1]->[2] += $s - $b->[1];
360             $b->[2] -= $s - $b->[1];
361             $b->[1] += $s - $b->[1];
362             }
363             if ($c != @b-1) {
364             my $e_ini = $e;
365             while ($s < $e) {
366             my $lev = unpack 'd',
367             substr $meds->[0], ($e-1)*$double_size, $double_size;
368             last if $lev <= $av;
369             $e--;
370             }
371             my $add = $e - $s;
372             $add = $ext_beg if $add > $ext_beg;
373             $e -= $add;
374             $b[$c+1]->[2] += $e_ini - $e;
375             $b[$c+1]->[1] -= $e_ini - $e;
376             $b->[2] -= $e_ini - $e;
377             }
378             my $min_sil = ($c == 0 ? $min_silence_s :
379             ($c == $#b ? $min_silence_e : $min_silence));
380             $b->[0] = SOUND if $b->[2] < $min_sil;
381             } # After ignoring short silence, need to merge similar blocks
382             merge_blocks \@b
383             }, 'b4', 'local_level_ignore_pre_chunks', 'local_level_ignore_pre_rel',
384             'local_level_ignore_post_chunks', 'local_level_ignore_post_rel',
385             'medians', 'local_threshold_factor', 'extend_track_begin_chunks',
386             'extend_track_end_chunks', 'min_actual_silence_chunks',
387             'min_start_silence_chunks', 'min_end_silence_chunks'],
388             );
389              
390             my %recipes = (
391             map(($_ => {default => $defaults{$_}}), keys %defaults),
392             map(($_ => {filter => [sub {shift}, $mirror_from{$_}]}), keys %mirror_from),
393             %chunk_times,
394             map( ($_ => {default => undef}),
395             @recognized),
396             map(($_ => {filter => $filters{$_}}), keys %filters),
397             map(($_ => {prerequisites => ['rms_data']}), 'chunks', 'min', 'max'),
398             fh => {self_filter =>
399             [sub { my ($self, $cmd) = (shift, shift); local *FH;
400             if ($cmd) { $cmd = '"' . join('" "', @$cmd) . '"';
401             open FH, "$cmd |" or die "pipe open($cmd) error: $!";
402             } else {
403             my $filename = shift;
404             return \*STDIN unless defined $filename;
405             open FH, "< $filename" or die "open($filename) error: $!";
406             }
407             $self->set(close_fh => 1) unless $self->already_set('close_fh');
408             return *FH }, 'preprocess_input', 'filename']},
409             rms_data => { oo_output => sub {
410             my $s = shift;
411             my $d = $s->get('rms_data_arr_f');
412             if (defined $d) {
413             $s->set(chunks => $d->[2]);
414             return $d;
415             }
416             return read_averages($s);
417             }},
418             );
419              
420 1     1   421 sub __s_size() {length pack "d2 ${long}2", 0, 0, 0, 0}
421              
422             sub read_averages ($) {
423 0     0 0 0 my $self = shift;
424 0         0 my $fh = $self->get('fh_bin');
425 0         0 my $vals = {};
426 0 0       0 $vals = wav_eat_header($fh) unless $self->get('raw_pcm');
427 0 0       0 if ($self->get('override_header_info')) {
428 0         0 for my $k (keys %$vals) {
429 0 0       0 $self->set($k => $vals->{$k}) unless $self->already_set($k)
430             }
431             } else {
432 0         0 for my $k (keys %$vals) {
433 0         0 $self->set($k => $vals->{$k})
434             }
435             }
436 0         0 my $out_fh = $self->get('out_fh_bin');
437 0         0 my $buf = $vals->{buf};
438 0 0 0     0 syswrite $out_fh, $buf or die "Error duping output: $!"
      0        
439             if $out_fh and $vals->{header}; # in PCM mode we write later
440 0 0       0 my $off = ($vals->{header} ? 0 : length $buf);
441 0         0 my @stats = (pack "d2 ${long}2", 0, 0, $long_max, $long_min) x $self->get('channels');
442              
443 0         0 my $read = $self->get('bytes_per_chunk') - $off;
444 0         0 my $rem = $self->get('sizedata');
445 0 0       0 $rem = MY_INF if $rem == 0x7fffffff; # Lame puts this sometimes...
446 0 0       0 defined (my $cnt = read $fh, $buf, $read, $off)
447             or die "Error reading the first chunk: $!";
448 0 0 0     0 syswrite $out_fh, $buf or die "Error duping output: $!"
449             if $out_fh;
450 0         0 $rem -= $cnt;
451 0 0 0     0 die "short read" unless $rem <= 0 or $rem == MY_INF or $cnt == $read;
      0        
452 0         0 my @d = '';
453 0         0 my ($c, $b_p_s, $channels, $subchunk, $b_p_c) =
454             (0, map $self->get($_), qw(bytes_per_sample channels subchunk_size bytes_per_chunk));
455 0         0 while (1) {
456 0 0       0 my $p = le_short_sample_multichannel($b_p_s, 2, $channels, \@stats,
457             $subchunk, $buf) or last;
458 0         0 my $max_level = 0;
459 0         0 for my $s (@stats) { # Take maximum per channel
460 0         0 my $level = unpack 'd', $s;
461 0 0       0 $max_level = $level if $max_level < $level;
462 0         0 substr($s, 0, 2*$double_size) = pack 'd2', 0, 0; # Reset per-chunk sums
463             }
464 0         0 $d[0] .= pack 'd', $max_level / $p;
465 0         0 $c++;
466             #warn "avg = ", $sum_square / $p / @stats;
467 0 0       0 last unless $rem;
468 0 0       0 defined ($cnt = read $fh, $buf, $b_p_c)
469             or die "Error reading: $!";
470 0         0 $rem -= $cnt;
471 0 0 0     0 die "short read: rem=$rem, cnt=$cnt, b_p_c=$b_p_c" unless $rem <= 0 or $rem == MY_INF or $cnt == $b_p_c;
      0        
472 0 0 0     0 syswrite $out_fh, $buf or die "Error duping output: $!"
      0        
473             if $cnt and $out_fh;
474 0 0       0 last unless $cnt;
475             }
476 0 0 0     0 close $fh or die "Error closing input: $!" if $self->get('close_fh');
477 0         0 $self->set(chunks => $c);
478 0         0 $c = 0;
479 0         0 my (@min, @max);
480 0         0 for my $s (@stats) { # Take maximum per channel
481 0         0 (undef, undef, my $min, my $max) = unpack "d2 ${long}2", $s;
482 0         0 $min[$c] = $min;
483 0         0 $max[$c++] = $max;
484             }
485 0         0 $self->set(min => \@min);
486 0         0 $self->set(max => \@max);
487 0 0       0 if ($self->get('write_to_rms_file')) {
488 0         0 local *RMS;
489 0         0 local $\ = '';
490 0         0 my $f = $self->get('rms_filename_actual');
491 0 0       0 open RMS, "> $f"
492             or die "Can't open RMS file `$f' for write: $!";
493 0         0 binmode RMS;
494 0         0 print RMS "GramoFile Binary RMS Data\n";
495 0         0 print RMS pack "${long}2", map $self->get($_), qw(samples_per_chunk chunks);
496 0         0 print RMS $d[0];
497 0 0       0 close RMS or die "closing RMS file `$f' for write: $!";
498             }
499             #print "lev=$_" for map sqrt, unpack 'd*', $opts->{avgs};
500 0         0 push @d, $self->get('samples_per_chunk'), $c;
501             \@d
502 0         0 }
503              
504             sub format_hms ($) {
505 1386     1386 0 2473 my $t = shift;
506 1386         1751 my $h = int($t/3600);
507 1386         1977 my $m = int(($t - 3600*$h)/60);
508 1386         7240 my $s = $t - 3600*$h - $m*60;
509 1386 100 66     10612 $s = ($h || $m) ? (sprintf '%04.1f', $s) : sprintf '%3.1f', $s;
510 1386 100       3358 $m = $h ? (sprintf '%02dm', $m) : ( $m ? "${m}m" : '');
    50          
511 1386 50       1988 $h = $h ? "${h}h" : '';
512 1386         298814 "$h$m$s"
513             }
514              
515             my @represent = ('', ':', '>');
516              
517             sub output_level ($$;$) {
518 1386     1386 0 2244 my ($n, $d, $l) = (shift, shift, shift);
519 1386         2717 my $db = 10*log(($l * 2)/(1<<30))/log(10); # Max amplitude sine wave = 0db
520 1386         1676 my $l2 = sqrt($l);
521 1386         2217 $db = sprintf "%.0f", $db;
522 1386         3362 my $s = '#' x (($db+96)/3) . $represent[$db % 3];
523 1386         2717 printf "%6d:%11s:%7.1f=%4.0fdB: %s\n", $n, format_hms($n*$d), sqrt($l), $db, $s;
524             }
525              
526             sub output_levels ($;$) {
527 2     2 1 593 my ($self, $what) = (shift, shift);
528 2         8 local $\ = "";
529 2   50     11 $what ||= 'rms_data'; # 1-element array with a 'd'-packed elt
530 2         4 my ($opts,$o) = {};
531 2         6 for $o ($what, qw(frequency bytes_per_sample channels sec_per_chunk
532             bytes_per_chunk)) {
533 12         154 $opts->{$o} = $self->get($o);
534             }
535 2         15 for $o (qw(min max)) { # Not available from RMS cache
536 4         73 eval { $opts->{$o} = $self->get($o) };
  4         9  
537             }
538 2         78 print <
539             Frequency: $opts->{frequency}. Stride: $opts->{bytes_per_sample}; $opts->{channels} channels.
540             Chunk=$opts->{sec_per_chunk}sec=$opts->{bytes_per_chunk}bytes.
541             EOP
542 2         6 for my $c (0..$opts->{channels}-1) {
543 4 50       15 next unless $opts->{min};
544 0 0       0 print "\t" if $c;
545 0         0 my @l = map $opts->{$_}[$c], 'min', 'max';
546 0         0 my @db = map 20*log(abs($_)/(1<<15))/log(10), @l;
547 0         0 printf "ch%d: %.1f .. %.1f (%.0fdB;%.0fdB).", $c, @l, @db;
548             }
549 2         4 print "\n";
550 2         4 my $n = 0;
551 2         88 output_level($n++, $opts->{sec_per_chunk}, $_) for unpack 'd*', $opts->{$what}[0];
552 2         72 $self;
553             }
554              
555             sub output_blocks ($;$) {
556 2     2 1 797 my $self = shift;
557 2         5 my $opts = shift;
558 2         3 my $type = 'b';
559 2         7 local $\ = "";
560 2 50 33     8 if ($opts and not ref $opts) {
561 0         0 $type = $opts;
562 0         0 $opts = {};
563             }
564 2   50     46 $opts ||= {};
565 2         9 my %opts = (format => 'long', %$opts);
566 2   33     13 my $blocks = $self->get(shift || $type);
567 2         27 my $l = $self->get('sec_per_chunk');
568 2 50       33 printf "# threshold: %s (in %s .. %s)\n",
569             map $self->get($_), qw(threshold threshold_min threshold_max)
570             if $opts{format} eq 'long';
571 2         94 my ($gap, $c, $b) = (0, 0);
572 2         5 for $b (@$blocks) {
573 2 50       16 $gap = $b->[2] * $l, next if $b->[0] < 0;
574 0 0       0 printf("%s\t=%s\t# %s len=%s\n",
575             $b->[1] * $l, ($b->[1] + $b->[2]) * $l, ++$c, $b->[2] * $l), next
576             if $opts{format} eq 'short';
577 0         0 printf "%s\t=%s\t# n=%s duration %s; gap %s (%s .. %s; %s)\n",
578             $b->[1] * $l, ($b->[1] + $b->[2]) * $l, ++$c,
579             $b->[2] * $l, $gap,
580             format_hms($b->[1] * $l), format_hms(($b->[1] + $b->[2]) * $l), format_hms($b->[2] * $l);
581             }
582             }
583              
584             my $splitter_loaded;
585              
586             sub split_file ($;$$) {
587 0     0 1 0 my ($self, $opt) = (shift, shift);
588 0   0     0 my $blocks = $self->get(shift || 'b');
589 0         0 my $t = $self->get('input_type');
590 0 0 0     0 die "Only MP3 split supported" unless $t and $t eq 'mp3';
591 0         0 my $l = $self->get('sec_per_chunk');
592 0 0       0 my @req = map [$_->[1] * $l, $_->[2] * $l], grep $_->[0] > 0, @$blocks
593             or return;
594 0         0 require MP3::Splitter;
595 0 0 0     0 die "MP3::Splitter v0.02 required"
596             if !$splitter_loaded++ and 0.02 > MP3::Splitter->VERSION;
597 0   0     0 MP3::Splitter::mp3split($self->get('filename'), $opt || {}, @req);
598 0         0 $self;
599             }
600              
601             sub new {
602 2     2 1 384 my $class = shift;
603 2         25 my $s = new Data::Flow \%recipes;
604 2         31 $s->set(@_);
605 2         47 bless \$s, $class;
606             }
607 0     0 1 0 sub set ($$$) { ${$_[0]}->set($_[1],$_[2]); $_[0] }
  0         0  
  0         0  
608 46     46 1 3602 sub get ($$) { ${$_[0]}->get($_[1]) }
  46         152  
609              
610             my @exchange = qw(chunks rms_data medians sorted channels min max
611             frequency bytes_per_sample sec_per_chunk bytes_per_chunk);
612              
613             sub get_rmsinfo ($) {
614 0     0 1   my $i = ${$_[0]};
  0            
615 0           map $i->get($_), @exchange;
616             }
617              
618             sub set_rmsinfo ($@) {
619 0     0 1   my ($self, %h) = shift;
620 0           @h{@exchange} = @_;
621 0           map $$self->set($_, $h{$_}), @exchange;
622 0           $self
623             }
624              
625             1;
626             __END__