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