File Coverage

blib/lib/Ogg/Vorbis/Header/PurePerl.pm
Criterion Covered Total %
statement 201 240 83.7
branch 56 92 60.8
condition 12 23 52.1
subroutine 19 19 100.0
pod 5 7 71.4
total 293 381 76.9


line stmt bran cond sub pod time code
1             package Ogg::Vorbis::Header::PurePerl;
2              
3 3     3   128201 use 5.006;
  3         27  
4 3     3   15 use strict;
  3         5  
  3         53  
5 3     3   12 use warnings;
  3         6  
  3         118  
6              
7             # First four bytes of stream are always OggS
8 3     3   18 use constant OGGHEADERFLAG => 'OggS';
  3         5  
  3         6629  
9              
10             our $VERSION = '1.04';
11              
12             sub new {
13 5     5 1 1983 my $class = shift;
14 5         10 my $file = shift;
15              
16 5         10 my %data = ();
17              
18 5 100       17 if (ref $file) {
19 1         3 binmode $file;
20              
21 1         10 %data = (
22             'filesize' => -s $file,
23             'fileHandle' => $file,
24             );
25              
26             } else {
27              
28 4 100       160 open my $fh, '<', $file or do {
29 1         26 warn "$class: File $file does not exist or cannot be read: $!";
30 1         13 return undef;
31             };
32              
33             # make sure dos-type systems can handle it...
34 3         15 binmode $fh;
35              
36 3         48 %data = (
37             'filename' => $file,
38             'filesize' => -s $file,
39             'fileHandle' => $fh,
40             );
41             }
42              
43 4 100       18 if ( _init(\%data) ) {
44 3         12 _load_info(\%data);
45 3         11 _load_comments(\%data);
46 3         9 _calculate_track_length(\%data);
47             }
48              
49 4         40 undef $data{'fileHandle'};
50              
51 4         31 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       16 return $self->{'INFO'} unless $key;
60              
61             # otherwise, return the value for the given key
62 1         6 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         12  
  1         4  
71             }
72              
73             sub comment {
74 4     4 1 10 my $self = shift;
75 4         7 my $key = shift;
76              
77             # if the user supplied key does not exist, return undef
78 4 50       17 return undef unless($self->{'COMMENTS'}{lc $key});
79              
80             return wantarray
81 1         5 ? @{$self->{'COMMENTS'}{lc $key}}
82 4 100       20 : $self->{'COMMENTS'}{lc $key}->[0];
83             }
84              
85             sub path {
86 1     1 1 3 my $self = shift;
87              
88 1         5 return $self->{'filename'};
89             }
90              
91             # "private" methods
92             sub _init {
93 4     4   7 my $data = shift;
94              
95             # check the header to make sure this is actually an Ogg-Vorbis file
96 4   100     10 $data->{'startInfoHeader'} = _check_header($data) || return undef;
97            
98 3         11 return 1;
99             }
100              
101             sub _skip_id3_header {
102 4     4   6 my $fh = shift;
103              
104 4         76 read $fh, my $buffer, 3;
105            
106 4         12 my $byte_count = 3;
107            
108 4 100       15 if ($buffer eq 'ID3') {
109              
110 1         10 while (read $fh, $buffer, 4096) {
111              
112 13         22 my $found;
113 13 50       34 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 13         61 $byte_count += 4096;
119             }
120             }
121              
122             } else {
123 3         33 seek $fh, 0, 0;
124             }
125              
126 4         14 return tell($fh);
127             }
128              
129             sub _check_header {
130 4     4   8 my $data = shift;
131              
132 4         10 my $fh = $data->{'fileHandle'};
133 4         8 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 4         11 my $byte_count = _skip_id3_header($fh);
140            
141             # Remember the start of the Ogg data
142 4         12 $data->{startHeader} = $byte_count;
143              
144             # check that the first four bytes are 'OggS'
145 4         26 read($fh, $buffer, 27);
146              
147 4 100       21 if (substr($buffer, 0, 4) ne OGGHEADERFLAG) {
148 1         11 warn "This is not an Ogg bitstream (no OggS header).";
149 1         10 return undef;
150             }
151              
152 3         16 $byte_count += 4;
153              
154             # check the stream structure version (1 byte, should be 0x00)
155 3 50       23 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       8 if (ord(substr($buffer, 5, 1)) != 0x02) {
169 0         0 warn "Invalid header type flag (trying to go ahead anyway).";
170             }
171              
172 3         6 $byte_count += 1;
173              
174             # read the number of page segments
175 3         7 $page_seg_count = ord(substr($buffer, 26, 1));
176 3         5 $byte_count += 21;
177              
178             # read $page_seg_count bytes, then throw 'em out
179 3         28 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         20 read($fh, $buffer, 7);
184 3 50       29 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         61 $byte_count += 1;
190              
191             # check that the packet identifies itself as 'vorbis'
192 3 50       18 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         17 return $byte_count;
201             }
202              
203             sub _load_info {
204 3     3   5 my $data = shift;
205              
206 3         7 my $start = $data->{'startInfoHeader'};
207 3         6 my $fh = $data->{'fileHandle'};
208              
209 3         6 my $byte_count = $start + 23;
210 3         6 my %info = ();
211              
212 3         29 seek($fh, $start, 0);
213              
214             # read the vorbis version
215 3         28 read($fh, my $buffer, 23);
216 3         34 $info{'version'} = _decode_int(substr($buffer, 0, 4, ''));
217              
218             # read the number of audio channels
219 3         11 $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         13 $info{'bitrate_upper'} = _decode_int(substr($buffer, 0, 4, ''));
226              
227             # read the bitrate nominal
228 3         19 $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         9 $info{'blocksize_1'} = 2 << (ord($blocksize) & 0x0F);
239              
240             # read the framing_flag
241 3         8 $info{'framing_flag'} = ord(substr($buffer, 0, 1, ''));
242              
243             # bitrate_window is -1 in the current version of vorbisfile
244 3         11 $info{'bitrate_window'} = -1;
245              
246 3         6 $data->{'startCommentHeader'} = $byte_count;
247              
248 3         9 $data->{'INFO'} = \%info;
249             }
250              
251             sub _load_comments {
252 3     3   6 my $data = shift;
253              
254 3         5 my $fh = $data->{'fileHandle'};
255 3         7 my $start = $data->{'startHeader'};
256              
257 3         8 $data->{COMMENT_KEYS} = [];
258              
259             # Comment parsing code based on Image::ExifTool::Vorbis
260 3         5 my $MAX_PACKETS = 2;
261 3         5 my $done;
262 3         8 my ($page, $packets, $streams) = (0,0,0,0);
263 3         6 my ($buff, $flag, $stream, %val);
264              
265 3         30 seek $fh, $start, 0;
266              
267 3         8 while (1) {
268 9 50 33     85 if (!$done && read( $fh, $buff, 28 ) == 28) {
269             # validate magic number
270 9 50       40 unless ( $buff =~ /^OggS/ ) {
271 0         0 warn "No comment header?";
272 0         0 last;
273             }
274              
275 9         23 $flag = get8u(\$buff, 5); # page flag
276 9         28 $stream = get32u(\$buff, 14); # stream serial number
277 9 100       23 ++$streams if $flag & 0x02; # count start-of-stream pages
278 9 50       20 ++$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         7 delete $val{$stream};
293             # only read the first $MAX_PACKETS packets from each stream
294 3 50       8 if ($packets > $MAX_PACKETS * $streams) {
295             # all done (success!)
296 3 50       18 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     16 last if $packets > $MAX_PACKETS * $streams and not %val;
304            
305             # continue processing the current page
306             # page sequence number
307 6         11 my $page_num = get32u(\$buff, 18);
308              
309             # number of segments
310 6         11 my $nseg = get8u(\$buff, 26);
311              
312             # calculate total data length
313 6         9 my $data_len = get8u(\$buff, 27);
314            
315 6 50       13 if ($nseg) {
316 6 50       25 read( $fh, $buff, $nseg-1 ) == $nseg-1 or last;
317 6         26 my @segs = unpack('C*', $buff);
318             # could check that all these (but the last) are 255...
319 6         32 foreach (@segs) { $data_len += $_ }
  48         65  
320             }
321              
322 6 50       15 if (defined $page) {
323 6 50       21 if ($page == $page_num) {
324 6         9 ++$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       36 read($fh, $buff, $data_len) == $data_len or last;
333              
334 6 50       22 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         9 $val{$stream} = $buff;
343             }
344             }
345            
346 6 50 66     26 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         5 my $pos = 7;
360 3         6 my $end = length $$data_pt;
361            
362 3         7 my $num;
363             my %comments;
364            
365 3         11 while (1) {
366 9 50       20 last if $pos + 4 > $end;
367 9         16 my $len = get32u($data_pt, $pos);
368 9 50       22 last if $pos + 4 + $len > $end;
369 9         20 my $start = $pos + 4;
370 9         20 my $buff = substr($$data_pt, $start, $len);
371 9         14 $pos = $start + $len;
372 9         14 my ($tag, $val);
373 9 100       19 if (defined $num) {
374 6 50       27 $buff =~ /(.*?)=(.*)/s or last;
375 6         19 ($tag, $val) = ($1, $2);
376             } else {
377 3         6 $tag = 'vendor';
378 3         6 $val = $buff;
379 3 50       14 $num = ($pos + 4 < $end) ? get32u($data_pt, $pos) : 0;
380 3         7 $pos += 4;
381             }
382            
383 9         20 my $lctag = lc $tag;
384            
385 9         15 push @{$comments{$lctag}}, $val;
  9         23  
386 9         15 push @{$data->{COMMENT_KEYS}}, $lctag;
  9         21  
387            
388             # all done if this was our last tag
389 9 100       22 if ( !$num-- ) {
390 3         8 $data->{COMMENTS} = \%comments;
391 3         8 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 37 return unpack( "x$_[1] C", ${$_[0]} );
  21         55  
402             }
403              
404             sub get32u {
405 27     27 0 48 return unpack( "x$_[1] V", ${$_[0]} );
  27         64  
406             }
407              
408             sub _calculate_track_length {
409 3     3   6 my $data = shift;
410              
411 3         6 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         6 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       37 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         8 $len = $data->{'filesize'};
425             } else {
426 0         0 $len = $data->{'INFO'}{'blocksize_0'};
427             }
428              
429 3 50       16 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         11 my $buf = '';
438 3         5 my $found_header = 0;
439 3         5 my $block = $len;
440              
441             SEEK:
442 3   33     49 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         57 for (my $i = $block; $i >= 0; $i--) {
447 90 100       551 if (substr($buf, $i, 4) eq OGGHEADERFLAG) {
448 3         8 substr($buf, 0, ($i+4), '');
449 3         6 $found_header = 1;
450 3         7 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       11 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         8 substr($buf, 0, 1, '');
476              
477 3         10 my $granule_position = _decode_int(substr($buf, 0, 8, ''));
478              
479 3 50 33     10 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         25 $data->{'INFO'}{'length'} = 0;
484             }
485             }
486              
487             sub _decode_int {
488 18     18   33 my $bytes = shift;
489              
490 18         25 my $num_bytes = length($bytes);
491 18         24 my $num = 0;
492 18         23 my $mult = 1;
493              
494 18         38 for (my $i = 0; $i < $num_bytes; $i ++) {
495              
496 84         129 $num += ord(substr($bytes, 0, 1, '')) * $mult;
497 84         143 $mult *= 256;
498             }
499              
500 18         39 return $num;
501             }
502              
503             1;
504              
505             __END__