/*  cdrdao - write audio CD-Rs in disc-at-once mode
 *
 *  Copyright (C) 1998-2002 Andreas Mueller <andreas@daneb.de>
 *
 *  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., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

#include <config.h>

#include <string.h>
#include <time.h>
#include <assert.h>

#include "GenericMMC.h"

#include "port.h"
#include "Toc.h"
#include "util.h"
#include "PQSubChannel16.h"
#include "PWSubChannel96.h"
#include "CdTextEncoder.h"



// Variants for cue sheet generation

// do not set ctl flags for ISRC cue sheet entries
#define CUE_VAR_ISRC_NO_CTL 0x1

// do not set start of lead-in time in lead-in cue sheet entry
#define CUE_VAR_CDTEXT_NO_TIME 0x2

#define CUE_VAR_MAX 0x3 


// Variants for write parameters mode page

//do not set data block type to block size 2448 if a CD-TEXT lead-in is written
#define WMP_VAR_CDTEXT_NO_DATA_BLOCK_TYPE 0x1

#define WMP_VAR_MAX 0x1


GenericMMC::GenericMMC(ScsiIf *scsiIf, unsigned long options)
  : CdrDriver(scsiIf, options)
{
  int i;
  driverName_ = "Generic SCSI-3/MMC - Version 2.0";
  
  speed_ = 0;
  simulate_ = 1;
  encodingMode_ = 1;

  scsiTimeout_ = 0;
  
  cdTextEncoder_ = NULL;

  driveInfo_ = NULL;

  memset(&diskInfo_, 0, sizeof(DiskInfo));

  for (i = 0; i < maxScannedSubChannels_; i++) {
    if (options_ & OPT_MMC_USE_PQ) 
      scannedSubChannels_[i] = new PQSubChannel16;
    else
      scannedSubChannels_[i] = new PWSubChannel96;
  }

  // MMC drives usually return little endian samples
  audioDataByteOrder_ = 0;
}

GenericMMC::~GenericMMC()
{
  int i;

  for (i = 0; i < maxScannedSubChannels_; i++) {
    delete scannedSubChannels_[i];
    scannedSubChannels_[i] = NULL;
  }

  delete cdTextEncoder_;
  cdTextEncoder_ = NULL;

  delete driveInfo_;
  driveInfo_ = NULL;
}

// static constructor
CdrDriver *GenericMMC::instance(ScsiIf *scsiIf, unsigned long options)
{
  return new GenericMMC(scsiIf, options);
}


int GenericMMC::checkToc(const Toc *toc)
{
  int err = CdrDriver::checkToc(toc);
  int e;

  if (options_ & OPT_MMC_CD_TEXT) {
    if ((e = toc->checkCdTextData()) > err)
      err = e;
  }

  return err;
}

int GenericMMC::subChannelEncodingMode(TrackData::SubChannelMode sm) const
{
  int ret = 0;

  switch (sm) {
  case TrackData::SUBCHAN_NONE:
    ret = 0;
    break;

  case TrackData::SUBCHAN_RW:
#if 0
    if (options_ & OPT_MMC_NO_RW_PACKED) 
      ret = 1; // have to encode the R-W sub-channel data
    else 
      ret = 0;
#endif
    ret = 0;
    break;

  case TrackData::SUBCHAN_RW_RAW:
    // raw R-W sub-channel writing is assumed to be always supported
    ret = 1;
    break;
  }

  return ret;
}

// sets speed
// return: 0: OK
//         1: illegal speed
int GenericMMC::speed(int s)
{
  speed_ = s;

  if (selectSpeed(0) != 0)
    return 1;
  
  return 0;
}

int GenericMMC::speed()
{
  DriveInfo *di;

  delete driveInfo_;
  driveInfo_ = NULL;

  if ((di = driveInfo(1)) == NULL) {
    return 0;
  }

  return speed2Mult(di->currentWriteSpeed);

}

// loads ('unload' == 0) or ejects ('unload' == 1) tray
// return: 0: OK
//         1: scsi command failed
int GenericMMC::loadUnload(int unload) const
{
  unsigned char cmd[6];

  memset(cmd, 0, 6);

  cmd[0] = 0x1b; // START/STOP UNIT
  if (unload) {
    cmd[4] = 0x02; // LoUnlo=1, Start=0
  }
  else {
    cmd[4] = 0x03; // LoUnlo=1, Start=1
  }
  
  if (sendCmd(cmd, 6, NULL, 0, NULL, 0) != 0) {
    message(-2, "Cannot load/unload medium.");
    return 1;
  }

  return 0;
}

// Checks for ready status of the drive after a write operation
// Return: 0: drive ready
//         1: error occured
//         2: drive not ready

int GenericMMC::checkDriveReady() const
{
  unsigned char cmd[10];
  unsigned char data[4];
  int ret;

  ret = testUnitReady(0);

  if (ret == 0) {
    // testUnitReady reports ready status but this might actually not
    // be the truth -> additionally check the READ DISK INFO command 
    
    memset(cmd, 0, 10);

    cmd[0] = 0x51; // READ DISK INFORMATION
    cmd[8] = 4;
    
    ret = sendCmd(cmd, 10, NULL, 0, data, 4, 0);
    
    if (ret == 2) {
      const unsigned char *sense;
      int senseLen;
      
      ret = 0; // indicates ready status

      sense = scsiIf_->getSense(senseLen);
      
      if (senseLen >= 14 && (sense[2] & 0x0f) == 0x2 && sense[7] >= 6 &&
          sense[12] == 0x4 && 
          (sense[13] == 0x8 || sense[13] == 0x7)) {
        // Not Ready, long write in progress
        ret = 2; // indicates not ready status
      }
    }
  }

  return ret;
}

// Performs complete blanking of a CD-RW.
// return: 0: OK
//         1: scsi command failed
int GenericMMC::blankDisk(BlankingMode mode)
{
  unsigned char cmd[12];
  int ret, progress;
  time_t startTime, endTime;

  setSimulationMode(0);


  memset(cmd, 0, 12);

  cmd[0] = 0xa1; // BLANK

  switch (mode) {
  case BLANK_FULL:
    cmd[1] = 0x0; // erase complete disk
    break;
  case BLANK_MINIMAL:
    cmd[1] = 0x1; // erase PMA, lead-in and 1st track's pre-gap
    break;
  }

  cmd[1] |= 1 << 4; // immediate return

  sendBlankCdProgressMsg(0);

  if (sendCmd(cmd, 12, NULL, 0, NULL, 0, 1) != 0) {
    message(-2, "Cannot erase CD-RW.");
    return 1;
  }

  time(&startTime);
  progress = 0;

  do {
    mSleep(2000);

    ret = checkDriveReady();

    if (ret == 1) {
      message(-2, "Test Unit Ready command failed.");
    }

    progress += 10;
    sendBlankCdProgressMsg(progress);

    if (progress >= 1000)
      progress = 0;

  } while (ret == 2);

  if (ret == 0)
    sendBlankCdProgressMsg(1000);

  time(&endTime);

  message(2, "Blanking time: %ld seconds", endTime - startTime);

  return ret;
}

// sets read/write speed and simulation mode
// return: 0: OK
//         1: scsi command failed
int GenericMMC::selectSpeed(int readSpeed)
{
  unsigned char cmd[12];
  int spd;

  memset(cmd, 0, 12);

  cmd[0] = 0xbb; // SET CD SPEED

  // select maximum read speed
  if (readSpeed == 0) {
    cmd[2] = 0xff;
    cmd[3] = 0xff;
  }
  else {
    spd = mult2Speed(readSpeed);
    cmd[2] = spd >> 8;
    cmd[3] = spd;
  }

  // select maximum write speed
  if (speed_ == 0) {
    cmd[4] = 0xff;
    cmd[5] = 0xff;
  }
  else {
    spd = mult2Speed(speed_);
    cmd[4] = spd >> 8;
    cmd[5] = spd;
  }

  if (sendCmd(cmd, 12, NULL, 0, NULL, 0) != 0) {
    message(-2, "Cannot set cd speed.");
    return 1;
  }

  return 0;
}

// Determins start and length of lead-in and length of lead-out.
// return: 0: OK
//         1: SCSI command failed
int GenericMMC::getSessionInfo()
{
  unsigned char cmd[10];
  unsigned long dataLen = 34;
  unsigned char data[34];

  memset(cmd, 0, 10);
  memset(data, 0, dataLen);

  cmd[0] = 0x51; // READ DISK INFORMATION
  cmd[7] = dataLen >> 8;
  cmd[8] = dataLen;

  if (sendCmd(cmd, 10, NULL, 0, data, dataLen) != 0) {
    message(-2, "Cannot retrieve disk information.");
    return 1;
  }

  leadInStart_ = Msf(data[17], data[18], data[19]);

  if (leadInStart_.lba() >= Msf(80, 0, 0).lba()) {
    leadInLen_ = 450000 - leadInStart_.lba();
    leadOutLen_ = Msf(1, 30, 0).lba(); // 90 seconds lead-out
  }
  else {
    leadInLen_ = Msf(1, 0, 0).lba();
    leadOutLen_ = Msf(0, 30, 0).lba();
  }


  message(4, "Lead-in start: %s length: %ld", leadInStart_.str(),
	  leadInLen_);
  message(4, "Lead-out length: %ld", leadOutLen_);

  return 0;
}

int GenericMMC::readBufferCapacity(long *capacity)
{
  unsigned char cmd[10];
  unsigned char data[12];

  memset(cmd, 0, 10);
  memset(data, 0, 12);

  cmd[0] = 0x5c; // READ BUFFER CAPACITY
  cmd[8] = 12;

  if (sendCmd(cmd, 10, NULL, 0, data, 12) != 0) {
    message(-2, "Read buffer capacity failed.");
    return 1;
  }

  *capacity = (data[8] << 24) | (data[9] << 16) | (data[10] << 8) | data[11];

  return 0;
}

int GenericMMC::performPowerCalibration()
{
  unsigned char cmd[10];
  int ret;

  memset(cmd, 0, 10);

  cmd[0] = 0x54; // SEND OPC INFORMATION
  cmd[1] = 1;

  message(2, "Executing power calibration...");

  if ((ret = sendCmd(cmd, 10, NULL, 0, NULL, 0)) != 0) {
    if (ret == 2) {
      const unsigned char *sense;
      int senseLen;

      sense = scsiIf_->getSense(senseLen);

      if(senseLen >= 14 && (sense[2] & 0x0f) == 0x5 && sense[7] >= 6 &&
	   sense[12] == 0x20 && sense[13] == 0x0) {
	message(2, "Power calibration not supported.");
	return 0;
      }
      else {
	message(-2, "Power calibration failed.");
      }
    }
    else {
      message(-2, "Power calibration failed.");
    }

    return 1;
  }
  
  message(2, "Power calibration successful.");

  return 0;
}

// Sets write parameters via mode page 0x05.
// return: 0: OK
//         1: scsi command failed
int GenericMMC::setWriteParameters(unsigned long variant)
{
  unsigned char mp[0x38];
  unsigned char mpHeader[8];
  unsigned char blockDesc[8];

  if (getModePage(5/*write parameters mode page*/, mp, 0x38,
		  mpHeader, blockDesc, 1) != 0) {
    message(-2, "Cannot retrieve write parameters mode page.");
    return 1;
  }

  mp[0] &= 0x7f; // clear PS flag

  mp[2] &= 0xe0;
  mp[2] |= 0x02; // write type: Session-at-once
  if (simulate_) {
    mp[2] |= 1 << 4; // test write
  }

  DriveInfo *di;
  if ((di = driveInfo(1)) != NULL && di->burnProof) {
    // This drive has BURN-Proof function.
    // Enable it unless explicitly disabled.
    if (options_ & OPT_MMC_NO_BURNPROOF) {
      message(2, "Turning BURN-Proof off");
      mp[2] &= ~0x40;
    } else {
      message(2, "Turning BURN-Proof on");
      mp[2] |= 0x40;
    }
  }

  mp[3] &= 0x3f; // Multi-session: No B0 pointer, next session not allowed

  if (multiSession_ != 0)
    mp[3] |= 0x03 << 6; // open next session
  else if (!diskInfo_.empty)
    mp[3] |= 0x01 << 6; // use B0=FF:FF:FF when closing last session of a
                        // multi session CD-R

  message(4, "Multi session mode: %d", mp[3] >> 6);

  mp[4] &= 0xf0; // Data Block Type: raw data, block size: 2352 (I think not
                 // used for session at once writing)

  if (cdTextEncoder_ != NULL) {
    if ((variant & WMP_VAR_CDTEXT_NO_DATA_BLOCK_TYPE) == 0)
      mp[4] |= 3; /* Data Block Type: raw data with raw P-W sub-channel data,
		     block size 2448, required for CD-TEXT lead-in writing
		     according to the SCSI/MMC-3 manual
		  */
  }

  message(4, "Data block type: %u",  mp[4] & 0x0f);

  mp[8] = sessionFormat();

  if (!diskInfo_.empty) {
    // use the toc type of the already recorded sessions
    switch (diskInfo_.diskTocType) {
    case 0x00:
    case 0x10:
    case 0x20:
      mp[8] = diskInfo_.diskTocType;
      break;
    }
  }

  message(4, "Toc type: 0x%x", mp[8]);

  if (setModePage(mp, mpHeader, NULL, 0) != 0) {
    message(-2, "Cannot set write parameters mode page.");
    return 1;
  }

  return 0;
}

// Sets simulation mode via mode page 0x05.
// return: 0: OK
//         1: scsi command failed
int GenericMMC::setSimulationMode(int showMessage)
{
  unsigned char mp[0x38];
  unsigned char mpHeader[8];

  if (getModePage(5/*write parameters mode page*/, mp, 0x38,
		  mpHeader, NULL, showMessage) != 0) {
    if (showMessage)
      message(-2, "Cannot retrieve write parameters mode page.");
    return 1;
  }

  mp[0] &= 0x7f; // clear PS flag

  if (simulate_)
    mp[2] |= 1 << 4; // test write
  else
    mp[2] &= ~(1 << 4);

  if (setModePage(mp, mpHeader, NULL, showMessage) != 0) {
    if (showMessage)
      message(-2, "Cannot set write parameters mode page.");
    return 1;
  }

  return 0;
}

int GenericMMC::getNWA(long *nwa)
{
  unsigned char cmd[10];
  int infoblocklen = 16;
  unsigned char info[16];
  long lba = 0;

  cmd[0] = 0x52; // READ TRACK INFORMATION
  cmd[1] = 0x01; // track instead of lba designation
  cmd[2] = 0x00;
  cmd[3] = 0x00;
  cmd[4] = 0x00;
  cmd[5] = 0xff; // invisible track
  cmd[6] = 0x00; // reserved
  cmd[7] = infoblocklen << 8;
  cmd[8] = infoblocklen; // alloc length
  cmd[9] = 0x00; // Control Byte

  if (sendCmd(cmd, 10, NULL, 0, info, infoblocklen) != 0) {
    message(-2, "Cannot get Track Information Block.");
    return 1;
  }

#if 0
  message(3,"Track Information Block");
  for (int i=0;i<infoblocklen;i++) message(3,"byte %02x : %02x",i,info[i]);
#endif

  if ((info[6] & 0x40) && (info[7] & 0x01) && !(info[6] & 0xb0))
  {
      message(4,"Track is Blank, Next Writable Address is valid");
      lba |= info[12] << 24; // MSB of LBA
      lba |= info[13] << 16;
      lba |= info[14] << 8;
      lba |= info[15];       // LSB of LBA
  }

  message(4, "NWA: %ld", lba);

  if (nwa != NULL) 
    *nwa = lba;

  return 0;
}

// Determines first writable address of data area of current empty session. 
// lba: set to start of data area
// return: 0: OK
//         1: error occured
int GenericMMC::getStartOfSession(long *lba)
{
  unsigned char mp[0x38];
  unsigned char mpHeader[8];

  // first set the writing mode because it influences which address is
  // returned with 'READ TRACK INFORMATION'

  if (getModePage(5/*write parameters mode page*/, mp, 0x38,
		  mpHeader, NULL, 0) != 0) {
    return 1;
  }

  mp[0] &= 0x7f; // clear PS flag

  mp[2] &= 0xe0;
  mp[2] |= 0x02; // write type: Session-at-once

  if (setModePage(mp, mpHeader, NULL, 1) != 0) {
    message(-2, "Cannot set write parameters mode page.");
    return 1;
  }

  return getNWA(lba);
}

static unsigned char leadInOutDataMode(TrackData::Mode mode)
{
  unsigned char ret;

  switch (mode) {
  case TrackData::AUDIO:
    ret = 0x01;
    break;

  case TrackData::MODE0: // should not happen
  case TrackData::MODE1:
  case TrackData::MODE1_RAW:
    ret = 0x14;
    break;

  case TrackData::MODE2_FORM1:
  case TrackData::MODE2_FORM2:
  case TrackData::MODE2_FORM_MIX:
  case TrackData::MODE2_RAW: // assume it contains XA sectors
    ret = 0x24;
    break;

  case TrackData::MODE2:
    ret = 0x34;
    break;
  }

  return ret;
}


unsigned char GenericMMC::subChannelDataForm(TrackData::SubChannelMode sm,
					     int encodingMode)
{
  unsigned char ret = 0;

  switch (sm) {
  case TrackData::SUBCHAN_NONE:
    break;

  case TrackData::SUBCHAN_RW:
  case TrackData::SUBCHAN_RW_RAW:
    if (encodingMode == 0)
      ret = 0xc0;
    else
      ret = 0x40;
    break;
  }

  return ret;
}
		  

// Creates cue sheet for current toc.
// cueSheetLen: filled with length of cue sheet in bytes
// return: newly allocated cue sheet buffer or 'NULL' on error
unsigned char *GenericMMC::createCueSheet(unsigned long variant,
					  long *cueSheetLen)
{
  const Track *t;
  int trackNr;
  Msf start, end, index;
  unsigned char *cueSheet;
  long len = 3; // entries for lead-in, gap, lead-out
  long n; // index into cue sheet
  unsigned char ctl; // control nibbles of cue sheet entry CTL/ADR
  long i;
  unsigned char dataMode;
  int trackOffset = 0;
  long lbaOffset = 0;

  if (!diskInfo_.empty && diskInfo_.append) {
    trackOffset = diskInfo_.lastTrackNr;
    lbaOffset = diskInfo_.thisSessionLba;
  }

  TrackIterator itr(toc_);

  if (itr.first(start, end) == NULL) {
    return NULL;
  }

  if (toc_->catalogValid()) {
    len += 2;
  }

  for (t = itr.first(start, end), trackNr = 1;
       t != NULL; t = itr.next(start, end), trackNr++) {
    len += 1; // entry for track
    if (t->start().lba() != 0 && trackNr > 1) {
      len += 1; // entry for pre-gap
    }
    if (t->type() == TrackData::AUDIO && t->isrcValid()) {
      len += 2; // entries for ISRC code
    }
    len += t->nofIndices(); // entry for each index increment
  }

  cueSheet = new (unsigned char)[len * 8];
  n = 0;

  if (toc_->leadInMode() == TrackData::AUDIO) {
    ctl = 0;
  }
  else {
    ctl = 0x40;
  }

  if (toc_->catalogValid()) {
    // fill catalog number entry
    cueSheet[n*8] = 0x02 | ctl;
    cueSheet[(n+1)*8] = 0x02 | ctl;
    for (i = 1; i <= 13; i++) {
      if (i < 8) {
	cueSheet[n*8 + i] = toc_->catalog(i-1) + '0';
      }
      else {
	cueSheet[(n+1)*8 + i - 7] = toc_->catalog(i-1) + '0';
      }
    }
    cueSheet[(n+1)*8+7] = 0;
    n += 2;
  }

  // entry for lead-in
  cueSheet[n*8] = 0x01 | ctl; // CTL/ADR
  cueSheet[n*8+1] = 0;    // Track number
  cueSheet[n*8+2] = 0;    // Index

  if (cdTextEncoder_ != NULL) {
    cueSheet[n*8+3] = 0x41; // Data Form: CD-DA with P-W sub-channels,
                            // main channel data generated by device
  }
  else {
    cueSheet[n*8+3] = leadInOutDataMode(toc_->leadInMode());
  }

  cueSheet[n*8+4] = 0;    // Serial Copy Management System

  if (cdTextEncoder_ != NULL &&
      (variant & CUE_VAR_CDTEXT_NO_TIME) == 0) {
    cueSheet[n*8+5] = leadInStart_.min();
    cueSheet[n*8+6] = leadInStart_.sec();
    cueSheet[n*8+7] = leadInStart_.frac();
  }
  else {
    cueSheet[n*8+5] = 0;    // MIN
    cueSheet[n*8+6] = 0;    // SEC
    cueSheet[n*8+7] = 0;    // FRAME
  }

  n++;

  int firstTrack = 1;

  for (t = itr.first(start, end), trackNr = trackOffset + 1;
       t != NULL;
       t = itr.next(start, end), trackNr++) {
    if (encodingMode_ == 0) {
      // just used for some experiments with raw writing
      dataMode = 0;
    }
    else {
      switch (t->type()) {
      case TrackData::AUDIO:
	dataMode = 0;
	break;
      case TrackData::MODE1:
      case TrackData::MODE1_RAW:
	dataMode = 0x10;
	break;
      case TrackData::MODE2:
	dataMode = 0x30;
	break;
      case TrackData::MODE2_RAW: // assume it contains XA sectors
      case TrackData::MODE2_FORM1:
      case TrackData::MODE2_FORM2:
      case TrackData::MODE2_FORM_MIX:
	dataMode = 0x20;
	break;
      default:
	dataMode = 0;
	break;
      }
    }

    // add mode for sub-channel writing
    dataMode |= subChannelDataForm(t->subChannelType(),
				   subChannelEncodingMode(t->subChannelType()));

    ctl = 0;
    if (t->copyPermitted()) {
      ctl |= 0x20;
    }

    if (t->type() == TrackData::AUDIO) {
      // audio track
      if (t->preEmphasis()) {
	ctl |= 0x10;
      }
      if (t->audioType() == 1) {
	ctl |= 0x80;
      }

      if (t->isrcValid()) {
	if ((variant & CUE_VAR_ISRC_NO_CTL) == 0)
	  cueSheet[n*8] = ctl | 0x03;
	else
	  cueSheet[n*8] = 0x03;

	cueSheet[n*8+1] = trackNr;
	cueSheet[n*8+2] = t->isrcCountry(0);
	cueSheet[n*8+3] = t->isrcCountry(1);
	cueSheet[n*8+4] = t->isrcOwner(0);  
	cueSheet[n*8+5] = t->isrcOwner(1);   
	cueSheet[n*8+6] = t->isrcOwner(2);  
	cueSheet[n*8+7] = t->isrcYear(0) + '0';
	n++;

	if ((variant & CUE_VAR_ISRC_NO_CTL) == 0)
	  cueSheet[n*8] = ctl | 0x03;
	else
	  cueSheet[n*8] = 0x03;

	cueSheet[n*8+1] = trackNr;
	cueSheet[n*8+2] = t->isrcYear(1) + '0';
	cueSheet[n*8+3] = t->isrcSerial(0) + '0';
	cueSheet[n*8+4] = t->isrcSerial(1) + '0';
	cueSheet[n*8+5] = t->isrcSerial(2) + '0';
	cueSheet[n*8+6] = t->isrcSerial(3) + '0';
	cueSheet[n*8+7] = t->isrcSerial(4) + '0';
	n++;
      }
    }
    else {
      // data track
      ctl |= 0x40;
    }
	 

    if (firstTrack) {
      Msf sessionStart(lbaOffset);

      // entry for gap before first track
      cueSheet[n*8]   = ctl | 0x01;
      cueSheet[n*8+1] = trackNr;
      cueSheet[n*8+2] = 0;    // Index 0
      cueSheet[n*8+3] = dataMode;    // Data Form
      cueSheet[n*8+4] = 0;    // Serial Copy Management System
      cueSheet[n*8+5] = sessionStart.min();
      cueSheet[n*8+6] = sessionStart.sec();
      cueSheet[n*8+7] = sessionStart.frac();
      n++;
    }
    else if (t->start().lba() != 0) {
      // entry for pre-gap
      Msf pstart(lbaOffset + start.lba() - t->start().lba() + 150);

      cueSheet[n*8]   = ctl | 0x01;
      cueSheet[n*8+1] = trackNr;
      cueSheet[n*8+2] = 0; // Index 0 indicates pre-gap
      cueSheet[n*8+3] = dataMode; // Data Form
      cueSheet[n*8+4] = 0; // no alternate copy bit
      cueSheet[n*8+5] = pstart.min();
      cueSheet[n*8+6] = pstart.sec();
      cueSheet[n*8+7] = pstart.frac();
      n++;
    }

    Msf tstart(lbaOffset + start.lba() + 150);
    
    cueSheet[n*8]   = ctl | 0x01;
    cueSheet[n*8+1] = trackNr;
    cueSheet[n*8+2] = 1; // Index 1
    cueSheet[n*8+3] = dataMode; // Data Form
    cueSheet[n*8+4] = 0; // no alternate copy bit
    cueSheet[n*8+5] = tstart.min();
    cueSheet[n*8+6] = tstart.sec();
    cueSheet[n*8+7] = tstart.frac();
    n++;

    for (i = 0; i < t->nofIndices(); i++) {
      index = tstart + t->getIndex(i);
      cueSheet[n*8]   = ctl | 0x01;
      cueSheet[n*8+1] = trackNr;
      cueSheet[n*8+2] = i+2; // Index
      cueSheet[n*8+3] = dataMode; // Data Form
      cueSheet[n*8+4] = 0; // no alternate copy bit
      cueSheet[n*8+5] = index.min();
      cueSheet[n*8+6] = index.sec();
      cueSheet[n*8+7] = index.frac();
      n++;
    }

    firstTrack = 0;
  }

  assert(n == len - 1);

  // entry for lead out
  Msf lostart(lbaOffset + toc_->length().lba() + 150);
  ctl = toc_->leadOutMode() == TrackData::AUDIO ? 0 : 0x40;
    
  cueSheet[n*8]   = ctl | 0x01;
  cueSheet[n*8+1] = 0xaa;
  cueSheet[n*8+2] = 1; // Index 1
  cueSheet[n*8+3] = leadInOutDataMode(toc_->leadOutMode());
  cueSheet[n*8+4] = 0; // no alternate copy bit
  cueSheet[n*8+5] = lostart.min();
  cueSheet[n*8+6] = lostart.sec();
  cueSheet[n*8+7] = lostart.frac();

  message(3, "\nCue Sheet (variant %lx):", variant);
  message(3, "CTL/  TNO  INDEX  DATA  SCMS  MIN  SEC  FRAME");
  message(3, "ADR               FORM");
  for (n = 0; n < len; n++) {
    message(3, "%02x    %02x    %02x     %02x    %02x   %02d   %02d   %02d",
	   cueSheet[n*8],
	   cueSheet[n*8+1], cueSheet[n*8+2], cueSheet[n*8+3], cueSheet[n*8+4],
	   cueSheet[n*8+5], cueSheet[n*8+6], cueSheet[n*8+7]);
  }

  *cueSheetLen = len * 8;
  return cueSheet;
}

int GenericMMC::sendCueSheet()
{
  unsigned char cmd[10];
  long cueSheetLen;
  unsigned long variant;
  unsigned char *cueSheet;
  int cueSheetSent = 0;

  for (variant = 0; variant <= CUE_VAR_MAX; variant++) {

    if (cdTextEncoder_ == NULL &&
	(variant & CUE_VAR_CDTEXT_NO_TIME) != 0) {
      // skip CD-TEXT variants if no CD-TEXT has to be written
      continue;
    }

    cueSheet = createCueSheet(variant, &cueSheetLen);

    if (cueSheet != NULL) {
      memset(cmd, 0, 10);

      cmd[0] = 0x5d; // SEND CUE SHEET

      cmd[6] = cueSheetLen >> 16;
      cmd[7] = cueSheetLen >> 8;
      cmd[8] = cueSheetLen;

      if (sendCmd(cmd, 10, cueSheet, cueSheetLen, NULL, 0, 0) != 0) {
	delete[] cueSheet;
      }
      else {
	message(3, "Drive accepted cue sheet variant %lx.", variant);
	delete[] cueSheet;
	cueSheetSent = 1;
	break;
      }
    }
  }

  if (cueSheetSent) {
    return 0;
  }
  else {
    message(-2,
	    "Drive does not accept any cue sheet variant - please report.");
    return 1;
  }
}

int GenericMMC::initDao(const Toc *toc)
{
  long n;
  blockLength_ = AUDIO_BLOCK_LEN + MAX_SUBCHANNEL_LEN;
  blocksPerWrite_ = scsiIf_->maxDataLen() / blockLength_;

  assert(blocksPerWrite_ > 0);

  toc_ = toc;

  if (options_ & OPT_MMC_CD_TEXT) {
    delete cdTextEncoder_;
    cdTextEncoder_ = new CdTextEncoder(toc_);
    if (cdTextEncoder_->encode() != 0) {
      message(-2, "CD-TEXT encoding failed.");
      return 1;
    }

    if (cdTextEncoder_->getSubChannels(&n) == NULL || n == 0) {
      delete cdTextEncoder_;
      cdTextEncoder_ = NULL;
    }
    
    //return 1;
  }

  diskInfo();

  if (!diskInfo_.valid.empty || !diskInfo_.valid.append) {
    message(-2, "Cannot determine status of inserted medium.");
    return 1;
  }

  if (!diskInfo_.append) {
    message(-2, "Inserted medium is not appendable.");
    return 1;
  }

  if (selectSpeed(0) != 0 ||
      getSessionInfo() != 0) {
    return 1;
  }


  // allocate buffer for writing zeros
  n = blocksPerWrite_ * blockLength_;
  delete[] zeroBuffer_;
  zeroBuffer_ = new char[n];
  memset(zeroBuffer_, 0, n);

  return 0;
}

int GenericMMC::startDao()
{
  unsigned long variant;
  int writeParametersSet = 0;

  scsiTimeout_ = scsiIf_->timeout(3 * 60);

  for (variant = 0; variant <= WMP_VAR_MAX; variant++) {
    if (cdTextEncoder_ == NULL &&
	(variant & WMP_VAR_CDTEXT_NO_DATA_BLOCK_TYPE) != 0) {
      // skip CD-TEXT variants if no CD-TEXT has to be written
      continue;
    }

    if (setWriteParameters(variant) == 0) {
      message(3, "Drive accepted write parameter mode page variant %lx.",
	      variant);
      writeParametersSet = 1;
      break;
    }
  }

  if (!writeParametersSet) {
    message(-2, "Cannot setup write parameters for session-at-once mode.");
    message(-2, "Please try to use the 'generic-mmc-raw' driver.");
    return 1;
  }

  if (!simulate_) {
    if (performPowerCalibration() != 0) {
      if (!force()) {
	message(-2, "Use option --force to ignore this error.");
	return 1;
      }
      else {
	message(-2, "Ignored because of option --force.");
      }
    }
  }

  // It does not hurt if the following command fails.
  // The Panasonic CW-7502 needs it, unfortunately it returns the wrong
  // data so we ignore the returned data and start writing always with
  // LBA -150.
  getNWA(NULL);

  if (sendCueSheet() != 0)
    return 1;

  //message(2, "Writing lead-in and gap...");

  if (writeCdTextLeadIn() != 0) {
    return 1;
  }


  long lba = diskInfo_.thisSessionLba - 150;

  TrackData::Mode mode = TrackData::AUDIO;
  TrackData::SubChannelMode subChanMode = TrackData::SUBCHAN_NONE;
  TrackIterator itr(toc_);
  const Track *tr;

  if ((tr = itr.first()) != NULL) {
    mode = tr->type();
    subChanMode = tr->subChannelType();
  }
  
  if (writeZeros(mode, subChanMode, lba, lba + 150, 150) != 0) {
    return 1;
  }
  
  return 0;
}

int GenericMMC::finishDao()
{
  int ret;

  flushCache(); /* Some drives never return to a ready state after writing
		 * the lead-out. This is a try to solve this problem.
		 */

  while ((ret = checkDriveReady()) == 2) {
    mSleep(2000);
  }

  if (ret != 0)
    message(-1, "TEST UNIT READY failed after recording.");
  
  message(2, "Flushing cache...");
  
  if (flushCache() != 0) {
    return 1;
  }

  scsiIf_->timeout(scsiTimeout_);

  delete cdTextEncoder_;
  cdTextEncoder_ = NULL;

  delete[] zeroBuffer_, zeroBuffer_ = NULL;

  return 0;
}

void GenericMMC::abortDao()
{
  flushCache();

  delete cdTextEncoder_;
  cdTextEncoder_ = NULL;
}

// Writes data to target, the block length depends on the actual writing
// 'mode'. 'len' is number of blocks to write.
// 'lba' specifies the next logical block address for writing and is updated
// by this function.
// return: 0: OK
//         1: scsi command failed
int GenericMMC::writeData(TrackData::Mode mode, TrackData::SubChannelMode sm,
			  long &lba, const char *buf, long len)
{
  assert(blocksPerWrite_ > 0);
  int writeLen = 0;
  unsigned char cmd[10];
  long blockLength = blockSize(mode, sm);
  int retry;
  int ret;

#if 0
  long bufferCapacity;
  int waitForBuffer;
  int speedFrac;

  if (speed_ > 0)
    speedFrac = 75 * speed_;
  else
    speedFrac = 75 * 10; // adjust this value when the first >10x burner is out
#endif

#if 0
  long sum, i;

  sum = 0;

  for (i = 0; i < len * blockLength; i++) {
    sum += buf[i];
  }

  message(0, "W: %ld: %ld, %ld, %ld", lba, blockLength, len, sum);
#endif

  memset(cmd, 0, 10);
  cmd[0] = 0x2a; // WRITE1
  
  while (len > 0) {
    writeLen = (len > blocksPerWrite_ ? blocksPerWrite_ : len);

    cmd[2] = lba >> 24;
    cmd[3] = lba >> 16;
    cmd[4] = lba >> 8;
    cmd[5] = lba;

    cmd[7] = writeLen >> 8;
    cmd[8] = writeLen & 0xff;

#if 0
    do {
      waitForBuffer = 0;

      if (readBufferCapacity(&bufferCapacity) == 0) {
	//message(0, "Buffer Capacity: %ld", bufferCapacity);
	if (bufferCapacity < writeLen * blockLength) {
	  long t = 1000 * writeLen;
	  t /= speedFrac;
	  if (t <= 0)
	    t = 1;

	  message(0, "Waiting for %ld msec at lba %ld", t, lba);

	  mSleep(t);
	  waitForBuffer = 1;
	}
      }
    } while (waitForBuffer);
#endif

    do {
      retry = 0;

      ret = sendCmd(cmd, 10, (unsigned char *)buf, writeLen * blockLength,
		    NULL, 0, 0);

      if(ret == 2) {
        const unsigned char *sense;
        int senseLen;

        sense = scsiIf_->getSense(senseLen);

	// check if drive rejected the command because the internal buffer
	// is filled
	if(senseLen >= 14 && (sense[2] & 0x0f) == 0x2 && sense[7] >= 6 &&
	   sense[12] == 0x4 && sense[13] == 0x8) {
	  // Not Ready, long write in progress
	  mSleep(40);
          retry = 1;
        }
	else {
	  scsiIf_->printError();
	}
      }
    } while (retry);

    if (ret != 0) {
      message(-2, "Write data failed.");
      return 1;
    }

    buf += writeLen * blockLength;

    lba += writeLen;
    len -= writeLen;
  }
      
  return 0;
}

int GenericMMC::writeCdTextLeadIn()
{
  unsigned char cmd[10];
  const PWSubChannel96 **cdTextSubChannels;
  long cdTextSubChannelCount;
  long channelsPerCmd;
  long scp = 0;
  long lba = -150 - leadInLen_;
  long len = leadInLen_;
  long n;
  long i;
  int retry;
  int ret;
  unsigned char *p;

  if (cdTextEncoder_ == NULL)
    return 0;

  channelsPerCmd = scsiIf_->maxDataLen() / 96;

  cdTextSubChannels = cdTextEncoder_->getSubChannels(&cdTextSubChannelCount);

  assert(channelsPerCmd > 0);
  assert(cdTextSubChannels != NULL);
  assert(cdTextSubChannelCount > 0);

  message(2, "Writing CD-TEXT lead-in...");

  message(4, "Start LBA: %ld, length: %ld", lba, len);

  memset(cmd, 0, 10);
  cmd[0] = 0x2a; // WRITE1

  while (len > 0) {
    n = (len > channelsPerCmd) ? channelsPerCmd : len;

    cmd[2] = lba >> 24;
    cmd[3] = lba >> 16;
    cmd[4] = lba >> 8;
    cmd[5] = lba;

    cmd[7] = n >> 8;
    cmd[8] = n;

    p = transferBuffer_;
    
    for (i = 0; i < n; i++) {
      memcpy(p, cdTextSubChannels[scp]->data(), 96);
      p += 96;

      scp++;
      if (scp >= cdTextSubChannelCount)
	scp = 0;
    }

    message(5, "Writing %ld CD-TEXT sub-channels at LBA %ld.", n, lba);

    do {
      retry = 0;

      ret = sendCmd(cmd, 10, transferBuffer_, n * 96, NULL, 0, 0);

      if(ret == 2) {
        const unsigned char *sense;
        int senseLen;
	
        sense = scsiIf_->getSense(senseLen);

	// check if drive rejected the command because the internal buffer
	// is filled
	if(senseLen >= 14 && (sense[2] & 0x0f) == 0x2 && sense[7] >= 6 &&
	   sense[12] == 0x4 && sense[13] == 0x8) {
	  // Not Ready, long write in progress
	  mSleep(40);
          retry = 1;
        }
	else {
	  scsiIf_->printError();
	}
      }
    } while (retry);

    if (ret != 0) {
      message(-2, "Writing of CD-TEXT data failed.");
      return 1;
    }
      

    len -= n;
    lba += n;
  }

  return 0;
}

DiskInfo *GenericMMC::diskInfo()
{
  unsigned char cmd[10];
  unsigned long dataLen = 34;
  unsigned char data[34];
  char spd;

  memset(&diskInfo_, 0, sizeof(DiskInfo));

  // perform READ DISK INFORMATION
  memset(cmd, 0, 10);
  memset(data, 0, dataLen);

  cmd[0] = 0x51; // READ DISK INFORMATION
  cmd[7] = dataLen >> 8;
  cmd[8] = dataLen;

  if (sendCmd(cmd, 10, NULL, 0, data, dataLen, 0) == 0) {
    diskInfo_.cdrw = (data[2] & 0x10) ? 1 : 0;
    diskInfo_.valid.cdrw = 1;

    switch (data[2] & 0x03) {
    case 0:
      // disc is empty
      diskInfo_.empty = 1;
      diskInfo_.append = 1;

      diskInfo_.manufacturerId = Msf(data[17], data[18], data[19]);
      diskInfo_.valid.manufacturerId = 1;
      break;

    case 1:
      // disc is not empty but appendable
      diskInfo_.sessionCnt = data[4];
      diskInfo_.lastTrackNr = data[6];

      diskInfo_.diskTocType = data[8];

      switch ((data[2] >> 2) & 0x03) {
      case 0:
	// last session is empty
	diskInfo_.append = 1;

	// don't count the empty session and invisible track
	diskInfo_.sessionCnt -= 1;
	diskInfo_.lastTrackNr -= 1;

	if (getStartOfSession(&(diskInfo_.thisSessionLba)) == 0) {
	  // reserve space for pre-gap after lead-in
	  diskInfo_.thisSessionLba += 150;
	}
	else {
	  // try to guess start of data area from start of lead-in
	  // reserve space for 4500 lead-in and 150 pre-gap sectors
	  diskInfo_.thisSessionLba = Msf(data[17], data[18],
					 data[19]).lba() - 150 + 4650;
	}
	break;

      case 1:
	// last session is incomplete (not fixated)
	// we cannot append in DAO mode, just update the statistic data
	
	diskInfo_.diskTocType = data[8];

	// don't count the invisible track
	diskInfo_.lastTrackNr -= 1;
	break;
      }
      break;

    case 2:
      // disk is complete
      diskInfo_.sessionCnt = data[4];
      diskInfo_.lastTrackNr = data[6];
      diskInfo_.diskTocType = data[8];
      break;
    }

    diskInfo_.valid.empty = 1;
    diskInfo_.valid.append = 1;

    if (data[21] != 0xff || data[22] != 0xff || data[23] != 0xff) {
      diskInfo_.valid.capacity = 1;
      diskInfo_.capacity = Msf(data[21], data[22], data[23]).lba() - 150;
    }
  }

  // perform READ TOC to get session info
  memset(cmd, 0, 10);
  dataLen = 12;
  memset(data, 0, dataLen);

  cmd[0] = 0x43; // READ TOC
  cmd[2] = 1; // get session info
  cmd[8] = dataLen; // allocation length

  if (sendCmd(cmd, 10, NULL, 0, data, dataLen, 0) == 0) {
    diskInfo_.lastSessionLba = (data[8] << 24) | (data[9] << 16) |
                               (data[10] << 8) | data[11];

    if (!diskInfo_.valid.empty) {
      diskInfo_.valid.empty = 1;
      diskInfo_.empty = (data[3] == 0) ? 1 : 0;

      diskInfo_.sessionCnt = data[3];
    }
  }

  // read ATIP data
  dataLen = 28;
  memset(cmd, 0, 10);
  memset(data, 0, dataLen);

  cmd[0] = 0x43; // READ TOC/PMA/ATIP
  cmd[1] = 0x00;
  cmd[2] = 4; // get ATIP
  cmd[7] = 0;
  cmd[8] = dataLen; // data length
  
  if (sendCmd(cmd, 10, NULL, 0, data, dataLen, 0) == 0) {
    if (data[6] & 0x04) {
      diskInfo_.valid.recSpeed = 1;
      spd = (data[16] >> 4) & 0x07;
      diskInfo_.recSpeedLow = spd == 1 ? 2 : 0;
      
      spd = (data[16] & 0x0f);
      diskInfo_.recSpeedHigh = spd >= 1 && spd <= 4 ? spd * 2 : 0;
    }

    if (data[8] >= 80 && data[8] <= 99) {
      diskInfo_.manufacturerId = Msf(data[8], data[9], data[10]);
      diskInfo_.valid.manufacturerId = 1;
    }
  }

  return &diskInfo_;
}


// tries to read catalog number from disk and adds it to 'toc'
// return: 1 if valid catalog number was found, else 0
int GenericMMC::readCatalog(Toc *toc, long startLba, long endLba)
{
  unsigned char cmd[10];
  unsigned char data[24];
  char catalog[14];
  int i;

  if (options_ & OPT_MMC_SCAN_MCN) {
    if (readCatalogScan(catalog, startLba, endLba) == 0) {
      if (catalog[0] != 0) {
	if (toc->catalog(catalog) == 0)
	  return 1;
	else
	  message(-1, "Found illegal MCN data: %s", catalog);
      }
    }
  }
  else {
    memset(cmd, 0, 10);
    memset(data, 0, 24);

    cmd[0] = 0x42; // READ SUB-CHANNEL
    cmd[2] = 1 << 6;
    cmd[3] = 0x02; // get media catalog number
    cmd[8] = 24;   // transfer length
    
    if (sendCmd(cmd, 10, NULL, 0, data, 24) != 0) {
      message(-2, "Cannot get catalog number.");
      return 0;
    }
    
    if (data[8] & 0x80) {
      for (i = 0; i < 13; i++) {
	catalog[i] = data[0x09 + i];
      }
      catalog[13] = 0;
      
      if (toc->catalog(catalog) == 0) {
	return 1;
      }
    }
  }

  return 0;
}

int GenericMMC::readIsrc(int trackNr, char *buf)
{
  unsigned char cmd[10];
  unsigned char data[24];
  int i;

  buf[0] = 0;

  memset(cmd, 0, 10);
  memset(data, 0, 24);

  cmd[0] = 0x42; // READ SUB-CHANNEL
  cmd[2] = 1 << 6;
  cmd[3] = 0x03; // get media catalog number
  cmd[6] = trackNr;
  cmd[8] = 24;   // transfer length

  if (sendCmd(cmd, 10, NULL, 0, data, 24) != 0) {
    message(-2, "Cannot get ISRC code.");
    return 0;
  }

  if (data[8] & 0x80) {
    for (i = 0; i < 12; i++) {
      buf[i] = data[0x09 + i];
    }
    buf[12] = 0;
  }

  return 0;
}

int GenericMMC::analyzeTrack(TrackData::Mode mode, int trackNr, long startLba,
			     long endLba, Msf *indexIncrements,
			     int *indexIncrementCnt, long *pregap,
			     char *isrcCode, unsigned char *ctl)
{
  int ret;
  int noScan = 0;

  selectSpeed(0);

  if ((readCapabilities_ & CDR_AUDIO_SCAN_CAP) == 0) {
    ret = analyzeTrackSearch(mode, trackNr, startLba, endLba, indexIncrements,
			     indexIncrementCnt, pregap, isrcCode, ctl);
    noScan = 1;
  }
  else {
    ret = analyzeTrackScan(mode, trackNr, startLba, endLba,
			   indexIncrements, indexIncrementCnt, pregap,
			   isrcCode, ctl);
  }

  if (noScan || (options_ & OPT_MMC_READ_ISRC) != 0 ||
      (readCapabilities_ & CDR_READ_CAP_AUDIO_PQ_HEX) != 0) {
    // The ISRC code is usually not usable if the PQ channel data is
    // converted to hex numbers by the drive. Read them with the
    // appropriate command in this case

    *isrcCode = 0;
    if (mode == TrackData::AUDIO)
      readIsrc(trackNr, isrcCode);
  }

  return ret;
}

int GenericMMC::readSubChannels(TrackData::SubChannelMode sm,
				long lba, long len, SubChannel ***chans,
				Sample *audioData)
{
  int retries = 5;
  unsigned char cmd[12];
  int i;
  long blockLen;
  unsigned long subChanMode = 0;

  cmd[0] = 0xbe;  // READ CD
  cmd[1] = 0;
  cmd[2] = lba >> 24;
  cmd[3] = lba >> 16;
  cmd[4] = lba >> 8;
  cmd[5] = lba;
  cmd[6] = len >> 16;
  cmd[7] = len >> 8;
  cmd[8] = len;
  cmd[9] = 0xf8;
  cmd[11] = 0;

  switch (sm) {
  case TrackData::SUBCHAN_NONE:
    // no sub-channel data selected choose what is available

    if ((readCapabilities_ & CDR_READ_CAP_AUDIO_PW_RAW) != 0) {
      // reading of raw P-W sub-channel data is supported
      blockLen = AUDIO_BLOCK_LEN + 96;
      cmd[10] = 0x01;  // raw P-W sub-channel data

      subChanMode = CDR_READ_CAP_AUDIO_PW_RAW;
    }
    else if ((readCapabilities_ & 
	      (CDR_READ_CAP_AUDIO_PQ_BCD|CDR_READ_CAP_AUDIO_PQ_HEX)) != 0) {
      // reading of PQ sub-channel data is supported
      blockLen = AUDIO_BLOCK_LEN + 16;
      cmd[10] = 0x02;  // PQ sub-channel data

      if ((readCapabilities_ & CDR_READ_CAP_AUDIO_PQ_BCD) != 0)
	subChanMode = CDR_READ_CAP_AUDIO_PQ_BCD;
      else
	subChanMode = CDR_READ_CAP_AUDIO_PQ_HEX;
    }
    else {
      // no usable sub-channel reading mode is supported
      blockLen = AUDIO_BLOCK_LEN;
      cmd[10] = 0;

      subChanMode = 0;
    }
    break;

  case TrackData::SUBCHAN_RW:
    blockLen = AUDIO_BLOCK_LEN + 96;
    cmd[10] = 0x04;
    break;

  case TrackData::SUBCHAN_RW_RAW:
    blockLen = AUDIO_BLOCK_LEN + 96;
    cmd[10] = 0x01;
    break;
  }

  while (1) {
    if (sendCmd(cmd, 12, NULL, 0,
		transferBuffer_, len * blockLen, retries == 0 ? 1 : 0) != 0) {
      if (retries == 0) 
	return 1;
    }
    else {
      break;
    }

    retries--;
  }

#if 0
  if (lba > 5000) {
    char fname[200];
    sprintf(fname, "testout_%ld", lba);
    FILE *fp = fopen(fname, "w");
    fwrite(transferBuffer_, blockLen, len, fp);
    fclose(fp);
  }
#endif

  if (subChanMode != 0) {
    unsigned char *buf = transferBuffer_ + AUDIO_BLOCK_LEN;

    for (i = 0; i < len; i++) {
      switch (subChanMode) {
      case CDR_READ_CAP_AUDIO_PQ_HEX:
	// All numbers in sub-channel data are hex conforming to the
	// MMC standard. We have to convert them back to BCD for the
	// 'SubChannel' class.
	buf[1] = SubChannel::bcd(buf[1]);
	buf[2] = SubChannel::bcd(buf[2]);
	buf[3] = SubChannel::bcd(buf[3]);
	buf[4] = SubChannel::bcd(buf[4]);
	buf[5] = SubChannel::bcd(buf[5]);
	buf[6] = SubChannel::bcd(buf[6]);
	buf[7] = SubChannel::bcd(buf[7]);
	buf[8] = SubChannel::bcd(buf[8]);
	buf[9] = SubChannel::bcd(buf[9]);
	// fall through

      case CDR_READ_CAP_AUDIO_PQ_BCD:
	((PQSubChannel16*)scannedSubChannels_[i])->init(buf);
	if (scannedSubChannels_[i]->type() != SubChannel::QMODE_ILLEGAL) {
	  // the CRC of the sub-channel data is usually invalid -> mark the
	  // sub-channel object that it should not try to verify the CRC
	  scannedSubChannels_[i]->crcInvalid();
	}
	break;
      
      case CDR_READ_CAP_AUDIO_PW_RAW:
	((PWSubChannel96*)scannedSubChannels_[i])->init(buf);
	break;
      }

#if 0
      if (subChanMode == CDR_READ_CAP_AUDIO_PW_RAW) {
	// xxam!
	int j, k;
	
	message(0, "");
	for (j = 0; j < 4; j++) {
	  for (k = 0; k < 24; k++) {
	    unsigned char data = buf[j * 24 + k];
	    message(0, "%02x ", data&0x3f);
	  }
	  message(0, "");
	}
      }
#endif

      buf += blockLen;
    }
  }
  
  if (audioData != NULL) {
    unsigned char *p = transferBuffer_;

    for (i = 0; i < len; i++) {
      memcpy(audioData, p, AUDIO_BLOCK_LEN);

      audioData += SAMPLES_PER_BLOCK;

      switch (sm) {
      case TrackData::SUBCHAN_NONE:
	break;

      case TrackData::SUBCHAN_RW:
      case TrackData::SUBCHAN_RW_RAW:
	memcpy(audioData, p + AUDIO_BLOCK_LEN, PW_SUBCHANNEL_LEN);
	audioData += PW_SUBCHANNEL_LEN / SAMPLE_LEN;
	break;
      }

      p += blockLen;
    }
  }

  if (subChanMode == 0)
    *chans = NULL;
  else
    *chans = scannedSubChannels_;

  return 0;
}


// Tries to retrieve configuration feature 'feature' and fills data to
// provided buffer 'buf' with maximum length 'bufLen'.
// Return: 0: OK
//         1: feature not available
//         2: SCSI error
int GenericMMC::getFeature(unsigned int feature, unsigned char *buf,
			   unsigned long bufLen, int showMsg)
{
  unsigned char header[8];
  unsigned char *data;
  unsigned char cmd[10];
  unsigned long len;

  memset(cmd, 0, 10);
  memset(header, 0, 8);

  cmd[0] = 0x46; // GET CONFIGURATION
  cmd[1] = 0x02; // return single feature descriptor
  cmd[2] = feature >> 8;
  cmd[3] = feature;
  cmd[8] = 8; // allocation length

  if (sendCmd(cmd, 10, NULL, 0, header, 8, showMsg) != 0) {
    if (showMsg)
      message(-2, "Cannot get feature 0x%x.", feature);
    return 2;
  }

  len = (header[0] << 24) | (header[1] << 16) | (header[2] << 8) | header[3];

  message(4, "getFeature: data len: %lu", len);

  if (len < 8)
    return 1; // feature not defined

  if (bufLen == 0)
    return 0;

  len -= 4; 

  if (len > bufLen)
    len = bufLen;

  data = new (unsigned char)[len + 8];

  cmd[7] = (len + 8) >> 8;
  cmd[8] = (len + 8);

  if (sendCmd(cmd, 10, NULL, 0, data, len + 8, showMsg) != 0) {
    if (showMsg)
      message(-2, "Cannot get data for feature 0x%x.", feature);
    
    delete[] data;
    return 2;
  }
  
  len = (header[0] << 24) | (header[1] << 16) | (header[2] << 8) | header[3];
  
  message(4, "getFeature: data len: %lu", len);

  if (len < 8) {
    delete[] data;
    return 1; // feature not defined
  }

  len -= 4;

  if (len > bufLen)
    len = bufLen;

  memcpy(buf, data + 8, len);

  delete[] data;
  
  return 0;
}

DriveInfo *GenericMMC::driveInfo(int showErrorMsg)
{
  unsigned char mp[32];

  if (driveInfo_ != NULL)
    return driveInfo_;

  driveInfo_ = new DriveInfo;

  if (getModePage(0x2a, mp, 32, NULL, NULL, showErrorMsg) != 0) {
    if (showErrorMsg) {
      message(-2, "Cannot retrieve drive capabilities mode page.");
    }
    delete driveInfo_;
    driveInfo_ = NULL;
    return NULL;
  }

  driveInfo_->burnProof = (mp[4] & 0x80) ? 1 : 0;
  driveInfo_->accurateAudioStream = mp[5] & 0x02 ? 1 : 0;

  driveInfo_->maxReadSpeed = (mp[8] << 8) | mp[9];
  driveInfo_->currentReadSpeed = (mp[14] << 8) | mp[15];

  driveInfo_->maxWriteSpeed = (mp[18] << 8) | mp[19];
  driveInfo_->currentWriteSpeed = (mp[20] << 8) | mp[21];

#if 0
  unsigned char cdMasteringFeature[8];
  if (getFeature(0x2e, cdMasteringFeature, 8, 1) == 0) {
    message(0, "Feature: %x %x %x %x %x %x %x %x", cdMasteringFeature[0],
	    cdMasteringFeature[1], cdMasteringFeature[2],
	    cdMasteringFeature[3], cdMasteringFeature[4],
	    cdMasteringFeature[5], cdMasteringFeature[6],
	    cdMasteringFeature[7]);
  }
#endif

  return driveInfo_;
}

TrackData::Mode GenericMMC::getTrackMode(int, long trackStartLba)
{
  unsigned char cmd[12];
  unsigned char data[AUDIO_BLOCK_LEN];

  memset(cmd, 0, 12);
  cmd[0] = 0xbe;  // READ CD

  cmd[2] = trackStartLba >> 24;
  cmd[3] = trackStartLba >> 16;
  cmd[4] = trackStartLba >> 8;
  cmd[5] = trackStartLba;

  cmd[8] = 1;

  cmd[9] = 0xf8;

  if (sendCmd(cmd, 12, NULL, 0, data, AUDIO_BLOCK_LEN) != 0) {
    message(-2, "Cannot read sector of track.");
    return TrackData::MODE0;
  }

  if (memcmp(CdrDriver::syncPattern, data, 12) != 0) {
    // cannot be a data sector
    return TrackData::MODE0;
  }

  TrackData::Mode mode = determineSectorMode(data + 12);

  if (mode == TrackData::MODE0) {
    // illegal
    message(-2, "Found illegal mode in sector %ld.", trackStartLba);
  }

  return mode;
}

CdRawToc *GenericMMC::getRawToc(int sessionNr, int *len)
{
  unsigned char cmd[10];
  unsigned short dataLen;
  unsigned char *data = NULL;;
  unsigned char reqData[4]; // buffer for requestion the actual length
  unsigned char *p;
  int i, entries;
  CdRawToc *rawToc;

  assert(sessionNr >= 1);

  // read disk toc length
  memset(cmd, 0, 10);
  cmd[0] = 0x43; // READ TOC
  cmd[2] = 2;
  cmd[6] = sessionNr;
  cmd[8] = 4;

  if (sendCmd(cmd, 10, NULL, 0, reqData, 4) != 0) {
    message(-2, "Cannot read disk toc.");
    return NULL;
  }

  dataLen = ((reqData[0] << 8) | reqData[1]) + 2;
  
  message(4, "Raw toc data len: %d", dataLen);

  data = new (unsigned char)[dataLen];
  
  // read disk toc
  cmd[7] = dataLen >> 8;
  cmd[8] = dataLen;

  if (sendCmd(cmd, 10, NULL, 0, data, dataLen) != 0) {
    message(-2, "Cannot read disk toc.");
    delete[] data;
    return NULL;
  }

  entries = (((data[0] << 8) | data[1]) - 2) / 11;

  rawToc = new CdRawToc[entries];

  for (i = 0, p = data + 4; i < entries; i++, p += 11 ) {
#if 0
    message(5, "%d %02x %02d %2x %02d:%02d:%02d %02d %02d:%02d:%02d",
	    p[0], p[1], p[2], p[3], p[4], p[5], p[6], p[7], p[8], p[9], p[10]);
#endif
    rawToc[i].sessionNr = p[0];
    rawToc[i].adrCtl = p[1];
    rawToc[i].point = p[3];
    rawToc[i].pmin = p[8];
    rawToc[i].psec = p[9];
    rawToc[i].pframe = p[10];
  }

  delete[] data;

  *len = entries;

  return rawToc;
}

long GenericMMC::readTrackData(TrackData::Mode mode,
			       TrackData::SubChannelMode sm,
			       long lba, long len, unsigned char *buf)
{
  long i;
  long inBlockLen = AUDIO_BLOCK_LEN;
  unsigned char cmd[12];
  const unsigned char *sense;
  int senseLen;

  memset(cmd, 0, 12);

  cmd[0] = 0xbe; // READ CD
  cmd[1] = 0;
  cmd[2] = lba >> 24;
  cmd[3] = lba >> 16;
  cmd[4] = lba >> 8;
  cmd[5] = lba;
  cmd[6] = len >> 16;
  cmd[7] = len >> 8;
  cmd[8] = len;
  cmd[9] = 0xf8;

  switch (sm) {
  case TrackData::SUBCHAN_NONE:
    cmd[10] = 0; // no sub-channel reading
    break;

  case TrackData::SUBCHAN_RW:
    cmd[10] = 0x4;
    inBlockLen += PW_SUBCHANNEL_LEN;
    break;

  case TrackData::SUBCHAN_RW_RAW:
    cmd[10] = 0x1;
    inBlockLen += PW_SUBCHANNEL_LEN;
    break;
  }

  switch (sendCmd(cmd, 12, NULL, 0, transferBuffer_, len * inBlockLen, 0)) {
  case 0:
    break;

  case 2:
    sense = scsiIf_->getSense(senseLen);

    if (senseLen > 0x0c) {
      if ((sense[2] & 0x0f) == 5) { // Illegal request
	switch (sense[12]) {
	case 0x63: // End of user area encountered on this track
	case 0x64: // Illegal mode for this track
	  return -2;
	  break;

	case 0x20: // INVALID COMMAND OPERATION CODE
	case 0x24: // INVALID FIELD IN CDB
	case 0x26: // INVALID FIELD IN PARAMETER LIST
	  /* These error codes mean that something was wrong with the
	   * command we are sending. Report them as hard errors to the
	   * upper level.
	   */
	  scsiIf_->printError();
	  return -1;
	  break;
	}
      }
      else if ((sense[2] & 0x0f) == 4) { // Hardware Error
	switch (sense[12]) {
	case 0x9: // focus servo failure
	  return -2;
	  break;
	}
      }    
      else if ((sense[2] & 0x0f) == 3) { // Medium error
	switch (sense[12]) {
	case 0x02: // No seek complete, sector not found
	case 0x06: // no reference position found
	case 0x11: // L-EC error
	case 0x15: // random positioning error
	  return -2;
	  break;
	}
      }
    }

    /* All other errors are unexpected. They will be treated like L-EC errors
     * by the upper layer. Just print the error code so that we can decice
     * later to add the errors to the known possible error list.
     */
    
    scsiIf_->printError();
    return -2;
    break;

  default:
    message(-2, "Read error at LBA %ld, len %ld", lba, len);
    return -2;
    break;
  }

  unsigned char *sector = transferBuffer_;
  for (i = 0; i < len; i++) {
    if (buf != NULL) {
      switch (mode) {
      case TrackData::MODE1:
	memcpy(buf, sector + 16, MODE1_BLOCK_LEN);
	buf += MODE1_BLOCK_LEN;
	break;
      case TrackData::MODE1_RAW:
	memcpy(buf, sector, AUDIO_BLOCK_LEN);
	buf += AUDIO_BLOCK_LEN;
	break;
      case TrackData::MODE2:
      case TrackData::MODE2_FORM_MIX:
	memcpy(buf, sector + 16, MODE2_BLOCK_LEN);
	buf += MODE2_BLOCK_LEN;
	break;
      case TrackData::MODE2_FORM1:
	memcpy(buf, sector + 24, MODE2_FORM1_DATA_LEN);
	buf += MODE2_FORM1_DATA_LEN;
	break;
      case TrackData::MODE2_FORM2:
	memcpy(buf, sector + 24, MODE2_FORM2_DATA_LEN);
	buf += MODE2_FORM2_DATA_LEN;
	break;
      case TrackData::MODE2_RAW:
	memcpy(buf, sector, AUDIO_BLOCK_LEN);
	buf += AUDIO_BLOCK_LEN;
	break;
      case TrackData::MODE0:
      case TrackData::AUDIO:
	message(-3, "GenericMMC::readTrackData: Illegal mode.");
	return 0;
	break;
      }

      // copy sub-channel data
      switch (sm) {
      case TrackData::SUBCHAN_NONE:
	break;

      case TrackData::SUBCHAN_RW:
      case TrackData::SUBCHAN_RW_RAW:
	memcpy(buf, sector + AUDIO_BLOCK_LEN, PW_SUBCHANNEL_LEN);
	buf += PW_SUBCHANNEL_LEN;
	break;
      }
    }

#if 0
    // xxam!
    int j, k;

    message(0, "");
    for (j = 0; j < 4; j++) {
      for (k = 0; k < 24; k++) {
	unsigned char data = sector[AUDIO_BLOCK_LEN + j * 24 + k];
	message(0, "%02x ", data&0x3f);
      }
      message(0, "");
    }
#endif

    sector += inBlockLen;
  }

  return len;
}

int GenericMMC::readAudioRange(ReadDiskInfo *rinfo, int fd, long start,
			       long end, int startTrack,
			       int endTrack, TrackInfo *info)
{
  if (!onTheFly_) {
    if (((readCapabilities_ & CDR_READ_CAP_AUDIO_PQ_BCD) == 0 &&
	 (readCapabilities_ & CDR_READ_CAP_AUDIO_PW_RAW) == 0) ||
	(options_ & OPT_MMC_READ_ISRC) != 0) {
      int t;
      long pregap = 0;

      // The ISRC code is usually not usable if the PQ channel data is
      // converted to hex numbers by the drive. Read them with the
      // appropriate command in this case

      message(1, "Analyzing...");


      for (t = startTrack; t <= endTrack; t++) {
	long totalProgress;

	message(1, "Track %d...", t + 1);

	totalProgress = t * 1000;
	totalProgress /= rinfo->tracks;
	sendReadCdProgressMsg(RCD_ANALYZING, rinfo->tracks, t + 1, 0,
			      totalProgress);

	if ((readCapabilities_ & CDR_AUDIO_SCAN_CAP) == 0) {
	  // we have to use the binary search method to find pre-gap and
	  // index marks if the drive cannot read sub-channel data
	  if (!fastTocReading_) {
	    long slba, elba;
	    int i, indexCnt;
	    Msf index[98];
	    unsigned char ctl;

	    if (pregap > 0)
	      message(2, "Found pre-gap: %s", Msf(pregap).str());

	    slba = info[t].start;
	    if (info[t].mode == info[t + 1].mode)
	      elba = info[t + 1].start;
	    else
	      elba = info[t + 1].start - 150;
	    
	    pregap = 0;
	    if (analyzeTrackSearch(TrackData::AUDIO, t + 1, slba, elba,
				   index, &indexCnt, &pregap, info[t].isrcCode,
				   &ctl) != 0)
	      return 1;
	  
	    for (i = 0; i < indexCnt; i++)
	      info[t].index[i] = index[i].lba();
	    
	    info[t].indexCnt = indexCnt;
	    
	    if (t < endTrack)
	      info[t + 1].pregap = pregap;
	  }
	  else {
	    info[t].indexCnt = 0;
	    info[t + 1].pregap = 0;
	  }
	}


	info[t].isrcCode[0] = 0;
	readIsrc(t + 1, info[t].isrcCode);
	if (info[t].isrcCode[0] != 0)
	  message(2, "Found ISRC code.");

	totalProgress = (t + 1) * 1000;
	totalProgress /= rinfo->tracks;
	sendReadCdProgressMsg(RCD_ANALYZING, rinfo->tracks, t + 1, 1000,
			      totalProgress);
      }

      message(1, "Reading...");
    }
  }

  if (subChanReadMode_ == TrackData::SUBCHAN_NONE) {
    return CdrDriver::readAudioRangeParanoia(rinfo, fd, start, end, startTrack,
					     endTrack, info);
  }
  else {
    return CdrDriver::readAudioRangeStream(rinfo, fd, start, end, startTrack,
					   endTrack, info);
  }
}

int GenericMMC::getTrackIndex(long lba, int *trackNr, int *indexNr,
			      unsigned char *ctl)
{
  unsigned char cmd[12];
  unsigned short dataLen = 0x30;
  unsigned char data[0x30];
  int waitLoops = 10;
  int waitFailed = 0;

  // play one audio block
  memset(cmd, 0, 10);
  cmd[0] = 0x45; // PLAY AUDIO
  cmd[2] = lba >> 24;
  cmd[3] = lba >> 16;
  cmd[4] = lba >> 8;
  cmd[5] = lba;
  cmd[7] = 0;
  cmd[8] = 1;

  if (sendCmd(cmd, 10, NULL, 0, NULL, 0) != 0) {
    message(-2, "Cannot play audio block.");
    return 1;
  }

  // wait until the play command finished
  memset(cmd, 0, 12);
  cmd[0] = 0xbd; // MECHANISM STATUS
  cmd[9] = 8;

  while (waitLoops > 0) {
    if (sendCmd(cmd, 12, NULL, 0, data, 8, 0) == 0) {
      //message(0, "%d, %x", waitLoops, data[1]);
      if ((data[1] >> 5) == 1) // still playing?
	waitLoops--;
      else
	waitLoops = 0;
    }
    else {
      waitFailed = 1;
      waitLoops = 0;
    }
  }

  if (waitFailed) {
    // The play operation immediately returns success status and the waiting
    // loop above failed. Wait here for a while until the desired block is
    // played. It takes ~13 msecs to play a block but access time is in the
    // order of several 100 msecs
    mSleep(300);
  }

  // read sub channel information
  memset(cmd, 0, 10);
  cmd[0] = 0x42; // READ SUB CHANNEL
  cmd[2] = 0x40; // get sub channel data
  cmd[3] = 0x01; // get sub Q channel data
  cmd[6] = 0;
  cmd[7] = dataLen >> 8;
  cmd[8] = dataLen;

  if (sendCmd(cmd, 10, NULL, 0, data, dataLen) != 0) {
    message(-2, "Cannot read sub Q channel data.");
    return 1;
  }

  *trackNr = data[6];
  *indexNr = data[7];
  if (ctl != NULL) {
    *ctl = data[5] & 0x0f;
  }

  //message(0, "%d %d", *trackNr, *indexNr);

  return 0;
}

/*
 * Checks if a certain sub-channel reading mode is supported.
 * lba: start address for reading
 * len: maximum number of sectors available for testing
 * subChanMode: 1: read PQ sub-channels
 *              2: read raw P-W sub-channels
 *              3: read cooked R-W sub-channels
 * Return: 0 sub-channel read mode not supported
 *         1 sub-channel read mode supported (BCD for PQ)
 *         2 sub-channel read mode supported (HEX for PQ)
 *         3 sub-channel read mode PQ supported but cannot determine data
 *           format
*/

int GenericMMC::readCdTest(long lba, long len, int subChanMode) const
{
  unsigned char cmd[12];
  long blockLen;
  int ret;
  int successRead = 0;
  int pqSubChanBcdOk = 0;
  int pqSubChanHexOk = 0;

  //message(0, "readCdTest: %ld %ld %d", lba, len, subChanMode);

  if (len <= 0)
    return 0;

  cmd[0] = 0xbe;  // READ CD
  cmd[1] = 0;
  cmd[6] = 0;
  cmd[7] = 0;
  cmd[8] = 1; // transfer length: 1
  cmd[9] = 0xf8;
  cmd[10] = 0;
  cmd[11] = 0;

  blockLen = AUDIO_BLOCK_LEN;

  switch (subChanMode) {
  case 1: // PQ
    blockLen += PQ_SUBCHANNEL_LEN;
    cmd[10] = 0x02;
    if (len > 300)
      len = 300; /* we have to check many sub-channels here to determine the
		  * data mode (BCD or HEX)
		  */
    break;

  case 2: // PW_RAW
    cmd[10] = 0x01;
    blockLen +=  PW_SUBCHANNEL_LEN;
    if (len > 10)
      len = 10;
    break;

  case 3: // RW_COOKED
    cmd[10] = 0x04;
    blockLen +=  PW_SUBCHANNEL_LEN;
    if (len > 10)
      len = 10;
    break;
  }

  while (len > 0) {
    cmd[2] = lba >> 24;
    cmd[3] = lba >> 16;
    cmd[4] = lba >> 8;
    cmd[5] = lba;
    
    if ((ret = sendCmd(cmd, 12, NULL, 0, transferBuffer_, blockLen, 0)) == 0) {
      successRead++;

      if (subChanMode == 1) {
	unsigned char *buf = transferBuffer_ + AUDIO_BLOCK_LEN;

#if 0
	{
 	  PQSubChannel16 chan;
	  chan.init(buf);
	  chan.print();
	}
#endif

	// check if Q sub-channel values are in BCD or HEX format
	if (SubChannel::isBcd(buf[1]) &&
	    SubChannel::isBcd(buf[2]) &&
	    SubChannel::isBcd(buf[3]) &&
	    SubChannel::isBcd(buf[4]) &&
	    SubChannel::isBcd(buf[5]) &&
	    SubChannel::isBcd(buf[6]) &&
	    SubChannel::isBcd(buf[7]) &&
	    SubChannel::isBcd(buf[8]) &&
	    SubChannel::isBcd(buf[9])) {
	  PQSubChannel16 chan;
	  chan.init(buf);

	  chan.type(SubChannel::QMODE1DATA);

	  long pqlba =
	    Msf(chan.amin(), chan.asec(), chan.aframe()).lba() - 150;

	  long diff = pqlba - lba;
	  if (diff < 0)
	    diff = -diff;
	  
	  if (diff < 20) {
	    pqSubChanBcdOk++;
	  }
	}

	if (buf[7] < 100 && buf[8] < 60 && buf[9] < 75) {
	  long pqlba = Msf(buf[7], buf[8], buf[9]).lba() - 150;

	  //message(0, "readCdTest: pqlba: %ld", pqlba);
	  long diff = pqlba - lba;
	  if (diff < 0)
	    diff = -diff;
	  
	  if (diff < 20) {
	    pqSubChanHexOk++;
	  }
	}
      }
    }


    len--;
    lba++;
  }

  if (successRead) {
    if (subChanMode == 1) {
      if (pqSubChanBcdOk > pqSubChanHexOk)
	return 1;
      else if (pqSubChanHexOk > pqSubChanBcdOk)
	return 2;
      else return 3;
    }
    else {
      return 1;
    }
  }

  return 0;
}

unsigned long GenericMMC::getReadCapabilities(const CdToc *toc,
					      int nofTracks) const
{
  unsigned long caps = 0;
  int audioRawPWChecked = 0;
  int audioPQChecked = 0;
  int audioCookedRWChecked = 0;
  int dataRawPWChecked = 0;
  int dataPQChecked = 0;
  int dataCookedRWChecked = 0;
  int t;

  if ((options_ & OPT_MMC_NO_SUBCHAN) != 0) {
    // driver options indicate that PQ and raw RW sub-channel reading for
    // audio tracks is not supported so skip all corresponding tests
    audioPQChecked = 1;
    audioRawPWChecked = 1;
  }
  else if ((options_ & OPT_MMC_USE_PQ) != 0) {
    // driver options indicated that PQ sub-channel reading is supported for
    // audio/data tracks and RW sub-channel reading is not supported, skip
    // the corresponding checks and set the capabilities appropriately
    audioPQChecked = 1;
    audioRawPWChecked = 1;
    dataPQChecked = 1;
    
    if ((options_ & OPT_MMC_PQ_BCD) != 0)
      caps |= CDR_READ_CAP_AUDIO_PQ_BCD | CDR_READ_CAP_DATA_PQ_BCD;
    else
      caps |= CDR_READ_CAP_AUDIO_PQ_HEX | CDR_READ_CAP_DATA_PQ_HEX;
  }
  else if ((options_ & OPT_MMC_USE_RAW_RW) != 0) {
    // driver options indicated that raw PW sub-channel reading is supported
    // audio tracks and raw PW sub-channel reading is not supported, skip
    // the corresponding checks and set the capabilities appropriately
    audioPQChecked = 1;
    audioRawPWChecked = 1;

    caps |= CDR_READ_CAP_DATA_PW_RAW;
  }

  for (t = 0; t < nofTracks; t++) {
    long tlen = toc[t+1].start - toc[t].start;

    if ((toc[t].adrCtl & 0x04) != 0) {
      // data track
      if (!dataPQChecked) {
	dataPQChecked = 1;

	message(3, "Checking for PQ sub-channel reading support (data track)...");
	switch (readCdTest(toc[t].start, tlen, 1)) {
	case 0:
	  message(3, "PQ sub-channel reading (data track) not supported.");
	  break;

	case 1:
	  message(2, "PQ sub-channel reading (data track) is supported, data format is BCD.");
	  caps |= CDR_READ_CAP_DATA_PQ_BCD;
	  break;
	  
	case 2:
	  message(2, "PQ sub-channel reading (data track) is supported, data format is HEX.");
	  caps |= CDR_READ_CAP_DATA_PQ_HEX;
	  break;
	  
	case 3:
	  message(2, "PQ sub-channel reading (data track) seems to be supported but cannot determine data format.");
	  message(2, "Please use driver option '--driver generic-mmc:0x1' or '--driver generic-mmc:0x3' to set the data format explicitly.");
	  break;
	}
      }
	
      if (!dataRawPWChecked) {
	dataRawPWChecked = 1;

	message(3, "Checking for raw P-W sub-channel reading support (data track)...");
	if (readCdTest(toc[t].start, tlen, 2)) {
	  message(2, "Raw P-W sub-channel reading (data track) is supported.");
	  caps |= CDR_READ_CAP_DATA_PW_RAW;
	}
	else {
	  message(3, "Raw P-W sub-channel reading (data track) is not supported.");
	}
      }

      if (!dataCookedRWChecked) {
	dataCookedRWChecked = 1;
      
	message(3, "Checking for cooked R-W sub-channel reading support (data track)...");
	if (readCdTest(toc[t].start, tlen, 3)) {
	  message(2, "Cooked R-W sub-channel reading (data track) is supported.");
	  caps |= CDR_READ_CAP_DATA_RW_COOKED;
	}
	else {
	  message(3, "Cooked R-W sub-channel reading (data track) is not supported.");
	}
      }
    }
    else {
      // audio track
      if (!audioPQChecked) {
	audioPQChecked = 1;

	message(3, "Checking for PQ sub-channel reading support (audio track)...");
	switch (readCdTest(toc[t].start, tlen, 1)) {
	case 0:
	  message(3, "PQ sub-channel reading (audio track) is not supported.");
	  break;

	case 1:
	  message(2, "PQ sub-channel reading (audio track) is supported, data format is BCD.");
	  caps |= CDR_READ_CAP_AUDIO_PQ_BCD;
	  break;

	case 2:
	  message(2, "PQ sub-channel reading (audio track) is supported, data format is HEX.");
	  caps |= CDR_READ_CAP_AUDIO_PQ_HEX;
	  break;

	case 3:
	  message(2, "PQ sub-channel reading (audio track) seems to be supported but cannot determine data format.");
	  message(2, "Please use driver option '--driver generic-mmc:0x1' or '--driver generic-mmc:0x3' to set the data format explicitly.");
	  break;
	}
      }

      if (!audioRawPWChecked) {
	audioRawPWChecked = 1;

	message(3, "Checking for raw P-W sub-channel reading support (audio track)...");
	if (readCdTest(toc[t].start, tlen, 2)) {
	  message(2, "Raw P-W sub-channel reading (audio track) is supported.");
	  caps |= CDR_READ_CAP_AUDIO_PW_RAW;
	}
	else {
	  message(3, "Raw P-W sub-channel reading (audio track) is not supported.");
	}
      }

      if (!audioCookedRWChecked) {
	audioCookedRWChecked = 1;

	message(3, "Checking for cooked R-W sub-channel reading support (audio track)...");
	if (readCdTest(toc[t].start, tlen, 3)) {
	  message(2, "Cooked R-W sub-channel reading (audio track) is supported.");
	  caps |= CDR_READ_CAP_AUDIO_RW_COOKED;
	}
	else {
	  message(3, "Raw R-W sub-channel reading (audio track) is not supported.");
	}
      }
    }
  }

  return caps;
}
