File Coverage

blib/lib/MP3/Splitter.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1             package MP3::Splitter;
2              
3 1     1   17565 use 5.005;
  1         4  
  1         41  
4 1     1   6 use strict;
  1         2  
  1         50  
5              
6             require Exporter;
7 1     1   8 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
  1         7  
  1         154  
8             @ISA = qw(Exporter);
9              
10             # Items to export into callers namespace by default. Note: do not export
11             # names by default without a very good reason. Use EXPORT_OK instead.
12             # Do not simply export all your public functions/methods/constants.
13              
14             # This allows declaration use MP3::Splitter ':all';
15             # If you do not need this, moving things directly into @EXPORT or @EXPORT_OK
16             # will save memory.
17             %EXPORT_TAGS = ( 'all' => [ qw(
18             mp3split
19             mp3split_read
20             ) ] );
21              
22             @EXPORT = ( @{ $EXPORT_TAGS{'all'} } );
23              
24             @EXPORT_OK = qw(
25            
26             );
27              
28             $VERSION = '0.04';
29              
30             # Preloaded methods go here.
31              
32 1     1   1506 use MPEG::Audio::Frame 0.04;
  0            
  0            
33             die "This version of MPEG::Audio::Frame unsupported"
34             if 0.07 == MPEG::Audio::Frame->VERSION;
35              
36             sub piece_open ($$$$$) {
37             my ($fname, $piece, $track, $Xing, $opts) = @_;
38             my $callback = $piece->[2] || $opts->{name_callback};
39             my $name = &$callback($track, $fname, $piece, $Xing, $opts);
40             local *OUT;
41             die "file `$name' exists" if not $opts->{overwrite} and -f $name;
42             open OUT, "> $name" or die "open `$name' for write: $!";
43             binmode OUT or die;
44             ($name, *OUT); # Ouch!
45             }
46              
47             sub make_sec ($) {
48             my $t = shift;
49             my ($h, $m, $s) =
50             $t =~ /^(?:([\d.]+)(?:h|:(?=.*[m:])))?(?:([\d.]+)[m:])?(?:([\d.]+)s?)?$/
51             or die "Unexpected format of time: `$t'";
52             for my $p ($h, $m, $s) {
53             next unless defined $p;
54             $p =~ /^(\d+\.?|\d*\.\d+)$/ or die "Unexpected format of time: `$t'";
55             }
56             ($h || 0) * 3600 + ($m || 0) * 60 + ($s || 0);
57             }
58              
59             sub MY_INF () {1e100}
60              
61             sub piece_decl ($$;@) {
62             my ($start, $end, %piece_opts) = @{shift()};
63             my $was = shift;
64             (my $rel_start, $start) = $start =~ /^(>?)(.*)/;
65             (my $abs_end, $end) = $end =~ /^(=?)(.*)/;
66             $start = make_sec $start;
67             $start += $was if $rel_start;
68             if ($end eq 'INF') {
69             if (@_) {
70             $end = make_sec $_[0][0]; # Start of the next chunk
71             } else { # Go to the end of the file
72             $end = MY_INF; # Soft infinity
73             $piece_opts{lax} = $end;
74             }
75             } else {
76             $end = make_sec $end unless $end eq MY_INF;
77             $end += $start unless $abs_end;
78             }
79             ($start, $end, %piece_opts);
80             }
81              
82             sub _Xing ($) { # Guesswork... What is the correct \0* ?
83             my $f = shift;
84             my $c = $f->content;
85             $c =~ /^(\0{4,}(Xing|Info))(.{112})/s or return;
86             length($1)+4, $2, unpack 'N3 C100', $3; # FramesOffset, Type, Flags, Frames, Bytes, Offsets
87             }
88              
89             sub _Xing_h ($$$$$$$) {
90             my ($Xing, $frames_off, $frames, $bytes, $time, $end, $off) = @_;
91             my @o;
92             if ($end >= MY_INF) { # [time, frames, pos] - know it's a final write
93             # Need to interpolate
94             my ($last_time, $last_frac, $i) = (0, 0);
95             for $i (@$off) {
96             my $this_time = $i->[0]/$time * 100;
97             next if $this_time == $last_time;
98             my $this_frac = $i->[2]/$bytes * 256;
99             while (@o <= $this_time) { # Fuzz ok: actually need only 99 of 100
100             push @o, $last_frac
101             + ($this_frac - $last_frac)*(@o - $last_time)/($this_time - $last_time);
102             }
103             $last_time = $this_time;
104             $last_frac = $this_frac;
105             }
106             } elsif (@$off) {
107             @o = map int($_->[2]/$bytes*256), @$off;
108             } else { # Before writing, assume linear flow
109             @o = map $_*256/100, 0..99;
110             }
111             @o = map { $_ > 255 ? 255 : $_ } @o[0..99];
112             my $c = $Xing->content;
113             substr($c, $frames_off, 108) = pack 'N2 C100', $frames, $bytes, @o;
114             my $crc = $Xing->crc;
115             $crc = '' unless defined $crc;
116             $Xing->header() . $crc . $c;
117             }
118              
119             sub mp3split ($;@) {
120             my $f = shift;
121              
122             return unless @_; # Nothing to do
123             my %opts = ( lax => 0.02, # close to 1/75 - tolerate terminations that early
124             verbose => 0, append => sub{''}, prepend => sub{''},
125             name_callback => sub {sprintf "%02d_%s", shift, shift},
126             after_write => sub {}, keep_Xing => 1, update_Xing => 1,
127             );
128             %opts = (%opts, %{shift()}) unless 'ARRAY' eq ref $_[0];
129             return unless @_; # Nothing to do
130              
131             local *F;
132             open F, $f or die("open `$f': $!");
133             binmode F;
134              
135             my $frame;
136             my $trk = 0; # Before first track
137              
138             my ($frames, $piece_frames) = (0, 0); # frames written
139             my ($ttime, $ptime) = (0,0); # total and piece time
140              
141             my $piece = shift or return; # start, duration, name-callback, finish-callback, user-data
142             my ($start, $end, %piece_opts) = (0, 0);
143             ($start, $end, %piece_opts) = piece_decl $piece, $end, @_;
144             %piece_opts = (%opts, %piece_opts);
145              
146             my ($outf, $out, $finished); # output file and its name, etc
147             my ($Xing, $Xing_off, $av_fr, $vbr, $tot_len, $frt, @off, $Xing_tell, $l);
148              
149             print STDERR "`$f'\n" if $opts{verbose};
150             while ( $frame = MPEG::Audio::Frame->read(\*F) or ++$finished) {
151             # Check whether it is an Xing frame
152             if ( !$frames and !$finished
153             and ($Xing_off, undef, undef, my $fr, my $b) = _Xing($frame) ) {
154             $av_fr = $b/$fr; # Average length of a frame
155             $frt = $frame->seconds; # Depends on layer and samplerate only
156             $vbr = $av_fr/$frt/125; # kbits is 1000 bits = 1000/8 bytes
157             $tot_len = $fr * $frt;
158             $Xing = $frame;
159             printf STDERR "VBR: %.1fkBit/sec. Total: %.2fsec (from Xing header)\n", $vbr, $tot_len
160             if $piece_opts{verbose};
161             }
162             # Check what to do with this frame
163             if ( $ttime > $end or $finished ) { # Need to end the current piece
164             die "Unexpected end of piece" unless $outf;
165             my $cb = $piece_opts{append};
166              
167             my $append =
168             &$cb($f, $piece, $trk, $ttime, $ptime, $out, $frames, $piece_frames,
169             ($Xing and $piece_opts{keep_Xing}), $Xing, \%piece_opts, $outf);
170             print $outf $append or die if length $append;
171              
172             if ($Xing and $piece_opts{keep_Xing} and $piece_opts{update_Xing}) {
173             # Print actual header
174             my $pos = tell $outf;
175             seek $outf, 0, $Xing_tell or die;
176             push @off, ([$ptime, $piece_frames, $pos]) x (100 - @off)
177             if @off < 100;
178             push @off, [$ptime, $piece_frames, $pos] if $end >= MY_INF;
179             print $outf _Xing_h($Xing, $Xing_off, $piece_frames,
180             $pos, $ptime, $end, \@off);
181             }
182              
183             close $outf or die "Error closing `$out' for write: $!";
184             $cb = $piece_opts{after_write};
185             &$cb($f, $piece, $trk, $ttime, $ptime, $out, $frames, $piece_frames,
186             ($Xing and $piece_opts{keep_Xing}), $Xing, \%piece_opts);
187             undef $outf;
188             die "end of audio file before all the tracks written"
189             if $finished and (@_ or $ttime < $end - $piece_opts{lax});
190             last if $finished;
191             $piece = shift or last;
192             ($start, $end, %piece_opts) = piece_decl $piece, $end, @_;
193             %piece_opts = (%opts, %piece_opts);
194             }
195             my $len = $frame->seconds;
196             $ttime += $len;
197             $ptime += $len;
198             $frames++;
199             next if $frames == 1 and $Xing;
200             next if $ttime < $start; # Does not intersect the next interval
201              
202             # Need to write this piece
203             unless ($outf) {
204             ($out, $outf) = piece_open($f, $piece, ++$trk, $Xing, \%piece_opts);
205             $ptime = $len;
206             $piece_frames = $l = 0;
207             @off = ();
208             my $prepend =
209             &{$piece_opts{prepend}}($trk, $f, $piece, $Xing, \%piece_opts, $out, $outf);
210             print $outf $prepend or die if length $prepend;
211             if ($Xing and $piece_opts{keep_Xing}) { # Print estimated header
212             $Xing_tell = tell $outf;
213             print $outf _Xing_h($Xing, $Xing_off, ($end - $start)/$frt,
214             ($end - $start)/$frt*$av_fr, 0, 0, \@off);
215             $piece_frames++;
216             }
217             printf STDERR " %2d \@ %17s (=%8s) %s\n",
218             $trk, "$start-$end", $end-$start, $out if $piece_opts{verbose};
219             }
220              
221             # For Xing header
222             if ($end < MY_INF) {
223             my $perc = $end > $start ? int($ptime/($end-$start)*100) : -1;
224             push @off, ([$ptime, $piece_frames, tell $outf]) x ($perc - @off + 1)
225             if $perc >= @off;
226             } elsif ($l * 1.01 <= $piece_frames) {
227             push @off, [$ptime, $piece_frames, tell $outf];
228             $l = $piece_frames;
229             }
230              
231             # Copy frame data.
232             print $outf $frame->asbin;
233             $piece_frames++;
234             }
235             }
236              
237             sub mp3split_read ($$;$) {
238             my ($file, $datafile, $opts, @p) = (shift, shift, shift || {});
239             local(*IN, $_);
240             open IN, "< $datafile" or die "Can't open `$datafile' for read: $!";
241             while () {
242             next if /^\s*($|#)/;
243             /^\s*(>?[\d.hms:]+)\s+(=?([\d.hms:]+|INF))?\s*($|#)/
244             or die "unrecognized line: `$_'";
245             push @p, [$1, defined $2 ? $2 : 'INF'];
246             }
247             close IN or die "Can't close `$datafile' for read: $!";
248             mp3split($file, $opts, @p);
249             }
250              
251             1;
252             __END__