File Coverage

blib/lib/Ogg/Vorbis/Header/PurePerl.pm
Criterion Covered Total %
statement 193 240 80.4
branch 52 92 56.5
condition 11 23 47.8
subroutine 18 19 94.7
pod 5 7 71.4
total 279 381 73.2


line stmt bran cond sub pod time code
1             package Ogg::Vorbis::Header::PurePerl;
2              
3 3     3   140890 use 5.006;
  3         28  
4 3     3   19 use strict;
  3         6  
  3         75  
5 3     3   16 use warnings;
  3         8  
  3         119  
6              
7             # First four bytes of stream are always OggS
8 3     3   18 use constant OGGHEADERFLAG => 'OggS';
  3         6  
  3         7475  
9              
10             our $VERSION = '1.03';
11              
12             sub new {
13 4     4 1 2169 my $class = shift;
14 4         10 my $file = shift;
15              
16 4         10 my %data = ();
17              
18 4 100       16 if (ref $file) {
19 1         4 binmode $file;
20              
21 1         14 %data = (
22             'filesize' => -s $file,
23             'fileHandle' => $file,
24             );
25              
26             } else {
27              
28 3 100       137 open my $fh, '<', $file or do {
29 1         39 warn "$class: File $file does not exist or cannot be read: $!";
30 1         17 return undef;
31             };
32              
33             # make sure dos-type systems can handle it...
34 2         9 binmode $fh;
35              
36 2         33 %data = (
37             'filename' => $file,
38             'filesize' => -s $file,
39             'fileHandle' => $fh,
40             );
41             }
42              
43 3 50       16 if ( _init(\%data) ) {
44 3         13 _load_info(\%data);
45 3         10 _load_comments(\%data);
46 3         10 _calculate_track_length(\%data);
47             }
48              
49 3         32 undef $data{'fileHandle'};
50              
51 3         28 return bless \%data, $class;
52             }
53              
54             sub info {
55 3     3 1 8 my $self = shift;
56 3         5 my $key = shift;
57              
58             # if the user did not supply a key, return the entire hash
59 3 100       17 return $self->{'INFO'} unless $key;
60              
61             # otherwise, return the value for the given key
62 1         7 return $self->{'INFO'}{lc $key};
63             }
64              
65             sub comment_tags {
66 1     1 1 3 my $self = shift;
67              
68 1         3 my %keys = ();
69              
70 1         2 return grep { !$keys{$_}++ } @{$self->{'COMMENT_KEYS'}};
  3         13  
  1         4  
71             }
72              
73             sub comment {
74 4     4 1 10 my $self = shift;
75 4         8 my $key = shift;
76              
77             # if the user supplied key does not exist, return undef
78 4 50       16 return undef unless($self->{'COMMENTS'}{lc $key});
79              
80             return wantarray
81 1         5 ? @{$self->{'COMMENTS'}{lc $key}}
82 4 100       22 : $self->{'COMMENTS'}{lc $key}->[0];
83             }
84              
85             sub path {
86 0     0 1 0 my $self = shift;
87              
88 0         0 return $self->{'fileName'};
89             }
90              
91             # "private" methods
92             sub _init {
93 3     3   6 my $data = shift;
94              
95             # check the header to make sure this is actually an Ogg-Vorbis file
96 3   50     12 $data->{'startInfoHeader'} = _check_header($data) || return undef;
97            
98 3         12 return 1;
99             }
100              
101             sub _skip_id3_header {
102 3     3   6 my $fh = shift;
103              
104 3         62 read $fh, my $buffer, 3;
105            
106 3         12 my $byte_count = 3;
107            
108 3 50       14 if ($buffer eq 'ID3') {
109              
110 0         0 while (read $fh, $buffer, 4096) {
111              
112 0         0 my $found;
113 0 0       0 if (($found = index($buffer, OGGHEADERFLAG)) >= 0) {
114 0         0 $byte_count += $found;
115 0         0 seek $fh, $byte_count, 0;
116 0         0 last;
117             } else {
118 0         0 $byte_count += 4096;
119             }
120             }
121              
122             } else {
123 3         33 seek $fh, 0, 0;
124             }
125              
126 3         14 return tell($fh);
127             }
128              
129             sub _check_header {
130 3     3   6 my $data = shift;
131              
132 3         8 my $fh = $data->{'fileHandle'};
133 3         7 my $buffer;
134             my $page_seg_count;
135              
136             # stores how far into the file we've read, so later reads into the file can
137             # skip right past all of the header stuff
138              
139 3         11 my $byte_count = _skip_id3_header($fh);
140            
141             # Remember the start of the Ogg data
142 3         10 $data->{startHeader} = $byte_count;
143              
144             # check that the first four bytes are 'OggS'
145 3         25 read($fh, $buffer, 27);
146              
147 3 50       22 if (substr($buffer, 0, 4) ne OGGHEADERFLAG) {
148 0         0 warn "This is not an Ogg bitstream (no OggS header).";
149 0         0 return undef;
150             }
151              
152 3         20 $byte_count += 4;
153              
154             # check the stream structure version (1 byte, should be 0x00)
155 3 50       24 if (ord(substr($buffer, 4, 1)) != 0x00) {
156 0         0 warn "This is not an Ogg bitstream (invalid structure version).";
157 0         0 return undef;
158             }
159              
160 3         7 $byte_count += 1;
161              
162             # check the header type flag
163             # This is a bitfield, so technically we should check all of the bits
164             # that could potentially be set. However, the only value this should
165             # possibly have at the beginning of a proper Ogg-Vorbis file is 0x02,
166             # so we just check for that. If it's not that, we go on anyway, but
167             # give a warning (this behavior may (should?) be modified in the future.
168 3 50       11 if (ord(substr($buffer, 5, 1)) != 0x02) {
169 0         0 warn "Invalid header type flag (trying to go ahead anyway).";
170             }
171              
172 3         5 $byte_count += 1;
173              
174             # read the number of page segments
175 3         8 $page_seg_count = ord(substr($buffer, 26, 1));
176 3         3 $byte_count += 21;
177              
178             # read $page_seg_count bytes, then throw 'em out
179 3         31 seek($fh, $page_seg_count, 1);
180 3         8 $byte_count += $page_seg_count;
181              
182             # check packet type. Should be 0x01 (for indentification header)
183 3         21 read($fh, $buffer, 7);
184 3 50       31 if (ord(substr($buffer, 0, 1)) != 0x01) {
185 0         0 warn "Wrong vorbis header type, giving up.";
186 0         0 return undef;
187             }
188              
189 3         9 $byte_count += 1;
190              
191             # check that the packet identifies itself as 'vorbis'
192 3 50       12 if (substr($buffer, 1, 6) ne 'vorbis') {
193 0         0 warn "This does not appear to be a vorbis stream, giving up.";
194 0         0 return undef;
195             }
196              
197 3         6 $byte_count += 6;
198              
199             # at this point, we assume the bitstream is valid
200 3         22 return $byte_count;
201             }
202              
203             sub _load_info {
204 3     3   7 my $data = shift;
205              
206 3         6 my $start = $data->{'startInfoHeader'};
207 3         8 my $fh = $data->{'fileHandle'};
208              
209 3         5 my $byte_count = $start + 23;
210 3         8 my %info = ();
211              
212 3         30 seek($fh, $start, 0);
213              
214             # read the vorbis version
215 3         29 read($fh, my $buffer, 23);
216 3         28 $info{'version'} = _decode_int(substr($buffer, 0, 4, ''));
217              
218             # read the number of audio channels
219 3         12 $info{'channels'} = ord(substr($buffer, 0, 1, ''));
220              
221             # read the sample rate
222 3         11 $info{'rate'} = _decode_int(substr($buffer, 0, 4, ''));
223              
224             # read the bitrate maximum
225 3         12 $info{'bitrate_upper'} = _decode_int(substr($buffer, 0, 4, ''));
226              
227             # read the bitrate nominal
228 3         13 $info{'bitrate_nominal'} = _decode_int(substr($buffer, 0, 4, ''));
229              
230             # read the bitrate minimal
231 3         10 $info{'bitrate_lower'} = _decode_int(substr($buffer, 0, 4, ''));
232              
233             # read the blocksize_0 and blocksize_1
234             # these are each 4 bit fields, whose actual value is 2 to the power
235             # of the value of the field
236 3         9 my $blocksize = substr($buffer, 0, 1, '');
237 3         10 $info{'blocksize_0'} = 2 << ((ord($blocksize) & 0xF0) >> 4);
238 3         10 $info{'blocksize_1'} = 2 << (ord($blocksize) & 0x0F);
239              
240             # read the framing_flag
241 3         10 $info{'framing_flag'} = ord(substr($buffer, 0, 1, ''));
242              
243             # bitrate_window is -1 in the current version of vorbisfile
244 3         7 $info{'bitrate_window'} = -1;
245              
246 3         6 $data->{'startCommentHeader'} = $byte_count;
247              
248 3         11 $data->{'INFO'} = \%info;
249             }
250              
251             sub _load_comments {
252 3     3   7 my $data = shift;
253              
254 3         7 my $fh = $data->{'fileHandle'};
255 3         13 my $start = $data->{'startHeader'};
256              
257 3         10 $data->{COMMENT_KEYS} = [];
258              
259             # Comment parsing code based on Image::ExifTool::Vorbis
260 3         6 my $MAX_PACKETS = 2;
261 3         5 my $done;
262 3         10 my ($page, $packets, $streams) = (0,0,0,0);
263 3         5 my ($buff, $flag, $stream, %val);
264              
265 3         34 seek $fh, $start, 0;
266              
267 3         11 while (1) {
268 9 50 33     95 if (!$done && read( $fh, $buff, 28 ) == 28) {
269             # validate magic number
270 9 50       51 unless ( $buff =~ /^OggS/ ) {
271 0         0 warn "No comment header?";
272 0         0 last;
273             }
274              
275 9         29 $flag = get8u(\$buff, 5); # page flag
276 9         22 $stream = get32u(\$buff, 14); # stream serial number
277 9 100       24 ++$streams if $flag & 0x02; # count start-of-stream pages
278 9 50       29 ++$packets unless $flag & 0x01; # keep track of packet count
279             }
280             else {
281             # all done unless we have to process our last packet
282 0 0       0 last unless %val;
283 0         0 ($stream) = sort keys %val; # take a stream
284 0         0 $flag = 0; # no continuation
285 0         0 $done = 1; # flag for done reading
286             }
287            
288             # can finally process previous packet from this stream
289             # unless this is a continuation page
290 9 100 66     37 if (defined $val{$stream} and not $flag & 0x01) {
291 3         11 _process_comments( $data, \$val{$stream} );
292 3         9 delete $val{$stream};
293             # only read the first $MAX_PACKETS packets from each stream
294 3 50       11 if ($packets > $MAX_PACKETS * $streams) {
295             # all done (success!)
296 3 50       12 last unless %val;
297             # process remaining stream(s)
298 0         0 next;
299             }
300             }
301              
302             # stop processing Ogg Vorbis if we have scanned enough packets
303 6 50 33     17 last if $packets > $MAX_PACKETS * $streams and not %val;
304            
305             # continue processing the current page
306             # page sequence number
307 6         18 my $page_num = get32u(\$buff, 18);
308              
309             # number of segments
310 6         15 my $nseg = get8u(\$buff, 26);
311              
312             # calculate total data length
313 6         13 my $data_len = get8u(\$buff, 27);
314            
315 6 50       15 if ($nseg) {
316 6 50       30 read( $fh, $buff, $nseg-1 ) == $nseg-1 or last;
317 6         27 my @segs = unpack('C*', $buff);
318             # could check that all these (but the last) are 255...
319 6         34 foreach (@segs) { $data_len += $_ }
  48         70  
320             }
321              
322 6 50       19 if (defined $page) {
323 6 50       15 if ($page == $page_num) {
324 6         12 ++$page;
325             } else {
326 0         0 warn "Missing page(s) in Ogg file\n";
327 0         0 undef $page;
328             }
329             }
330            
331             # read page data
332 6 50       44 read($fh, $buff, $data_len) == $data_len or last;
333              
334 6 50       25 if (defined $val{$stream}) {
    50          
335             # add this continuation page
336 0         0 $val{$stream} .= $buff;
337             } elsif (not $flag & 0x01) {
338             # ignore remaining pages of a continued packet
339             # ignore the first page of any packet we aren't parsing
340 6 100 66     52 if ($buff =~ /^(.)vorbis/s and ord($1) == 3) {
341             # save this page, it has comments
342 3         12 $val{$stream} = $buff;
343             }
344             }
345            
346 6 50 66     33 if (defined $val{$stream} and $flag & 0x04) {
347             # process Ogg Vorbis packet now if end-of-stream bit is set
348 0         0 _process_comments($data, \$val{$stream});
349 0         0 delete $val{$stream};
350             }
351             }
352            
353 3         12 $data->{'INFO'}{offset} = tell $fh;
354             }
355              
356             sub _process_comments {
357 3     3   10 my ( $data, $data_pt ) = @_;
358            
359 3         6 my $pos = 7;
360 3         6 my $end = length $$data_pt;
361            
362 3         9 my $num;
363             my %comments;
364            
365 3         4 while (1) {
366 9 50       30 last if $pos + 4 > $end;
367 9         27 my $len = get32u($data_pt, $pos);
368 9 50       23 last if $pos + 4 + $len > $end;
369 9         15 my $start = $pos + 4;
370 9         22 my $buff = substr($$data_pt, $start, $len);
371 9         14 $pos = $start + $len;
372 9         15 my ($tag, $val);
373 9 100       19 if (defined $num) {
374 6 50       33 $buff =~ /(.*?)=(.*)/s or last;
375 6         23 ($tag, $val) = ($1, $2);
376             } else {
377 3         6 $tag = 'vendor';
378 3         6 $val = $buff;
379 3 50       12 $num = ($pos + 4 < $end) ? get32u($data_pt, $pos) : 0;
380 3         6 $pos += 4;
381             }
382            
383 9         21 my $lctag = lc $tag;
384            
385 9         14 push @{$comments{$lctag}}, $val;
  9         27  
386 9         20 push @{$data->{COMMENT_KEYS}}, $lctag;
  9         20  
387            
388             # all done if this was our last tag
389 9 100       26 if ( !$num-- ) {
390 3         9 $data->{COMMENTS} = \%comments;
391 3         18 return 1;
392             }
393             }
394            
395 0         0 warn "format error in Vorbis comments\n";
396            
397 0         0 return 0;
398             }
399              
400             sub get8u {
401 21     21 0 40 return unpack( "x$_[1] C", ${$_[0]} );
  21         61  
402             }
403              
404             sub get32u {
405 27     27 0 69 return unpack( "x$_[1] V", ${$_[0]} );
  27         63  
406             }
407              
408             sub _calculate_track_length {
409 3     3   7 my $data = shift;
410              
411 3         8 my $fh = $data->{'fileHandle'};
412              
413             # The original author was doing something pretty lame, and was walking the
414             # entire file to find the last granule_position. Instead, let's seek to
415             # the end of the file - blocksize_0, and read from there.
416 3         5 my $len = 0;
417              
418             # Bug 1155 - Seek further back to get the granule_position.
419             # However, for short tracks, don't seek that far back.
420 3 50       20 if (($data->{'filesize'} - $data->{'INFO'}{'offset'}) > ($data->{'INFO'}{'blocksize_0'} * 2)) {
    50          
421              
422 0         0 $len = $data->{'INFO'}{'blocksize_0'} * 2;
423             } elsif ($data->{'filesize'} < $data->{'INFO'}{'blocksize_0'}) {
424 3         7 $len = $data->{'filesize'};
425             } else {
426 0         0 $len = $data->{'INFO'}{'blocksize_0'};
427             }
428              
429 3 50       14 if ($data->{'INFO'}{'blocksize_0'} == 0) {
430 0         0 print "Ogg::Vorbis::Header::PurePerl:\n";
431 0         0 warn "blocksize_0 is 0! Should be a power of 2! http://www.xiph.org/ogg/vorbis/doc/vorbis-spec-ref.html\n";
432 0         0 return;
433             }
434              
435 3         35 seek($fh, -$len, 2);
436              
437 3         18 my $buf = '';
438 3         13 my $found_header = 0;
439 3         8 my $block = $len;
440              
441             SEEK:
442 3   33     93 while ($found_header == 0 && read($fh, $buf, $len)) {
443             # search the last read $block bytes for Ogg header flag
444             # the search is conducted backwards so that the last flag
445             # is found first
446 3         47 for (my $i = $block; $i >= 0; $i--) {
447 90 100       245 if (substr($buf, $i, 4) eq OGGHEADERFLAG) {
448 3         7 substr($buf, 0, ($i+4), '');
449 3         4 $found_header = 1;
450 3         12 last SEEK;
451             }
452             }
453              
454             # already read the whole file?
455 0 0       0 last if $len == $data->{'filesize'};
456              
457 0         0 $len += $block;
458 0 0       0 $len = $data->{'filesize'} if $len > $data->{'filesize'};
459              
460 0         0 seek($fh, -$len, 2);
461             }
462              
463 3 50       9 unless ($found_header) {
464 0         0 warn "Ogg::Vorbis::Header::PurePerl: Didn't find an ogg header - invalid file?\n";
465 0         0 return;
466             }
467              
468             # stream structure version - must be 0x00
469 3 50       12 if (ord(substr($buf, 0, 1, '')) != 0x00) {
470 0         0 warn "Ogg::Vorbis::Header::PurePerl: Invalid stream structure version: " . sprintf("%x", ord($buf));
471 0         0 return;
472             }
473              
474             # absolute granule position - this is what we need!
475 3         7 substr($buf, 0, 1, '');
476              
477 3         11 my $granule_position = _decode_int(substr($buf, 0, 8, ''));
478              
479 3 50 33     11 if ($granule_position && $data->{'INFO'}{'rate'}) {
480 0         0 $data->{'INFO'}{'length'} = int($granule_position / $data->{'INFO'}{'rate'});
481 0         0 $data->{'INFO'}{'bitrate_average'} = sprintf( "%d", ( $data->{'filesize'} * 8 ) / $data->{'INFO'}{'length'} );
482             } else {
483 3         29 $data->{'INFO'}{'length'} = 0;
484             }
485             }
486              
487             sub _decode_int {
488 18     18   32 my $bytes = shift;
489              
490 18         31 my $num_bytes = length($bytes);
491 18         23 my $num = 0;
492 18         26 my $mult = 1;
493              
494 18         43 for (my $i = 0; $i < $num_bytes; $i ++) {
495              
496 84         134 $num += ord(substr($bytes, 0, 1, '')) * $mult;
497 84         158 $mult *= 256;
498             }
499              
500 18         50 return $num;
501             }
502              
503             1;
504              
505             __END__