#!/usr/bin/perl
use strict;

use vars qw($VERSION);
$VERSION = 1.014_000;

=pod

=head1 NAME

mp3cd - Perl script to burn audio CDs from lists of MP3s

=head1 OPTIONS

 -h, --help      Shows help
 -t DIR          Change temp directory to another location.
 -s STAGE        Start at a certain stage of processing:
                        clean   Start fresh (default, requires m3u file)
                        build   Does not clean (requires m3u file)
                        decode  Turns mp3s 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

=head1 SYNOPSIS

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

=head1 DESCRIPTION

This will burn a list (.m3u) of MP3s and/or WAVs to an audio CD.  It
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.
Some thing are time-consuming (like writing the WAVs from MP3s) and if
the CD burn failed, it's much nicer not to have to start over from 
scratch.

=head1 PREREQUISITES

Requires C<cdrdao>, and that /dev/cdrecorder is a valid symlink to the
/dev/sg device that cdrdao will use.

Requires C<mpg123> to decode mp3 to WAV files.
 http://www.mpg123.de/

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

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

=head1 AUTHOR

 Kees Cook <kees@outflux.net>

=head1 SEE ALSO

perl(1), cdrdao(1), mpg123(1), sox(1), normalize(1).

=head1 COPYRIGHT

 $Id: mp3cd,v 1.22 2003/10/17 18:57:30 nemesis Exp $
 Copyright (C) 2000-2003 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="/scratch/mp3cd/".getpwuid($<);

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

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

my $start=0;
my $START_CLEAN    = 0;
my $START_BUILD    = $START_CLEAN+1;
my $START_DECODE   = $START_BUILD+1;
my $START_CORRECT  = $START_DECODE+1;
my $START_NORM     = $START_CORRECT+1;
my $START_TOC      = $START_NORM+1;
my $START_TOC_OKAY = $START_TOC+1;
my $START_CDR_OKAY = $START_TOC_OKAY+1;
my $START_BURN     = $START_CDR_OKAY+1;

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

# check for required tools
my %found;
my @PREREQS=('sox','mpg123','normalize','cdrdao');
foreach my $dir (split(/:/,$ENV{'PATH'})) {
	foreach my $prog (@PREREQS) {
		$found{$prog}=1 if (!$found{$prog} && -x "$dir/$prog");
	}
}
my $abort=undef;
foreach my $prog (@PREREQS) {
	if (!$found{$prog}) {
		warn "Cannot find '$prog'!\n";
		$abort=1;
	}
}
Usage() if ($abort);

# check for CDR device
if (! -w "/dev/cdrecorder") {
	warn "Cannot write to /dev/cdrecorder!\n";
	Usage();
}


my $quit=undef;
Usage() unless (@ARGV);
while ($ARGV[0] =~ /^-/) {
        my $arg=shift(@ARGV);
        Usage() if ($arg eq "-h" || $arg eq "--help");
        $quit=1 if ($arg eq "-q");
	if ($arg eq "-t") {
		$BURNDIR=shift(@ARGV);
		die "Need a temp directory!\n" if ($BURNDIR eq "");
	}
        if ($arg eq "-s") {
                my $stage=shift(@ARGV);
                if      ($stage eq "clean")     { $start=$START_CLEAN; }
                elsif   ($stage eq "build")     { $start=$START_BUILD; }
                elsif   ($stage eq "decode")    { $start=$START_DECODE; }
                elsif   ($stage eq "correct")   { $start=$START_CORRECT; }
                elsif   ($stage eq "norm")      { $start=$START_NORM; }
		elsif   ($stage eq "toc")       { $start=$START_TOC; }
                elsif   ($stage eq "toc_ok")    { $start=$START_TOC_OKAY; }
                elsif   ($stage eq "cdr_ok")    { $start=$START_CDR_OKAY; }
                elsif   ($stage eq "burn")      { $start=$START_BURN; }
                else                            { Usage(); }
        }
}

Do_Clean()      if ($start <= $START_CLEAN);
Do_Build()      if ($start <= $START_BUILD);
Do_Decode()     if ($start <= $START_DECODE);
Do_Correct()    if ($start <= $START_CORRECT);
Do_Normalize()  if ($start <= $START_NORM);
Do_TOC()        if ($start <= $START_TOC);
Do_TOC_Verify() if ($start <= $START_TOC_OKAY);
Do_CDR_Check()  if ($start <= $START_CDR_OKAY);
Do_Burn()       if ($start <= $START_BURN);

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

        # clear out burn dir
        system("rm -f $BURNDIR/*.mp3 $BURNDIR/*.wav $BURNDIR/$OUTPUT $BURNDIR/$CDTOC");

        die "Stopping at user request...\n" if (defined($quit));
}

sub Do_Build
{
        # make sure they gave us a filename
        Usage() unless (defined($ARGV[0]));

        # read list of files
        open(M3U,$ARGV[0]) || die "Cannot open '$ARGV[0]': $!\n";
        my @files=<M3U>;
        close(M3U);

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

	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=pop(@parts);
                $ext=~tr/A-Z/a-z/;

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

                if ($ext ne "mp3" && $ext ne "wav")
                {
                        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";
        }

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

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

        die "Stopping at user request...\n" if (defined($quit));
}

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

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

        # turn mp3s into wav files
        foreach my $mp3 (sort {$a <=> $b} @mp3s)
        {
                my @parts=split(/\./,$mp3);
                my $name=shift(@parts);
                my $file="${name}.wav";

                if (-f $file)
                {
                        print "Skipping track $name: $file exists.\n";
                }
                else
                {
                        print "Creating WAV for track $name ...\n";
#                       system("lame -S --decode $mp3 $file >$OUTPUT 2>&1") == 0
                        system("mpg123 --wav $file $mp3 >$OUTPUT 2>&1") == 0
                                or die("Decoding failed!\n");
                }
        }

        die "Stopping at user request...\n" if (defined($quit));
}

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 <=> $b} @wavs)
        {
                my @parts=split(/\./,$wav);
                my $name=shift(@parts);
		print "Checking WAV format for track $name ...\n";
                my $report=`sox -V $wav $wav.nul 2>&1`;
# sox: Reading Wave file: Microsoft PCM format, 2 channels, 44100 samp/sec
# sox:         176400 byte/sec, 4 block align, 16 bits/samp, 44886528 data bytes
                unless ($report =~ m|2 channels|s &&
                        $report =~ m|44100 samp/sec|s &&
		        $report =~ m|16 bits/samp|s)
                {

                        # only do a "resample" if rates aren't correct
                        my $resample="resample";
                        $resample="" if ($report =~ /44100 Hz/s);
                        print "Correcting WAV format for track $name ...\n";
                        system("sox $wav -r 44100 -c 2 -w 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.nul");
        }

        die "Stopping at user request...\n" if (defined($quit));
}

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

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

        die "Stopping at user request...\n" if (defined($quit));
}

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

        # create a TOC for cdrdao
        open(TOC,">$CDTOC") || die("Cannot write to '$CDTOC': $!\n");
        print TOC "CD_DA\n\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 <=> $b} @wavs)
        {
                die ("Yikes!  What happened to '$wav'?!\n") unless (-f $wav);
                print TOC "TRACK AUDIO\n";
                print TOC "FILE \"$wav\" 0 \n\n";
        }
	close TOC;

        die "Stopping at user request...\n" if (defined($quit));
}

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";

        die "Stopping at user request...\n" if (defined($quit));
}

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

        print "Checking for CDR...\n";
        system("cdrdao disk-info >$OUTPUT 2>&1") == 0
                or die "CDR not loaded?!\n";
        print "\tCDR found.\n";

        die "Stopping at user request...\n" if (defined($quit));
}

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

        #system("cdrdao simulate --eject $CDTOC") == 0
        system("cdrdao write --eject $CDTOC") == 0
                or die "BURN FAILED!\n";

        die "Stopping at user request...\n" if (defined($quit));
}

sub Usage
{
        die "Usage: $0 [-s stage] [-t directory] [-h] [m3u file]
-h, --help      Show this help
-t DIR          Change temp directory to another location.
-s STAGE        Start at a certain stage of processing:
                        clean   Start fresh (default, requires m3u file)
                        build   Does not clean (requires m3u file)
                        decode  Turns mp3s 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\n";
}

