#!/usr/bin/perl -w
require 5.004;
use strict;
use vars qw/ $ID /;

$ID = ' $Id: readcdda,v 1.1 2001/06/23 01:31:28 abuse Exp $ ';

use Data::Dumper;
use Getopt::Long;
use IO::File;

use lib 'lib';

use Device::SCSI;
use Device::SCSI::CDROM;

=head1 NAME

readcdda - Reads digital audio from a CD.

=head1 SYNOPSIS

readcdda [I<OPTION>]...

=head1 DESCRIPTION

This is a program to read (or "rip") CD digital audio from a CD and output
the PCM data. Typically this is an initial stage in encoding data to MP3
format.

The output data is raw 16 bit 44.1kHz stereo data. This format is directly
readable by software like B<sox> and various MP3 encoders, e.g.:

 readcdda -Dsg3 -v -F '|sox -r44100 -c2 -tsw - -twav %02d.wav'

Will read a CD and create WAV files in the current directory with names
I<00.wav>..I<99.wav>. Alternatively, you can read and encode directly to MP3
with something like:

 readcdda -Dsg3 -v -F '|mp3enc -v -sti -of %02d.mp3 -br 160000 -qual 6'

=over 4

=item B<-D>, B<--dev>, B<--device>=I<DEVICE>

SCSI device name or number to use.

=item B<-L>, B<--list>

Prints a list of all CD devices and their name/number, then exits.

=item B<-T>, B<--toc>

Prints a list of tracks on the CD, then exits.

=item B<-f>, B<--first>=I<TRACK>

Selects the first track to read. Defaults to the first track on the CD.

=item B<-l>, B<--last>=I<TRACK>

Selects the last track to read. Defaults to the same as B<-f> if that was
selected (i.e. read just one track) or the last track on the CD if it was
not (i.e. read the whole CD.)

=item B<-d>, B<--dir>, B<--directory>=I<DIR>

Output is saved in this directory, with names of "00".."99".

=item B<-s>, B<--stdout>

Output is sent to standard output.

=item B<-F>, B<--format>=I<FORMAT>

Output is sent to a name generated by a printf()-style format, e.g. "%02d".

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

Gives progress reports.

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

Givess script and module versions and exits.

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

Prints this text and exits.

=back

=head1 AUTHOR

All code and documentation by Peter Corlett <abuse@cabal.org.uk>.

=head1 COPYRIGHT

This module is Copyright (c) 2000,2001 Peter Corlett. All rights reserved.

You may distribute this under the terms of the GNU General Public License
version 2. Other licensing arrangements should be made with the author.

=head1 SUPPORT / WARRANTY

This is free software. IT COMES WITHOUT WARRANTY OF ANY KIND.

=cut

######################################################################

sub f2msf($) {
  my $frames=shift;
  return ( $frames/(60*75),
	   ($frames/75)%60,
	   $frames%75 );
}

######################################################################

sub usage {
  #       1         2         3         4         5         6         7
  #34567890123456789012345678901234567890123456789012345678901234567890123456789
  print STDERR "Usage: $0 [OPTION]
Reads digital audio from a CD.

 -D  --dev[ice]=DEVICE SCSI device name or number
 -L  --list            Shows all CD devices and their name/number
 -T  --toc             Shows list of tracks on the CD
 -f  --first=TRACK     First track to read (default: 1)
 -l  --last=TRACK      Last track to read (default, last on CD, or same as -f)

 -d  --dir[ectory]=DIR Output to files in this directory (named \"00\"..\"99\")
 -s  --stdout          Output all tracks to standard output
 -F  --format=FORMAT   Use a sprintf() format to generate filename (e.g. \"%02d\")

 -v  --verbose         Give progress reports
 -V  --version         Gives script and module versions

 -h  --help            Prints this text

Report bugs to abuse\@cabal.org.uk
";
  exit 1;
}

######################################################################

my($device, $list, $toc, $first, $last);
my($directory, $stdout, $format);
my($verbose, $version, $help);

Getopt::Long::config(qw/ bundling /);
my $result=GetOptions(
		      "D|dev|device=s" => \$device,
		      "L|list" => \$list,
		      "T|toc" => \$toc,
		      "f|first=i" => \$first,
		      "l|last=i" => \$last,

		      "d|dir|directory=s" => \$directory,
		      "s|stdout" => \$stdout,
		      "F|format=s" => \$format,

		      "v|verbose" => \$verbose,
		      "V|version" => \$version,
		      "h|help" => \$help,
		     );

# Give help if the user asks, or gets command line wrong
usage if($help or !$result);

if($version) {
#  foreach(($ID, $Device::SCSI::ID $Device::SCSI::CDROM::ID)) {
#    print "$_\n";
#  }
  print STDERR "$_\n";
  exit 1;
}

# Open all the SCSI CD-ROM devices
my %devices;
foreach (Device::SCSI->enumerate) {
  my $device=new Device::SCSI::CDROM $_;
  next unless defined $device;
  $devices{$_}=$device;
  my $inq=$device->inquiry;
  if($list) { # User wants to know what they've got
    my $label=($inq->{DEVICE}==5)
      ?"is a CD/DVD drive"
	:"is some other device (type ".$inq->{DEVICE}.")";
    printf STDERR "Device %s: %-8s %-16s %-4s %s\n",
      $_, @$inq{qw/ VENDOR PRODUCT REVISION /}, $label;
  }
  if($inq->{DEVICE} != 5) { # Not a CD-ROM device?
    $device->close;
    next;
  }
}

exit 1 if $list; # Quit if they just wanted a list of devices

# Which device did the user mean?
# Note that we redefine $device from an integer to an object representing same
unless (defined $device) { # User didn't give device, find one
  # The following line ensures there isn't a warning for the line after. The
  # intent is to do a numeric sort where possible, otherwise an alpha sort.
  local $^W;
  my @devices=sort {$a <=> $b || $a cmp $b} keys %devices;
  $device=$devices{$devices[0]};
} else {
  $device=$devices{$device};
}

my %toc=%{ $device->toc };

if($toc) {
  foreach ($toc{FIRST}..$toc{LAST}, 'CD') {
    my($start,$length);
    if ($_) {
      $start=$toc{$_}{START};
      $length=$toc{$_}{FINISH}-$start;
    }
    
    printf STDERR "Trk %2s: %02d:%02d +%02d:%02d %5.1fMB\n", $_,
    (f2msf $start)[0,1],
    (f2msf $length)[0,1],
    ($length/75*4*44100)/1024/1024,
    ($length/75*16000)/1024/1024,
    ($length/75*20000)/1024/1024;
  }
  exit 1;
}

# If user didn't specify first or last track, guess
if(defined $first) {
  $last=$first unless defined $last;
} else {
  $first=$toc{FIRST};
  $last=$toc{LAST} unless defined $last;
}

if(defined $format) {
  # Do nothing, it's fine as-is
} elsif(defined $directory) {
  # Default format is %02d - i.e. "00" .. "99" in the specified directory
  $format="$directory/%02d";
} elsif(defined $stdout) {
  # Stdout
  $format="-";
} else {
  print STDERR "Please use one of -d -s or -F\n";
  exit 1;
}

# Finally... read the tracks...
foreach my $track ($first..$last) {
  my $name=sprintf $format, $track;
  if($verbose) {
    print STDERR "Writing track $track to '$name'\n";
  }

  $name=">$name" unless $name=~/^[\|\>]/;
  open CDDA, "$name"
    or die "Can't write to $name: $!";
  do_cdda($device, $toc{$track}{START}, $toc{$track}{FINISH}, \*CDDA);
  close CDDA;
}

sub do_cdda {			# SCSI handle, start block, end block
  my($self, $start, $end, $fh)=@_;
  local $|=1;
  
  my $retries=0;
  my $now=time;
  my $block=$start;
  # Range that $ramp can vary through
  my($min_count,$max_count)=(6,12);
  # number of blocks to read in one go
  my $ramp=$min_count;
  # Used to smooth over the reported times by averaging last N samples
  my(@times,@blocks);

  while ($block<$end) {
    my $count=int($ramp);
    $count=$max_count if $count>$max_count;
    $count=$end-$block if $block+$count>$end;
    
    my($data, $sense)=
      $self->execute(
			 # READ CDDA:
			 # 0:OxD8 1:zero 2..5:block 6..9:count 10..11:zero
			 # Don't send data, raw blocks come back
			 pack("C x N N x2", 0xd8, $block, $count),
			 $count*2352
			);	# READ CDDA
    
    if ($sense->[0]) {
      $ramp=$min_count if $retries;
      $retries++;
      printf STDERR "SCSI error on try $retries reading block $block, sense:%s\n",
      join('', map { sprintf " %02X", $_ } @$sense);
      sleep ($retries-1);
      if($retries>15) {
	die "Too many SCSI errors";
      }
    } else {
      $ramp*=1.2;
      $retries=0;
      $block+=$count;
      if($verbose) {
	unshift @times, time-$now;
	unshift @blocks, $block;
	$#times=$#blocks=7 if $#times>7;
	my($eta,$speed)=(1,1);

	#print Data::Dumper->Dump(
	#[ \@times, \@blocks, $start, $end ],
	#[qw( *times *blocks *start *end )],
       #);
	
	foreach((0..$#times)) {
	  $eta*=$times[$_]/($blocks[$_]-$start)*($end-$blocks[$_]);
	  $speed*=($blocks[$_]-$start)/($times[$_]*75+1);
	}
	if(scalar @times) {
	  $eta**=1/scalar @times;
	  $speed**=1/scalar @times;
	  
	  printf STDERR "%d of %d, %ds to go (taken %ds) [%.2fx read at %.2fMB/s]  \r",
	  ($block-$start), ($end-$start),
	  $eta, $times[0], $speed, ($speed*44100*4)/1e6;
	}
      }
      print $fh $data;
    }
  }
  print "\n" if $verbose;
}

