Contributor: C.J. RANKIN

{==========================================================================}
{ CDDrive - an interface to the CD-ROM device driver.                      }
{--------------------------------------------------------------------------}
{ Copyright (c) 1996 C.J.Rankin   CJRankin@VossNet.Co.UK                   }
{                                                                          }
{ This unit provides an elementary interface for controlling a CD-ROM      }
{ in TP6+. It has been left open-ended so that, if you wish, it can be     }
{ extended to provide a more comprehensive range of CD-ROM functions.      }
{                                                                          }
{ Note that in its current form, it will *only* recognise the first CD-ROM }
{ in the system- not a problem for most of us.                             }
{                                                                          }
{--------------------------------------------------------------------------}
{                                                                          }
{ Note: Windows 95 uses a protected mode CD-ROM driver and interface       }
{       (MSCDEX v2.95). As a result, the standard way of requesting IOCTL  }
{       functions of opening a file handle to the driver using Assign()    }
{       and Reset() and then calling DOS services AX=$4402/$4403 DOES NOT  }
{       WORK: DOS cannot open the CD-ROM driver in the DOS driver-list and }
{       so opens a new file on the hard disc instead.                      }
{                                                                          }
{==========================================================================}
{$A-,B-,D+,F-,G+,I-,L+,O+,R-,S-,V-,X+}
unit CDDrive;

interface

const
  ex_CDROMUnknownUnit       =  1;
  ex_CDROMUnready           =  2;
  ex_CDROMUnknownCommand    =  3;
  ex_CDROMCRCError          =  4;
  ex_CDROMBadReqStrucLen    =  5;
  ex_CDROMBadSeek           =  6;
  ex_CDROMUnknownMedia      =  7;
  ex_CDROMUnknownSector     =  8;
  ex_CDROMReadError         = 11;
  ex_CDROMGeneralFailure    = 12;
  ex_CDROMMediaUnavailable  = 14;
  ex_CDROMInvalidDiscChange = 15;

const
  MaxCDROM = 26;
type
  TCDROMIndex  = 1..MaxCDROM;
  TCDROMLetter = 0..MaxCDROM-1;
  TCDROMNumber = 0..MaxCDROM;

{                                                             }
{ These are the explicit CD-ROM services provided by the unit }
{                                                             }
procedure Eject;      (* Eject CD-ROM                                      *)
procedure Close;      (* Close CD-ROM tray                                 *)
procedure Lock;       (* Lock CD-ROM drive                                 *)
procedure Unlock;     (* Unlock CD-ROM drive                               *)
procedure Reset;      (* Reinitialise CD-ROM: i.e. as if disc was changed. *)
function GetNumberOfCDROMDrives: TCDROMNumber;
function GetCDROMVersion: word;

{                                                                       }
{ Templates for CD-ROM service requests. To implement another device    }
{ or IOCTL request, create a descendant object with the requisite extra }
{ fields and pass it to the appropriate requestor function.             }
{                                                                       }
type
  PDeviceRequest = ^TDeviceRequest;
  TDeviceRequest = object
                     HeaderLength: byte;
                     SubUnit:      byte;
                     CommandCode:  byte;
                     Status:       word;
                     Reserved:     array[1..8] of byte;
                   end;

  PIOCTLRequest = ^TIOCTLRequest;
  TIOCTLRequest = object
                    SubFn: byte;
                  end;

  TRequestFunc = function(var Request: TDeviceRequest): word;

{                                                                          }
{ This constitutes the interface to the driver, enabling further functions }
{ to be added if desired. The return value is the driver's Status word:    }
{ Status Word:   Bit 15: Error flag. If set then Bits 0-7 are error code   }
{                Bit  8: Request done flag.                                }
{                Bit  9: Device busy flag.                                 }
{                                                                          }
function DriverRequest_v210(var Request: TDeviceRequest): word;
function DriverBasicRequest(var Request: TDeviceRequest): word;
function IOCTLInput(var Request: TIOCTLRequest; ReqLen: word): word;
function IOCTLOutput(var Request: TIOCTLRequest; ReqLen: word): word;

{                                                                          }
{ Errors are returned in this variable: the values are explained above ... }
{                                                                          }
var CDROMError: byte;

{                                                                          }
{ DriverRequest_v210 enables CD-ROM driver requests for MSCDEX v2.10+. For }
{ earlier versions of MSCDEX, DriverBasicRequest is used instead. The      }
{ appropriate function is assigned to the following procedural variable.   }
{                                                                          }
var DriverRequest: TRequestFunc;

implementation
uses DOS;

type
  TDrvName = array[1..8] of char;

  PCDROMDriver = ^TCDROMDriver;
  TCDROMDriver = record
                   NextDriver:         PCDROMDriver;
                   DeviceAttr:         word;
                   StrategyEntryPoint: word;
                   INTEntryPoint:      word;
                   DeviceName:         TDrvName;
                   Reserved:           word;
                   DriveLetter:        TCDROMNumber;
                   Units:              byte
                 end;

  TCDROMDriveEntry = record
                       SubUnit:     byte;
                       CDROMDriver: PCDROMDriver
                     end;

type
  PDeviceIOCTLRequest = ^TDeviceIOCTLRequest;
  TDeviceIOCTLRequest = object(TDeviceRequest)
                          Media:  byte;
                          BufPtr: pointer;
                          BufLen: byte;
                        end;

type
  TCDROMLock = (CDROM_Unlocked, CDROM_Locked);

  PCDROMLockRequest = ^TCDROMLockRequest;
  TCDROMLockRequest = object(TIOCTLRequest)
                        LockStatus: TCDROMLock;
                      end;

var Regs:                Registers;
var NumberOfCDROMDrives: TCDROMNumber;
var CDROMDriveLetter:    array[TCDROMIndex] of TCDROMLetter;

var CDROMDriverStrategyEntryPoint: procedure;
var CDROMDriverINTEntryPoint:      procedure;

{                                                                         }
{ This is the interface to the CD-ROM driver. The assumption here is that }
{ we are only dealing with the first CD-ROM drive in the system- not a    }
{ problem to us mere mortals who only HAVE one CD-ROM...                  }
{                                                                         }
function DriverRequest_v210(var Request: TDeviceRequest): word;
begin
  with Regs do
    begin
      ax := $1510;
      es := Seg(Request);
      bx := Ofs(Request);
      cx := CDROMDriveLetter[1]; (* Letter of CD-ROM drive: A=0,B=1,C=2... *)
      Intr($2f,Regs)
    end;
  with Request do
    begin
      if Status and (1 shl 15) <> 0 then (* Check the error flag...*)
        CDROMError := lo(Status)         (* ... return the error   *)
      else
        CDROMError := 0;                 (* ... return `no error'  *)
      DriverRequest_v210 := Status
    end
end;

{                                                                     }
{ This method of calling CD-ROM driver functions should work for all  }
{ versions of MSCDEX. Again, assume that there is only 1 CD-ROM ...   }
{                                                                     }
function DriverBasicRequest(var Request: TDeviceRequest): word;
begin
  with Request do
    begin
      SubUnit := 0; (* Only 1 CD-ROM, so it must be driver sub-unit 0 *)
      asm
        LES BX, [BP+OFFSET Request]
      end;
      CDROMDriverStrategyEntryPoint;
      CDROMDriverINTEntryPoint;
      if Status and (1 shl 15) <> 0 then (* Check the error flag...*)
        CDROMError := lo(Status)         (* ... return the error   *)
      else
        CDROMError := 0;                 (* ... return `no error'  *)
      DriverBasicRequest := Status
    end
end;

{                                                                        }
{ The CDROM driver can be asked to do LOTS of things; the IOCTL requests }
{ are only a very small part. In theory you could descend other buffers  }
{ from TDeviceRequest, fill in the HeaderLength, CommandCode and any new }
{ fields and send them off to DriverRequest for execution...             }
{                                                                        }
function IOCTLOutput(var Request: TIOCTLRequest; ReqLen: word): word;
var
  DeviceRequestHeader: TDeviceIOCTLRequest;
begin                               (* Descendant of TDeviceRequest *)
  with DeviceRequestHeader do
    begin
      HeaderLength := SizeOf(DeviceRequestHeader);
      CommandCode := $0C;
      BufPtr := @Request.SubFn;  (* These fields added to TDeviceRequest *)
      BufLen := ReqLen           (* for IOCTL commands...                *)
    end;
  IOCTLOutput := DriverRequest(DeviceRequestHeader)
end;

function IOCTLInput(var Request: TIOCTLRequest; ReqLen: word): word;
var
  DeviceRequestHeader: TDeviceIOCTLRequest;
begin                               (* Descendant of TDeviceRequest *)
  with DeviceRequestHeader do
    begin
      HeaderLength := SizeOf(DeviceRequestHeader);
      CommandCode := $03;
      BufPtr := @Request.SubFn;  (* These fields added to TDeviceRequest *)
      BufLen := ReqLen           (* for IOCTL commands...                *)
    end;
  IOCTLInput := DriverRequest(DeviceRequestHeader)
end;

{                                                                          }
{ Yes, I COULD have just put NumberOfCDROMDrives in the interface section, }
{ except that this number is important and I don't want users `fiddling'   }
{ with it. :-)                                                             }
{                                                                          }
function GetNumberOfCDROMDrives: TCDROMNumber;
begin
  GetNumberOfCDROMDrives := NumberOfCDROMDrives
end;

{                                                                     }
{ The mechanism used to perform device driver requests depends on the }
{ version of MSCDEX, so we need a method of finding this out.         }
{                                                                     }
function GetCDROMVersion: word;
begin
  with Regs do
    begin
      bx := 0;
      ax := $150c;
      Intr($2f,Regs);
      GetCDROMVersion := bx  (* Hi byte = Major version number *)
    end                      (* Lo byte = Minor version number *)
end;

procedure Eject;
var
  Request: TIOCTLRequest;
begin
  if NumberOfCDROMDrives > 0 then
    with Request do
      begin
        SubFn := 00;   (* IOCTL command code for Eject... *)
        IOCTLOutput(Request,SizeOf(Request))
      end
end;

procedure Reset;
var
  Request: TIOCTLRequest;
begin
  if NumberOfCDROMDrives > 0 then
    with Request do
      begin
        SubFn := 02;   (* IOCTL command code to reset the CD-ROM drive *)
        IOCTLOutput(Request,SizeOf(Request))
      end
end;

procedure Close;
var
  Request: TIOCTLRequest;
begin
  if NumberOfCDROMDrives > 0 then
    with Request do
      begin
        SubFn := 05;   (* IOCTL command code to close the CD-ROM drive *)
        IOCTLOutput(Request,SizeOf(Request))
      end
end;

{
{ This routine seems to require a CD in the drive. Otherwise it returns  }
{ CDROMError = 2 (on my machine anyway...)                               }
{                                                                        }
procedure Lock;
var
  Request: TCDROMLockRequest;
begin
  if NumberOfCDROMDrives > 0 then
    with Request do
      begin
        SubFn := 01;   (* ... locking the CDROM... *)
        LockStatus := CDROM_Locked;
        IOCTLOutput(Request,SizeOf(Request))
      end
end;

procedure Unlock;
var
  Request: TCDROMLockRequest;
begin
  if NumberOfCDROMDrives > 0 then
    with Request do
      begin
        SubFn := 01;   (* ... and unlocking ... *)
        LockStatus := CDROM_Unlocked;
        IOCTLOutput(Request,SizeOf(Request))
      end
end;

{                                                                       }
{ Store the Strategy and Interrupt Entry Points for the CD-ROM device   }
{ driver... Again, assume that there is only 1 CD-ROM drive, and so     }
{ SubUnit will always = 0                                               }
{                                                                       }
procedure SetUpEntryPoints;
var
  CDDriveList: array[TCDROMIndex] of TCDROMDriveEntry;
begin
  with Regs do
    begin
      ax := $1501;
      es := Seg(CDDriveList);
      bx := Ofs(CDDriveList);
      Intr($2f,Regs)
    end;
  with CDDriveList[1] do
    begin
      @CDROMDriverStrategyEntryPoint :=
                  Ptr(Seg(CDROMDriver^),CDROMDriver^.StrategyEntryPoint);
      @CDROMDriverINTEntryPoint :=
                  Ptr(Seg(CDROMDriver^),CDROMDriver^.INTEntryPoint)

    end
end;

{                                                                          }
{ Initialisation code: neither the number of CD-ROM drives nor their drive }
{ letters will change during execution, so we shall identify the drives    }
{ NOW and not do it again.                                                 }
{                                                                          }
begin
{                                         }
{ Get number of CD-ROMs in the system ... }
{                                         }
  with Regs do
    begin
      ax := $1500;
      bx := 0;
      Intr($2f,Regs);
      NumberOfCDROMDrives := bl;
{                                                                         }
{ Get the drive-letters for CD-ROMSs... There is an MSCDEX function to do }
{ this for v2.00+, viz AX=$150D INT $2F. However we are assuming only one }
{ CD-ROM and so the drive letter has already been determined.             }
{                                                                         }
      if NumberOfCDROMDrives > 0 then
        begin
          CDROMDriveLetter[1] := cl;
{                                                                 }
{ Determine the entry points for direct device-driver requests... }
{                                                                 }
          SetUpEntryPoints;
{                                                                 }
{ Determine which mechanism to use for CD-ROM driver requests ... }
{                                                                 }
          if GetCDROMVersion < 2*256 + 10 then
            DriverRequest := DriverBasicRequest
          else
            DriverRequest := DriverRequest_v210
        end
    end
end.

{ ----------------------   DEMO PROGRAM ----------------------- }

{$A+,B-,D+,E-,F-,G+,I+,L+,N-,O-,R-,S-,V-,X+}
{$M 16384,0,655360}
program CDDemo;
uses CDDrive;

var
  Version: word;
begin
  if GetNumberOfCDROMDrives = 0 then
    writeln( 'This machine has no CD-ROM drive.' )
  else
    begin
      Version := GetCDROMVersion;
      writeln( 'MSCDEX v', hi(Version), '.', lo(Version) );
      Lock;
      writeln( 'Lock: Error = ', CDROMError );
      Unlock;
      writeln( 'Unlock: Error = ', CDROMError );
      Eject;
      writeln( 'Eject: Error = ', CDROMError );
      Close;
      writeln( 'Close: Error = ', CDROMError );
      Reset;
      writeln( 'Reset: Error = ', CDROMError )
    end
end.