1# $Id: Flac.pm,v 1.5 2003/12/14 09:43:56 ianb Exp $ 2package MP3::Archive::Lint::Tools::Flac; 3 4use strict; 5use warnings; 6 7use vars qw(@ISA $VERSION); 8@ISA = qw(MP3::Archive::Lint::Tool); 9$VERSION = '0.01'; 10 11=head1 flac - Tool to check .flac files 12 13Checks .flac files using B<flac> and B<metaflac> from the flac 14distribution at L<http:E<sol>E<sol>flac.sourceforge.netE<sol>download.html> 15 16It also uses L<md5sum(1)>, see L<mp3lint(1)> for more details on 17needed software. 18 19=head2 Tests 20 21=over 4 22 23=cut 24 25sub new 26{ 27 my $proto=shift; 28 my $class=ref($proto) || $proto; 29 my $opts=shift; 30 my $self=$class->SUPER::new($opts);; 31 bless($self,$class); 32 33 $self->{metaflac}=$self->findprogram("metaflac"); 34 $self->{flac}=$self->findprogram("flac"); 35 $self->{md5sum}=$self->findprogram("md5sum"); 36 unless (defined $self->{metaflac}) 37 { 38 $self->debug("metaflac not found,tests skipped"); 39 return $self; 40 } 41 my @tests=("tracknum","artist","album","track","samplerate","samplesize","channels"); 42 if($self->config->quickscan) 43 { 44 $self->debug("quick mode enabled, skipping md5sum"); 45 } 46 else 47 { 48 if(defined($self->{flac})) 49 { 50 if(defined($self->{md5sum})) 51 { 52 push(@tests,"md5sum"); 53 } 54 else 55 { 56 $self->debug("md5sum not found, md5sum test skipped"); 57 } 58 } 59 else 60 { 61 $self->debug("flac not found, md5sum test skipped"); 62 } 63 } 64 65 $self->settests(@tests); 66 67 return $self; 68} 69 70 71sub initscan 72{ 73 my $self=shift; 74 75 return 0 unless (defined($self->{tests}) && scalar(@{$self->{tests}})); 76 return 0 unless($self->{filename}=~/\.flac?$/i); 77 return 0 if (-z $self->{file}); 78 return 0 if (-d $self->{file}); 79 80 my $opts='--show-md5sum --show-sample-rate --show-bps --show-channels --export-vc-to=-'; 81 unless(open(I,"$self->{metaflac} $opts $self->{qfile} 2>/dev/null |")) 82 { 83 $self->say("cannot fork:$!"); 84 return 0; 85 } 86 87 my @output=<I>; 88 unless(close(I)) # metaflac returned nonzero 89 { 90 $self->say("corrupt:flac corrupt"); 91 return 0; 92 } 93 94 chomp(@output); 95 my $max=$#output; 96 if($max < 3) 97 { 98 $self->say("corrupt:flac corrupt"); 99 return 0; 100 } 101 my $offset=0; 102 %{$self->{flacinfo}}=(); 103 104 $self->{flacinfo}{md5sum}=$output[$offset++]; 105 $self->{flacinfo}{samplerate}=$output[$offset++]; 106 $self->{flacinfo}{bitsize}=$output[$offset++]; 107 $self->{flacinfo}{channels}=$output[$offset++]; 108 while($offset<=$max) 109 { 110 my($key,$val)=split(/=/,$output[$offset++]); 111 $self->{flacinfo}{$key}=$val; 112 } 113 114 return 1; 115} 116 117=item B<tracknum> 118 119Tests for presence of a vorbis TRACKNUMBER comment, and compares it to 120the filename. If the B<-n> option is given to mp3lint, this test only 121checks that the tracknum is a valid number, not that it matches the 122filename. 123 124=cut 125 126sub tracknum 127{ 128 my $self=shift; 129 130 return unless($self->isalbum()); 131 132 my $found=0; 133 my $filenum; 134 if((exists($self->{flacinfo}{TRACKNUMBER})) && 135 (defined($self->{flacinfo}{TRACKNUMBER}))) 136 { 137 $found++; 138 my $num=$self->{flacinfo}{TRACKNUMBER}; 139 if($num!~/^\d+$/) 140 { 141 $self->say("not a number"); 142 } 143 elsif((!$self->config->skipnametests()) && 144 ($filenum=$self->config->archive->tracknum($self->{pathfilename}))) 145 { 146 if($num != $filenum) 147 { 148 $self->say("file/comment mismatch:$num"); 149 } 150 } 151 } 152 if(!$found) 153 { 154 $self->say("no TRACKNUMBER comment"); 155 } 156} 157 158=item B<artist> 159 160Tests for presence of a vorbis ARTIST comment, and compares it to the 161filename. The filename test is suppressed if the B<-n> option is given 162to mp3lint. 163 164=cut 165 166sub artist 167{ 168 my $self=shift; 169 my $found=0; 170 if((exists($self->{flacinfo}{ARTIST})) && 171 (defined($self->{flacinfo}{ARTIST}))) 172 { 173 $found++; 174 if(!$self->config->skipnametests()) 175 { 176 my $artist=$self->{flacinfo}{ARTIST}; 177 my $fileartist; 178 if($fileartist=$self->config->archive->artist($self->{pathfilename})) 179 { 180 if($artist ne $fileartist) 181 { 182 $self->say("file/comment mismatch:$artist"); 183 } 184 } 185 } 186 } 187 if(!$found) 188 { 189 $self->say("no ARTIST comment"); 190 } 191} 192 193=item B<album> 194 195Tests for presence of a vorbis ALBUM comment, and compares it to the 196filename. The filename test is suppressed if the B<-n> option is 197given to mp3lint. 198 199=cut 200 201sub album 202{ 203 my $self=shift; 204 205 return unless($self->isalbum()); 206 207 my $found=0; 208 if((exists($self->{flacinfo}{ALBUM})) && 209 (defined($self->{flacinfo}{ALBUM}))) 210 { 211 $found++; 212 if(!$self->config->skipnametests()) 213 { 214 my $album=$self->{flacinfo}{ALBUM}; 215 my $filealbum; 216 if($filealbum=$self->config->archive->album($self->{pathfilename})) 217 { 218 if($album ne $filealbum) 219 { 220 $self->say("file/comment mismatch:$album"); 221 } 222 } 223 } 224 } 225 if(!$found) 226 { 227 $self->say("no ALBUM comment"); 228 } 229} 230 231=item B<track> 232 233Tests for presence of a vorbis TITLE comment (track name), and 234compares it to the filename. The filename test is suppressed if the 235B<-n> option is given to mp3lint. 236 237=cut 238 239sub track 240{ 241 my $self=shift; 242 my $found=0; 243 if((exists($self->{flacinfo}{TITLE})) && 244 (defined($self->{flacinfo}{TITLE}))) 245 { 246 $found++; 247 if(!$self->config->skipnametests()) 248 { 249 my $track=$self->{flacinfo}{TITLE}; 250 my $filetrack; 251 if($filetrack=$self->config->archive->track($self->{pathfilename})) 252 { 253 if($track ne $filetrack) 254 { 255 $self->say("file/comment mismatch:$track"); 256 } 257 } 258 } 259 } 260 if(!$found) 261 { 262 $self->say("no TITLE comment"); 263 } 264} 265 266=item B<channels> 267 268Tests if flac is stereo 269 270=cut 271 272sub channels 273{ 274 my $self=shift; 275 if((exists($self->{flacinfo}{channels})) && 276 (defined($self->{flacinfo}{channels}))) 277 { 278 my $channels=$self->{flacinfo}{channels}; 279 if($channels==1) 280 { 281 $self->say("mono"); 282 } 283 elsif($channels!=2) 284 { 285 $self->say("not stereo:$channels channels"); 286 } 287 } 288} 289 290=item B<samplerate> 291 292Tests samplerate is at least $lint_minsamplerate Hz. 293 294Variables: 295 296=over 4 297 298=item $lint_minsamplerate 299 300(number, default 44100 Hz) 301 302=back 303 304=cut 305 306sub samplerate 307{ 308 my $self=shift; 309 my $minsamplerate=$self->config->get("lint_minsamplerate"); 310 if((exists($self->{flacinfo}{samplerate})) && 311 (defined($self->{flacinfo}{samplerate}))) 312 { 313 my $rate=$self->{flacinfo}{samplerate}; 314 if($rate < $minsamplerate) 315 { 316 $self->say("low samplerate:$rate (min $minsamplerate)"); 317 } 318 } 319} 320 321=item B<samplesize> 322 323Tests sample bitsize is at least $lint_minbitsize bits. 324 325Variables: 326 327=over 4 328 329=item $lint_minbitsize 330 331(number, default 16 bits) 332 333=back 334 335=cut 336 337sub samplesize 338{ 339 my $self=shift; 340 my $min=$self->config->get("lint_minbitsize"); 341 my $actual=$self->{flacinfo}{bitsize}; 342 if($actual < $min) 343 { 344 $self->say("low bitsize:$actual (should be $min)"); 345 } 346} 347 348=item B<md5sum> 349 350Decompresses and checksums audio using md5sum, and compares it to the 351md5 checksum for the original data, stored in the STREAMINFO metadata block. 352 353Assumes the original uncompressed audio was signed and little-endian. 354 355=cut 356 357sub md5sum 358{ 359 my $self=shift; 360 # Portability? works on i386 with data from sox on i386 361 my $cmd=($self->{flac}. 362 " -d -c --force-raw-format --endian=little --sign=signed ". 363 $self->{qfile}. 364 " 2>/dev/null |". 365 $self->{md5sum}. 366 "|"); 367 368 unless(open(P,$cmd)) 369 { 370 $self->say("cannot fork:$!"); 371 return; 372 } 373 my $line=<P>; 374 if((!close(P)) ||(!defined($line))) 375 { 376 $self->say("problem running flac/md5sum:$!"); 377 return; 378 } 379 my $sum; 380 if($line=~/^(\S+)/) 381 { 382 $sum=$1; 383 } 384 else 385 { 386 $self->say("error parsing md5sum output:$line"); 387 } 388 389 if($sum ne $self->{flacinfo}{md5sum}) 390 { 391 $self->say("checksum error - flac corrupt"); 392 } 393} 394 395=back 396 397=head2 Bugs 398 399Does not yet handle flac files embedded in .ogg files. 400 401Does not yet parse id3 tags. Although the reference implementation 402only supports them to the extent of noticing and skipping them, some 403players use them. 404 405=cut 406 4071; 408