File Coverage

blib/lib/Audio/FLAC/Header.pm
Criterion Covered Total %
statement 309 386 80.0
branch 61 116 52.5
condition 24 61 39.3
subroutine 23 28 82.1
pod 9 9 100.0
total 426 600 71.0


line stmt bran cond sub pod time code
1             package Audio::FLAC::Header;
2              
3             # $Id$
4              
5 6     6   177771 use strict;
  6         14  
  6         184  
6 6     6   33 use File::Basename;
  6         11  
  6         35781  
7              
8             our $VERSION = '2.4';
9             our $HAVE_XS = 0;
10              
11             # First four bytes of stream are always fLaC
12             my $FLACHEADERFLAG = 'fLaC';
13             my $ID3HEADERFLAG = 'ID3';
14              
15             # Masks for METADATA_BLOCK_HEADER
16             my $LASTBLOCKFLAG = 0x80000000;
17             my $BLOCKTYPEFLAG = 0x7F000000;
18             my $BLOCKLENFLAG = 0x00FFFFFF;
19              
20             # Enumerated Block Types
21             my $BT_STREAMINFO = 0;
22             my $BT_PADDING = 1;
23             my $BT_APPLICATION = 2;
24             my $BT_SEEKTABLE = 3;
25             my $BT_VORBIS_COMMENT = 4;
26             my $BT_CUESHEET = 5;
27             my $BT_PICTURE = 6;
28              
29             my $VENDOR_STRING = __PACKAGE__ . " v$VERSION";
30              
31             my %BLOCK_TYPES = (
32             $BT_STREAMINFO => '_parseStreamInfo',
33             $BT_APPLICATION => '_parseAppBlock',
34             # The seektable isn't actually useful yet, and is a big performance hit.
35             # $BT_SEEKTABLE => '_parseSeekTable',
36             $BT_VORBIS_COMMENT => '_parseVorbisComments',
37             $BT_CUESHEET => '_parseCueSheet',
38             $BT_PICTURE => '_parsePicture',
39             );
40              
41             XS_BOOT: {
42             # If I inherit DynaLoader then I inherit AutoLoader
43             require DynaLoader;
44              
45             # DynaLoader calls dl_load_flags as a static method.
46             *dl_load_flags = DynaLoader->can('dl_load_flags');
47              
48             $HAVE_XS = eval {
49              
50             do {__PACKAGE__->can('bootstrap') || \&DynaLoader::bootstrap}->(__PACKAGE__, $VERSION);
51              
52             return 1;
53             };
54              
55             # Try to use the faster code first.
56             if ($HAVE_XS) {
57             *new = \&_new_XS;
58             *write = \&_write_XS;
59             } else {
60             *new = \&_new_PP;
61             *write = \&_write_PP;
62             }
63             }
64              
65             sub _new_PP {
66 7     7   11227 my ($class, $file) = @_;
67              
68             # open up the file
69 7 50       334 open(my $fh, $file) or die "[$file] does not exist or cannot be read: $!";
70              
71             # make sure dos-type systems can handle it...
72 7         28 binmode($fh);
73              
74 7         111 my $self = {
75             'fileSize' => -s $file,
76             'filename' => $file,
77             };
78              
79 7         24 bless $self, $class;
80              
81             # check the header to make sure this is actually a FLAC file
82 7   50     36 my $byteCount = $self->_checkHeader($fh) || 0;
83              
84 7 50       23 if ($byteCount <= 0) {
85              
86 0         0 close($fh);
87 0         0 die "[$file] does not appear to be a FLAC file!";
88             }
89              
90 7         39 $self->{'startMetadataBlocks'} = $byteCount;
91              
92             # Grab the metadata blocks from the FLAC file
93 7 50       27 if (!$self->_getMetadataBlocks($fh)) {
94              
95 0         0 close($fh);
96 0         0 die "[$file] Unable to read metadata from FLAC!";
97             };
98              
99             # Always set to empty hash in the case of no comments.
100 7         19 $self->{'tags'} = {};
101              
102 7         13 for my $block (@{$self->{'metadataBlocks'}}) {
  7         23  
103              
104 28   100     124 my $method = $BLOCK_TYPES{ $block->{'blockType'} } || next;
105              
106 16         69 $self->$method($block);
107             }
108              
109 7         118 close($fh);
110              
111 7         38 return $self;
112             }
113              
114             sub info {
115 14     14 1 2859 my $self = shift;
116 14         23 my $key = shift;
117              
118             # if the user did not supply a key, return a hashref
119 14 100       54 return $self->{'info'} unless $key;
120              
121             # otherwise, return the value for the given key
122 10         69 return $self->{'info'}->{$key};
123             }
124              
125             sub tags {
126 5     5 1 5056 my $self = shift;
127 5         11 my $key = shift;
128              
129             # if the user did not supply a key, return a hashref
130 5 100       28 return $self->{'tags'} unless $key;
131              
132             # otherwise, return the value for the given key
133 2         22 return $self->{'tags'}->{$key};
134             }
135              
136             sub cuesheet {
137 2     2 1 251 my $self = shift;
138              
139             # if the cuesheet block exists, return it as an arrayref
140 2 50       14 return $self->{'cuesheet'} if exists($self->{'cuesheet'});
141              
142             # otherwise, return an empty arrayref
143 0         0 return [];
144             }
145              
146             sub seektable {
147 0     0 1 0 my $self = shift;
148              
149             # if the seekpoint table block exists, return it as an arrayref
150 0 0       0 return $self->{'seektable'} if exists($self->{'seektable'});
151              
152             # otherwise, return an empty arrayref
153 0         0 return [];
154             }
155              
156             sub application {
157 1     1 1 251 my $self = shift;
158 1   50     5 my $appID = shift || "default";
159              
160             # if the application block exists, return it's content
161 1 50       7 return $self->{'application'}->{$appID} if exists($self->{'application'}->{$appID});
162              
163             # otherwise, return nothing
164 0         0 return undef;
165             }
166              
167             sub picture {
168 0     0 1 0 my $self = shift;
169 0         0 my $type = shift;
170 0 0       0 $type = 3 unless defined ($type); # defaults to front cover
171              
172 0 0       0 if ($type eq 'all') {
173 0 0       0 return $self->{'allpictures'} if exists($self->{'allpictures'});
174             }
175              
176             # Also look for other types of images
177             # http://flac.sourceforge.net/format.html#metadata_block_picture
178 0         0 my @types = ($type, 4, 0, 5..20);
179              
180             # if the picture block exists, return it's content
181 0         0 for (@types) {
182 0 0       0 return $self->{'picture'}->{$_} if exists $self->{'picture'}->{$_};
183             }
184              
185             # otherwise, return nothing
186 0         0 return undef;
187             }
188              
189             sub vendor_string {
190 1     1 1 6 my $self = shift;
191              
192 1   50     6 return $self->{'tags'}->{'VENDOR'} || '';
193             }
194              
195             sub set_vendor_string {
196 0     0 1 0 my $self = shift;
197 0   0     0 my $value = shift || $VENDOR_STRING;
198              
199 0         0 return $self->{'tags'}->{'VENDOR'} = $value;
200             }
201              
202             sub set_separator {
203 0     0 1 0 my $self = shift;
204              
205 0         0 $self->{'separator'} = shift;
206             }
207              
208             sub _write_PP {
209 1     1   10 my $self = shift;
210              
211 1         4 my @tagString = ();
212 1         2 my $numTags = 0;
213 1         2 my $numBlocks = 0;
214              
215 1         1 my ($idxVorbis,$idxPadding);
216 1         2 my $totalAvail = 0;
217 1         2 my $metadataBlocks = $FLACHEADERFLAG;
218 1         2 my $tmpnum;
219              
220             # Make a list of the tags and lengths for packing into the vorbis metadata block
221 1         2 foreach (keys %{$self->{'tags'}}) {
  1         4  
222              
223 1 50       5 unless (/^VENDOR$/) {
224 1         5 push @tagString, $_ . "=" . $self->{'tags'}{$_};
225 1         4 $numTags++;
226             }
227             }
228              
229             # Create the contents of the vorbis comment metablock with the number of tags
230 1         2 my $vorbisComment = "";
231              
232             # Vendor comment must come first.
233 1   33     13 _addStringToComment(\$vorbisComment, ($self->{'tags'}->{'VENDOR'} || $VENDOR_STRING));
234              
235 1         2 $vorbisComment .= _packInt32($numTags);
236              
237             # Finally, each tag string (with length)
238 1         3 foreach (@tagString) {
239 1         3 _addStringToComment(\$vorbisComment, $_);
240             }
241              
242             # Is there enough space for this new header?
243             # Determine the length of the old comment block and the length of the padding available
244 1         4 $idxVorbis = $self->_findMetadataIndex($BT_VORBIS_COMMENT);
245 1         4 $idxPadding = $self->_findMetadataIndex($BT_PADDING);
246              
247 1 50       4 if ($idxVorbis >= 0) {
248             # Add the length of the block
249 0         0 $totalAvail += $self->{'metadataBlocks'}[$idxVorbis]->{'blockSize'};
250             } else {
251             # Subtract 4 (min size of block when added)
252 1         2 $totalAvail -= 4;
253             }
254              
255 1 50       3 if ($idxPadding >= 0) {
256             # Add the length of the block
257 1         4 $totalAvail += $self->{'metadataBlocks'}[$idxPadding]->{'blockSize'};
258             } else {
259             # Subtract 4 (min size of block when added)
260 0         0 $totalAvail -= 4;
261             }
262              
263             # Check for not enough space to write tag without
264             # re-writing entire file (not within scope)
265 1 50       4 if ($totalAvail - length($vorbisComment) < 0) {
266 0         0 warn "Unable to write Vorbis tags - not enough header space!";
267 0         0 return 0;
268             }
269              
270             # Modify the metadata blocks to reflect new header sizes
271              
272             # Is there a Vorbis metadata block?
273 1 50       4 if ($idxVorbis < 0) {
274             # no vorbis block, so add one
275 1         3 _addNewMetadataBlock($self, $BT_VORBIS_COMMENT, $vorbisComment);
276             } else {
277             # update the vorbis block
278 0         0 _updateMetadataBlock($self, $idxVorbis, $vorbisComment);
279             }
280              
281             # Is there a Padding block?
282             # Change the padding to reflect the new vorbis comment size
283 1 50       3 if ($idxPadding < 0) {
284             # no padding block
285 0         0 _addNewMetadataBlock($self, $BT_PADDING , "\0" x ($totalAvail - length($vorbisComment)));
286             } else {
287             # update the padding block
288 1         24 _updateMetadataBlock($self, $idxPadding, "\0" x ($totalAvail - length($vorbisComment)));
289             }
290              
291 1         2 $numBlocks = @{$self->{'metadataBlocks'}};
  1         3  
292              
293             # Sort so that all the padding is at the end.
294             # Our version of FLAC__metadata_chain_sort_padding()
295 1         5 for (my $i = 0; $i < $numBlocks; $i++) {
296              
297 3         7 my $block = $self->{'metadataBlocks'}->[$i];
298              
299 3 100       11 if ($block->{'blockType'} == $BT_PADDING) {
300              
301 2 100       2 if (my $next = splice(@{$self->{'metadataBlocks'}}, $i+1, 1)) {
  2         13  
302 1         2 splice(@{$self->{'metadataBlocks'}}, $i, 1, $next);
  1         3  
303 1         1 push @{$self->{'metadataBlocks'}}, $block;
  1         4  
304             }
305             }
306             }
307              
308             # Now set the last block.
309 1         3 $self->{'metadataBlocks'}->[-1]->{'lastBlockFlag'} = 1;
310              
311             # Create the metadata block structure for the FLAC file
312 1         2 foreach (@{$self->{'metadataBlocks'}}) {
  1         3  
313 3         4 $tmpnum = $_->{'lastBlockFlag'} << 31;
314 3         4 $tmpnum |= $_->{'blockType'} << 24;
315 3         4 $tmpnum |= $_->{'blockSize'};
316 3         6 $metadataBlocks .= pack "N", $tmpnum;
317 3         32 $metadataBlocks .= $_->{'contents'};
318             }
319              
320             # open FLAC file and write new metadata blocks
321 1 50       47 open FLACFILE, "+<$self->{'filename'}" or return 0;
322 1         3 binmode FLACFILE;
323              
324             # overwrite the existing metadata blocks
325 1         23 my $ret = syswrite(FLACFILE, $metadataBlocks, length($metadataBlocks), 0);
326              
327 1         13 close FLACFILE;
328              
329 1         6 return $ret;
330             }
331              
332             # private methods to this class
333             sub _checkHeader {
334 7     7   17 my ($self, $fh) = @_;
335              
336             # check that the first four bytes are 'fLaC'
337 7 50       237 read($fh, my $buffer, 4) or return -1;
338              
339 7 100       42 if (substr($buffer,0,3) eq $ID3HEADERFLAG) {
340              
341 1         7 $self->{'ID3V2Tag'} = 1;
342              
343 1         3 my $id3size = '';
344              
345             # How big is the ID3 header?
346             # Skip the next two bytes - major & minor version number.
347 1 50       5 read($fh, $buffer, 2) or return -1;
348              
349             # The size of the ID3 tag is a 'synchsafe' 4-byte uint
350             # Read the next 4 bytes one at a time, unpack each one B7,
351             # and concatenate. When complete, do a bin2dec to determine size
352 1         5 for (my $c = 0; $c < 4; $c++) {
353 4 50       13 read($fh, $buffer, 1) or return -1;
354 4         18 $id3size .= substr(unpack ("B8", $buffer), 1);
355             }
356              
357 1         6 seek $fh, _bin2dec($id3size) + 10, 0;
358 1 50       14 read($fh, $buffer, 4) or return -1;
359             }
360              
361 7 50       35 if ($buffer ne $FLACHEADERFLAG) {
362 0         0 warn "Unable to identify $self->{'filename'} as a FLAC bitstream!\n";
363 0         0 return -2;
364             }
365              
366             # at this point, we assume the bitstream is valid
367 7         37 return tell($fh);
368             }
369              
370             sub _getMetadataBlocks {
371 7     7   15 my ($self, $fh) = @_;
372              
373 7         15 my $metadataBlockList = [];
374 7         15 my $numBlocks = 0;
375 7         34 my $lastBlockFlag = 0;
376 7         17 my $buffer;
377              
378             # Loop through all of the metadata blocks
379 7         34 while ($lastBlockFlag == 0) {
380              
381             # Read the next metadata_block_header
382 28 50       80 read($fh, $buffer, 4) or return 0;
383              
384 28         65 my $metadataBlockHeader = unpack('N', $buffer);
385              
386             # Break out the contents of the metadata_block_header
387 28         43 my $metadataBlockType = ($BLOCKTYPEFLAG & $metadataBlockHeader)>>24;
388 28         38 my $metadataBlockLength = ($BLOCKLENFLAG & $metadataBlockHeader);
389 28         41 $lastBlockFlag = ($LASTBLOCKFLAG & $metadataBlockHeader)>>31;
390              
391             # If the block size is zero go to the next block
392 28 50       55 next unless $metadataBlockLength;
393              
394             # Read the contents of the metadata_block
395 28 50       398 read($fh, my $metadataBlockData, $metadataBlockLength) or return 0;
396              
397             # Store the parts in the list
398 28         220 $metadataBlockList->[$numBlocks++] = {
399             'lastBlockFlag' => $lastBlockFlag,
400             'blockType' => $metadataBlockType,
401             'blockSize' => $metadataBlockLength,
402             'contents' => $metadataBlockData
403             };
404             }
405              
406             # Store the metadata blocks in the hash
407 7         22 $self->{'metadataBlocks'} = $metadataBlockList;
408 7         19 $self->{'startAudioData'} = tell $fh;
409              
410 7         34 return 1;
411             }
412              
413             sub _parseStreamInfo {
414 7     7   16 my ($self, $block) = @_;
415              
416 7         14 my $info = {};
417              
418             # Convert to binary string, since there's some unfriendly lengths ahead
419 7         63 my $metaBinString = unpack('B144', $block->{'contents'});
420              
421 7         17 my $x32 = 0 x 32;
422              
423 7         57 $info->{'MINIMUMBLOCKSIZE'} = unpack('N', pack('B32', substr($x32 . substr($metaBinString, 0, 16), -32)));
424 7         38 $info->{'MAXIMUMBLOCKSIZE'} = unpack('N', pack('B32', substr($x32 . substr($metaBinString, 16, 16), -32)));
425 7         38 $info->{'MINIMUMFRAMESIZE'} = unpack('N', pack('B32', substr($x32 . substr($metaBinString, 32, 24), -32)));
426 7         32 $info->{'MAXIMUMFRAMESIZE'} = unpack('N', pack('B32', substr($x32 . substr($metaBinString, 56, 24), -32)));
427              
428 7         40 $info->{'SAMPLERATE'} = unpack('N', pack('B32', substr($x32 . substr($metaBinString, 80, 20), -32)));
429 7         44 $info->{'NUMCHANNELS'} = unpack('N', pack('B32', substr($x32 . substr($metaBinString, 100, 3), -32))) + 1;
430 7         35 $info->{'BITSPERSAMPLE'} = unpack('N', pack('B32', substr($x32 . substr($metaBinString, 103, 5), -32))) + 1;
431              
432             # Calculate total samples in two parts
433 7         31 my $highBits = unpack('N', pack('B32', substr($x32 . substr($metaBinString, 108, 4), -32)));
434              
435 7         50 $info->{'TOTALSAMPLES'} = $highBits * 2 ** 32 +
436             unpack('N', pack('B32', substr($x32 . substr($metaBinString, 112, 32), -32)));
437              
438             # Return the MD5 as a 32-character hexadecimal string
439             #$info->{'MD5CHECKSUM'} = unpack('H32',substr($self->{'metadataBlocks'}[$idx]->{'contents'},18,16));
440 7         53 $info->{'MD5CHECKSUM'} = unpack('H32',substr($block->{'contents'}, 18, 16));
441              
442             # Store in the data hash
443 7         21 $self->{'info'} = $info;
444              
445             # Calculate the track times
446 7         49 my $totalSeconds = $info->{'TOTALSAMPLES'} / $info->{'SAMPLERATE'};
447              
448 7 50       49 if ($totalSeconds == 0) {
449 0         0 warn "totalSeconds is 0 - we couldn't find either TOTALSAMPLES or SAMPLERATE!\n" .
450             "setting totalSeconds to 1 to avoid divide by zero error!\n";
451              
452 0         0 $totalSeconds = 1;
453             }
454              
455 7         25 $self->{'trackTotalLengthSeconds'} = $totalSeconds;
456              
457 7         33 $self->{'trackLengthMinutes'} = int(int($totalSeconds) / 60);
458 7         22 $self->{'trackLengthSeconds'} = int($totalSeconds) % 60;
459 7         28 $self->{'trackLengthFrames'} = ($totalSeconds - int($totalSeconds)) * 75;
460 7         25 $self->{'bitRate'} = 8 * ($self->{'fileSize'} - $self->{'startAudioData'}) / $totalSeconds;
461              
462 7         19 return 1;
463             }
464              
465             sub _parseVorbisComments {
466 5     5   18 my ($self, $block) = @_;
467              
468 5         10 my $tags = {};
469 5         13 my $rawTags = [];
470              
471             # Parse out the tags from the metadata block
472 5         21 my $tmpBlock = $block->{'contents'};
473 5         52 my $offset = 0;
474              
475             # First tag in block is the Vendor String
476 5         17 my $tagLen = unpack('V', substr($tmpBlock, $offset, 4));
477 5         21 $tags->{'VENDOR'} = substr($tmpBlock, ($offset += 4), $tagLen);
478              
479             # Now, how many additional tags are there?
480 5         22 my $numTags = unpack('V', substr($tmpBlock, ($offset += $tagLen), 4));
481              
482 5         11 $offset += 4;
483              
484 5         53 for (my $tagi = 0; $tagi < $numTags; $tagi++) {
485              
486             # Read the tag string
487 32         71 my $tagLen = unpack('V', substr($tmpBlock, $offset, 4));
488 32         102 my $tagStr = substr($tmpBlock, ($offset += 4), $tagLen);
489              
490             # Save the raw tag
491 32         66 push(@$rawTags, $tagStr);
492              
493             # Match the key and value
494 32 50       229 if ($tagStr =~ /^(.*?)=(.*?)[\r\n]*$/s) {
495              
496 32         67 my $tkey = $1;
497              
498             # Stick it in the tag hash - and handle multiple tags
499             # of the same name.
500 32 50 33     150 if (exists $tags->{$tkey} && ref($tags->{$tkey}) ne 'ARRAY') {
    50          
501              
502 0         0 my $oldValue = $tags->{$tkey};
503              
504 0         0 $tags->{$tkey} = [ $oldValue, $2 ];
505              
506             } elsif (ref($tags->{$tkey}) eq 'ARRAY') {
507              
508 0         0 push @{$tags->{$tkey}}, $2;
  0         0  
509              
510             } else {
511              
512 32         109 $tags->{$tkey} = $2;
513             }
514             }
515              
516 32         86 $offset += $tagLen;
517             }
518              
519 5         17 $self->{'tags'} = $tags;
520 5         14 $self->{'rawTags'} = $rawTags;
521              
522 5         16 return 1;
523             }
524              
525             sub _parseCueSheet {
526 2     2   4 my ($self, $block) = @_;
527              
528 2         3 my $cuesheet = [];
529              
530             # Parse out the tags from the metadata block
531 2         6 my $tmpBlock = $block->{'contents'};
532              
533             # First field in block is the Media Catalog Number
534 2         5 my $catalog = substr($tmpBlock,0,128);
535 2         18 $catalog =~ s/\x00+.*$//gs; # trim nulls off of the end
536              
537 2 50       15 push (@$cuesheet, "CATALOG $catalog\n") if length($catalog) > 0;
538 2         5 $tmpBlock = substr($tmpBlock,128);
539              
540             # metaflac uses "dummy.wav" but we're going to use the actual filename
541             # this will help external parsers that have to associate the resulting
542             # cuesheet with this flac file.
543 2         131 push (@$cuesheet, "FILE \"" . basename("$self->{'filename'}") ."\" FLAC\n");
544              
545             # Next field is the number of lead-in samples for CD-DA
546 2         7 my $highbits = unpack('N', substr($tmpBlock,0,4));
547 2         8 my $leadin = $highbits * 2 ** 32 + unpack('N', (substr($tmpBlock,4,4)));
548 2         10 $tmpBlock = substr($tmpBlock,8);
549              
550             # Flag to determine if this represents a CD
551 2         7 my $bits = unpack('B8', substr($tmpBlock, 0, 1));
552 2         6 my $isCD = substr($bits, 0, 1);
553              
554             # Some sanity checking related to the CD flag
555 2 50 33     32 if ($isCD && length($catalog) != 13 && length($catalog) != 0) {
      33        
556 0         0 warn "Invalid Catalog entry\n";
557 0         0 return -1;
558             }
559              
560 2 50 33     11 if (!$isCD && $leadin > 0) {
561 0         0 warn "Lead-in detected for non-CD cue sheet.\n";
562 0         0 return -1;
563             }
564              
565             # The next few bits should be zero.
566 2         12 my $reserved = _bin2dec(substr($bits, 1, 7));
567 2         57 $reserved += unpack('B*', substr($tmpBlock, 1, 258));
568              
569 2 50       11 if ($reserved != 0) {
570 0         0 warn "Either the cue sheet is corrupt, or it's a newer revision than I can parse\n";
571             #return -1; # ?? may be harmless to continue ...
572             }
573              
574 2         5 $tmpBlock = substr($tmpBlock,259);
575              
576             # Number of tracks
577 2         10 my $numTracks = _bin2dec(unpack('B8',substr($tmpBlock,0,1)));
578 2         6 $tmpBlock = substr($tmpBlock,1);
579              
580 2 50 33     35 if ($numTracks < 1 || ($isCD && $numTracks > 100)) {
      33        
581 0         0 warn "Invalid number of tracks $numTracks\n";
582 0         0 return -1;
583             }
584              
585             # Parse individual tracks now
586 2         6 my %seenTracknumber = ();
587 2         4 my $leadout = 0;
588 2         3 my $leadouttracknum = 0;
589              
590 2         9 for (my $i = 1; $i <= $numTracks; $i++) {
591              
592 16         38 $highbits = unpack('N', substr($tmpBlock,0,4));
593              
594 16         36 my $trackOffset = $highbits * 2 ** 32 + unpack('N', (substr($tmpBlock,4,4)));
595              
596 16 50 33     81 if ($isCD && $trackOffset % 588) {
597 0         0 warn "Invalid track offset $trackOffset\n";
598 0         0 return -1;
599             }
600              
601 16   33     60 my $tracknum = _bin2dec(unpack('B8',substr($tmpBlock,8,1))) || do {
602              
603             warn "Invalid track numbered \"0\" detected\n";
604             return -1;
605             };
606              
607 16 50 66     100 if ($isCD && $tracknum > 99 && $tracknum != 170) {
      66        
608 0         0 warn "Invalid track number for a CD $tracknum\n";
609 0         0 return -1;
610             }
611              
612 16 50       47 if (defined $seenTracknumber{$tracknum}) {
613 0         0 warn "Invalid duplicate track number $tracknum\n";
614 0         0 return -1;
615             }
616              
617 16         42 $seenTracknumber{$tracknum} = 1;
618              
619 16         31 my $isrc = substr($tmpBlock,9,12);
620 16         62 $isrc =~ s/\x00+.*$//;
621              
622 16 50 33     46 if ((length($isrc) != 0) && (length($isrc) != 12)) {
623 0         0 warn "Invalid ISRC code $isrc\n";
624 0         0 return -1;
625             }
626              
627 16         40 $bits = unpack('B8', substr($tmpBlock, 21, 1));
628 16         36 my $isAudio = !substr($bits, 0, 1);
629 16         27 my $preemphasis = substr($bits, 1, 1);
630              
631             # The next few bits should be zero.
632 16         46 $reserved = _bin2dec(substr($bits, 2, 6));
633 16         100 $reserved += unpack('B*', substr($tmpBlock, 22, 13));
634              
635 16 50       42 if ($reserved != 0) {
636 0         0 warn "Either the cue sheet is corrupt, " .
637             "or it's a newer revision than I can parse\n";
638             #return -1; # ?? may be harmless to continue ...
639             }
640              
641 16         45 my $numIndexes = _bin2dec(unpack('B8',substr($tmpBlock,35,1)));
642              
643 16         34 $tmpBlock = substr($tmpBlock,36);
644              
645             # If we're on the lead-out track, stop before pushing TRACK info
646 16 100       38 if ($i == $numTracks) {
647 2         4 $leadout = $trackOffset;
648              
649 2 50 33     16 if ($isCD && $tracknum != 170) {
650 0         0 warn "Incorrect lead-out track number $tracknum for CD\n";
651 0         0 return -1;
652             }
653              
654 2         5 $leadouttracknum = $tracknum;
655 2         8 next;
656             }
657              
658             # Add TRACK info to cuesheet
659 14 50       61 my $trackline = sprintf(" TRACK %02d %s\n", $tracknum, $isAudio ? "AUDIO" : "DATA");
660              
661 14         28 push (@$cuesheet, $trackline);
662 14 50       29 push (@$cuesheet, " FLAGS PRE\n") if ($preemphasis);
663 14 50       33 push (@$cuesheet, " ISRC " . $isrc . "\n") if ($isrc);
664              
665 14 50 33     96 if ($numIndexes < 1 || ($isCD && $numIndexes > 100)) {
      33        
666 0         0 warn "Invalid number of Indexes $numIndexes for track $tracknum\n";
667 0         0 return -1;
668             }
669              
670             # Itterate through the indexes for this track
671 14         42 for (my $j = 0; $j < $numIndexes; $j++) {
672              
673 26         57 $highbits = unpack('N', substr($tmpBlock,0,4));
674              
675 26         345 my $indexOffset = $highbits * 2 ** 32 + unpack('N', (substr($tmpBlock,4,4)));
676              
677 26 50 33     117 if ($isCD && $indexOffset % 588) {
678 0         0 warn "Invalid index offset $indexOffset\n";
679 0         0 return -1;
680             }
681              
682 26         81 my $indexnum = _bin2dec(unpack('B8',substr($tmpBlock,8,1)));
683             #TODO: enforce sequential indexes
684              
685 26         44 $reserved = 0;
686 26         85 $reserved += unpack('B*', substr($tmpBlock, 9, 3));
687              
688 26 50       64 if ($reserved != 0) {
689 0         0 warn "Either the cue sheet is corrupt, " .
690             "or it's a newer revision than I can parse\n";
691             #return -1; # ?? may be harmless to continue ...
692             }
693              
694 26         74 my $timeoffset = _samplesToTime(($trackOffset + $indexOffset), $self->{'info'}->{'SAMPLERATE'});
695              
696 26 50       66 return -1 unless defined ($timeoffset);
697              
698 26         68 my $indexline = sprintf (" INDEX %02d %s\n", $indexnum, $timeoffset);
699              
700 26         56 push (@$cuesheet, $indexline);
701              
702 26         121 $tmpBlock = substr($tmpBlock,12);
703             }
704             }
705              
706             # Add final comments just like metaflac would
707 2         7 push (@$cuesheet, "REM FLAC__lead-in " . $leadin . "\n");
708 2         8 push (@$cuesheet, "REM FLAC__lead-out " . $leadouttracknum . " " . $leadout . "\n");
709              
710 2         6 $self->{'cuesheet'} = $cuesheet;
711              
712 2         10 return 1;
713             }
714              
715             sub _parsePicture {
716 1     1   2 my ($self, $block) = @_;
717              
718             # Parse out the tags from the metadata block
719 1         3 my $tmpBlock = $block->{'contents'};
720 1         2 my $offset = 0;
721              
722 1         3 my $pictureType = unpack('N', substr($tmpBlock, $offset, 4));
723 1         2 my $mimeLength = unpack('N', substr($tmpBlock, ($offset += 4), 4));
724 1         3 my $mimeType = substr($tmpBlock, ($offset += 4), $mimeLength);
725 1         3 my $descLength = unpack('N', substr($tmpBlock, ($offset += $mimeLength), 4));
726 1         3 my $description = substr($tmpBlock, ($offset += 4), $descLength);
727 1         2 my $width = unpack('N', substr($tmpBlock, ($offset += $descLength), 4));
728 1         3 my $height = unpack('N', substr($tmpBlock, ($offset += 4), 4));
729 1         3 my $depth = unpack('N', substr($tmpBlock, ($offset += 4), 4));
730 1         3 my $colorIndex = unpack('N', substr($tmpBlock, ($offset += 4), 4));
731 1         3 my $imageLength = unpack('N', substr($tmpBlock, ($offset += 4), 4));
732 1         57 my $imageData = substr($tmpBlock, ($offset += 4), $imageLength);
733              
734 1         6 $self->{'picture'}->{$pictureType}->{'mimeType'} = $mimeType;
735 1         3 $self->{'picture'}->{$pictureType}->{'description'} = $description;
736 1         3 $self->{'picture'}->{$pictureType}->{'width'} = $width;
737 1         3 $self->{'picture'}->{$pictureType}->{'height'} = $height;
738 1         2 $self->{'picture'}->{$pictureType}->{'depth'} = $depth;
739 1         8 $self->{'picture'}->{$pictureType}->{'colorIndex'} = $colorIndex;
740 1         3 $self->{'picture'}->{$pictureType}->{'imageData'} = $imageData;
741 1         3 $self->{'picture'}->{$pictureType}->{'pictureType'} = $pictureType;
742              
743             # Create array of hashes with picture data from all the picture metadata blocks
744 1         2 push ( @{$self->{'allpictures'}}, {%{$self->{'picture'}->{$pictureType}}} );
  1         3  
  1         13  
745              
746 1         4 return 1;
747             }
748              
749             sub _parseSeekTable {
750 0     0   0 my ($self, $block) = @_;
751              
752 0         0 my $seektable = [];
753              
754             # grab the seekpoint table
755 0         0 my $tmpBlock = $block->{'contents'};
756 0         0 my $offset = 0;
757              
758             # parse out the seekpoints
759 0         0 while (my $seekpoint = substr($tmpBlock, $offset, 18)) {
760              
761             # Sample number of first sample in the target frame
762 0         0 my $highbits = unpack('N', substr($seekpoint,0,4));
763 0         0 my $sampleNumber = $highbits * 2 ** 32 + unpack('N', (substr($seekpoint,4,4)));
764              
765             # Detect placeholder seekpoint
766             # since the table is sorted, a placeholder means were finished
767 0 0       0 last if ($sampleNumber == (0xFFFFFFFF * 2 ** 32 + 0xFFFFFFFF));
768              
769             # Offset (in bytes) from the first byte of the first frame header
770             # to the first byte of the target frame's header.
771 0         0 $highbits = unpack('N', substr($seekpoint,8,4));
772 0         0 my $streamOffset = $highbits * 2 ** 32 + unpack('N', (substr($seekpoint,12,4)));
773              
774             # Number of samples in the target frame
775 0         0 my $frameSamples = unpack('n', (substr($seekpoint,16,2)));
776              
777             # add this point to our copy of the table
778 0         0 push (@$seektable, {
779             'sampleNumber' => $sampleNumber,
780             'streamOffset' => $streamOffset,
781             'frameSamples' => $frameSamples,
782             });
783              
784 0         0 $offset += 18;
785             }
786              
787 0         0 $self->{'seektable'} = $seektable;
788              
789 0         0 return 1;
790             }
791              
792             sub _parseAppBlock {
793 1     1   3 my ($self, $block) = @_;
794              
795             # Parse out the tags from the metadata block
796 1         12 my $appID = unpack('N', substr($block->{'contents'}, 0, 4, ''));
797              
798 1         11 $self->{'application'}->{$appID} = $block->{'contents'};
799              
800 1         4 return 1;
801             }
802              
803             # Take an offset as number of flac samples
804             # and return CD-DA style mm:ss:ff
805             sub _samplesToTime {
806 26     26   34 my $samples = shift;
807 26         31 my $samplerate = shift;
808              
809 26 50       52 if ($samplerate == 0) {
810 0         0 warn "Couldn't find SAMPLERATE for time calculation!\n";
811 0         0 return;
812             }
813              
814 26         40 my $totalSeconds = $samples / $samplerate;
815              
816 26 100       69 if ($totalSeconds == 0) {
817             # handled specially to avoid division by zero errors
818 2         6 return "00:00:00";
819             }
820              
821 24         44 my $trackMinutes = int(int($totalSeconds) / 60);
822 24         37 my $trackSeconds = int($totalSeconds % 60);
823 24         53 my $trackFrames = ($totalSeconds - int($totalSeconds)) * 75;
824              
825             # Poor man's rounding. Needed to match the output of metaflac.
826 24         32 $trackFrames = int($trackFrames + 0.5);
827              
828 24         69 my $formattedTime = sprintf("%02d:%02d:%02d", $trackMinutes, $trackSeconds, $trackFrames);
829              
830 24         53 return $formattedTime;
831             }
832              
833             sub _bin2dec {
834             # Freely swiped from Perl Cookbook p. 48 (May 1999)
835 79     79   313 return unpack ('N', pack ('B32', substr(0 x 32 . $_[0], -32)));
836             }
837              
838             sub _packInt32 {
839             # Packs an integer into a little-endian 32-bit unsigned int
840 3     3   13 return pack('V', $_[0]);
841             }
842              
843             sub _findMetadataIndex {
844 2     2   3 my $self = shift;
845 2         3 my $htype = shift;
846 2   50     11 my $idx = shift || 0;
847              
848 2         2 my $found = 0;
849              
850             # Loop through the metadata_blocks until one of $htype is found
851 2         4 while ($idx < @{$self->{'metadataBlocks'}}) {
  5         18  
852              
853             # Check the type to see if it's a $htype block
854 4 100       23 if ($self->{'metadataBlocks'}[$idx]->{'blockType'} == $htype) {
855 1         2 $found++;
856 1         2 last;
857             }
858              
859 3         4 $idx++;
860             }
861              
862             # No streaminfo found. Error.
863 2 100       8 return -1 if $found == 0;
864 1         12 return $idx;
865             }
866              
867             sub _addStringToComment {
868 2     2   4 my $self = shift;
869 2         5 my $addString = shift;
870              
871 2         7 $$self .= _packInt32(length($addString));
872 2         7 $$self .= $addString;
873             }
874              
875             sub _addNewMetadataBlock {
876 1     1   2 my $self = shift;
877 1         2 my $htype = shift;
878 1         2 my $contents = shift;
879              
880 1         1 my $numBlocks = @{$self->{'metadataBlocks'}};
  1         3  
881              
882             # create a new block
883 1         4 $self->{'metadataBlocks'}->[$numBlocks]->{'lastBlockFlag'} = 0;
884 1         3 $self->{'metadataBlocks'}->[$numBlocks]->{'blockType'} = $htype;
885 1         2 $self->{'metadataBlocks'}->[$numBlocks]->{'blockSize'} = length($contents);
886 1         3 $self->{'metadataBlocks'}->[$numBlocks]->{'contents'} = $contents;
887             }
888              
889             sub _updateMetadataBlock {
890 1     1   2 my $self = shift;
891 1         2 my $blockIdx = shift;
892 1         2 my $contents = shift;
893              
894             # Update the block
895 1         3 $self->{'metadataBlocks'}->[$blockIdx]->{'blockSize'} = length($contents);
896 1         3 $self->{'metadataBlocks'}->[$blockIdx]->{'contents'} = $contents;
897             }
898              
899             1;
900              
901             __END__