# $Id: Ogg.pm,v 1.6 2003/12/14 23:52:04 ianb Exp $ package MP3::Archive::Lint::Tools::Ogg; use strict; use warnings; use vars qw(@ISA $VERSION); @ISA = qw(MP3::Archive::Lint::Tool); $VERSION = '0.01'; =head1 ogg - Tool to check .ogg files Checks Ogg Vorbis files. Requires the program C, available from LEwww.vorbis.comEdownload_unix.psp> On debian systems, it can be installed by typing C as root. =head2 Tests =over 4 =cut sub new { my $proto=shift; my $class=ref($proto) || $proto; my $opts=shift; my $self=$class->SUPER::new($opts);; bless($self,$class); $self->{cmd}=$self->findprogram("ogginfo"); unless (defined $self->{cmd}) { $self->say("ogginfo not found,tests skipped"); return $self; } if($self->config->quickscan) { $self->debug("quick mode enabled, tests skipped"); return $self; } $self->settests("header","short","tags","tracknum","artist","album","track", "serial","bitrate","samplerate","channels","stream"); return $self; } sub initscan { my $self=shift; return 0 unless (defined($self->{tests}) && scalar(@{$self->{tests}})); return 0 unless($self->{filename}=~/\.ogg$/i); return 0 if (-z $self->{file}); return 0 if (-d $self->{file}); unless(open(I,"$self->{cmd} $self->{qfile} 2>/dev/null |")) { $self->say("cannot run ogginfo:$!"); return; } @{$self->{ogginfo}}=; chomp(@{$self->{ogginfo}}); unless(close(I)) # ogginfo returned nonzero { # fake message from stream $self->say("stream:ogg corrupt"); } } =item B
Tests integrity of Ogg header. =cut sub header { my $self=shift; if(scalar(grep(/(?:header_integrity=fail|could not decode vorbis header|Vorbis stream \d+ does not have headers|Invalid header page)/i,@{$self->{ogginfo}}))) { $self->say("integrity failure"); } } =item B Tests if Ogg is truncated. =cut sub short { my $self=shift; if(scalar(grep(/(?:stream_truncated=true|EOS not set)/i,@{$self->{ogginfo}}))) { $self->say("ogg truncated"); } } =item B Tests for corrupt comment tags =cut sub tags { my $self=shift; if(scalar(grep(/(?:Invalid comment fieldname|Illegal UTF-8 sequence)/i,@{$self->{ogginfo}}))) { $self->say("corrupt comment tag"); } } =item B Tests if Ogg has a TRACKNUMBER comment, that it is a number, and matches the filename. If the B<-n> option is given to mp3lint, this test only checks that the tracknum is a valid number, not that it matches the filename. =cut sub tracknum { my $self=shift; return unless($self->isalbum()); my $found=0; my $filenum; for my $line (@{$self->{ogginfo}}) { if($line=~/^\s*TRACKNUMBER=(.*)/) { $found++; my $num=$1; if($num!~/^\d+$/) { $self->say("not a number"); } elsif((!$self->config->skipnametests()) && ($filenum=$self->config->archive->tracknum($self->{pathfilename}))) { if($num != $filenum) { $self->say("file/comment mismatch:$num"); } } } } if(!$found) { $self->say("no TRACKNUMBER comment"); } } =item B Tests if Ogg has a ARTIST comment, and that it matches the filename. The filename test is suppressed if the B<-n> option is given to mp3lint. =cut sub artist { my $self=shift; my $found=0; for my $line (@{$self->{ogginfo}}) { if($line=~/^\s*ARTIST=(.*)/) { $found++; if(!$self->config->skipnametests()) { my $artist=$1; my $fileartist; if($fileartist=$self->config->archive->artist($self->{pathfilename})) { if($artist ne $fileartist) { $self->say("file/comment mismatch:$artist"); } } } } } if(!$found) { $self->say("no ARTIST comment"); } } =item B Tests if Ogg has a ALBUM comment, and that it matches the filename. The filename test is suppressed if the B<-n> option is given to mp3lint. =cut sub album { my $self=shift; return unless($self->isalbum()); my $found=0; for my $line (@{$self->{ogginfo}}) { if($line=~/^\s*ALBUM=(.*)/) { $found++; if(!$self->config->skipnametests()) { my $album=$1; my $filealbum; if($filealbum=$self->config->archive->album($self->{pathfilename})) { if($album ne $filealbum) { $self->say("file/comment mismatch:$album"); } } } } } if(!$found) { $self->say("no ALBUM comment"); } } =item B Tests if Ogg has a TITLE comment (track name), and that it matches the filename. The filename test is suppressed if the B<-n> option is given to mp3lint. =cut sub track { my $self=shift; my $found=0; for my $line (@{$self->{ogginfo}}) { if($line=~/^\s*TITLE=(.*)/) { $found++; if(!$self->config->skipnametests()) { my $track=$1; my $filetrack; if($filetrack=$self->config->archive->track($self->{pathfilename})) { if($track ne $filetrack) { $self->say("file/comment mismatch:$track"); } } } } } if(!$found) { $self->say("no TITLE comment"); } } =item B Tests if Ogg has multiple streams in it by the presence of multiple serial numbers. =cut sub serial { my $self=shift; my $count=scalar(grep(/^(?:\s*serial=|.*stream.*serial\:)/,@{$self->{ogginfo}})); if($count>1) { $self->say("multiple streams:$count"); } } =item B Tests Ogg nominal (requested) bitrate is at least $lint_minbitrate_ogg kbps. Variables: =over 4 =item $lint_minbitrate_ogg (number, default 100 kbps) =back =cut sub bitrate { my $self=shift; my $minbitrate=$self->config->get("lint_minbitrate_ogg"); if($minbitrate<1000) { $minbitrate *= 1000; } for my $line (@{$self->{ogginfo}}) { # go for nominal (intended) not average (actual) if($line=~/bitrate_nominal=(.*)/) { my $nominal=$1; if($nominal<$minbitrate) { $self->say(sprintf("%s%.2fkbps (min %.2fkbps)","low bitrate:",$nominal/1000,$minbitrate/1000)); } } elsif($line=~/Nominal\s+bitrate:\s+([\d\.]+)/i) { my $nominal=$1; $nominal *= 1000; # convert to bps $nominal=sprintf("%.0f",$nominal); if($nominal<$minbitrate) { $self->say(sprintf("%s%.2fkbps (min %.2fkbps)","low bitrate:",$nominal/1000,$minbitrate/1000)); } } } } =item B Tests if Ogg is stereo. =cut sub channels { my $self=shift; for my $line (@{$self->{ogginfo}}) { if($line=~/channels(?:=|:)\s*(\d+)/i) { my $channels=$1; if($channels==1) { $self->say("mono"); } elsif($channels!=2) { $self->say("not stereo:$channels channels"); } } } } =item B Tests Ogg samplerate is at least $lint_minsamplerate Hz. Variables: =over 4 =item $lint_minsamplerate (number, default 44100 Hz) =back =cut sub samplerate { my $self=shift; my $minsamplerate=$self->config->get("lint_minsamplerate"); for my $line (@{$self->{ogginfo}}) { if($line=~/^\s*rate(?:=|:)\s*(\d+)/i) { if($1 < $minsamplerate) { $self->say("low samplerate:$1 (min $minsamplerate)"); } } } } =item B Tests integrity of Ogg stream[s] in file. =cut sub stream { my $self=shift; for my $line (@{$self->{ogginfo}}) { 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) { $self->say("integrity failure"); last; } } } =back =head2 Bugs Does not yet handle flac files embedded in .ogg files. =cut #filename=Hydrate-Kenny_Beltrey.ogg # #serial=6109 #header_integrity=pass #TITLE=Hydrate - Kenny Beltrey #ARTIST=Kenny Beltrey #ALBUM=Favorite Things #DATE=2002 #COMMENT=http://www.kahvi.org #TRACKNUMBER=2 #vendor=Xiph.Org libVorbis I 20020713 #version=0 #channels=2 #rate=44100 #bitrate_upper=0 #bitrate_nominal=128003 #bitrate_lower=0 #stream_integrity=pass #bitrate_average=117573 #length=264.568163 #playtime=4:24 #stream_truncated=false # #total_length=264.568163 #total_playtime=4:24 1;