File Coverage

blib/lib/Image/ExifTool/APE.pm
Criterion Covered Total %
statement 81 89 91.0
branch 37 58 63.7
condition 8 18 44.4
subroutine 5 5 100.0
pod 0 2 0.0
total 131 172 76.1


line stmt bran cond sub pod time code
1             #------------------------------------------------------------------------------
2             # File: APE.pm
3             #
4             # Description: Read Monkey's Audio meta information
5             #
6             # Revisions: 11/13/2006 - P. Harvey Created
7             #
8             # References: 1) http://www.monkeysaudio.com/
9             # 2) http://www.personal.uni-jena.de/~pfk/mpp/sv8/apetag.html
10             #------------------------------------------------------------------------------
11              
12             package Image::ExifTool::APE;
13              
14 2     2   4382 use strict;
  2         4  
  2         74  
15 2     2   12 use vars qw($VERSION);
  2         4  
  2         91  
16 2     2   11 use Image::ExifTool qw(:DataAccess :Utils);
  2         4  
  2         2975  
17              
18             $VERSION = '1.07';
19              
20             # APE metadata blocks
21             %Image::ExifTool::APE::Main = (
22             GROUPS => { 2 => 'Audio' },
23             NOTES => q{
24             Tags found in Monkey's Audio (APE) information. Only a few common tags are
25             listed below, but ExifTool will extract any tag found. ExifTool supports
26             APEv1 and APEv2 tags, as well as ID3 information in APE files, and will also
27             read APE metadata from MP3 and MPC files.
28             },
29             Album => { },
30             Artist => { },
31             Genre => { },
32             Title => { },
33             Track => { },
34             Year => { },
35             DURATION => {
36             Name => 'Duration',
37             ValueConv => '$val += 4294967296 if $val < 0 and $val >= -2147483648; $val * 1e-7',
38             PrintConv => 'ConvertDuration($val)',
39             },
40             'Tool Version' => { Name => 'ToolVersion' },
41             'Tool Name' => { Name => 'ToolName' },
42             );
43              
44             # APE MAC header version 3.97 or earlier
45             %Image::ExifTool::APE::OldHeader = (
46             PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData,
47             GROUPS => { 1 => 'MAC', 2 => 'Audio' },
48             FORMAT => 'int16u',
49             NOTES => 'APE MAC audio header for version 3.97 or earlier.',
50             0 => {
51             Name => 'APEVersion',
52             ValueConv => '$val / 1000',
53             },
54             1 => 'CompressionLevel',
55             # 2 => 'FormatFlags',
56             3 => 'Channels',
57             4 => { Name => 'SampleRate', Format => 'int32u' },
58             # 6 => { Name => 'HeaderBytes', Format => 'int32u' }, # WAV header bytes
59             # 8 => { Name => 'TerminatingBytes', Format => 'int32u' },
60             10 => { Name => 'TotalFrames', Format => 'int32u' },
61             12 => { Name => 'FinalFrameBlocks', Format => 'int32u' },
62             );
63              
64             # APE MAC header version 3.98 or later
65             %Image::ExifTool::APE::NewHeader = (
66             PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData,
67             GROUPS => { 1 => 'MAC', 2 => 'Audio' },
68             FORMAT => 'int16u',
69             NOTES => 'APE MAC audio header for version 3.98 or later.',
70             0 => 'CompressionLevel',
71             # 1 => 'FormatFlags',
72             2 => { Name => 'BlocksPerFrame', Format => 'int32u' },
73             4 => { Name => 'FinalFrameBlocks', Format => 'int32u' },
74             6 => { Name => 'TotalFrames', Format => 'int32u' },
75             8 => 'BitsPerSample',
76             9 => 'Channels',
77             10 => { Name => 'SampleRate', Format => 'int32u' },
78             );
79              
80             # APE Composite tags
81             %Image::ExifTool::APE::Composite = (
82             GROUPS => { 2 => 'Audio' },
83             Duration => {
84             Require => {
85             0 => 'APE:SampleRate',
86             1 => 'APE:TotalFrames',
87             2 => 'APE:BlocksPerFrame',
88             3 => 'APE:FinalFrameBlocks',
89             },
90             RawConv => '($val[0] && $val[1]) ? (($val[1] - 1) * $val[2] + $val[3]) / $val[0]: undef',
91             PrintConv => 'ConvertDuration($val)',
92             },
93             );
94              
95             # add our composite tags
96             Image::ExifTool::AddCompositeTags('Image::ExifTool::APE');
97              
98             #------------------------------------------------------------------------------
99             # Make tag info hash for specified tag
100             # Inputs: 0) tag name, 1) tag table ref
101             # - must only call if tag doesn't exist
102             sub MakeTag($$)
103             {
104 3     3 0 10 my ($tag, $tagTablePtr) = @_;
105 3         8 my $name = ucfirst(lc($tag));
106             # remove invalid characters in tag name and capitalize following letters
107 3         30 $name =~ s/[^\w-]+(.?)/\U$1/sg;
108 3         9 $name =~ s/([a-z0-9])_([a-z])/$1\U$2/g;
109 3         9 my %tagInfo = ( Name => $name );
110 3 100 100     33 $tagInfo{Groups} = { 2 => 'Preview' } if $tag =~ /^Cover Art/ and $tag !~ /Desc$/;
111 3         11 AddTagToTable($tagTablePtr, $tag, \%tagInfo);
112             }
113              
114             #------------------------------------------------------------------------------
115             # Extract information from an APE file
116             # Inputs: 0) ExifTool object reference, 1) dirInfo reference
117             # - Just looks for APE trailer if FileType is already set
118             # Returns: 1 on success, 0 if this wasn't a valid APE file
119             sub ProcessAPE($$)
120             {
121 3     3 0 9 my ($et, $dirInfo) = @_;
122              
123             # must first check for leading/trailing ID3 information
124 3 100       12 unless ($$et{DoneID3}) {
125 1         870 require Image::ExifTool::ID3;
126 1 50       18 Image::ExifTool::ID3::ProcessID3($et, $dirInfo) and return 1;
127             }
128 3         6 my $raf = $$dirInfo{RAF};
129 3         13 my $verbose = $et->Options('Verbose');
130 3         8 my ($buff, $i, $header, $tagTablePtr, $dataPos, $oldIndent);
131              
132 3         16 $$et{DoneAPE} = 1;
133              
134             # check APE signature and process audio information
135             # unless this is some other type of file
136 3 100       10 unless ($$et{FileType}) {
137 1 50       4 $raf->Read($buff, 32) == 32 or return 0;
138 1 50       8 $buff =~ /^(MAC |APETAGEX)/ or return 0;
139 1         6 $et->SetFileType();
140 1         6 SetByteOrder('II');
141              
142 1 50       6 if ($buff =~ /^APETAGEX/) {
143             # we already read the APE header
144 0         0 $header = 1;
145             } else {
146             # process the MAC header
147 1         7 my $vers = Get16u(\$buff, 4);
148 1         2 my $table;
149 1 50       3 if ($vers <= 3970) {
150 0         0 $buff = substr($buff, 4);
151 0         0 $table = GetTagTable('Image::ExifTool::APE::OldHeader');
152             } else {
153 1         7 my $dlen = Get32u(\$buff, 8);
154 1         5 my $hlen = Get32u(\$buff, 12);
155 1 50 33     5 unless ($dlen & 0x80000000 or $hlen & 0x80000000) {
156 1 50 33     5 if ($raf->Seek($dlen, 0) and $raf->Read($buff, $hlen) == $hlen) {
157 1         16 $table = GetTagTable('Image::ExifTool::APE::NewHeader');
158             }
159             }
160             }
161 1 50       12 $et->ProcessDirectory( { DataPt => \$buff }, $table) if $table;
162             }
163             }
164             # look for APE trailer unless we already found an APE header
165 3 50       12 unless ($header) {
166             # look for the APE trailer footer...
167 3         5 my $footPos = -32;
168             # (...but before the ID3v1 trailer if it exists)
169 3 100       168 $footPos -= $$et{DoneID3} if $$et{DoneID3} > 1;
170 3 50       79 $raf->Seek($footPos, 2) or return 1;
171 3 50       16 $raf->Read($buff, 32) == 32 or return 1;
172 3 100       25 $buff =~ /^APETAGEX/ or return 1;
173 2         6 SetByteOrder('II');
174             }
175             #
176             # Read the APE data (we have just read the APE header or footer into $buff)
177             #
178 2         9 my ($version, $size, $count, $flags) = unpack('x8V4', $buff);
179 2         8 $version /= 1000;
180 2         4 $size -= 32; # get size of data only
181 2 50 33     14 if (($size & 0x80000000) == 0 and
      33        
      33        
182             ($header or $raf->Seek(-$size-32, 1)) and
183             $raf->Read($buff, $size) == $size)
184             {
185 2 50       5 if ($verbose) {
186 0         0 $oldIndent = $$et{INDENT};
187 0         0 $$et{INDENT} .= '| ';
188 0         0 $et->VerboseDir("APEv$version", $count, $size);
189 0         0 $et->VerboseDump(\$buff, DataPos => $raf->Tell() - $size);
190             }
191 2         14 $tagTablePtr = GetTagTable('Image::ExifTool::APE::Main');
192 2         7 $dataPos = $raf->Tell() - $size;
193             } else {
194 0         0 $count = -1;
195             }
196             #
197             # Process the APE tags
198             #
199 2         5 my $pos = 0;
200 2         8 for ($i=0; $i<$count; ++$i) {
201             # read next APE tag
202 20 50       175 last if $pos + 8 > $size;
203 20         55 my $len = Get32u(\$buff, $pos);
204 20         53 my $flags = Get32u(\$buff, $pos + 4);
205 20         54 pos($buff) = $pos + 8;
206 20 50       103 last unless $buff =~ /\G(.*?)\0/sg;
207 20         48 my $tag = $1;
208             # avoid conflicts with our special table entries
209 20 50       46 $tag .= '.' if $Image::ExifTool::specialTags{$tag};
210 20         33 $pos = pos($buff);
211 20 50       44 last if $pos + $len > $size;
212 20         46 my $val = substr($buff, $pos, $len);
213 20 100       63 MakeTag($tag, $tagTablePtr) unless $$tagTablePtr{$tag};
214             # handle binary-value tags
215 20 100       44 if (($flags & 0x06) == 0x02) {
216 2         5 my $buf2 = $val;
217 2         3 $val = \$buf2;
218             # extract cover art description separately (hackitty hack)
219 2 50       8 if ($tag =~ /^Cover Art/) {
220 2         13 $buf2 =~ s/^([\x20-\x7f]*)\0//;
221 2 50       7 if ($1) {
222 2         6 my $t = "$tag Desc";
223 2         5 my $v = $1;
224 2 100       8 MakeTag($t, $tagTablePtr) unless $$tagTablePtr{$t};
225 2         8 $et->HandleTag($tagTablePtr, $t, $v);
226             }
227             }
228             }
229 20         67 $et->HandleTag($tagTablePtr, $tag, $val,
230             Index => $i,
231             DataPt => \$buff,
232             DataPos => $dataPos,
233             Start => $pos,
234             Size => $len,
235             );
236 20         58 $pos += $len;
237             }
238 2 50       14 $i == $count or $et->Warn('Bad APE trailer');
239 2 50       8 $$et{INDENT} = $oldIndent if defined $oldIndent;
240 2         7 return 1;
241             }
242              
243             1; # end
244              
245             __END__