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