1# $Id: Ogg.pm,v 1.6 2003/12/14 23:52:04 ianb Exp $ 2package MP3::Archive::Lint::Tools::Ogg; 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 ogg - Tool to check .ogg files 12 13Checks Ogg Vorbis files. Requires the program C<ogginfo>, available 14from L<http:E<sol>E<sol>www.vorbis.comE<sol>download_unix.psp> 15 16On debian systems, it can be installed by typing 17C<apt-get install vorbis-tools> as root. 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->{cmd}=$self->findprogram("ogginfo"); 34 unless (defined $self->{cmd}) 35 { 36 $self->say("ogginfo not found,tests skipped"); 37 return $self; 38 } 39 40 if($self->config->quickscan) 41 { 42 $self->debug("quick mode enabled, tests skipped"); 43 return $self; 44 } 45 46 $self->settests("header","short","tags","tracknum","artist","album","track", 47 "serial","bitrate","samplerate","channels","stream"); 48 49 return $self; 50} 51 52 53sub initscan 54{ 55 my $self=shift; 56 57 return 0 unless (defined($self->{tests}) && scalar(@{$self->{tests}})); 58 return 0 unless($self->{filename}=~/\.ogg$/i); 59 return 0 if (-z $self->{file}); 60 return 0 if (-d $self->{file}); 61 62 unless(open(I,"$self->{cmd} $self->{qfile} 2>/dev/null |")) 63 { 64 $self->say("cannot run ogginfo:$!"); 65 return; 66 } 67 68 @{$self->{ogginfo}}=<I>; 69 chomp(@{$self->{ogginfo}}); 70 71 unless(close(I)) # ogginfo returned nonzero 72 { 73 # fake message from stream 74 $self->say("stream:ogg corrupt"); 75 } 76} 77 78=item B<header> 79 80Tests integrity of Ogg header. 81 82=cut 83 84sub header 85{ 86 my $self=shift; 87 if(scalar(grep(/(?:header_integrity=fail|could not decode vorbis header|Vorbis stream \d+ does not have headers|Invalid header page)/i,@{$self->{ogginfo}}))) 88 { 89 $self->say("integrity failure"); 90 } 91} 92 93=item B<short> 94 95Tests if Ogg is truncated. 96 97=cut 98 99sub short 100{ 101 my $self=shift; 102 if(scalar(grep(/(?:stream_truncated=true|EOS not set)/i,@{$self->{ogginfo}}))) 103 { 104 $self->say("ogg truncated"); 105 } 106} 107 108=item B<tags> 109 110Tests for corrupt comment tags 111 112=cut 113 114sub tags 115{ 116 my $self=shift; 117 if(scalar(grep(/(?:Invalid comment fieldname|Illegal UTF-8 sequence)/i,@{$self->{ogginfo}}))) 118 { 119 $self->say("corrupt comment tag"); 120 } 121} 122 123 124=item B<tracknum> 125 126Tests if Ogg has a TRACKNUMBER comment, that it is a number, and 127matches the filename. If the B<-n> option is given to mp3lint, this 128test only checks that the tracknum is a valid number, not that it 129matches the filename. 130 131=cut 132 133sub tracknum 134{ 135 my $self=shift; 136 137 return unless($self->isalbum()); 138 139 my $found=0; 140 my $filenum; 141 for my $line (@{$self->{ogginfo}}) 142 { 143 if($line=~/^\s*TRACKNUMBER=(.*)/) 144 { 145 $found++; 146 my $num=$1; 147 if($num!~/^\d+$/) 148 { 149 $self->say("not a number"); 150 } 151 elsif((!$self->config->skipnametests()) && 152 ($filenum=$self->config->archive->tracknum($self->{pathfilename}))) 153 { 154 if($num != $filenum) 155 { 156 $self->say("file/comment mismatch:$num"); 157 } 158 } 159 } 160 } 161 if(!$found) 162 { 163 $self->say("no TRACKNUMBER comment"); 164 } 165} 166 167=item B<artist> 168 169Tests if Ogg has a ARTIST comment, and that it matches the filename. 170The filename test is suppressed if the B<-n> option is given to 171mp3lint. 172 173=cut 174 175sub artist 176{ 177 my $self=shift; 178 my $found=0; 179 for my $line (@{$self->{ogginfo}}) 180 { 181 if($line=~/^\s*ARTIST=(.*)/) 182 { 183 $found++; 184 if(!$self->config->skipnametests()) 185 { 186 my $artist=$1; 187 my $fileartist; 188 if($fileartist=$self->config->archive->artist($self->{pathfilename})) 189 { 190 if($artist ne $fileartist) 191 { 192 $self->say("file/comment mismatch:$artist"); 193 } 194 } 195 } 196 } 197 } 198 if(!$found) 199 { 200 $self->say("no ARTIST comment"); 201 } 202} 203 204=item B<album> 205 206Tests if Ogg has a ALBUM comment, and that it matches the filename. 207The filename test is suppressed if the B<-n> option is given to 208mp3lint. 209 210=cut 211 212sub album 213{ 214 my $self=shift; 215 216 return unless($self->isalbum()); 217 218 my $found=0; 219 for my $line (@{$self->{ogginfo}}) 220 { 221 if($line=~/^\s*ALBUM=(.*)/) 222 { 223 $found++; 224 if(!$self->config->skipnametests()) 225 { 226 my $album=$1; 227 my $filealbum; 228 if($filealbum=$self->config->archive->album($self->{pathfilename})) 229 { 230 if($album ne $filealbum) 231 { 232 $self->say("file/comment mismatch:$album"); 233 } 234 } 235 } 236 } 237 } 238 if(!$found) 239 { 240 $self->say("no ALBUM comment"); 241 } 242} 243 244=item B<track> 245 246Tests if Ogg has a TITLE comment (track name), and that it matches the 247filename. The filename test is suppressed if the B<-n> option is 248given to mp3lint. 249 250=cut 251 252sub track 253{ 254 my $self=shift; 255 my $found=0; 256 for my $line (@{$self->{ogginfo}}) 257 { 258 if($line=~/^\s*TITLE=(.*)/) 259 { 260 $found++; 261 if(!$self->config->skipnametests()) 262 { 263 my $track=$1; 264 my $filetrack; 265 if($filetrack=$self->config->archive->track($self->{pathfilename})) 266 { 267 if($track ne $filetrack) 268 { 269 $self->say("file/comment mismatch:$track"); 270 } 271 } 272 } 273 } 274 } 275 if(!$found) 276 { 277 $self->say("no TITLE comment"); 278 } 279} 280 281=item B<serial> 282 283Tests if Ogg has multiple streams in it by the presence of multiple 284serial numbers. 285 286=cut 287 288sub serial 289{ 290 my $self=shift; 291 292 my $count=scalar(grep(/^(?:\s*serial=|.*stream.*serial\:)/,@{$self->{ogginfo}})); 293 if($count>1) 294 { 295 $self->say("multiple streams:$count"); 296 } 297} 298 299=item B<bitrate> 300 301Tests Ogg nominal (requested) bitrate is at least $lint_minbitrate_ogg 302kbps. 303 304Variables: 305 306=over 4 307 308=item $lint_minbitrate_ogg 309 310(number, default 100 kbps) 311 312=back 313 314=cut 315 316sub bitrate 317{ 318 my $self=shift; 319 my $minbitrate=$self->config->get("lint_minbitrate_ogg"); 320 if($minbitrate<1000) 321 { 322 $minbitrate *= 1000; 323 } 324 for my $line (@{$self->{ogginfo}}) 325 { 326 # go for nominal (intended) not average (actual) 327 if($line=~/bitrate_nominal=(.*)/) 328 { 329 my $nominal=$1; 330 if($nominal<$minbitrate) 331 { 332 $self->say(sprintf("%s%.2fkbps (min %.2fkbps)","low bitrate:",$nominal/1000,$minbitrate/1000)); 333 } 334 } 335 elsif($line=~/Nominal\s+bitrate:\s+([\d\.]+)/i) 336 { 337 my $nominal=$1; 338 $nominal *= 1000; # convert to bps 339 $nominal=sprintf("%.0f",$nominal); 340 if($nominal<$minbitrate) 341 { 342 $self->say(sprintf("%s%.2fkbps (min %.2fkbps)","low bitrate:",$nominal/1000,$minbitrate/1000)); 343 } 344 } 345 } 346} 347 348=item B<channels> 349 350Tests if Ogg is stereo. 351 352=cut 353 354sub channels 355{ 356 my $self=shift; 357 for my $line (@{$self->{ogginfo}}) 358 { 359 if($line=~/channels(?:=|:)\s*(\d+)/i) 360 { 361 my $channels=$1; 362 if($channels==1) 363 { 364 $self->say("mono"); 365 } 366 elsif($channels!=2) 367 { 368 $self->say("not stereo:$channels channels"); 369 } 370 } 371 } 372} 373 374 375=item B<samplerate> 376 377Tests Ogg samplerate is at least $lint_minsamplerate Hz. 378 379Variables: 380 381=over 4 382 383=item $lint_minsamplerate 384 385(number, default 44100 Hz) 386 387=back 388 389=cut 390 391sub samplerate 392{ 393 my $self=shift; 394 my $minsamplerate=$self->config->get("lint_minsamplerate"); 395 for my $line (@{$self->{ogginfo}}) 396 { 397 if($line=~/^\s*rate(?:=|:)\s*(\d+)/i) 398 { 399 if($1 < $minsamplerate) 400 { 401 $self->say("low samplerate:$1 (min $minsamplerate)"); 402 } 403 } 404 } 405} 406 407 408=item B<stream> 409 410Tests integrity of Ogg stream[s] in file. 411 412=cut 413 414sub stream 415{ 416 my $self=shift; 417 for my $line (@{$self->{ogginfo}}) 418 { 419 if($line=~/(?:stream_integrity=fail|Warning: granulepos|Hole in data|Illegally placed page|Warning: stream start flag|sequence number gap|Input probably not ogg)/i) 420 { 421 $self->say("integrity failure"); 422 last; 423 } 424 } 425} 426 427=back 428 429=head2 Bugs 430 431Does not yet handle flac files embedded in .ogg files. 432 433=cut 434 435#filename=Hydrate-Kenny_Beltrey.ogg 436# 437#serial=6109 438#header_integrity=pass 439#TITLE=Hydrate - Kenny Beltrey 440#ARTIST=Kenny Beltrey 441#ALBUM=Favorite Things 442#DATE=2002 443#COMMENT=http://www.kahvi.org 444#TRACKNUMBER=2 445#vendor=Xiph.Org libVorbis I 20020713 446#version=0 447#channels=2 448#rate=44100 449#bitrate_upper=0 450#bitrate_nominal=128003 451#bitrate_lower=0 452#stream_integrity=pass 453#bitrate_average=117573 454#length=264.568163 455#playtime=4:24 456#stream_truncated=false 457# 458#total_length=264.568163 459#total_playtime=4:24 460 461 4621; 463