1#!/usr/local/bin/perl
2use strict;
3use warnings;
4use Getopt::Long qw(:config no_ignore_case bundling);
5use File::Path;
6use Pod::Usage;
7use Cwd ('abs_path');
8
9our $VERSION = 1.027_000;
10
11=pod
12
13=head1 NAME
14
15mp3cd - Burns normalized audio CDs from lists of MP3s/WAVs/Oggs/FLACs
16
17=head1 SYNOPSIS
18
19mp3cd [OPTIONS] [playlist|files...]
20
21 -s, --stage STAGE  Start at a certain stage of processing:
22                        clean   Start fresh (default, requires playlist)
23                        build   Does not clean (requires playlist)
24                        decode  Turns MP3s/Oggs/FLACs into WAVs
25                        correct Fix up any WAV formats
26                        norm    Normalizes WAV volumes
27                        toc     Builds a Table of Contents from WAVs
28                        toc_ok  Checks TOC validity
29                        cdr_ok  Checks for a CDR
30                        burn    Burns from the TOC
31 -q                 Quits after one stage of processing
32 -t, --tempdir DIR  Set working dir (default "/tmp/mp3cd-$USER")
33 -d, --device PATH  Look for CDR at "PATH" (default "/dev/cdrecorder")
34 -r, --driver TYPE  Use CDR driver TYPE (default up to cdrdao)
35 -n, --simulate     Don't actually burn a disc but do everything else.
36 -E, --no-eject     Don't eject drive after the burn.
37 -L, --no-log       Don't redirect output to "tool-output.txt"
38 -T, --no-cd-text   Don't attempt to write CD-TEXT tags to the audio CD
39 -c, --cdrdao ARGS  Pass the option string ARGS to cdrdao.
40 -S, --skip STAGES  Skip the comma-separated list of stages in STAGES.
41 -V, --version      Report which version of the script this is.
42 -v, --verbose      Shows commands as they are executed.
43 -h, --usage        Shows brief usage summary.
44     --help         Shows detailed help summary.
45     --longhelp     Shows complete help.
46
47=head1 OPTIONS
48
49=over 8
50
51=item B<-s STAGE>, B<--stage STAGE>
52
53Starts processing at a given stage. This is used in
54case you had to stop processing, or a file was missing, or things
55generally blew up. It is especially useful if a burn fails because then
56you don't have to start totally over and re-WAV the files. If you just
57want to perform a single step, use B<--quit> to abort after the stage
58you request with B<--stage>. Also see B<--skip>.
59
60=over 8
61
62=item B<clean>
63
64This is the default starting stage. The temp directory is cleared out.
65A playlist is required, since we expect to move to the B<build> stage
66next, which requires it.
67
68=item B<build>
69
70This stage examines the playlist from the command line, and tries to
71create a list of symlinks from the given playlist. So far, C<mp3cd>
72can understand ".m3u" files, XMLPlaylist files, and lists of files.
73
74=item B<decode>
75
76All the files are converted into WAVs. So far, C<mp3cd> knows how to
77decode MP3, Ogg, and FLAC files. (WAVs will be left as they are during
78this stage.)
79
80=item B<correct>
81
82The WAV files are corrected to have the correct bitrate and number of
83channels, as required for an audio CD.
84
85=item B<norm>
86
87The WAV files' volumes are normalized so any large differences in volume
88between records will be less noticeable.
89
90=item B<toc>
91
92Generates a Table of Contents for the audio CD.
93
94=item B<toc_ok>
95
96Validates the TOC, just in case something went really wrong with
97the WAV files.
98
99=item B<cdr_ok>
100
101Verifies that there is a CDR ready for burning.
102
103=item B<burn>
104
105Actually performs the burn of all the WAV files to the waiting CDR.
106
107=back
108
109=item B<-q>, B<--quit>
110
111Aborts after one stage of processing. See B<--stage>.
112
113=item B<-t DIR>, B<--tempdir DIR>
114
115Use a working directory other than "/tmp/mp3cd-B<username>". This is
116where all the file processing occurs. You will generally need at least
117650M free here (or more depending on the recording length of your destination
118CD).
119
120=item B<-d PATH>, B<--device PATH>
121
122Use a device path other than "/dev/cdrecorder".
123
124=item B<-r TYPE>, B<--driver TYPE>
125
126Use a CDRDAO driver other than what cdrdao automatically detects. Note that
127some drivers may not support CD-TEXT mode. In this case, try "generic-mmc-raw".
128
129=item B<-c ARGS>, B<--cdrdao ARGS>
130
131Pass the given option string of ARGS to cdrdao during each command.
132
133=item B<-n>, B<--simulate>
134
135Do not actually write to the disc but simulate the process instead.
136
137=item B<-E>, B<--no-eject>
138
139Don't eject drive after the burn.
140
141=item B<-L>, B<--no-log>
142
143Don't redirect output to "tool-output.txt". All information will instead be
144redirected to the terminal via standard output (STDOUT). This will cause a
145lot of low-level detail to be displayed.
146
147=item B<-T>, B<--no-cd-text>
148
149Don't attempt to write CD-TEXT tags to the audio CD. Some devices and drivers
150do not support this mode. See B<--driver> for more details.
151
152=item B<-S STAGES>, B<--skip STAGES>
153
154While processing, skips the stages listed in the comma-separated list of
155stages given in STAGES. This would only be used if you really know what
156you're doing. For example, if the audio is already normalized and you
157didn't want to burn a CD, you could skip the normalizing and burning stages
158by giving "--skip norm,burn". See B<--stage> and B<--quit>.
159
160=item B<-V>, B<--version>
161
162Report which version of mp3cd this is.
163
164=item B<-v>, B<--verbose>
165
166Shows commands as they are executed.
167
168=item B<-h>, B<--usage>
169
170Show brief usage summary.
171
172=item B<--help>
173
174Show detailed help summary.
175
176=item B<--longhelp>
177
178Shows the full command line instructions.
179
180=back
181
182=head1 DESCRIPTION
183
184This script implements the suggested methods outlined in the
185Linux MP3 CD Burning mini-HOWTO:
186 L<http://tldp.org/HOWTO/MP3-CD-Burning/>
187
188This will burn a playlist (.m3u, XMLPlaylist or command line list) of
189MP3s, Oggs, FLACs, and/or WAVs to an audio CD. The ".m3u" format is really
190nothing more than a list of fully qualified filenames. The script handles
191making the WAVs sane by resampling if needed, and normalizing the volume
192across all tracks.
193
194If a failure happens, earlier stages can be skipped with the '-s' flag.
195The file "tool-output.txt" in the temp directory can be examined to see what
196went wrong during the stage. Some things are time-consuming (like decoding
197the audio into WAVs) and if the CD burn fails, it's much nicer not to have to
198start over from scratch. When doing this, you will not need the m3u file any
199more, since the files have already been built. See the list of stages using
200'-h'.
201
202=head1 PREREQUISITES
203
204Requires C<cdrdao>, and that /dev/cdrecorder is a valid symlink to the
205/dev/sg device that cdrdao will use. Use .cdrdao to edit driver
206options. (See "man cdrdao" for details.)
207
208Requires C<sox> to decode MP3 and check/correct WAV formats.
209 http://www.spies.com/Sox/
210
211Requires C<normalize> to process the audio.
212 http://www.cs.columbia.edu/~cvaill/normalize/
213
214Optionally requires C<oggdec> to decode Ogg to WAV files.
215 http://www.gnu.org/directory/audio/ogg/OggEnc.html/
216
217Optionally requires C<flac> to decode flac to WAV files.
218 http://flac.sourceforge.net/
219
220Optionally requires C<Config::Simple> Perl module if you want to use
221the .mp3cdrc file.
222 http://search.cpan.org/~sherzodr/Config-Simple/
223
224=head1 FILES
225
226=over 8
227
228=item B<~/.mp3cdrc>
229
230Default options can be recorded in this file. The option names are the same
231as their command line long-name. Command line options will override these
232values. All options are run through perl's eval. For example:
233    tempdir: /scratch/mp3cd/$ENV{'USER'}
234    device: /dev/burner
235
236=back
237
238=head1 AUTHOR
239
240 Kees Cook <kees@outflux.net>
241
242 Contributors:
243
244 J. Katz (Ogg support)
245 Alex Rhomberg (XMLPlaylist support)
246 Kevin C. Krinke (filelist inspiration, and countless many patches)
247 James Greenhalgh (flac support)
248
249=head1 SEE ALSO
250
251perl(1), cdrdao(1), sox(1), oggdec(1), flac(1), sox(1), normalize(1).
252
253=head1 COPYRIGHT
254
255 Copyright (C) 2003-2011 Kees Cook
256 kees@outflux.net, http://outflux.net/
257
258 This program is free software; you can redistribute it and/or
259 modify it under the terms of the GNU General Public License
260 as published by the Free Software Foundation; either version 2
261 of the License, or (at your option) any later version.
262
263 This program is distributed in the hope that it will be useful,
264 but WITHOUT ANY WARRANTY; without even the implied warranty of
265 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
266 GNU General Public License for more details.
267
268 You should have received a copy of the GNU General Public License
269 along with this program; if not, write to the Free Software
270 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
271 http://www.gnu.org/copyleft/gpl.html
272
273=cut
274
275# Change this to a location where you'll have at least a CD's worth of
276# disk space available. (For the WAVs)
277# Its contents will be deleted, so be careful. :)
278my $BURNDIR="/tmp/mp3cd-".getpwuid($<);
279
280# Filename to redirect sub-tool stdout/stderr
281my $LOG="tool-output.txt";
282
283# Filename to write the TOC to
284my $CDTOC="cdda.toc";
285
286# Filename to write tag info to
287my $TAGS="tag.data";
288
289# List of audio files to burn (useful only for the "build" stage)
290my @FILES=();
291
292my %stage_func = (
293    "clean"    => \&Do_Clean,
294    "build"    => \&Do_Build,
295    "decode"   => \&Do_Decode,
296    "correct"  => \&Do_Correct,
297    "norm"     => \&Do_Normalize,
298    "toc"      => \&Do_TOC,
299    "toc_ok"   => \&Do_TOC_Verify,
300    "cdr_ok"   => \&Do_CDR_Check,
301    "burn"     => \&Do_Burn,
302);
303my $UNKNOWN="unknown-format";
304my %decoders = (
305    "flac" =>{ 'require' => 'flac',
306               'args'    => '--silent -d -F $input -o $output',
307               'normal'  => '--silent',
308               'verbose' => '',
309             },
310    "ogg" => { 'require' => 'oggdec',
311               'args'    => '$input -o $output',
312               'normal'  => '--quiet',
313               'verbose' => '',
314             },
315    "mp3" => { 'require' => 'sox',
316               'args'    => '$input $output',
317               'normal'  => '',
318               'verbose' => '-v',
319             },
320    "m4a" => { 'require' => 'faad',
321               'args'    => '-o $output $input',
322               'normal'  => '--quiet',
323               'verbose' => '',
324             },
325    $UNKNOWN =>  { 'require' => 'mplayer',
326               'args'    => '-hardframedrop -vc null -vo null -ao pcm:fast:file=$output $input',
327               'normal'  => '-quiet',
328               'verbose' => '',
329             },
330    # Dummy entry to recognize WAVs
331    "wav" => { 'require' => 'sox',
332             },
333);
334
335my @stages;
336my %stages;
337my $count=0;
338my $stage;
339foreach $stage (qw(clean build decode correct norm toc toc_ok cdr_ok burn)) {
340    push(@stages,$stage);
341    $stages{$stage}=$count++;
342}
343
344
345our $opt_help=undef;
346our $opt_longhelp=undef;
347our $opt_usage=undef;
348our $opt_version=undef;
349our $opt_quit=undef;
350our $opt_stage="clean";
351our $opt_tempdir=undef;
352our $opt_cdrdao="";
353our $opt_device="/dev/cdrecorder";
354our $opt_driver=undef;
355our $opt_simulate=undef;
356our $opt_no_eject=0;
357our $opt_no_log=0;
358our $opt_no_cd_text=0;
359our $opt_skip="";
360our $opt_verbose=0;
361
362my @options=(
363    'help',
364    'longhelp',
365    'usage|h',
366    'version|V',
367    'verbose|v',
368    'stage|s=s',
369    'skip|S=s',
370    'quit|q',
371    'tempdir|t=s',
372    'device|d=s',
373    'driver|r=s',
374    'cdrdao|c=s',
375    'simulate|n',
376    'no-eject|E',
377    'no-log|L',
378    'no-cd-text|T',
379);
380
381# Look for RC defaults
382my %rc;
383my $rcfile="$ENV{'HOME'}/.mp3cdrc";
384if (-r $rcfile) {
385    require Config::Simple;
386    Config::Simple->import_from($rcfile,\%rc);
387}
388foreach my $opt (@options) {
389    my ($name) = $opt =~ /^([^|]+)/;
390    $name=~s/-/_/g;
391    my $is_str = $opt =~ /=s$/ || 0;
392
393    if (defined($rc{$name})) {
394        eval "\$opt_$name = \"$rc{$name}\";";
395        if (!$is_str) {
396            eval "\$opt_$name = \$opt_$name ? 1 : 0;";
397        }
398    }
399}
400
401# Load command line options
402GetOptions(@options) or pod2usage( -exitval=>1, -verbose=>0 );
403
404# Handle help/usage
405pod2usage( -exitval=>0, -verbose=>2 ) if ($opt_longhelp);
406pod2usage( -exitval=>0, -verbose=>1 ) if ($opt_help);
407pod2usage( -exitval=>0, -verbose=>0 ) if ($opt_usage);
408Version() if ($opt_version);
409
410# cdrdao needs to pick up device and driver from the command line
411$opt_cdrdao .= " --device $opt_device";
412$opt_cdrdao .= " --driver $opt_driver" if (defined($opt_driver));
413
414# Validate starting stage
415if (!defined($stages{$opt_stage})) {
416    pod2usage(  -exitval=>1, -verbose=>0,
417                -msg=>"Unknown start stage '$opt_stage'!" );
418}
419$stage=$opt_stage;
420
421# Check if we need (or do not need) a playlist/filelist
422if ($stage eq "clean" ||
423    $stage eq "build")
424{
425    if (!defined($ARGV[0])) {
426        pod2usage(  -exitval=>1, -verbose=>0,
427                    -msg=>"Playlist/File list is required!" );
428    }
429}
430elsif (@ARGV) {
431    pod2usage(  -exitval=>1, -verbose=>0,
432                -msg=> "Playlists/Files are ignored past stage 'build'!" );
433}
434
435# Build a hash of the stages to skip
436my %skip_stage;
437foreach my $skip (split(/,/,$opt_skip)) {
438    if (!defined($stages{$skip})) {
439        pod2usage(  -exitval=>1, -verbose=>0,
440                    -msg=>"Unknown stage to skip '$skip'!" );
441    }
442    $skip_stage{$skip}=1;
443}
444# Skip all the stages after the selected one, in case of "--quit"
445my $cancel_rest = 0;
446foreach my $last (@stages) {
447    if ($cancel_rest) {
448        $skip_stage{$last}=1;
449    }
450    if ($opt_quit && $last eq $stage) {
451        $cancel_rest = 1;
452    }
453}
454
455# Figure out our burning directory
456$BURNDIR=$opt_tempdir if (defined($opt_tempdir));
457
458# check for directory
459if (!opendir(DIR, $BURNDIR)) {
460    eval { mkpath($BURNDIR) };
461    if ($@) {
462        die "Can't create working directory '$BURNDIR': $@\n";
463    }
464    opendir(DIR, $BURNDIR) || die "Can't open directory '$BURNDIR': $!\n";
465}
466closedir DIR;
467
468# if no_log print all to stdout
469my $OUTPUT = ( $opt_no_log ) ? "" : ">>$LOG";
470
471sub System
472{
473    my $cmd = $_[0];
474    print STDERR $cmd."\n" if $opt_verbose;
475    return system($cmd);
476}
477
478sub Backtick
479{
480    my $cmd = $_[0];
481    print STDERR $cmd."\n" if $opt_verbose;
482    # Cannot pipe to "tee" since it will mask exit codes
483    my $output = `$cmd 2>&1`;
484    my $rc = $?;
485
486    my $logfile;
487    open($logfile, ">>$LOG") or die "Cannot write to $LOG: $!\n";
488    print $logfile $output;
489    close($logfile);
490    print $output if ($opt_no_log);
491
492    return $rc, $output;
493}
494
495# For-sure needed tools
496my %PREREQS = (
497    'sox' => 'sox',
498    'cdrdao' => 'cdrdao',
499    'gst-launch' => 'gst-launch',
500);
501$PREREQS{'normalize'} = 'normalize,normalize-audio'
502    if (!defined($skip_stage{'norm'}));
503my %found;
504
505sub Lookup_tools
506{
507    # check for required tools
508    foreach my $requirement (sort keys %PREREQS) {
509        foreach my $dir (split(/:/,$ENV{'PATH'})) {
510            foreach my $prog (split(/,/,$PREREQS{$requirement})) {
511                if (!defined($found{$requirement}) && -x "$dir/$prog") {
512                    $found{$requirement}="$dir/$prog";
513                    last;
514                }
515            }
516        }
517    }
518    my $abort=undef;
519    foreach my $requirement (sort keys %PREREQS) {
520        if (!defined($found{$requirement})) {
521            my $tried = "Tried: ".$PREREQS{$requirement};
522            $tried =~ s/,/, /g;
523            warn "Cannot find program to handle '$requirement'!  $tried\n";
524            $abort=1;
525        }
526    }
527    return $abort;
528}
529
530# Load file list, update needed tools
531Load_file_list();
532pod2usage( -exitval => 1, -verbose => 0 ) if (Lookup_tools());
533
534# check for CDR device
535my $skip_cdr = defined($skip_stage{'cdr_ok'}) && defined($skip_stage{'burn'});
536if (!$skip_cdr && ! -w $opt_device) {
537    pod2usage(  -exitval=>1, -verbose=>0,
538                -msg=> "Cannot write to '$opt_device'!" );
539}
540
541# Run through all the stages we need to...
542for (;
543     defined($stage) && defined($stages{$stage});
544     $stage=$stages[$stages{$stage}+1]) {
545    if (defined($skip_stage{$stage})) {
546        print "Skipping '$stage' stage...\n";
547        next;
548    }
549
550    $stage_func{$stage}->();
551}
552
553# end of line
554exit(0);
555
556
557### Functions
558
559sub require_extension($$)
560{
561    my ($ext,$file) = @_;
562    my $lookup = $ext;
563    if (!defined($decoders{$lookup})) {
564        # Unknown audio file format
565        print STDERR "Not sure how to handle file type '$ext' ($file),\n";
566        print STDERR "falling back to ".$decoders{$UNKNOWN}->{'require'}.".\n";
567        $lookup = $UNKNOWN;
568    }
569    $PREREQS{"decoder:$lookup"}=$decoders{$lookup}->{'require'};
570}
571
572sub Load_file_list
573{
574    # Keep a count of how many files we've examined, and stop after, say,
575    # 1000, in case an m3u lists itself (which is REALLY unlikely, but would
576    # effectively put this code into a memory-eating endless loop).
577    my $toomany=1000;
578    while (my $file=shift @ARGV) {
579        $file =~ m/\.([^\.]+)$/i;
580        my $ext = lc($1 || "");
581        if ($ext eq "m3u" || $ext eq "pls" || $ext eq "xspf" || $ext eq "") {
582            # Playlist
583            open(M3U,$file) || die "Cannot open '$file': $!\n";
584            my @lines=<M3U>;
585            close(M3U);
586
587            my @files;
588            if (scalar(@lines) && $lines[0] =~ /<!DOCTYPE\s+XMLPlaylist>/i) {
589                # kaffeine playlists
590                require XML::Simple;
591                my $contents = XML::Simple::XMLin($file);
592                if (ref($contents->{entry}) eq 'ARRAY') {
593                    @files = map {$_->{url}} @{$contents->{entry}};
594                    s/^file:// for @files;
595                } else {
596                    @files = ($contents->{entry}->{url});
597                }
598            }
599            else {
600                # regular list of files
601                foreach (@lines) {
602                        chomp;
603                        next if (/^#/);
604                        push(@files,$_);
605                }
606            }
607            unshift(@ARGV,@files);
608        }
609        else {
610            require_extension($ext,$file);
611            push(@FILES,$file);
612        }
613        die ">1000 files in the list?!  I must have started looping forever.\n"
614            if (--$toomany<0);
615    }
616    # Get absolute locations
617    @FILES = map { abs_path($_) } @FILES;
618}
619
620sub Do_Clean
621{
622    print "Cleaning up...\n";
623
624    # clear out burn dir
625    my @list = ("$BURNDIR/$CDTOC","$BURNDIR/$LOG", "$BURNDIR/$TAGS");
626    foreach my $ext ("wav", sort keys %decoders) {
627        push(@list,"$BURNDIR/*.$ext");
628    }
629    System("rm -f ".join(" ",@list));
630}
631
632sub append_tag_info($$$)
633{
634    my ($media, $title, $path) = @_;
635    my $artist = "";
636    my ($rc, $output) = Backtick("gst-launch -t filesrc location=$media ! decodebin");
637    die "Could not extract tags: $!\n" if ($rc != 0);
638    my $tags = 0;
639    # Parse gst-launch -t output
640    # FOUND TAG      : found by element "qtdemux0".
641    #           title: Just Dance
642    #          artist: Lady GaGa & Colby O'Donis
643
644    foreach my $line (split("\n", $output)) {
645        if ($line =~ /^FOUND TAG/) {
646            $tags = 1;
647            next;
648        }
649        next if ($tags != 1);
650        if ($line =~ /^\S/) {
651            $tags = 0;
652            next;
653        }
654        my ($field, $value) = $line =~ /^\s*(\S*)\s*:\s*(.*)$/;
655        next if (!defined($field));
656        $title=$value  if ($field eq "title");
657        $artist=$value if ($field eq "artist");
658    }
659    my $tagfile;
660    open($tagfile,">>$TAGS") or die "Cannot write to $TAGS: $!\n";
661    print $tagfile "$title\n";
662    print $tagfile "$artist\n";
663    if ($opt_verbose) {
664        print "\ttitle: $title\n";
665        print "\tartist: $artist\n";
666    }
667}
668
669sub Do_Build
670{
671    # go there
672    chdir($BURNDIR) || die "Cannot chdir('$BURNDIR'): $!\n";
673
674    # Clear the tag file, since we're regenerating it
675    System("rm -f $TAGS");
676
677    my $error=undef;
678    my $count=0;
679    # make link for each file, and retain extension
680    foreach my $file (@FILES)
681    {
682        chomp($file);
683        next if ($file =~ /^#/);
684        my @parts=split(/\./,$file);
685        my $ext=lc(pop(@parts));
686        $ext=~tr/A-Z/a-z/;
687
688        @parts=split(/\//,$file);
689        my $name=pop(@parts);
690
691        if (!defined($decoders{$ext}) && !defined($decoders{$UNKNOWN})) {
692            warn "Error: '$file': unknown extension '$ext'!\n";
693            $error=1;
694            next;
695        }
696
697        if (!-f $file)
698        {
699            warn "Error: '$file': $!\n";
700            $error=1;
701            next;
702        }
703
704        $count++;
705        my $track=sprintf("%02d",$count);
706        print "$track: [...]/$name\n";
707        symlink($file,"$track.$ext") || die "symlink('$file','$count.$ext'): $!\n";
708        append_tag_info("$track.$ext", $name, $file);
709    }
710
711    die "Stopping due to errors...\n" if (defined($error));
712
713    # make sure we have some tracks
714    die("No tracks?!\n") unless ($count>0);
715}
716
717sub Do_Decode
718{
719    # go there
720    chdir($BURNDIR) || die "Cannot chdir('$BURNDIR'): $!\n";
721
722    # leave any WAVs in playlist alone
723    opendir(DIR, $BURNDIR) || die "Can't read directory '$BURNDIR': $!\n";
724    my @need_decode = grep { /^\d+\.[^\.]+$/i && !/\.wav$/ && -f "$BURNDIR/$_" } readdir(DIR);
725    closedir DIR;
726
727    # Re-check extensions and tools in case we're restarting
728    foreach my $to_decode (sort {$a cmp $b} @need_decode)
729    {
730        my @parts=split(/\./,$to_decode);
731        my $name=shift(@parts);
732        my $ext=pop(@parts);
733        require_extension($ext, $to_decode);
734    }
735    die "Cannot locate needed decoders\n" if (Lookup_tools());
736
737    # decode audio into WAV files
738    foreach my $to_decode (sort {$a cmp $b} @need_decode)
739    {
740        my @parts=split(/\./,$to_decode);
741        my $name=shift(@parts);
742        my $ext=pop(@parts);
743        my $file="${name}.wav";
744
745        if (-f $file)
746        {
747            print "Skipping track $name: $file exists.\n";
748        }
749        else
750        {
751            print "Creating WAV for track $name ...\n";
752            my $lookup = $ext;
753            if (!defined($decoders{$lookup})) {
754                $lookup = $UNKNOWN;
755            }
756            my $decoder = $decoders{$lookup};
757            if (!defined($decoder)) {
758                die("No decoder available for extension '$ext' - decoding failed!\n");
759            }
760            my @cmd = ($found{"decoder:$lookup"});
761
762            # chose verbosity level
763            if (!$opt_no_log) {
764                push(@cmd,$decoder->{'normal'});
765            }
766            else {
767                push(@cmd,$decoder->{'verbose'});
768            }
769
770            # set up arguments
771            my $input = $to_decode;
772            my $output = $file;
773            push(@cmd,eval "return \"$decoder->{'args'}\"");
774
775            # run decoder (don't need to worry about arg splits since we're
776            # operating against symlinked files with known names, etc)
777            my $cmd = join(" ",@cmd);
778            # redirect logging
779            $cmd="$cmd $OUTPUT 2>&1";
780
781            System($cmd) == 0
782                or die("Decoding failed!\n");
783        }
784    }
785}
786
787sub Do_Correct
788{
789    # go there
790    chdir($BURNDIR) || die "Cannot chdir('$BURNDIR'): $!\n";
791
792    # get list of wavs from directory
793    opendir(DIR, $BURNDIR) || die "Can't read directory '$BURNDIR': $!\n";
794    my @wavs = grep { /^\d+\.wav$/i && -f "$BURNDIR/$_" } readdir(DIR);
795    closedir DIR;
796
797    # correct any wav file formats
798    foreach my $wav (sort {$a cmp $b} @wavs)
799    {
800        my @parts=split(/\./,$wav);
801        my $name=shift(@parts);
802        print "Checking WAV format for track $name ...\n";
803        my $report=`sox -V $wav $wav.raw trim 0.1 1 2>&1`;
804
805        my ($channels, $frequency, $samples);
806        if ($report =~ /^Input File/m) {
807            # In version 13.0.0, the report format has changed
808
809            # Sample Size    : 8-bit (1 byte)
810            # Channels       : 1
811            # Sample Rate    : 11025
812            $report =~ m/Sample (?:Size|Encoding)\s*:\s+(\d+)-bit/s
813                or die "sox did not report sample size:\n$report";
814            $samples = $1;
815            $report =~ m/Channels\s+:\s+(\d+)/s
816                or die "sox did not report channel count:\n$report";
817            $channels = $1;
818            $report =~ m/Sample Rate\s+:\s+(\d+)/s
819                or die "sox did not report sample frequency:\n$report";
820            $frequency = $1;
821        }
822        else {
823            # sox: Reading Wave file: Microsoft PCM format, 2 channels,
824            # sox: 44100 samp/sec 176400 byte/sec,  block align, 16 bits/samp,
825            # sox: 44886528 data bytes
826            $report =~ m|(\d+) channels?|s
827                or die "sox did not report channel count:\n$report";
828            $channels = $1;
829            $report =~ m|(\d+) samp/sec|s
830                or die "sox did not report sample frequency:\n$report";
831            $frequency = $1;
832            $report =~ m|(\d+) bits/samp|s
833                or die "sox did not report sample size:\n$report";
834            $samples = $1;
835        }
836
837        unless ($channels == 2 &&
838                $frequency == 44100 &&
839                $samples == 16)
840        {
841
842            # only do a "resample" if frequency isn't correct
843            my $resample="resample";
844            $resample="" if ($frequency == 44100);
845            print "Correcting WAV format for track $name ...\n";
846            System("sox $wav -r 44100 -c 2 new-$wav $resample $OUTPUT 2>&1") == 0
847                or die("Correction failed!\n");
848            unlink($wav) || die "unlink('$wav'): $!\n";
849            rename("new-$wav",$wav) || die "rename('new-$wav','$wav'): $!\n";
850        }
851        unlink("$wav.raw");
852    }
853}
854
855sub Do_Normalize
856{
857    # go there
858    chdir($BURNDIR) || die "Cannot chdir('$BURNDIR'): $!\n";
859
860    # normalize the volumes
861    print "Normalizing volume levels...\n";
862    System("$found{'normalize'} -m [0-9]*.wav") == 0
863        or die("Normalizing failed!\n");
864    print "Normalizing finished.\n";
865}
866
867sub encode_cd_text_data($)
868{
869    my ($data) = @_;
870    my $encoded = "";
871    # Handle backslash and quotes
872    $data =~ s/\\/\\\\/g;
873    $data =~ s/"/\\"/g;
874    # Using the binary data method seems to fail (missing trailing 0?)
875#    if ($data =~ /"/) {
876#        $encoded = "{ " . join(", ",map(ord, split(//,$data))) . " }";
877#    }
878#    else {
879        $encoded = "\"" . $data . "\"";
880#    }
881    return $encoded;
882}
883
884sub cd_text($$)
885{
886    my ($title, $artist) = @_;
887    chomp($title);
888    chomp($artist);
889
890    my $text = "CD_TEXT {\n  LANGUAGE 0 {\n";
891    $text .= "    TITLE " . encode_cd_text_data($title) . "\n";
892    $text .= "    PERFORMER " . encode_cd_text_data($artist) . "\n";
893    $text .= "  }\n}\n";
894
895    return $text;
896}
897
898sub Do_TOC
899{
900    # go there
901    chdir($BURNDIR) || die "Cannot chdir('$BURNDIR'): $!\n";
902
903    print "Generating CDR Table of Contents...\n";
904
905    # Get ready to read tags
906    my $tagfile;
907    open($tagfile,"<$TAGS") || die "Cannot read $TAGS: $!\n";
908
909    # create a TOC for cdrdao
910    open(TOC,">$CDTOC") || die("Cannot write to '$CDTOC': $!\n");
911    print TOC "CD_DA\n";
912    if (!$opt_no_cd_text) {
913        # CDRDAO wants title/performer for the cd itself too, so leave them blank
914        print TOC <<EOM;
915CD_TEXT {
916  LANGUAGE_MAP {
917    0 : EN
918  }
919  LANGUAGE 0 {
920    TITLE ""
921    PERFORMER ""
922  }
923}
924EOM
925    }
926    print TOC "\n";
927
928    # get list of wavs
929    opendir(DIR, $BURNDIR) || die "Can't read directory '$BURNDIR': $!\n";
930    my @wavs = grep { /^\d+\.wav$/i && -f "$BURNDIR/$_" } readdir(DIR);
931    closedir DIR;
932
933    foreach my $wav (sort {$a cmp $b} @wavs)
934    {
935        die ("Yikes!  What happened to '$wav'?!\n") unless (-f $wav);
936        print TOC "TRACK AUDIO\n";
937        if (!$opt_no_cd_text) {
938            print TOC cd_text(scalar(<$tagfile>), scalar(<$tagfile>));
939        }
940        # The trailing space was (is?) needed for some versions of cdrdao
941        print TOC "FILE \"$wav\" 0 \n\n";
942    }
943    close TOC;
944    close $tagfile;
945}
946
947sub Do_TOC_Verify
948{
949    # go there
950    chdir($BURNDIR) || die "Cannot chdir('$BURNDIR'): $!\n";
951
952    print "Verifying generated Table of Contents...\n";
953    System(cdrdao('read-test')." $CDTOC $OUTPUT 2>&1") == 0
954        or die "Failed to create CD Table of Contents?!\n";
955}
956
957sub Do_CDR_Check
958{
959    # go there
960    chdir($BURNDIR) || die "Cannot chdir('$BURNDIR'): $!\n";
961
962    print "Checking for CDR...\n";
963    my ($rc, $report) = Backtick(cdrdao('disk-info'));
964    die "CDR not loaded?!\n" if ($rc != 0);
965    print "\tCDR found.\n";
966
967    if (!$opt_no_cd_text) {
968        my $options = undef;
969        my $driver_name = undef;
970        foreach my $line (split("\n",$report)) {
971            chomp($line);
972            if ($line =~ /^Using driver: (.*)\(options (0x[0-9a-fA-F]+)\)$/) {
973                $driver_name = $1;
974                $options = hex($2);
975            }
976        }
977        if (!defined($options)) {
978            die "Could not determine driver options!\n";
979        }
980        elsif ($opt_verbose) {
981            printf("\tDriver name: %s\n", $driver_name);
982            printf("\tDriver options: 0x%04x\n", $options);
983        }
984        # 0x10 == OPT_MMC_CD_TEXT  /usr/share/cdrdao/drivers
985        if (($driver_name =~ /raw writing/) || ($options & 0x10) == 0x10) {
986            print "\tCD-TEXT supported.\n";
987        }
988        else {
989            print "ERROR: It seems that driver selected by cdrdao for $opt_device\n";
990            print " does not support CD-TEXT writing. Either disable CD-TEXT via\n";
991            print " '--no-cd-text' or select a different driver (e.g. try using\n";
992            print " '--driver generic-mmc-raw').\n";
993            exit(1);
994        }
995    }
996}
997
998sub Do_Burn
999{
1000    # go there
1001    chdir($BURNDIR) || die "Cannot chdir('$BURNDIR'): $!\n";
1002
1003    my $cmd = cdrdao('write');
1004    $cmd.=" --eject" if (!$opt_no_eject);
1005    $cmd.=" -n $CDTOC";
1006    System($cmd) == 0
1007        or die "BURN FAILED!\n";
1008}
1009
1010sub Version
1011{
1012    # Create human-readable version with un-human-readable code
1013    print "mp3cd version ".
1014    join(".",map{$_+0} (sprintf("%.6f",$VERSION)
1015        =~/^(\d+)\.?(\d{3})?(\d{3})?$/))."\n";
1016    print <<'EOM';
1017Copyright 2003-2011 Kees Cook <kees@outflux.net>
1018This program is free software; you may redistribute it under the terms of
1019the GNU General Public License. This program has absolutely no warranty.
1020EOM
1021    exit(0);
1022}
1023
1024# return a good cdrdao command string prefix
1025sub cdrdao {
1026    my $operation = $_[0] || 'simulate';
1027    $operation = 'simulate' if ($opt_simulate && $operation eq 'write');
1028
1029   return "cdrdao $operation $opt_cdrdao";
1030}
1031
1032# /* vi:set ai ts=4 sw=4 expandtab: */
1033