/*
 * $Id: read_cdda.c,v 1.5 1996/01/18 14:05:49 jim Exp $
 * Read audio track from a CD using CDDA
 * 
 * Written by Jim Mintha (mintha@geog.ubc.ca)
 * Large portions are borrowed from the Workman 
 * sources written by Steven Grimm (koreth@hyperion.com)
 *
 * $Log: read_cdda.c,v $
 * Revision 1.5  1996/01/18 14:05:49  jim
 * Added ability to query drive type
 * moved some stuff back from header file
 *
 * Revision 1.4  1996/01/10 10:05:33  jim
 * Fixed display on song sectors
 *
 * Revision 1.3  1996/01/10 09:53:22  jim
 * mistake in checking starting time
 *
 * Revision 1.2  1996/01/10 09:44:01  jim
 * Added -q to help
 *
 * Revision 1.1  1996/01/10 09:11:32  jim
 * Initial revision
 *
 */

/*
 * Notes: (this is my understanding of things)
 * 
 * One CDDA block is 2368 bytes each block contains:
 * 588 frames and 16bytes for Q data.
 * Each frame is 4 bytes (presumably 2 16bit values)
 * 588 * 4 = 2352 + 16 = 2368
 * Each frame is 1/44100 th. of a second (44100 Hz)
 * therefore each block is 1/75 th. of a second
 */

#include "read_cdda.h"
#include "version.h"

/* disable X window stuff in my general utilities */
NO_X_WIN

/* Global variables - yuk */

/*
 * This is the fastest way to convert from BCD to 8-bit.
 */
unsigned char unbcd[256] = {
  0,  1,  2,  3,  4,  5,  6,  7,  8,  9,  0,0,0,0,0,0,
 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,  0,0,0,0,0,0,
 20, 21, 22, 23, 24, 25, 26, 27, 28, 29,  0,0,0,0,0,0,
 30, 31, 32, 33, 34, 35, 36, 37, 38, 39,  0,0,0,0,0,0,
 40, 41, 42, 43, 44, 45, 46, 47, 48, 49,  0,0,0,0,0,0,
 50, 51, 52, 53, 54, 55, 56, 57, 58, 59,  0,0,0,0,0,0,
 60, 61, 62, 63, 64, 65, 66, 67, 68, 69,  0,0,0,0,0,0,
 70, 71, 72, 73, 74, 75, 76, 77, 78, 79,  0,0,0,0,0,0,
 80, 81, 82, 83, 84, 85, 86, 87, 88, 89,  0,0,0,0,0,0,
 90, 91, 92, 93, 94, 95, 96, 97, 98, 99,  0,0,0,0,0,0,
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
};

/* Number of blocks to read at once; initialize to the maximum. */
int numblocks = 30;

/* where we are and where we want to be */
int current_block = 0;
int ending_block = 0;
int starting_sec = 0;
int ending_sec = 0;
boolean be_quiet = FALSE;
unsigned char *ulawmap;

int
main(int argc, char **argv)
{
  char *cddabuf;
  long cddabuf_len, result = 1;
  struct cdda_block block_info;
  int cd_fd, out_fd, track, fmt = CDR, ch;
  boolean toc = FALSE, swap = FALSE, drv_type = FALSE;
  extern char *optarg;
  extern int optind;
  struct option longopts[] = 
    {
      /* name, has_arg, *flag, val */
      { "help", 0, 0, 'h'},
      { "start", 1, 0, 's'},
      { "end", 1, 0, 'e'},
      { "au16", 0, 0, 'a'},
      { "au8", 0, 0, '8'},
      { "swap", 0, 0, 'x'},
      { "quiet", 0, 0, 'q'},
      { "toc", 0, 0, 't'},
      { "drvtype", 0, 0, 'd'},
      { "version", 0, 0, 'v'}
    };
	
  while((ch = getopt_long(argc, argv, "hs:e:a8xqtdv",
			  longopts, (int *) 0)) != EOF)
    {
      switch(ch)
	{
	case 'h':   /* help */
	  usage(argv[0]);
	  break;
	case 'q':   /* quiet */
	  be_quiet = TRUE;
	  break;
	case 't':   /* toc */
	  toc = TRUE;
	  break;
	case 'd':   /* drive type */
	  drv_type = TRUE;
	  break;
	case 'x':   /* swap */
	  swap = TRUE;
	  break;
	case '8':   /* Sun .au 8KHz */
	  fmt = AU8;
	  break;
	case 'a':   /* Sun .au 44.1KHz */
	  fmt = AU16;
	  break;
	case 's':   /* start time */
	  sscanf(optarg, "%d", &starting_sec);
	  if(starting_sec < 0)
	    pop_error(ERR_GEN, "Starting time: %d invalid", starting_sec);
	  break;
	case 'e':   /* end time */
	  sscanf(optarg, "%d", &ending_sec);
	  if(ending_sec < 0)
	    pop_error(ERR_GEN, "Ending time: %d invalid", ending_sec);
	  break;
	case 'v':
	  show_info(NONE, "%s\n", version_string);
	  exit(0);
	  break;
	case '?':
	  pop_error(NO_ERR, "Option not recognized: %c", ch);
	  usage(argv[0]);
	  break;
	}
    }
  
  if(starting_sec > ending_sec)
    pop_error(ERR_GEN, "Starting time is after ending time");

  if(!toc && !drv_type)
    {
      if(optind + 2 > argc)
	pop_error(ERR_GEN, "Must specify a filename and track number\n");
      
      out_fd = open(argv[optind], O_WRONLY | O_CREAT, 0666);
      if(out_fd < 0)
	pop_error(ERR_FILE, "Error opening output file: %s", argv[optind]);
      if(fmt == AU8 || fmt == AU16)
	write_sun_header(out_fd, fmt);
      
      sscanf(argv[optind + 1], "%d", &track);
      if(track < 0 || track > 99)
	pop_error(ERR_GEN, "Invalid track specified: %d", track);
    }
  
  cd_fd = cdda_init(&cddabuf, &cddabuf_len);

  if(drv_type)
    {
      get_drv_type(cd_fd);
      exit(0);
    }
  
  if(!be_quiet && !toc)
    printf("Writing track %d to %s\n", track, argv[optind]);
  
  read_toc(cd_fd, track, toc);

  if(toc)
    exit(0);
  
  while(result)
    {
      result = cdda_read(cd_fd, cddabuf, cddabuf_len, &block_info);
      if(result < 0)
	pop_error(ERR_FILE, "Error reading CD");

      cdda_write(out_fd, cddabuf, result, swap, fmt);
    }

  if(!be_quiet)
    printf("All Done.\n");
}

/*
 * Initialize CDDA data buffer and open device.
 */

int
cdda_init(char **bufadr, long *buflen)
{
  int fd;
  struct cdrom_cdda cdda;
  
  *bufadr = malloc(numblocks * CDDABLKSIZE + CDDABLKSIZE);
  if(*bufadr == NULL)
    pop_error(ERR_MALLOC, "Malloc of buffer failed");

  *buflen = numblocks * CDDABLKSIZE;

  /* open the device */
  fd = open("/dev/rdsk/c0t6d0s2", 0);
  if(fd < 0)
    pop_error(ERR_FILE, "Open of CD-ROM device failed");
  
  cdda.cdda_addr = 200;
  cdda.cdda_length = 1;
  cdda.cdda_data = *bufadr;
  cdda.cdda_subcode = CDROM_DA_SUBQ;
  
  if(ioctl(fd, CDROMCDDA, &cdda) < 0)
    pop_error(ERR_GEN, "Error initializing CD-ROM DA");
  
  return fd;
}

long
cdda_read(int fd, unsigned char *rawbuf, long buflen, struct cdda_block *block)
{
  struct cdrom_cdda cdda;
  int blk;
  unsigned char *q;

  if(current_block >= ending_block)
    {
      block->status = WMCDDA_DONE;
      return 0;
    }
  
  cdda.cdda_addr = current_block;
  if(ending_block && current_block + numblocks > ending_block)
    cdda.cdda_length = ending_block - current_block;
  else
    cdda.cdda_length = numblocks;
  cdda.cdda_data = rawbuf;
  cdda.cdda_subcode = CDROM_DA_SUBQ;
  
  if(ioctl(fd, CDROMCDDA, &cdda) < 0)
    {
      if(errno == ENXIO)
	{
	  pop_error(NO_ERR, "CD has been ejected\n");
	  return -1;
	}
      
      if(current_block + numblocks > ending_block)
	return 0;
      
      if(ioctl(fd, CDROMCDDA, &cdda) < 0)
	{
	  if(ioctl(fd, CDROMCDDA, &cdda) < 0)
	    {
	      if(ioctl(fd, CDROMCDDA, &cdda) < 0)
		{
		  pop_error(NO_ERR, "CDROM CDDA failed\n");
		  block->status = WMCDDA_ERROR;
		  return -1;
		}
	    }
	}
    }
  
      
  current_block += cdda.cdda_length;
  
  for(blk = 0; blk < numblocks; blk++)
    {
      q = &rawbuf[blk * CDDABLKSIZE + SAMPLES_PER_BLK * 4];
      if(*q == 1)
	{
	  block->status = WMCDDA_OK;
	  block->track =  unbcd[q[1]];
	  block->index =  unbcd[q[2]];
	  block->minute = unbcd[q[7]];
	  block->second = unbcd[q[8]];
	  block->frame =  unbcd[q[9]];
	}
    }
  
  return (cdda.cdda_length * CDDABLKSIZE);
}

/*
 * Write out the data
 */

void
cdda_write(int fd, unsigned char *buf, long len, boolean swap, int fmt)
{
  int blocks = len / CDDABLKSIZE, ctr, ctr2;
  int write_len = SAMPLES_PER_BLK * 2 * 2;   /* i.e. 2352 */
  unsigned char tmp, *ptr, *ptr2;
  
  for(ctr = 0; ctr < blocks; ctr++)
    {
      ptr = buf;
      ptr2 = buf;
      
      /* if they want swapping then we don't swap - make sense? */
      if(!swap)
	{
	  for(ctr2 = 0; ctr2 < SAMPLES_PER_BLK * 2; ctr2++)
	    {
	      tmp = *ptr++;
	      *ptr2++ = *ptr++;
	      *ptr2++ = tmp;
	    }
	}

      if(fmt == AU8)
	write_len = convert(buf);
      
      write(fd, buf, write_len);
      
      buf += CDDABLKSIZE;
    }
}

/*
 * Read the Table of Contents
 */

void
read_toc(int fd, int track, boolean toc)
{
  int ctr, starting_block = 0, last_block = 0, prev_time, song_length;
  struct cdrom_tochdr hdr;
  struct cdrom_tocentry entry;
  
  if(ioctl(fd, CDROMREADTOCHDR, &hdr))
    pop_error(ERR_GEN, "Error getting TOC header from CD");
  
  if(!be_quiet)
    printf("Number of tracks %d\n", hdr.cdth_trk1);

  for(ctr = 1; ctr <= hdr.cdth_trk1; ctr++)
    {
      entry.cdte_track = ctr;
      entry.cdte_format = CDROM_MSF;
      
      if(ioctl(fd, CDROMREADTOCENTRY, &entry))
	pop_error(ERR_GEN, "Error getting TOC entry from CD");
      
      if(!be_quiet)
	{
	  song_length = entry.cdte_addr.msf.minute * 60 + entry.cdte_addr.msf.second - prev_time;
	  if(ctr != 1)
	    printf("(%02d:%02d)\n", song_length / 60, song_length % 60);
	  printf("Track %02d: %02d:%02d:%02d  ", ctr,
		 entry.cdte_addr.msf.minute, 
		 entry.cdte_addr.msf.second,
		 entry.cdte_addr.msf.frame);
	  prev_time = entry.cdte_addr.msf.minute * 60 + entry.cdte_addr.msf.second;
	}
      
      if(ctr == track)
	{
	  starting_block = entry.cdte_addr.msf.minute * 60 * 75 +
	    entry.cdte_addr.msf.second * 75 + entry.cdte_addr.msf.frame;
	}

      if(ctr == track + 1)
	{
	  last_block = entry.cdte_addr.msf.minute * 60 * 75 +
	    entry.cdte_addr.msf.second * 75 + entry.cdte_addr.msf.frame;
	  ending_block = last_block;
	}
    }

  entry.cdte_track = CDROM_LEADOUT;
  entry.cdte_format = CDROM_MSF;
      
  if(ioctl(fd, CDROMREADTOCENTRY, &entry))
    pop_error(ERR_GEN, "Error getting TOC entry from CD");

  if(!be_quiet)
    {
      song_length = entry.cdte_addr.msf.minute * 60 + entry.cdte_addr.msf.second - prev_time;
      printf("(%02d:%02d)\n", song_length / 60, song_length % 60);
    }
  
  if(ending_block == 0)
    {
      ending_block = entry.cdte_addr.msf.minute * 60 * 75 +
	entry.cdte_addr.msf.second * 75 + entry.cdte_addr.msf.frame;
    }

  /* Move back two seconds - don't know why but works */
  starting_block -= 150;
  ending_block -= 150;

  if((starting_sec * 75 + starting_block) > ending_block ||
     (ending_sec * 75 + starting_block) > ending_block)
    pop_error(ERR_GEN, "Start/End times too long\n");
  
  current_block = starting_block + (starting_sec * 75);
  if(ending_sec != 0)
    ending_block = starting_block + (ending_sec * 75);
 
  if(ending_sec == 0)
    ending_sec = (ending_block - starting_block) / 75;

  if(!be_quiet && !toc)
    {
      printf("Song is sectors: %d to %d\n", starting_block, last_block);
      
      printf("Recording sectors: %d to %d\n", current_block, ending_block);
      printf("Song start time: %02d:%02d  end time: %02d:%02d\n",
	     starting_sec / 60, starting_sec % 60,
	     ending_sec / 60, ending_sec % 60);
    }
}

/*
 *  write_sun_header - Write sun .au fmt type header
 */

void
write_sun_header(int fd, int fmt)
{
  int wrote, val;
  typedef unsigned long	u_32;
  struct auheader {
    u_32 magic;
    u_32 hdr_size;
    u_32 data_size;
    u_32 encoding;
    u_32 sample_rate;
    u_32 channels;
  } hdr;
  
  /* initialize ulaw mappings */
  ulawmap = (unsigned char *) malloc(65536);
  if(ulawmap == NULL)
    pop_error(ERR_MALLOC, "Error mallocing memory for ulawmap");
  for(val = 0; val < 65536; val++)
    ulawmap[val] = linear_to_ulaw(val - 32768);
  ulawmap += 32768;

   hdr.magic = 0x2e736e64;
  hdr.hdr_size = sizeof(hdr) + 28;
  hdr.data_size = (fmt == AU16) ? 16 : 8;
  hdr.encoding = (fmt == AU16) ? 3 : 1;
  hdr.sample_rate = (fmt == AU16) ? 44100 : 8000;
  hdr.channels = (fmt == AU16) ? 2 : 1;
 
  wrote = write(fd, hdr, sizeof(hdr));
  if(wrote != sizeof(hdr))
    pop_error(ERR_FILE, "Error writing to output file");
  wrote = write(fd, "Recorded from CD by Read_CD", 28);
  if(wrote != 28)
    pop_error(ERR_FILE, "Error writing to output file");
}

int
convert(unsigned char *buf)
{
  short *buf16 = (short *) buf;
  int ctr, ctr2, samples;
  int mono_value;
  unsigned char *bufend = buf + SAMPLES_PER_BLK * 4;
    
  for(ctr = 0; buf16 < (short *)(bufend); ctr++)
    {
      samples = (ctr & 1) ? ((ctr % 20) ? 10 : 12) : 12;
      
      if(buf16 + samples > (short *)(bufend))
	samples = ((short *)bufend) - buf16;
     
      mono_value = 0;
      for(ctr2 = 0; ctr2 < samples; ctr2++)
	mono_value += *buf16++;
      mono_value /= samples;
      buf[ctr] = ulawmap[mono_value];
    }
  
  return ctr;
}

/*
 *  usage - Print out some command line help
 */

void
usage(char *prog_name)
{
  fprintf(stderr, "%s: Read audio track from CD to a file\n", prog_name);
  fprintf(stderr, "Usage: %s -hasdf <-s {start}> <-e {end}> {file} {track}\n", prog_name);
  fprintf(stderr, " {file}          File name for output\n");
  fprintf(stderr, " {track}         Track number to read\n");
  fprintf(stderr, " -s  --start     Starting second for the track\n");
  fprintf(stderr, " -e  --end       Ending second for the track\n");
  fprintf(stderr, " -t  --toc       Only print out the Table of Contents\n");
  fprintf(stderr, " -x  --swap      Swap bytes (for x86?)\n");
  fprintf(stderr, " -8  --au8       Output in Sun .au format 8KHz\n");
  fprintf(stderr, " -a  --au16      Output in Sun .au format 44.1KHz\n");
  fprintf(stderr, " -q  --quiet     Be quiet while running\n");
  fprintf(stderr, " -d  --drvtype   Print CD-ROM model type only\n");
  fprintf(stderr, " -v  --version   Print program version number only\n\n");
  fprintf(stderr, " Default is to output in CDR format the whole track\n");
  exit(0);
}

/*
** This routine converts from linear to ulaw.
**
** Craig Reese: IDA/Supercomputing Research Center
** Joe Campbell: Department of Defense
** 29 September 1989
**
** References:
** 1) CCITT Recommendation G.711  (very difficult to follow)
** 2) "A New Digital Technique for Implementation of Any
**     Continuous PCM Companding Law," Villeret, Michel,
**     et al. 1973 IEEE Int. Conf. on Communications, Vol 1,
**     1973, pg. 11.12-11.17
** 3) MIL-STD-188-113,"Interoperability and Performance Standards
**     for Analog-to_Digital Conversion Techniques,"
**     17 February 1987
**
** Input: Signed 16 bit linear sample
** Output: 8 bit ulaw sample
*/
#define ZEROTRAP    /* turn on the trap as per the MIL-STD */
#define BIAS 0x84               /* define the add-in bias for 16 bit samples */
#define CLIP 32635
 
unsigned char
linear_to_ulaw( sample )
int sample;
{
	static int exp_lut[256] = {0,0,1,1,2,2,2,2,3,3,3,3,3,3,3,3,
				   4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,
				   5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,
				   5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,
				   6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,
				   6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,
				   6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,
				   6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,
				   7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,
				   7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,
				   7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,
				   7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,
				   7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,
				   7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,
				   7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,
				   7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7};
	int sign, exponent, mantissa;
	unsigned char ulawbyte;
 
	/* Get the sample into sign-magnitude. */
	sign = (sample >> 8) & 0x80;            /* set aside the sign */
	if ( sign != 0 ) sample = -sample;              /* get magnitude */
	if ( sample > CLIP ) sample = CLIP;             /* clip the magnitude */
 
	/* Convert from 16 bit linear to ulaw. */
	sample = sample + BIAS;
	exponent = exp_lut[( sample >> 7 ) & 0xFF];
	mantissa = ( sample >> ( exponent + 3 ) ) & 0x0F;
	ulawbyte = ~ ( sign | ( exponent << 4 ) | mantissa );
#ifdef ZEROTRAP
	if ( ulawbyte == 0 ) ulawbyte = 0x02;   /* optional CCITT trap */
#endif
 
	return ulawbyte;
}
