'
' NeoLib - Cdrom Module
'
' Features:
'  14 (17) subs
'  10 functions
' For a total of: 24 (27) routines
'
' Specially designed and coded for AAP's QBCPC
' Official Library of the QuickBASIC Caliber Programming Compo (Summer & Autumn 2003)
'
' Part is sample code by Plasma, adapted and recoded by Neo Deus Ex Machina
' This recodation was specifically about making it understandable and
' able to use more CD-ROM drives (Plasma's code only used 1).
' Special thanks and credits to Plasma (Tha Pirate)
'

DECLARE FUNCTION neoCdromInit% ()
DECLARE FUNCTION neoCdromGetDriveLetters$ ()
DECLARE FUNCTION neoCdromDriveExist% (DriveLetter AS STRING)
DECLARE FUNCTION neoCdromGetVersion$ ()
DECLARE FUNCTION neoCdromChanged% (DriveLetter AS STRING)
DECLARE FUNCTION neoCdromIsError% ()
DECLARE FUNCTION neoCdromGetErrorMsg$ ()
DECLARE FUNCTION neoCdromSize& (DriveLetter AS STRING)
DECLARE FUNCTION neoCdromRB2HSG& (Minutes AS INTEGER, Seconds AS INTEGER, Frames AS INTEGER)
DECLARE FUNCTION neoCdromHeadPosition& (DriveLetter AS STRING)

DECLARE SUB neoCdromHSG2RB (Sectors AS LONG, Minutes AS INTEGER, Seconds AS INTEGER, Frames AS INTEGER)
DECLARE SUB neoCdromRead (DriveLetter AS STRING, StartSector AS LONG, NumSectors AS INTEGER, Buffer AS STRING)
DECLARE SUB neoCdromWrite (DriveLetter AS STRING, StartSector AS LONG, NumSectors AS INTEGER, Buffer AS STRING)
DECLARE SUB neoCdromClose (DriveLetter AS STRING)
DECLARE SUB neoCdromOpen (DriveLetter AS STRING)
DECLARE SUB neoCdromLock (DriveLetter AS STRING)
DECLARE SUB neoCdromUnlock (DriveLetter AS STRING)
DECLARE SUB neoCdromGetInfo (DriveLetter AS STRING, BitFlags AS LONG)
DECLARE SUB neoCdromDiskInfo (DriveLetter AS STRING, FirstTrack AS INTEGER, LastTrack AS INTEGER)
DECLARE SUB neoCdromTrackInfo (DriveLetter AS STRING, TrackNumber AS INTEGER, StartSector AS LONG, TrackType AS INTEGER)
DECLARE SUB neoCdromPlay (DriveLetter AS STRING, TrackNumber AS INTEGER, StartSector AS LONG, NumSectors AS LONG)
DECLARE SUB neoCdromResume (DriveLetter AS STRING)
DECLARE SUB neoCdromStop (DriveLetter AS STRING)
DECLARE SUB neoCdromPlayRaw (DriveLetter AS STRING, StartSector AS LONG, NumSectors AS LONG)

'internal routines (undocumented!!!)
DECLARE SUB neoCdromDoCall (Code AS INTEGER, Info AS STRING, DriveLetter AS STRING)
DECLARE SUB neoCdromOutIOCTL (DataString AS STRING, DriveLetter AS STRING)
DECLARE SUB neoCdromInIOCTL (DataString AS STRING, DriveLetter AS STRING)

DEFINT A-Z
'$INCLUDE: 'QB.BI'

CONST DAT = 0
CONST CDA = -1

DIM SHARED Regs AS RegType
DIM SHARED RegsX AS RegTypeX
DIM SHARED CdromError AS INTEGER

'/////////////////////////////////////////////////////////////////////////////
' FUNCTIONS
'/////////////////////////////////////////////////////////////////////////////
FUNCTION neoCdromGetDriveLetters$
	'returns all drive letters hooked to a cdrom drive

        'use int 2Fh, ax = 150Dh
        RegsX.ax = &H150D
        thebuffer$ =  SPACE$(26)
        RegsX.es = VARSEG(thebuffer$)
        RegsX.bx = SADD(thebuffer$)
        INTERRUPTX &H2F, RegsX, RegsX

        nCGDL$ = RTRIM$(thebuffer$)
        IF nCGDL$ = "" THEN neoCdromGetDriveLetters$ = "": EXIT FUNCTION
        FOR I = 1 TO LEN(nCGDL$)
                MID$(nCGDL$, I, 1) = CHR$(ASC("A") + ASC(MID$(nCGDL$, I, 1)))
        NEXT I
        neoCdromGetDriveLetters$ = nCGDL$
END FUNCTION

FUNCTION neoCdromDriveExist (DriveLetter AS STRING)
	'returns whether the specified drive is a cdrom drive and is usable under dos (0 or -1)
	' DriveLetter = a 1-byte drive letter (A-Z)
	'NOTE: don't do something like A: or such! Just 'A'

        DriveLetter = UCASE$(LEFT$(DriveLetter, 1))

	'use int 2Fh, ax = 150Bh
        Regs.ax = &H150B
        Regs.cx = ASC(DriveLetter) - ASC("A")
        INTERRUPT &H2F, Regs, Regs

        'now bx contains ADADh if MSCDEX is installed
        IF Regs.bx <> &HADAD THEN neoCdromDriveExist = 0: EXIT FUNCTION

        'now ax contains the support status code (0000h = not supported)
        IF Regs.ax = 0 THEN neoCdromDriveExist = 0: EXIT FUNCTION

        neoCdromDriveExist = -1
END FUNCTION

FUNCTION neoCdromGetVersion$
	'returns the version of mscdex

	'use int 2Fh, ax = 150Ch
	Regs.ax = &H150C
	Regs.bx = 0
        INTERRUPT &H2F, Regs, Regs

        'now version is in bh and bl
        bh = (Regs.bx AND &HFF00&) \ &H100
        bl = (Regs.bx AND &HFF)
        neoCdromGetVersion$ = LTRIM$(STR$(bh)) + "." + LTRIM$(STR$(bl))
END FUNCTION

FUNCTION neoCdromChanged (DriveLetter AS STRING)
	'checks if a CD was changed since the last operations
	'- DriveLetter: drive to check switch on
	'returns: 0: CD not changed, -1: CD changed

	'Media Changed function
	Dta$ = CHR$(9) + SPACE$(1)

	'input from IOCTL
	neoCdromInIOCTL Dta$, DriveLetter

	'check the output buffer
	IF ASC(MID$(Dta$, 2, 1)) = 1 THEN neoCdromChanged = 0 ELSE neoCdromChanged = -1
END FUNCTION

FUNCTION neoCdromIsError
	'returns 0 or -1 whether an error had occured
	IF CdromError > 0 THEN neoCdromIsError = -1 ELSE neoCdromIsError = 0
END FUNCTION

FUNCTION neoCdromGetErrorMsg$
	'returns the error, if one
	SELECT CASE CdRomError
		CASE 0: msg$ = "No Error"
		CASE 1: msg$ = "Write protect violation"
		CASE 2: msg$ = "Unknown unit"
		CASE 3: msg$ = "Drive not ready"
		CASE 4: msg$ = "Unknown command"
		CASE 5: msg$ = "CRC error"
		CASE 6: msg$ = "Bad request structure length"
		CASE 7: msg$ = "Seek error"
		CASE 8: msg$ = "Unknown media"
		CASE 9: msg$ = "Sector not found"
		CASE 10: msg$ = "Undefined error"
		CASE 11: msg$ = "Write fault"
		CASE 12: msg$ = "Read fault"
		CASE 13: msg$ = "General failure"
		CASE 14: msg$ = "Undefined error"
		CASE 15: msg$ = "Undefined error"
		CASE 16: msg$ = "The requested track is invalid"
		CASE 17: msg$ = "The requested track is not audio"
		CASE ELSE: msg$ = "No or undefined error"
	END SELECT
	neoCdromGetErrorMsg$ = msg$
END FUNCTION

FUNCTION neoCdromInit
	'initializes all cdrom drives, and returning values
	'whenever an error occured
	'Returns: 0: Cdroms initialized successfully
	'        -1: MSCDEX not installed
	'        -2: No Cdrom drives exist
	'        -3: No Cdrom drives supported by MSCDEX

	'set flag off
	CdromError = 0

	'interrupt 2Fh, ax=1500
	RegsX.ax = &H1500
  	INTERRUPTX &H2F, RegsX, RegsX

  	NumberOfDrives% = RegsX.bx
  	'check if no cdroms are installed
	IF NumberOfDrives% = 0 THEN neoCdromInit = -2: EXIT FUNCTION

  	'get all drive letters
  	DriveLetters$ = neoCdromGetDriveLetters$

	supported% = 0
	FOR I = 1 TO NumberOfDrives%

		Letter$ = MID$(DriveLetters$, I, 1)

		'perform a disk check
		RegsX.ax = &H150B
		RegsX.cx = ASC(Letter$) - ASC("A")
                INTERRUPTX &H2F, RegsX, RegsX

		'check if MSCDEX is installed
		IF RegsX.bx <> &HADAD THEN neoCdromInit = -1: EXIT FUNCTION

		'check if the drive is supported by MSCDEX
		IF RegsX.ax <> 0 THEN supported% = supported% + 1
	NEXT I

	'test if there are drives which are supported
	IF supported% = 0 THEN neoCdromInit = -3: EXIT FUNCTION

  	neoCdromInit = 0
END FUNCTION

FUNCTION neoCdromSize& (DriveLetter AS STRING)
	'returns the total used size on a cdrom
	'- DriveLetter: letter of the drive to check size of

	'Return volume size
	Dta$ = CHR$(8) + SPACE$(4)

	'call IOCTL input
	neoCdromInIOCTL Dta$, DriveLetter

	'get data
	neoCdromSize& = CVL(MID$(Dta$, 2, 4))
END FUNCTION

FUNCTION neoCdromRB2HSG& (Minutes AS INTEGER, Seconds AS INTEGER, Frames AS INTEGER)
	'converts RedBook addresses to HSG addresses
	'- Min: red book minutes
	'- Sec: red book seconds
	'- Frame: red book frames (1/75 sec)
	'Returns: HSG address equivalent with the RedBook address
	'(HSG address = number of frames in total)
	neoCdromRB2HSG& = Minutes * 4500& + Seconds * 75 + Frames
END FUNCTION

FUNCTION neoCdromHeadPosition& (DriveLetter AS STRING)
	'returns the sector which the drive head is at (HSG address)

	'Head location
	Dta$ = CHR$(1) + CHR$(0) + SPACE$(4)

	'make call
	neoCdromInIOCTL Dta$, DriveLetter

	'get data
	neoCdromHeadPosition& = CVL(MID$(Dta$, 3, 4))
END FUNCTION


'/////////////////////////////////////////////////////////////////////////////
' SUBS
'/////////////////////////////////////////////////////////////////////////////
SUB neoCdromInIOCTL (DataString AS STRING, DriveLetter AS STRING)
	'inputs from the IOCTL
	'- DataString: buffer storage for return values

	nfo$ = CHR$(0)
	nfo$ = nfo$ + MKI$(SADD(DataString))
	nfo$ = nfo$ + MKI$(VARSEG(DataString))
	nfo$ = nfo$ + MKI$(LEN(DataString))
	nfo$ = nfo$ + MKI$(0)
	nfo$ = nfo$ + MKL$(0)

	neoCdromDoCall 3, nfo$, DriveLetter
END SUB

SUB neoCdromOutIOCTL (DataString AS STRING, DriveLetter AS STRING)
	'outputs to the IOCTL
	' - DataString: databuffer for IOCTL

	nfo$ = CHR$(0)
	nfo$ = nfo$ + MKI$(SADD(DataString))
	nfo$ = nfo$ + MKI$(VARSEG(DataString))
	nfo$ = nfo$ + MKI$(LEN(DataString))
	nfo$ = nfo$ + MKI$(0)
	nfo$ = nfo$ + MKL$(0)

	neoCdromDoCall 12, nfo$, DriveLetter
END SUB

SUB neoCdromRead (DriveLetter AS STRING, StartSector AS LONG, NumSectors AS INTEGER, Buffer AS STRING)
        'reads the specified amount of sectors from the cdrom
        ' DriveLetter = drive letter to read from
        ' StartSector = the starting sector to begin reading
        ' NumSectors = the amount of sectors to read (each sector = 2336 bytes)
        ' Buffer = the buffer storage for read sectors
        'SOMEHOW DOESN'T WORK ON SOME OSes

        DriveLetter = UCASE$(LEFT$(DriveLetter, 1))
        Buffer = SPACE$(1168 * NumSectors)

	'use int 2Fh, ax = 1508h
        RegsX.ax = &H1508
        RegsX.es = VARSEG(Buffer)
        RegsX.bx = SADD(Buffer)
        RegsX.cx = ASC(DriveLetter) - ASC("A")
        RegsX.si = VARSEG(StartSector)
        RegsX.di = VARPTR(StartSector)
        RegsX.dx = NumSectors
        INTERRUPTX &H2F, RegsX, RegsX

        'PRINT HEX$(RegsX.ax AND &HFF)
END SUB

SUB neoCdromWrite (DriveLetter AS STRING, StartSector AS LONG, NumSectors AS INTEGER, Buffer AS STRING)
	'writes a specified amount of sectors to cdr or cdrw
	' DriveLetter = the drive letter to write to
	' StartSector = the startsector to write to
	' NumSectors = the number of sectors to write
	' Buffer() = the buffer containing data
	'SOMEHOW DOESN'T WORK ON SOME OSes

        DriveLetter = UCASE$(LEFT$(DriveLetter, 1))

        'use int 2Fh, ax = 1509h
        RegsX.ax = &H1509
        RegsX.es = VARSEG(Buffer)
        RegsX.bx = SADD(Buffer)
        RegsX.cx = ASC(DriveLetter) - ASC("A")
        RegsX.si = VARSEG(StartSector)
        RegsX.di = VARPTR(StartSector)
        RegsX.dx = NumSectors
        INTERRUPTX &H2F, RegsX, RegsX
END SUB

SUB neoCdromDoCall (Code AS INTEGER, Info AS STRING, DriveLetter AS STRING)
	'sends a driver request to the MSCDEX driver
	'- Code: command to send
	'- Info: return value buffer

	Buffer$ = CHR$(13) + CHR$(0) + CHR$(Code)
	Buffer$ = Buffer$ + SPACE$(10) + Info

	RegsX.ax = &H1510
	RegsX.bx = SADD(Buffer$)
	RegsX.cx = ASC(DriveLetter) - ASC("A")
	RegsX.es = VARSEG(Buffer$)

	INTERRUPTX &H2F, RegsX, RegsX

	CdromError = CVI(MID$(Buffer$, 4, 2)) AND &HFF
END SUB

SUB neoCdromClose (DriveLetter AS STRING)
	'closes a cdrom drive
	'- DriveLetter: letter of the drive to close

	'Close tray
	Dta$ = CHR$(5)
	neoCdromOutIOCTL Dta$, DriveLetter
END SUB

SUB neoCdromOpen (DriveLetter AS STRING)
	'opens a cdrom drive
	'- DriveLetter: letter of the drive to open

	'Open tray
	Dta$ = CHR$(0)
	neoCdromOutIOCTL Dta$, DriveLetter
END SUB

SUB neoCdromLock (DriveLetter AS STRING)
	'locks a cdrom drive (tray can't come out)
	'unlock with neoCdromUnlock before trying to open
	'- DriveLetter: letter of the drive to lock

	'Lock door
	Dta$ = CHR$(1) + CHR$(1)
	neoCdromOutIOCTL Dta$, DriveLetter
END SUB

SUB neoCdromUnlock (DriveLetter AS STRING)
	'unlocks a cdrom drive
	'- DriveLetter: letter of the drive to unlock

	'Unlock door
	Dta$ = CHR$(1) + CHR$(0)
	neoCdromOutIOCTL Dta$, DriveLetter
END SUB

SUB neoCdromGetInfo (DriveLetter AS STRING, BitFlags AS LONG)
	'gets information about the requested cdrom drive
	'- DriveLetter: letter of the drive to request info from
	'- BitFlags: bit flags of all attributes:
	'  = OpenFlag (bit 0): flag whether the drive is open or not
	'  = LockedFlag (bit 1): flag whether the drive is locked or not
	'  = CookedRawFlag (bit 2): whether the drive supports cooked and/or raw reading
	'  = ReadWriteFlag (bit 3): whether the drive has read and write capabilities
	'  = DatAVFlag (bit 4): whether the drive can play audio tracks as well
	'  = InterleavingFlag (bit 5): whether the drive supports interleaving
	'  = PrefetchingFlag (bit 7): whether the drive supports prefetching
	'  = AudioManipFlag (bit 8): whether the drive supports audio channel manipulation
	'  = HSGRBAddressingFlag (bit 9): whether the drive supports HSG and/or RB addressing

 	'Device status
 	Dta$ = CHR$(6) + SPACE$(4)
	neoCdromInIOCTL Dta$, DriveLetter

	BitFlags = CVL(MID$(Dta$, 2, 4))
END SUB

SUB neoCdromDiskInfo (DriveLetter AS STRING, FirstTrack AS INTEGER, LastTrack AS INTEGER)
	'gets the first and last track of a cdrom in drive
	'- DriveLetter: letter of the drive to get disk info from
	'- FirstTrack: variable to store first valid track in
	'- LastTrack: variable to store last valid track in

	'Disk info
	Dta$ = CHR$(10) + SPACE$(6)

	neoCdromInIOCTL Dta$, DriveLetter

	'get data from output buffer
	FirstTrack = ASC(MID$(Dta$, 2, 1))
	LastTrack = ASC(MID$(Dta$, 3, 1))
END SUB

SUB neoCdromTrackInfo (DriveLetter AS STRING, TrackNumber AS INTEGER, StartSector AS LONG, TrackType AS INTEGER)
	'returns information about a track on a cd in a drive
	'- DriveLetter: drive in which the requested cd is in
	'- TrackNumber: number of the track to get info about
	'- StartSector: starting sector of the track (return value)
	'- TrackType: returns the type of the track (data or cda audio, see DAT and CDA constants)

	'Track info
  	Dta$ = CHR$(11)
  	'track number
	Dta$ = Dta$ + CHR$(TrackNumber) + SPACE$(5)

	neoCdromInIOCTL Dta$, DriveLetter

	'get data
	Frame = ASC(MID$(Dta$, 3, 1))
	Sec = ASC(MID$(Dta$, 4, 1))
	Min = ASC(MID$(Dta$, 5, 1))
	StartSector = neoCdromRB2HSG&(Min, Sec, Frame) - 150

	'bit 6 is the data/audio track flag
	IF ASC(MID$(Dta$, 7, 1)) AND &H40 > 0 THEN TrackType = DAT ELSE TrackType = CDA
END SUB

SUB neoCdromHSG2RB (Sectors AS LONG, Minutes AS INTEGER, Seconds AS INTEGER, Frames AS INTEGER)
	'converts HSG addresses to RedBook addresses
	'- Sectors: the HSG address, in sectors
	'- Min: variable where amount of minutes is stored in
	'- Sec: variable where amount of seconds is stored in
	'- Frame: variable where amount of frames is stored in (1 frame = 1/75 second)
	Minutes = Sectors \ 4500
	Seconds = (Sectors MOD 4500) \ 75
	Frames = Sectors MOD 75
END SUB

SUB neoCdromPlay (DriveLetter AS STRING, TrackNumber AS INTEGER, StartSector AS LONG, NumSectors AS LONG)
	'plays a cd audio track
	'- DriveLetter: drive to play audio from
	'- TrackNumber: number of the track to play (must be valid)
	'- StartSector: returns the start sector of the track (HSG)
	'- NumSectors: returns the amount of sectors the track consists of (HSG)

	'first get the requested track info
	neoCdromDiskInfo DriveLetter, FirstTrack%, LastTrack%

	'check if the track number is valid
	IF TrackNumber < FirstTrack% OR TrackNumber > LastTrack% THEN CdromError = 16: EXIT SUB

	neoCdromTrackInfo DriveLetter, TrackNumber, StartSector, TrackType%
	StartSector = StartSector + 150

	'the track must be audio
	IF NOT TrackType% THEN CdromError = 17: EXIT SUB

	'gets the ending sector of the audio
	IF TrackNumber < LastTrack% THEN
		neoCdromTrackInfo DriveLetter, TrackNumber + 1, EndSector&, BLA%
	ELSE
		EndSector& = neoCdromSize&(DriveLetter)
	END IF
	NumSectors = EndSector& - StartSector

	'prepare call
	nfo$ = CHR$(0) + MKL$(StartSector) + MKL$(NumSectors)

	'make call
	neoCdromDoCall 132, nfo$, DriveLetter
END SUB

SUB neoCdromResume (DriveLetter AS STRING)
	'resumes playing of an audio cd track
	'- DriveLetter: letter of the drive to resume

	'make a call
	neoCdromDoCall 136, "", DriveLetter
END SUB

SUB neoCdromStop (DriveLetter AS STRING)
	'stops the playing of an audio cd track
	'- DriveLetter: letter of the drive to stop

	'make a call
	neoCdromDoCall 133, "", DriveLetter
END SUB

SUB neoCdromPlayRaw (DriveLetter AS STRING, StartSector AS LONG, NumSectors AS LONG)
	'plays a part of a cd audio track
	'- DriveLetter: the letter of the drive to play from
	'- StartSector: sector to start playing from (HSG)
	'- NumSectors: sectors to play (HSG)

	'prepare call
	nfo$ = CHR$(0) + MKL$(StartSector) + MKL$(NumSectors)

	'call
	neoCdromDoCall 132, nfo$, DriveLetter
END SUB
