#!/usr/bin/perl
#
# rip'n'code
# cd digital audio to mp3 converter by mao & the symbolic LynX
#
# if you're lucky this script runs without modification,
# otherwise go ahead and tweak it
#
# tested on solaris and linux
#
# requires cdda2wav or cdparanoia or galette
# may find lame, l3enc or bladeenc useful
# also eject or cd-console
#
# obtain beautiful mp3 music from http://symbolic.pages.de

require 'getopt.pl';
&Getopt('q');


# TUNE OPTIONS


# lame:
#
# forget everything i say about l3enc and bladeenc in the lines below
# lame is by far the better encoder of all, so use it whenever you can!
#
# also forget everything you heard about files with constant 128 bitrate
# being close to cd quality. you should always use VBR unless you really
# know what you are doing and want it that way. it also perfectly safe
# to use 32 as minimum bitrate with all non-historic versions of lame.
#
# old lame 3.5 configuration
#$lameopts =	$opt_m ? '-a -m m ': '-m s ';
#$lameopts .=	'-c -v -b 32 -V ';
#$lameopts .=	$opt_q ? $opt_q : '6';
#$hqlameopts =	$lameopts . ' -h';
#$lameopts =	$hqlameopts if $opt_q && $opt_q < 5;
#
# lame 3.9 configuration
#			-m default: we use automatic j/s depending on bitrate
#$lameopts = $opt_m ? '-da -mm ': '';
$lameopts = $opt_m ? '-mm ': '';
$lameopts .= '-v ' unless $opt_s;
$lameopts .= '--nspsytune -c -V';

# the lowpass actually makes more high tones pass, since the default
# lame config filters everything over 17 kHz
if ($opt_q) {
	$lameopts .= $opt_q;
	if ($opt_q and $opt_q < 4) {
		$lameopts .= ' -q1 --lowpass 19.5';
	} elsif ($opt_q and $opt_q < 7) {
		$lameopts .= ' -q3 --lowpass 19';
	} else {
		$lameopts .= ' --lowpass 18';
	}
} else {
	# let's use r3mix by default!
	$lameopts .= '--r3mix';
#	$lameopts .= '6 --lowpass 18';
}

# l3enc:
#
# l3enc's 112k is okay for many 90s music recordings and the output files are
# very small. only some sounds, mixes and vocals may be engineered in a way
# that the weaknesses of mp3 appear audible to the ear. in that case you will
# probably be successful if you call this program with the '-h' switch, this
# will put l3enc into "high quality" mode.
#
# l3enc with -h takes about 3 times as much time to code, which is painstaking,
# but in the end you have an extremely small file with amazing quality,
# optimal for website publishing. -h seems to have more impact than just
# raising the bitrate to 128k or 160k, but this differs for every music
# recording, apparently.
#
# if you're encoding speech or things you recorded off a tv or radio, you
# might want to reduce the bitrate and think of appending -dm for mono downmix.
#
# very high quality, very large files.
#$l3encopts =	'-br 192000';
#
# ok quality, medium files.
#$l3encopts =	'-br 128000';
#
# good quality, medium files. l3enc does not provide this bitrate.
#$l3encopts =	'-br 160000';
#
# popular quality, small files.
$l3encopts =	'';
#
# low quality and mono: extremely small files
#$l3encopts =	'-dm -br 64000';
#
# what to do when -h is given, append l3enc's -hq flag.
$hql3encopts =	"$l3encopts -hq";


# bladeenc:
#
# to my surprise i found out that l3enc's 112 encodings usually sound better
# than bladeenc's 128k encodings! so it may be stated that bladeenc is only
# useful for encoding with bitrates around 256k (as the author admits in the
# documentation). haven't compared l3enc's 192k quality to bladeenc's
# yet, but i have tried blade's 160k against l3enc's 112k with hq. depending
# on the music sometimes one or the other algorhythm will produce better
# sounding files. higher bitrates are no final guarantee for higher quality,
# just for higher disk space consumption.
#
# that's why we encode with 160k by default when using bladeenc
$blencopts =	'-br 160000';
#
# what to do when -h is given, raise the rate to 192k
$hqblencopts =	'-br 192000';


# where do we put temporary files?
# will default to /tmp if this directory does not exist
#
$tempdir = '/fat/windows/temp';
$tempdir = '/tmp' unless -d $tempdir;

#
# END OF TUNABLES

$debug = ' - debug mode' if $opt_d;
print <<X;
rip'n'code cd audio to mp3 converter by lynx & mao - version 4.3 $debug
X

$opt_r = 1 if $0 =~ /\brip$/i and not $opt_c;
$opt_c = 1 if $0 =~ /\bncode$/i and not $opt_r;

print <<X if $opt_r;
[rip-only mode]
X

print <<X if $opt_c;
[ncode-only mode]
X


# let's find out what executables are available on this system
# YOU MIGHT WANT TO TWEAK THE OPTIONS BEING USED

# first let's find a way to eject the cd
if ($opt_e) {
	# sun style, now also popular on linux
	$ejeccmd = &which('eject');
	unless ($ejeccmd) {
		# this hack often works on old linux installations
		my $cdcons = &which('cd-console');
		$ejeccmd = "echo eq|$cdcons >/dev/null";
	}
}

# now let's look what encoders are installed
if ($has_lame = &which('lame')) {
	# we're lucky
	$encopts = $lameopts;
# it is always a good idea to renice the encoder so you can still
# work on the machine pleasurably. if the machine is bored it will
# give the whole processing power to the encoder anyway.
#
	$encocmd = "nice -15 $has_lame";
} else {
	print STDERR "you should install yourself 'lame' real soon!\n";
	$has_l3enc = &which('l3enc');
	$has_blade = &which('bladeenc');
	$encocmd = $has_l3enc || $has_blade;

	die <<X unless $opt_r or $encocmd;
	no lame, l3enc or bladeenc installed on this system, sorry
X

	$encocmd = $opt_b && $has_blade ? $has_blade : $encocmd;
	$encopts = $encocmd eq $has_l3enc ?
		$hql3encopts : $hqblencopts if $opt_h;

# since it doesn't hurt to pipe 'N' into a registered l3enc we always do.
# that however shouldn't keep you from buying a registered version.
# then again, you may prefer lame over l3enc.
#
	$encocmd = "echo 'N' | nice -15 $encocmd" if $encocmd eq $has_l3enc;
}

$has_cdda2wav	= &which('cdda2wav');
$has_paranoia	= &which('cdparanoia');

if ($opt_p) {
	die <<X unless $has_paranoia;
no cdparanoia installed on this system, sorry
X
	print <<X if $opt_v;
using $has_cdparanoia in paranoid mode for ripping
X
	$listcmd = 'cdparanoia -sQ';
	$rippcmd = 'cdparanoia';
}
elsif ($has_cdda2wav) {
	print <<X if $opt_v;
using $has_cdda2wav for ripping. monitoring option -m is available.
X
	# cdda2wav 0.95 doesn't return version info properly - if it
	# did i could check for the version number and set up flags
	# accordingly. grrr. so these are the settings for 0.95:

	my $e = $opt_M ? '-e ' : '';		# -p 95
	# offline? dont lookup cddb!
	my $L = $opt_o ? '' : '-L 0';
#	$rippcmd = "$has_cdda2wav -D /dev/cdrom -F -w -x -P -q -H $m -t";
#	-F	find extremes (what for?)
#	-x	set maximum cd quality (what else?)
#	-P 1	initial sector for jitter correction (why mess w/it?)
#	-q -Q	quietnesses
#	-w	waits for signal (disbales generation of inf files)
#	-v	verbose-level (disable, all, toc, summary..)
	my $v = "-v summary,indices,catalog,trackid";
	my $v = ""; #-v summary,catalog";
	$rippcmd = "$has_cdda2wav $v -q -Q -D /dev/cdrom $L $e -t";
# Option -w 'wait for audio signal' disables generation of info files!

	# -t 0 is an ugly hack to make cdda2wav 0.95 list the disc contents
	#$listcmd = "$has_cdda2wav -D /dev/cdrom -N -t 0";
	$listcmd = "$has_cdda2wav -D /dev/cdrom -J";
# Add -N if you dont want any info files

# recently cdda2wav does all sorts of weird things traversing the
# complete CD just to give out disc info, so lets use cdparanoia
# to obtain the stupid total track number from the TOC
	$listcmd = "$has_paranoia -Q" if $has_paranoia;

	# newer cdda2wav leaves an even greater mess behind
	$cleancmd = 'rm -f .cddb *.wav.info $tempdir/*.wav.info';
}
elsif ($has_paranoia) {
	print <<X if $opt_v;
using $has_cdparanoia in relaxed mode for ripping
X
	$listcmd = "$has_paranoia -Q";
	$rippcmd = "$has_paranoia -Y";
}
elsif ($has_galette = &which('galette')) {
	print <<X if $opt_v;
using $has_galette for ripping.
X
	$listcmd = "$has_galette -i";
	$rippcmd = "$has_galette -e -f WAV -t";
}
else {
	die <<X;
error: no cdda ripping software available on this system

following rippers are supported by $0:
	cdda2wav, cdparanoia or galette

X
}

# THE FOLLOWING FUNCTIONS MIGHT NEED TWEAKING
# especially when adding support for a new ripper or such

# this is how the output files are named: x##.mp3
# rename them using 'mv-i x*'
sub outputname {
	local($i, $_) = @_;
	$_ = "x$i";
	s/x(\d)$/x0\1/o;
	return $_;
}
# if your system doesn't have "which" we're in trouble
sub which {
	my $cmd = shift;
	$_ = `which $cmd`;
	/^(\S+)\s*$/;
	return $1;
}
# actually encode a file, tuned to deal with l3enc
sub do_encode {
    local($src, $trg) = @_;

    if ($debug) {
	&system("echo '[pretending to encode]'; cp $src $trg.mp3; sleep 9");
    } elsif ($opt_s) {
	open(E, "$encocmd \"$src\" \"$trg.mp3\" $encopts 2>&1 |");
	while(<E>) {
		print unless /\|/o; # || /^\s*$/o || /\*\*\*/o;
	}
	close E;
    } else {
	&system("$encocmd \"$src\" \"$trg.mp3\" $encopts");
    }
}
# do disc listing or at least find out how many tracks are on it
sub do_discinfo {
	local($loud) = @_;

	print <<X if $opt_v || $debug;
	popen: $listcmd
X
	open(G, "$listcmd 2>&1 |") || die "cannot execute $listcmd";
	if ($listcmd =~ /galette/i) {
	    while(<G>) {
		chop;
		s/^CDROM://o;
		$tracks = $1 if /^ \| +(\d+) \|/o;

		s/( SIZE    \|)$/\1  MP3 SIZE/o;
		if (m/ (\d+\.\d\d) MB \|$/o) {
			$_ .= sprintf('  %2.2f MB', $1/12.5);
		}
		print $_,"\n" if $loud;
	    }
	}
	elsif ($listcmd =~ /paranoia/i) {
	    while(<G>) {
		$tracks = $1 if /^\s*(\d+)\.\s+\d+ \[\d+/o;
		print if $loud;
	    }
	}
	elsif ($listcmd =~ /cdda2wav/i) {
	    while(<G>) {
		# $tracks = $1 if /\s*(\d+)\.\(\s*\d+\)\s*$/;
		# $tracks = $1 if /\s*(\d+)\.\(\s*\d+\), lead-out/;
		$tracks = $1 if / total tracks:(\d+)\b/;
		$totalTime = $1 if /total time (\d+:\d\d\S*?)\b/;
		last if /Error/i;
		print if $loud;
	    }
	}
	else {
	    print while <G>;
	}
	close G;
	print "\n" if $loud;
	print <<X if $totalTime;
total running time: $totalTime
X
	print <<X if $tracks;
found $tracks tracks on this disc.

X
}
# actually rip a track off the audio compact disc
sub do_rip {
    my $i = shift;
    my $final = shift;
    my $temp = shift;

    print <<X if $temp;

ripping track $i in background..
X
    print <<X unless $temp;

ripping track $i..
X
    if ($rippcmd =~ /paranoia/i) {
	if ($temp) {
	    &system("(cd $tempdir;$rippcmd -q $i;mv -f cdda.wav $final) &");
	} else {
	    &system("$rippcmd $i;mv -f cdda.wav $final");
	}
    } elsif ($rippcmd =~ /galette/i) {
	if ($temp) {
	    &system("($rippcmd $i >$temp; mv -f $temp $final) &");
	} else {
	    &system("$rippcmd $i >$final");
	}
    } else {
	if ($temp) {
	    &system("($rippcmd $i $temp; mv -f $temp $final) &");
	} else {
	    &system("$rippcmd $i $final");
	}
    }
}



# MAIN CODE THAT SHOULDN'T NEED ANY TWEAKING

$tempdir = '/tmp' unless -d $tempdir;
$encowav = "$tempdir/cd2mp3-being-encoded.wav";
$rippwav = "$tempdir/cd2mp3-being-ripped.wav";
$waitwav = "$tempdir/cd2mp3-next-in-line.wav";

if ($opt_f) {
	print <<X;

ok.. forcing file removal
X
	!-r $encowav || unlink($encowav) || die "cannot remove $encowav";
	!-r $waitwav || unlink($waitwav) || die "cannot remove $waitwav";
	!-r $rippwav || unlink($rippwav) || die "cannot remove $rippwav";
}

### NCODE-ONLY MODE START
if ($opt_c) {
	unless (@ARGV) {
		print <<X;

Usage: $0 -c [ <options> ] <waves>
X
		&common_options;
	}
	while (@ARGV) {
		my $in = shift;
		my $out = $in;
		if ($out =~ s/\.wav$//i) {
			print $has_lame ? "\n" : <<X;
Ncoding $in into $out.mp3
X
			&do_encode($in, $out);
		}
	}
	exit;
}
### NCODE-ONLY MODE END

# print "\n" if $opt_l;
&do_discinfo($opt_l);

unless ($tracks) {
	print <<X;

either no disc in drive or access denied.
X
	&syntax;
}

$exclude = $include = 0;
MODE: {
	if ($opt_a) {
		print <<X;
selecting tracks 1-$tracks
X
		&syntax if $#ARGV >= 0;
		last MODE;
	}
	if ($#ARGV < 0) {
		&syntax unless $opt_l || $opt_f;
		exit;
	}
	if ($opt_x) {
		$exclude = ' ' . join(' ', @ARGV) . ' ';
		print <<X;
selecting tracks 1-$tracks excluding$exclude
X
		last MODE;
	}
	if ($opt_i) {
		$include = ' ' . join(' ', @ARGV) . ' ';
		print <<X;
selecting tracks$include
X
		last MODE;
	}
	&syntax;
}

$stime = time();

while (-r $rippwav) {
	print <<X;
waiting for $rippwav to disappear
X
	sleep 15;
}

for ($j=0, $i=1; $i <= $tracks; $i++) {
	next if index($exclude, " $i ") != -1;
	next if $include && index($include, " $i ") == -1;

	$trtime = time();
	if ($opt_r) {
		# &system("$rippcmd $i >" . &outputname($i) . '.wav');
		&do_rip($i, &outputname($i).'.wav');

		$tetime = time();
		$riptime = $tetime - $trtime;

		printf "track $j: RipTime: $riptime secs\n";
		next;
	}
	if (!$j) {
		if (-r $rippwav) {
			print <<X;

some other process is ripping into $rippwav
aborting
X
			exit;
		} else {
			# &system("$rippcmd $i > $rippwav");
			&do_rip($i, $rippwav);
			while (-r $waitwav) {
				print <<X;
waiting for $waitwav to disappear
X
				sleep 10;
			}
			rename($rippwav, $waitwav);
		}
		while (-r $encowav) {
			print <<X;
waiting for $encowav to disappear
X
			sleep 10;
		}
		$j = $i;
		next;
	}
	# &system("($rippcmd $i > $rippwav;mv -f $rippwav $waitwav) &");
	&do_rip($i, $waitwav, $rippwav);
	until (-r $waitwav) {
		print <<X;
waiting for track $j to appear as $waitwav..
X
		sleep 10;
	}
	&encode($j);
	$j = $i;
}
if ($j) {
	$trtime = time();
	until (-r $waitwav) {
		print <<X;
waiting for track $j to appear as $waitwav..
X
		sleep 10;
	}
}
if ($ejeccmd) {
	&system($ejeccmd);
	print <<X;

done ripping. disc ejected.
X
} else {
	print <<X;

done ripping. disc not ejected.
X
}
print <<X;
you may want to insert a new disc and start ripping it.

X
&encode($j) if $j;
&system($cleancmd) if $cleancmd;
exit;



sub encode {
	local($j) = @_;

	return if $opt_r;	# just for safety

	while (-r $encowav) {
		if (unlink($encowav)) {
			print <<X;
removed some old $encowav lying around
X
		} else {
			print <<X;
waiting for $encowav to disappear
X
			sleep 10;
		}
	}
	rename($waitwav, $encowav);
	print <<X;
encoding track $j..
X
	$tetime = time();

	&do_encode($encowav, &outputname($j));

	$tqtime = time();

	$riptime = $tetime - $trtime;
	$enctime = $tqtime - $tetime;

	print <<X;
track $j: RipTime: $riptime secs, EncodingTime: $enctime secs.
X
	unlink($encowav);
} 

sub system {
	local($cmd) = @_;
	print <<X if $opt_v || $debug;
	exec: $cmd
X
	sleep 3 if $debug;
	system $cmd unless $debug;
}

#   -b(ladeenc) use bladeenc instead of l3enc, if available
#   -h(igh)	better quality output, makes l3enc very slow
sub common_options {
	print <<X;

encoder options:
    -q <value>	quality setting for variable bitrate: 9 worst, 1 best
    -s(ilent)	do not show encoding progress
    -m(ono)	mono, not stereo, makes file half its usual size

general options:
    -d(ebug)	do not actually perform actions
    -v(erbose)	show commands being executed
X
}

sub syntax {
	print <<X;

Usage: $0 [ <options> ] (-a | -i | -x) [ <tracknums> ]

commands:
    -a(ll)	encode all tracks
    -i(nclude)	encode only following space-seperated list of tracks
    -x(clude)	encode all except the following space-seperated list of tracks
    -c(ode)	encode only, see $0 -c for syntax

ripper options:
    -l(ist)	show listing of loaded disc
    -r(ip only)	do not encode, just rip the tracks into current dir
    -o(ffline)	do not attempt CDDB lookups
    -f(orce)	remove temporary files, even if they are still being used
    -M(onitor)	hear audio data as it is being ripped (cdda2wav only)
    -e(ject)	eject disc when done (when available)
    -p(aranoid)	use cdparanoia in paranoid ripping mode, if available
X
	&common_options;
	exit;
}
