#!/usr/bin/perl
use strict;
use warnings;
use Getopt::Long qw(:config no_ignore_case bundling);
use File::Path;
use Pod::Usage;
use Cwd ('abs_path');

our $VERSION = 1.027_000;

=pod

=head1 NAME

mp3cd - Burns normalized audio CDs from lists of MP3s/WAVs/Oggs/FLACs

=head1 SYNOPSIS

mp3cd [OPTIONS] [playlist|files...]

 -s, --stage STAGE  Start at a certain stage of processing:
                        clean   Start fresh (default, requires playlist)
                        build   Does not clean (requires playlist)
                        decode  Turns MP3s/Oggs/FLACs into WAVs
                        correct Fix up any WAV formats
                        norm    Normalizes WAV volumes
                        toc     Builds a Table of Contents from WAVs
                        toc_ok  Checks TOC validity
                        cdr_ok  Checks for a CDR
                        burn    Burns from the TOC
 -q                 Quits after one stage of processing
 -t, --tempdir DIR  Set working dir (default "/tmp/mp3cd-$USER")
 -d, --device PATH  Look for CDR at "PATH" (default "/dev/cdrecorder")
 -r, --driver TYPE  Use CDR driver TYPE (default up to cdrdao)
 -n, --simulate     Don't actually burn a disc but do everything else.
 -E, --no-eject     Don't eject drive after the burn.
 -L, --no-log       Don't redirect output to "tool-output.txt"
 -T, --no-cd-text   Don't attempt to write CD-TEXT tags to the audio CD
 -c, --cdrdao ARGS  Pass the option string ARGS to cdrdao.
 -S, --skip STAGES  Skip the comma-separated list of stages in STAGES.
 -V, --version      Report which version of the script this is.
 -v, --verbose      Shows commands as they are executed.
 -h, --usage        Shows brief usage summary.
     --help         Shows detailed help summary.
     --longhelp     Shows complete help.

=head1 OPTIONS

=over 8

=item B<-s STAGE>, B<--stage STAGE>

Starts processing at a given stage. This is used in
case you had to stop processing, or a file was missing, or things
generally blew up. It is especially useful if a burn fails because then
you don't have to start totally over and re-WAV the files. If you just
want to perform a single step, use B<--quit> to abort after the stage
you request with B<--stage>. Also see B<--skip>.

=over 8

=item B<clean>

This is the default starting stage. The temp directory is cleared out.
A playlist is required, since we expect to move to the B<build> stage
next, which requires it.

=item B<build>

This stage examines the playlist from the command line, and tries to
create a list of symlinks from the given playlist. So far, C<mp3cd>
can understand ".m3u" files, XMLPlaylist files, and lists of files.

=item B<decode>

All the files are converted into WAVs. So far, C<mp3cd> knows how to
decode MP3, Ogg, and FLAC files. (WAVs will be left as they are during
this stage.)

=item B<correct>

The WAV files are corrected to have the correct bitrate and number of
channels, as required for an audio CD.

=item B<norm>

The WAV files' volumes are normalized so any large differences in volume
between records will be less noticeable.

=item B<toc>

Generates a Table of Contents for the audio CD.

=item B<toc_ok>

Validates the TOC, just in case something went really wrong with
the WAV files.

=item B<cdr_ok>

Verifies that there is a CDR ready for burning.

=item B<burn>

Actually performs the burn of all the WAV files to the waiting CDR.

=back

=item B<-q>, B<--quit>

Aborts after one stage of processing. See B<--stage>.

=item B<-t DIR>, B<--tempdir DIR>

Use a working directory other than "/tmp/mp3cd-B<username>". This is
where all the file processing occurs. You will generally need at least
650M free here (or more depending on the recording length of your destination
CD).

=item B<-d PATH>, B<--device PATH>

Use a device path other than "/dev/cdrecorder".

=item B<-r TYPE>, B<--driver TYPE>

Use a CDRDAO driver other than what cdrdao automatically detects. Note that
some drivers may not support CD-TEXT mode. In this case, try "generic-mmc-raw".

=item B<-c ARGS>, B<--cdrdao ARGS>

Pass the given option string of ARGS to cdrdao during each command.

=item B<-n>, B<--simulate>

Do not actually write to the disc but simulate the process instead.

=item B<-E>, B<--no-eject>

Don't eject drive after the burn.

=item B<-L>, B<--no-log>

Don't redirect output to "tool-output.txt". All information will instead be
redirected to the terminal via standard output (STDOUT). This will cause a
lot of low-level detail to be displayed.

=item B<-T>, B<--no-cd-text>

Don't attempt to write CD-TEXT tags to the audio CD. Some devices and drivers
do not support this mode. See B<--driver> for more details.

=item B<-S STAGES>, B<--skip STAGES>

While processing, skips the stages listed in the comma-separated list of
stages given in STAGES. This would only be used if you really know what
you're doing. For example, if the audio is already normalized and you
didn't want to burn a CD, you could skip the normalizing and burning stages
by giving "--skip norm,burn". See B<--stage> and B<--quit>.

=item B<-V>, B<--version>

Report which version of mp3cd this is.

=item B<-v>, B<--verbose>

Shows commands as they are executed.

=item B<-h>, B<--usage>

Show brief usage summary.

=item B<--help>

Show detailed help summary.

=item B<--longhelp>

Shows the full command line instructions.

=back

=head1 DESCRIPTION

This script implements the suggested methods outlined in the
Linux MP3 CD Burning mini-HOWTO:
 L<http://tldp.org/HOWTO/MP3-CD-Burning/>

This will burn a playlist (.m3u, XMLPlaylist or command line list) of
MP3s, Oggs, FLACs, and/or WAVs to an audio CD. The ".m3u" format is really
nothing more than a list of fully qualified filenames. The script handles
making the WAVs sane by resampling if needed, and normalizing the volume
across all tracks.

If a failure happens, earlier stages can be skipped with the '-s' flag.
The file "tool-output.txt" in the temp directory can be examined to see what
went wrong during the stage. Some things are time-consuming (like decoding
the audio into WAVs) and if the CD burn fails, it's much nicer not to have to
start over from scratch. When doing this, you will not need the m3u file any
more, since the files have already been built. See the list of stages using
'-h'.

=head1 PREREQUISITES

Requires C<cdrdao>, and that /dev/cdrecorder is a valid symlink to the
/dev/sg device that cdrdao will use. Use .cdrdao to edit driver
options. (See "man cdrdao" for details.)

Requires C<sox> to decode MP3 and check/correct WAV formats.
 http://www.spies.com/Sox/

Requires C<normalize> to process the audio.
 http://www.cs.columbia.edu/~cvaill/normalize/

Optionally requires C<oggdec> to decode Ogg to WAV files.
 http://www.gnu.org/directory/audio/ogg/OggEnc.html/

Optionally requires C<flac> to decode flac to WAV files.
 http://flac.sourceforge.net/

Optionally requires C<Config::Simple> Perl module if you want to use
the .mp3cdrc file.
 http://search.cpan.org/~sherzodr/Config-Simple/

=head1 FILES

=over 8

=item B<~/.mp3cdrc>

Default options can be recorded in this file. The option names are the same
as their command line long-name. Command line options will override these
values. All options are run through perl's eval. For example:
    tempdir: /scratch/mp3cd/$ENV{'USER'}
    device: /dev/burner

=back

=head1 AUTHOR

 Kees Cook <kees@outflux.net>

 Contributors:

 J. Katz (Ogg support)
 Alex Rhomberg (XMLPlaylist support)
 Kevin C. Krinke (filelist inspiration, and countless many patches)
 James Greenhalgh (flac support)

=head1 SEE ALSO

perl(1), cdrdao(1), sox(1), oggdec(1), flac(1), sox(1), normalize(1).

=head1 COPYRIGHT

 Copyright (C) 2003-2011 Kees Cook
 kees@outflux.net, http://outflux.net/

 This program is free software; you can redistribute it and/or
 modify it under the terms of the GNU General Public License
 as published by the Free Software Foundation; either version 2
 of the License, or (at your option) any later version.

 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 GNU General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with this program; if not, write to the Free Software
 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 http://www.gnu.org/copyleft/gpl.html

=cut

# Change this to a location where you'll have at least a CD's worth of
# disk space available. (For the WAVs)
# Its contents will be deleted, so be careful. :)
my $BURNDIR="/tmp/mp3cd-".getpwuid($<);

# Filename to redirect sub-tool stdout/stderr
my $LOG="tool-output.txt";

# Filename to write the TOC to
my $CDTOC="cdda.toc";

# Filename to write tag info to
my $TAGS="tag.data";

# List of audio files to burn (useful only for the "build" stage)
my @FILES=();

my %stage_func = (
    "clean"    => \&Do_Clean,
    "build"    => \&Do_Build,
    "decode"   => \&Do_Decode,
    "correct"  => \&Do_Correct,
    "norm"     => \&Do_Normalize,
    "toc"      => \&Do_TOC,
    "toc_ok"   => \&Do_TOC_Verify,
    "cdr_ok"   => \&Do_CDR_Check,
    "burn"     => \&Do_Burn,
);
my $UNKNOWN="unknown-format";
my %decoders = (
    "flac" =>{ 'require' => 'flac',
               'args'    => '--silent -d -F $input -o $output',
               'normal'  => '--silent',
               'verbose' => '',
             },
    "ogg" => { 'require' => 'oggdec',
               'args'    => '$input -o $output',
               'normal'  => '--quiet',
               'verbose' => '',
             },
    "mp3" => { 'require' => 'sox',
               'args'    => '$input $output',
               'normal'  => '',
               'verbose' => '-v',
             },
    "m4a" => { 'require' => 'faad',
               'args'    => '-o $output $input',
               'normal'  => '--quiet',
               'verbose' => '',
             },
    $UNKNOWN =>  { 'require' => 'mplayer',
               'args'    => '-hardframedrop -vc null -vo null -ao pcm:fast:file=$output $input',
               'normal'  => '-quiet',
               'verbose' => '',
             },
    # Dummy entry to recognize WAVs
    "wav" => { 'require' => 'sox',
             },
);

my @stages;
my %stages;
my $count=0;
my $stage;
foreach $stage (qw(clean build decode correct norm toc toc_ok cdr_ok burn)) {
    push(@stages,$stage);
    $stages{$stage}=$count++;
}


our $opt_help=undef;
our $opt_longhelp=undef;
our $opt_usage=undef;
our $opt_version=undef;
our $opt_quit=undef;
our $opt_stage="clean";
our $opt_tempdir=undef;
our $opt_cdrdao="";
our $opt_device="/dev/cdrecorder";
our $opt_driver=undef;
our $opt_simulate=undef;
our $opt_no_eject=0;
our $opt_no_log=0;
our $opt_no_cd_text=0;
our $opt_skip="";
our $opt_verbose=0;

my @options=(
    'help',
    'longhelp',
    'usage|h',
    'version|V',
    'verbose|v',
    'stage|s=s',
    'skip|S=s',
    'quit|q',
    'tempdir|t=s',
    'device|d=s',
    'driver|r=s',
    'cdrdao|c=s',
    'simulate|n',
    'no-eject|E',
    'no-log|L',
    'no-cd-text|T',
);

# Look for RC defaults
my %rc;
my $rcfile="$ENV{'HOME'}/.mp3cdrc";
if (-r $rcfile) {
    require Config::Simple;
    Config::Simple->import_from($rcfile,\%rc);
}
foreach my $opt (@options) {
    my ($name) = $opt =~ /^([^|]+)/;
    $name=~s/-/_/g;
    my $is_str = $opt =~ /=s$/ || 0;

    if (defined($rc{$name})) {
        eval "\$opt_$name = \"$rc{$name}\";";
        if (!$is_str) {
            eval "\$opt_$name = \$opt_$name ? 1 : 0;";
        }
    }
}

# Load command line options
GetOptions(@options) or pod2usage( -exitval=>1, -verbose=>0 );

# Handle help/usage
pod2usage( -exitval=>0, -verbose=>2 ) if ($opt_longhelp);
pod2usage( -exitval=>0, -verbose=>1 ) if ($opt_help);
pod2usage( -exitval=>0, -verbose=>0 ) if ($opt_usage);
Version() if ($opt_version);

# cdrdao needs to pick up device and driver from the command line
$opt_cdrdao .= " --device $opt_device";
$opt_cdrdao .= " --driver $opt_driver" if (defined($opt_driver));

# Validate starting stage
if (!defined($stages{$opt_stage})) {
    pod2usage(  -exitval=>1, -verbose=>0,
                -msg=>"Unknown start stage '$opt_stage'!" );
}
$stage=$opt_stage;

# Check if we need (or do not need) a playlist/filelist
if ($stage eq "clean" ||
    $stage eq "build")
{
    if (!defined($ARGV[0])) {
        pod2usage(  -exitval=>1, -verbose=>0,
                    -msg=>"Playlist/File list is required!" );
    }
}
elsif (@ARGV) {
    pod2usage(  -exitval=>1, -verbose=>0,
                -msg=> "Playlists/Files are ignored past stage 'build'!" );
}

# Build a hash of the stages to skip
my %skip_stage;
foreach my $skip (split(/,/,$opt_skip)) {
    if (!defined($stages{$skip})) {
        pod2usage(  -exitval=>1, -verbose=>0,
                    -msg=>"Unknown stage to skip '$skip'!" );
    }
    $skip_stage{$skip}=1;
}
# Skip all the stages after the selected one, in case of "--quit"
my $cancel_rest = 0;
foreach my $last (@stages) {
    if ($cancel_rest) {
        $skip_stage{$last}=1;
    }
    if ($opt_quit && $last eq $stage) {
        $cancel_rest = 1;
    }
}

# Figure out our burning directory
$BURNDIR=$opt_tempdir if (defined($opt_tempdir));

# check for directory
if (!opendir(DIR, $BURNDIR)) {
    eval { mkpath($BURNDIR) };
    if ($@) {
        die "Can't create working directory '$BURNDIR': $@\n";
    }
    opendir(DIR, $BURNDIR) || die "Can't open directory '$BURNDIR': $!\n";
}
closedir DIR;

# if no_log print all to stdout
my $OUTPUT = ( $opt_no_log ) ? "" : ">>$LOG";

sub System
{
    my $cmd = $_[0];
    print STDERR $cmd."\n" if $opt_verbose;
    return system($cmd);
}

sub Backtick
{
    my $cmd = $_[0];
    print STDERR $cmd."\n" if $opt_verbose;
    # Cannot pipe to "tee" since it will mask exit codes
    my $output = `$cmd 2>&1`;
    my $rc = $?;

    my $logfile;
    open($logfile, ">>$LOG") or die "Cannot write to $LOG: $!\n";
    print $logfile $output;
    close($logfile);
    print $output if ($opt_no_log);

    return $rc, $output;
}

# For-sure needed tools
my %PREREQS = (
    'sox' => 'sox',
    'cdrdao' => 'cdrdao',
    'gst-launch' => 'gst-launch',
);
$PREREQS{'normalize'} = 'normalize,normalize-audio'
    if (!defined($skip_stage{'norm'}));
my %found;

sub Lookup_tools
{
    # check for required tools
    foreach my $requirement (sort keys %PREREQS) {
        foreach my $dir (split(/:/,$ENV{'PATH'})) {
            foreach my $prog (split(/,/,$PREREQS{$requirement})) {
                if (!defined($found{$requirement}) && -x "$dir/$prog") {
                    $found{$requirement}="$dir/$prog";
                    last;
                }
            }
        }
    }
    my $abort=undef;
    foreach my $requirement (sort keys %PREREQS) {
        if (!defined($found{$requirement})) {
            my $tried = "Tried: ".$PREREQS{$requirement};
            $tried =~ s/,/, /g;
            warn "Cannot find program to handle '$requirement'!  $tried\n";
            $abort=1;
        }
    }
    return $abort;
}

# Load file list, update needed tools
Load_file_list();
pod2usage( -exitval => 1, -verbose => 0 ) if (Lookup_tools());

# check for CDR device
my $skip_cdr = defined($skip_stage{'cdr_ok'}) && defined($skip_stage{'burn'});
if (!$skip_cdr && ! -w $opt_device) {
    pod2usage(  -exitval=>1, -verbose=>0,
                -msg=> "Cannot write to '$opt_device'!" );
}

# Run through all the stages we need to...
for (;
     defined($stage) && defined($stages{$stage});
     $stage=$stages[$stages{$stage}+1]) {
    if (defined($skip_stage{$stage})) {
        print "Skipping '$stage' stage...\n";
        next;
    }

    $stage_func{$stage}->();
}

# end of line
exit(0);


### Functions

sub require_extension($$)
{
    my ($ext,$file) = @_;
    my $lookup = $ext;
    if (!defined($decoders{$lookup})) {
        # Unknown audio file format
        print STDERR "Not sure how to handle file type '$ext' ($file),\n";
        print STDERR "falling back to ".$decoders{$UNKNOWN}->{'require'}.".\n";
        $lookup = $UNKNOWN;
    }
    $PREREQS{"decoder:$lookup"}=$decoders{$lookup}->{'require'};
}

sub Load_file_list
{
    # Keep a count of how many files we've examined, and stop after, say,
    # 1000, in case an m3u lists itself (which is REALLY unlikely, but would
    # effectively put this code into a memory-eating endless loop).
    my $toomany=1000;
    while (my $file=shift @ARGV) {
        $file =~ m/\.([^\.]+)$/i;
        my $ext = lc($1 || "");
        if ($ext eq "m3u" || $ext eq "pls" || $ext eq "xspf" || $ext eq "") {
            # Playlist
            open(M3U,$file) || die "Cannot open '$file': $!\n";
            my @lines=<M3U>;
            close(M3U);

            my @files;
            if (scalar(@lines) && $lines[0] =~ /<!DOCTYPE\s+XMLPlaylist>/i) { 
                # kaffeine playlists
                require XML::Simple;
                my $contents = XML::Simple::XMLin($file);
                if (ref($contents->{entry}) eq 'ARRAY') {
                    @files = map {$_->{url}} @{$contents->{entry}};
                    s/^file:// for @files;
                } else {
                    @files = ($contents->{entry}->{url});
                }
            }
            else {
                # regular list of files
                foreach (@lines) {
                        chomp;
                        next if (/^#/);
                        push(@files,$_);
                }
            }
            unshift(@ARGV,@files);
        }
        else {
            require_extension($ext,$file);
            push(@FILES,$file);
        }
        die ">1000 files in the list?!  I must have started looping forever.\n"
            if (--$toomany<0);
    }
    # Get absolute locations
    @FILES = map { abs_path($_) } @FILES;
}

sub Do_Clean
{
    print "Cleaning up...\n";

    # clear out burn dir
    my @list = ("$BURNDIR/$CDTOC","$BURNDIR/$LOG", "$BURNDIR/$TAGS");
    foreach my $ext ("wav", sort keys %decoders) {
        push(@list,"$BURNDIR/*.$ext");
    }
    System("rm -f ".join(" ",@list));
}

sub append_tag_info($$$)
{
    my ($media, $title, $path) = @_;
    my $artist = "";
    my ($rc, $output) = Backtick("gst-launch -t filesrc location=$media ! decodebin");
    die "Could not extract tags: $!\n" if ($rc != 0);
    my $tags = 0;
    # Parse gst-launch -t output
    # FOUND TAG      : found by element "qtdemux0".
    #           title: Just Dance
    #          artist: Lady GaGa & Colby O'Donis

    foreach my $line (split("\n", $output)) {
        if ($line =~ /^FOUND TAG/) {
            $tags = 1;
            next;
        }
        next if ($tags != 1);
        if ($line =~ /^\S/) {
            $tags = 0;
            next;
        }
        my ($field, $value) = $line =~ /^\s*(\S*)\s*:\s*(.*)$/;
        next if (!defined($field));
        $title=$value  if ($field eq "title");
        $artist=$value if ($field eq "artist");
    }
    my $tagfile;
    open($tagfile,">>$TAGS") or die "Cannot write to $TAGS: $!\n";
    print $tagfile "$title\n";
    print $tagfile "$artist\n";
    if ($opt_verbose) {
        print "\ttitle: $title\n";
        print "\tartist: $artist\n";
    }
}

sub Do_Build
{
    # go there
    chdir($BURNDIR) || die "Cannot chdir('$BURNDIR'): $!\n";

    # Clear the tag file, since we're regenerating it
    System("rm -f $TAGS");

    my $error=undef;
    my $count=0;
    # make link for each file, and retain extension
    foreach my $file (@FILES)
    {
        chomp($file);
        next if ($file =~ /^#/);
        my @parts=split(/\./,$file);
        my $ext=lc(pop(@parts));
        $ext=~tr/A-Z/a-z/;

        @parts=split(/\//,$file);
        my $name=pop(@parts);

        if (!defined($decoders{$ext}) && !defined($decoders{$UNKNOWN})) {
            warn "Error: '$file': unknown extension '$ext'!\n";
            $error=1;
            next;
        }

        if (!-f $file)
        {
            warn "Error: '$file': $!\n";
            $error=1;
            next;
        }

        $count++;
        my $track=sprintf("%02d",$count);
        print "$track: [...]/$name\n";
        symlink($file,"$track.$ext") || die "symlink('$file','$count.$ext'): $!\n";
        append_tag_info("$track.$ext", $name, $file);
    }

    die "Stopping due to errors...\n" if (defined($error));

    # make sure we have some tracks
    die("No tracks?!\n") unless ($count>0);
}

sub Do_Decode
{
    # go there
    chdir($BURNDIR) || die "Cannot chdir('$BURNDIR'): $!\n";

    # leave any WAVs in playlist alone
    opendir(DIR, $BURNDIR) || die "Can't read directory '$BURNDIR': $!\n";
    my @need_decode = grep { /^\d+\.[^\.]+$/i && !/\.wav$/ && -f "$BURNDIR/$_" } readdir(DIR);
    closedir DIR;

    # Re-check extensions and tools in case we're restarting
    foreach my $to_decode (sort {$a cmp $b} @need_decode)
    {
        my @parts=split(/\./,$to_decode);
        my $name=shift(@parts);
        my $ext=pop(@parts);
        require_extension($ext, $to_decode);
    }
    die "Cannot locate needed decoders\n" if (Lookup_tools());

    # decode audio into WAV files
    foreach my $to_decode (sort {$a cmp $b} @need_decode)
    {
        my @parts=split(/\./,$to_decode);
        my $name=shift(@parts);
        my $ext=pop(@parts);
        my $file="${name}.wav";

        if (-f $file)
        {
            print "Skipping track $name: $file exists.\n";
        }
        else
        {
            print "Creating WAV for track $name ...\n";
            my $lookup = $ext;
            if (!defined($decoders{$lookup})) {
                $lookup = $UNKNOWN;
            }
            my $decoder = $decoders{$lookup};
            if (!defined($decoder)) {
                die("No decoder available for extension '$ext' - decoding failed!\n");
            }
            my @cmd = ($found{"decoder:$lookup"});

            # chose verbosity level
            if (!$opt_no_log) {
                push(@cmd,$decoder->{'normal'});
            }
            else {
                push(@cmd,$decoder->{'verbose'});
            }

            # set up arguments
            my $input = $to_decode;
            my $output = $file;
            push(@cmd,eval "return \"$decoder->{'args'}\"");

            # run decoder (don't need to worry about arg splits since we're
            # operating against symlinked files with known names, etc)
            my $cmd = join(" ",@cmd);
            # redirect logging
            $cmd="$cmd $OUTPUT 2>&1";

            System($cmd) == 0
                or die("Decoding failed!\n");
        }
    }
}

sub Do_Correct
{
    # go there
    chdir($BURNDIR) || die "Cannot chdir('$BURNDIR'): $!\n";

    # get list of wavs from directory
    opendir(DIR, $BURNDIR) || die "Can't read directory '$BURNDIR': $!\n";
    my @wavs = grep { /^\d+\.wav$/i && -f "$BURNDIR/$_" } readdir(DIR);
    closedir DIR;

    # correct any wav file formats
    foreach my $wav (sort {$a cmp $b} @wavs)
    {
        my @parts=split(/\./,$wav);
        my $name=shift(@parts);
        print "Checking WAV format for track $name ...\n";
        my $report=`sox -V $wav $wav.raw trim 0.1 1 2>&1`;

        my ($channels, $frequency, $samples);
        if ($report =~ /^Input File/m) {
            # In version 13.0.0, the report format has changed

            # Sample Size    : 8-bit (1 byte)
            # Channels       : 1
            # Sample Rate    : 11025
            $report =~ m/Sample (?:Size|Encoding)\s*:\s+(\d+)-bit/s
                or die "sox did not report sample size:\n$report";
            $samples = $1;
            $report =~ m/Channels\s+:\s+(\d+)/s
                or die "sox did not report channel count:\n$report";
            $channels = $1;
            $report =~ m/Sample Rate\s+:\s+(\d+)/s
                or die "sox did not report sample frequency:\n$report";
            $frequency = $1;
        }
        else {
            # sox: Reading Wave file: Microsoft PCM format, 2 channels,
            # sox: 44100 samp/sec 176400 byte/sec,  block align, 16 bits/samp,
            # sox: 44886528 data bytes
            $report =~ m|(\d+) channels?|s
                or die "sox did not report channel count:\n$report";
            $channels = $1;
            $report =~ m|(\d+) samp/sec|s
                or die "sox did not report sample frequency:\n$report";
            $frequency = $1;
            $report =~ m|(\d+) bits/samp|s
                or die "sox did not report sample size:\n$report";
            $samples = $1;
        }

        unless ($channels == 2 &&
                $frequency == 44100 &&
                $samples == 16)
        {

            # only do a "resample" if frequency isn't correct
            my $resample="resample";
            $resample="" if ($frequency == 44100);
            print "Correcting WAV format for track $name ...\n";
            System("sox $wav -r 44100 -c 2 new-$wav $resample $OUTPUT 2>&1") == 0
                or die("Correction failed!\n");
            unlink($wav) || die "unlink('$wav'): $!\n";
            rename("new-$wav",$wav) || die "rename('new-$wav','$wav'): $!\n";
        }
        unlink("$wav.raw");
    }
}

sub Do_Normalize
{
    # go there
    chdir($BURNDIR) || die "Cannot chdir('$BURNDIR'): $!\n";

    # normalize the volumes
    print "Normalizing volume levels...\n";
    System("$found{'normalize'} -m [0-9]*.wav") == 0
        or die("Normalizing failed!\n");
    print "Normalizing finished.\n";
}

sub encode_cd_text_data($)
{
    my ($data) = @_;
    my $encoded = "";
    # Handle backslash and quotes
    $data =~ s/\\/\\\\/g;
    $data =~ s/"/\\"/g;
    # Using the binary data method seems to fail (missing trailing 0?)
#    if ($data =~ /"/) {
#        $encoded = "{ " . join(", ",map(ord, split(//,$data))) . " }";
#    }
#    else {
        $encoded = "\"" . $data . "\"";
#    }
    return $encoded;
}

sub cd_text($$)
{
    my ($title, $artist) = @_;
    chomp($title);
    chomp($artist);

    my $text = "CD_TEXT {\n  LANGUAGE 0 {\n";
    $text .= "    TITLE " . encode_cd_text_data($title) . "\n";
    $text .= "    PERFORMER " . encode_cd_text_data($artist) . "\n";
    $text .= "  }\n}\n";

    return $text;
}

sub Do_TOC
{
    # go there
    chdir($BURNDIR) || die "Cannot chdir('$BURNDIR'): $!\n";

    print "Generating CDR Table of Contents...\n";

    # Get ready to read tags
    my $tagfile;
    open($tagfile,"<$TAGS") || die "Cannot read $TAGS: $!\n";

    # create a TOC for cdrdao
    open(TOC,">$CDTOC") || die("Cannot write to '$CDTOC': $!\n");
    print TOC "CD_DA\n";
    if (!$opt_no_cd_text) {
        # CDRDAO wants title/performer for the cd itself too, so leave them blank
        print TOC <<EOM;
CD_TEXT {
  LANGUAGE_MAP {
    0 : EN
  }
  LANGUAGE 0 {
    TITLE ""
    PERFORMER ""
  }
}
EOM
    }
    print TOC "\n";

    # get list of wavs
    opendir(DIR, $BURNDIR) || die "Can't read directory '$BURNDIR': $!\n";
    my @wavs = grep { /^\d+\.wav$/i && -f "$BURNDIR/$_" } readdir(DIR);
    closedir DIR;

    foreach my $wav (sort {$a cmp $b} @wavs)
    {
        die ("Yikes!  What happened to '$wav'?!\n") unless (-f $wav);
        print TOC "TRACK AUDIO\n";
        if (!$opt_no_cd_text) {
            print TOC cd_text(scalar(<$tagfile>), scalar(<$tagfile>));
        }
        # The trailing space was (is?) needed for some versions of cdrdao
        print TOC "FILE \"$wav\" 0 \n\n";
    }
    close TOC;
    close $tagfile;
}

sub Do_TOC_Verify
{
    # go there
    chdir($BURNDIR) || die "Cannot chdir('$BURNDIR'): $!\n";

    print "Verifying generated Table of Contents...\n";
    System(cdrdao('read-test')." $CDTOC $OUTPUT 2>&1") == 0
        or die "Failed to create CD Table of Contents?!\n";
}

sub Do_CDR_Check
{
    # go there
    chdir($BURNDIR) || die "Cannot chdir('$BURNDIR'): $!\n";

    print "Checking for CDR...\n";
    my ($rc, $report) = Backtick(cdrdao('disk-info'));
    die "CDR not loaded?!\n" if ($rc != 0);
    print "\tCDR found.\n";

    if (!$opt_no_cd_text) {
        my $options = undef;
        my $driver_name = undef;
        foreach my $line (split("\n",$report)) {
            chomp($line);
            if ($line =~ /^Using driver: (.*)\(options (0x[0-9a-fA-F]+)\)$/) {
                $driver_name = $1;
                $options = hex($2);
            }
        }
        if (!defined($options)) {
            die "Could not determine driver options!\n";
        }
        elsif ($opt_verbose) {
            printf("\tDriver name: %s\n", $driver_name);
            printf("\tDriver options: 0x%04x\n", $options);
        }
        # 0x10 == OPT_MMC_CD_TEXT  /usr/share/cdrdao/drivers
        if (($driver_name =~ /raw writing/) || ($options & 0x10) == 0x10) {
            print "\tCD-TEXT supported.\n";
        }
        else {
            print "ERROR: It seems that driver selected by cdrdao for $opt_device\n";
            print " does not support CD-TEXT writing. Either disable CD-TEXT via\n";
            print " '--no-cd-text' or select a different driver (e.g. try using\n";
            print " '--driver generic-mmc-raw').\n";
            exit(1);
        }
    }
}

sub Do_Burn
{
    # go there
    chdir($BURNDIR) || die "Cannot chdir('$BURNDIR'): $!\n";

    my $cmd = cdrdao('write');
    $cmd.=" --eject" if (!$opt_no_eject);
    $cmd.=" -n $CDTOC";
    System($cmd) == 0
        or die "BURN FAILED!\n";
}

sub Version
{
    # Create human-readable version with un-human-readable code
    print "mp3cd version ".
    join(".",map{$_+0} (sprintf("%.6f",$VERSION)
        =~/^(\d+)\.?(\d{3})?(\d{3})?$/))."\n";
    print <<'EOM';
Copyright 2003-2011 Kees Cook <kees@outflux.net>
This program is free software; you may redistribute it under the terms of
the GNU General Public License. This program has absolutely no warranty.
EOM
    exit(0);
}

# return a good cdrdao command string prefix
sub cdrdao {
    my $operation = $_[0] || 'simulate';
    $operation = 'simulate' if ($opt_simulate && $operation eq 'write');

   return "cdrdao $operation $opt_cdrdao";
}

# /* vi:set ai ts=4 sw=4 expandtab: */
