UNIT CMFTool; {** Unit - uses SBFMDRV.COM **} INTERFACE USES Dos; TYPE CMFFileTyp = FILE; CMFDataTyp = Pointer; CMFHeader = RECORD CMFFileID : ARRAY[0..3] OF CHAR; CMFVersion : WORD; CMFInstrBlockOfs : WORD; CMFMusicBlockOfs : WORD; CMFTickPerBeat : WORD; CMFClockTicksPS : WORD; CMFFileTitleOfs : WORD; CMFComposerOfs : WORD; CMFMusicRemarkOfs : WORD; CMFChannelsUsed : ARRAY[0..15] OF CHAR; CMFInstrNumber : WORD; CMFBasicTempo : WORD; END; CONST CMFToolVersion = 'v1.0'; VAR CMFStatusByte : BYTE; CMFErrStat : WORD; CMFDriverInstalled : BOOLEAN; CMFDriverIRQ : WORD; CMFSongPaused : BOOLEAN; OldExitProc : Pointer; PROCEDURE PrintCMFErrMessage; FUNCTION CMFGetSongBuffer(VAR CMFBuffer : Pointer; CMFFile : STRING):BOOLEAN; FUNCTION CMFFreeSongBuffer (VAR CMFBuffer : Pointer):BOOLEAN; FUNCTION CMFInitDriver : BOOLEAN; FUNCTION CMFGetVersion : WORD; PROCEDURE CMFSetStatusByte; FUNCTION CMFSetInstruments(VAR CMFBuffer : Pointer):BOOLEAN; FUNCTION CMFSetSingleInstruments(VAR CMFInstrument:Pointer; No:WORD):BOOLEAN; PROCEDURE CMFSetSysClock(Frequency : WORD); PROCEDURE CMFSetDriverClock(Frequency : WORD); PROCEDURE CMFSetTransposeOfs (Offset : INTEGER); FUNCTION CMFPlaySong(VAR CMFBuffer : Pointer) : BOOLEAN; FUNCTION CMFStopSong : BOOLEAN; FUNCTION CMFResetDriver:BOOLEAN; FUNCTION CMFPauseSong : BOOLEAN; FUNCTION CMFContinueSong : BOOLEAN; IMPLEMENTATION TYPE TypeCastTyp = ARRAY [0..6000] of Char; VAR Regs : Registers; CMFIntern : ^CMFHeader; { Internal pointer to CMF structure } PROCEDURE PrintCMFErrMessage; { PURPOSE : Displays SB error as text; no change to error status. } BEGIN CASE CMFErrStat OF 100 : Write(' SBFMDRV sound driver not found '); 110 : Write(' Driver reset successful '); 200 : Write(' CMF file not found '); 210 : Write(' No memory free for CMF file '); 220 : Write(' File not in CMF format '); 300 : Write(' Memory allocation error occurred '); 400 : Write(' Too many instruments defined '); 500 : Write(' CMF data could not be played '); 510 : Write(' CMF data could not be stopped '); 520 : Write(' CMF data could not be paused '); 530 : Write(' CMF data could not be continued '); END; END; FUNCTION Exists (Filename : STRING):BOOLEAN; { PURPOSE : Checks for the existence of a file, and returns a Boolean exp. } VAR F : File; BEGIN Assign(F,Filename); {$I-} Reset(F); Close(F); {$I+} Exists := (IoResult = 0) AND (Filename <> ''); END; PROCEDURE AllocateMem (VAR Pt : Pointer; Size : LongInt); { Reserves as many bytes as Size allows, then sets the pointer in the Pt variable. If not enough memory is available, Pt is set to NIL. } VAR SizeIntern : WORD; BEGIN Inc(Size,15); SizeIntern := (Size shr 4); Regs.AH := $48; Regs.BX := SizeIntern; MsDos(Regs); IF (Regs.BX <> SizeIntern) THEN Pt := NIL ELSE Pt := Ptr(Regs.AX,0); END; FUNCTION CheckFreeMem (VAR CMFBuffer : Pointer; CMFSize : LongInt):BOOLEAN; { Ensures that enough memory has been allocated for CMF file. } BEGIN AllocateMem(CMFBuffer,CMFSize); CheckFreeMem := CMFBuffer <> NIL; END; FUNCTION CMFGetSongBuffer(VAR CMFBuffer : Pointer; CMFFile : STRING):BOOLEAN; { Loads file into memory; returns TRUE if load successful, FALSE if not. } CONST FileCheck : STRING[4] = 'CTMF'; VAR CMFFileSize : LongInt; FPresent : BOOLEAN; VFile : CMFFileTyp; Segs : WORD; Read : WORD; Checkcount : BYTE; BEGIN FPresent := Exists(CMFFile); { CMF file could not be found } IF Not(FPresent) THEN BEGIN CMFGetSongBuffer := FALSE; CMFErrStat := 200; EXIT END; Assign(VFile,CMFFile); Reset(VFile,1); CMFFileSize := Filesize(VFile); AllocateMem(CMFBuffer,CMFFileSize); { Insufficient memory for CMF file } IF (CMFBuffer = NIL) THEN BEGIN Close(VFile); CMFGetSongBuffer := FALSE; CMFErrStat := 210; EXIT; END; Segs := 0; REPEAT Blockread(VFile,Ptr(seg(CMFBuffer^)+4096*Segs,Ofs(CMFBuffer^))^,$FFFF,Read ); Inc(Segs); UNTIL Read = 0; Close(VFile); { File not in CMF format } CMFIntern := CMFBuffer; CheckCount := 1; REPEAT IF FileCheck[CheckCount] = CMFIntern^.CMFFileID[CheckCount-1] THEN Inc(CheckCount) ELSE CheckCount := $FF; UNTIL CheckCount >= 3; IF NOT(CheckCount = 3) THEN BEGIN CMFGetSongBuffer := FALSE; CMFErrStat := 220; EXIT; END; { Load was successful } CMFGetSongBuffer := TRUE; CMFErrStat := 0; END; FUNCTION CMFFreeSongBuffer (VAR CMFBuffer : Pointer):BOOLEAN; { Frees memory allocated for CMF file. } BEGIN Regs.AH := $49; Regs.ES := seg(CMFBuffer^); MsDos(Regs); CMFFreeSongBuffer := TRUE; IF (Regs.AX = 7) OR (Regs.AX = 9) THEN BEGIN CMFFreeSongBuffer := FALSE; CMFErrStat := 300 END; END; FUNCTION CMFInitDriver : BOOLEAN; { Checks for SBFMDRV.COM resident in memory, and resets driver } CONST DriverCheck :STRING[5] = 'FMDRV'; VAR ScanIRQ, CheckCount : BYTE; IRQPtr, DummyPtr : Pointer; BEGIN { Possible SBFMDRV interrupts lie in range $80 - $BF } FOR ScanIRQ := $80 TO $BF DO BEGIN GetIntVec(ScanIRQ, IRQPtr); DummyPtr := Ptr(Seg(IRQPtr^), $102); { Check for string 'FMDRV' in interrupt program. } CheckCount := 1; REPEAT IF DriverCheck[CheckCount] = TypeCastTyp(DummyPtr^)[CheckCount] THEN Inc(CheckCount) ELSE CheckCount := $FF; UNTIL CheckCount >= 5; IF (CheckCount = 5) THEN BEGIN { String found; reset executed } Regs.BX := 08; CMFDriverIRQ := ScanIRQ; Intr(CMFDriverIRQ, Regs); IF Regs.AX = 0 THEN CMFInitDriver := TRUE ELSE BEGIN CMFInitDriver := FALSE; CMFErrStat := 110; END; Exit; END ELSE BEGIN { String not found } CMFInitDriver := FALSE; CMFErrStat := 100; END; END; END; FUNCTION CMFGetVersion : WORD; { Gets version number from SBFMDRV driver. } BEGIN Regs.BX := 0; Intr(CMFDriverIRQ,Regs); CMFGetVersion := Regs.AX; END; PROCEDURE CMFSetStatusByte; { Place driver status byte in CMFStatusByte variable. } BEGIN Regs.BX:= 1; Regs.DX:= Seg(CMFStatusByte); Regs.AX:= Ofs(CMFStatusByte); Intr(CMFDriverIRQ, Regs); END; FUNCTION CMFSetInstruments(VAR CMFBuffer : Pointer):BOOLEAN; { Sets SB card FM registers to instrumentation stated in CMF file. } BEGIN CMFIntern := CMFBuffer; IF CMFIntern^.CMFInstrNumber > 128 THEN BEGIN CMFErrStat := 400; CMFSetInstruments := FALSE; Exit; END; Regs.BX := 02; Regs.CX := CMFIntern^.CMFInstrNumber; Regs.DX := Seg(CMFBuffer^); Regs.AX := Ofs(CMFBuffer^)+CMFIntern^.CMFInstrBlockOfs; Intr(CMFDriverIRQ, Regs); CMFSetInstruments := TRUE; END; FUNCTION CMFSetSingleInstruments(VAR CMFInstrument:Pointer; No:WORD):BOOLEAN; { Sets SB FM registers to instrument values corresponding to the data structure following the CMFInstrument pointer. } BEGIN IF No > 128 THEN BEGIN CMFErrStat := 400; CMFSetSingleInstruments := FALSE; Exit; END; Regs.BX := 02; Regs.CX := No; Regs.DX := Seg(CMFInstrument^); Regs.AX := Ofs(CMFInstrument^); Intr(CMFDriverIRQ, Regs); CMFSetSingleInstruments := TRUE; END; PROCEDURE CMFSetSysClock(Frequency : WORD); { Sets default value of timer 0 to new value. } BEGIN Regs.BX := 03; Regs.AX := (1193180 DIV Frequency); Intr(CMFDriverIRQ, Regs); END; PROCEDURE CMFSetDriverClock(Frequency : WORD); { Sets driver timer frequency to new value. } BEGIN Regs.BX := 04; Regs.AX := (1193180 DIV Frequency); Intr(CMFDriverIRQ, Regs); END; PROCEDURE CMFSetTransposeOfs (Offset : INTEGER); { Transposes all notes in the CMF file by "Offset." } BEGIN Regs.BX := 05; Regs.AX := Offset; Intr(CMFDriverIRQ, Regs); END; FUNCTION CMFPlaySong(VAR CMFBuffer : Pointer) : BOOLEAN; { Initializes all important parameters and starts song playback. } VAR Check : BOOLEAN; BEGIN CMFIntern := CMFBuffer; { Set driver clock frequency } CMFSetDriverClock(CMFIntern^.CMFClockTicksPS); { Set instruments } Check := CMFSetInstruments(CMFBuffer); IF Not(Check) THEN Exit; Regs.BX := 06; Regs.DX := Seg(CMFIntern^); Regs.AX := Ofs(CMFIntern^)+CMFIntern^.CMFMusicBlockOfs; Intr(CMFDriverIRQ, Regs); IF Regs.AX = 0 THEN BEGIN CMFPlaySong := TRUE; CMFSongPaused := FALSE; END ELSE BEGIN CMFPlaySong := FALSE; CMFErrStat := 500; END; END; FUNCTION CMFStopSong : BOOLEAN; { Attempts to stop song playback. } BEGIN Regs.BX := 07; Intr(CMFDriverIRQ, Regs); IF Regs.AX = 0 THEN CMFStopSong := TRUE ELSE BEGIN CMFStopSong := FALSE; CMFErrStat := 510; END; END; FUNCTION CMFResetDriver:BOOLEAN; { Resets driver to starting status. } BEGIN Regs.BX := 08; Intr(CMFDriverIRQ, Regs); IF Regs.AX = 0 THEN CMFResetDriver := TRUE ELSE BEGIN CMFResetDriver := FALSE; CMFErrStat := 110; END; END; FUNCTION CMFPauseSong : BOOLEAN; { Attempts to pause song playback. If pause is possible, this function sets the CMFSongPaused variable to TRUE. } BEGIN Regs.BX := 09; Intr(CMFDriverIRQ, Regs); IF Regs.AX = 0 THEN BEGIN CMFPauseSong := TRUE; CMFSongPaused := TRUE; END ELSE BEGIN CMFPauseSong := FALSE; CMFErrStat := 520; END; END; FUNCTION CMFContinueSong : BOOLEAN; { Attempts to continue playback of a paused song. If continuation is possible, this function sets CMFSongPaused to FALSE. } BEGIN Regs.BX := 10; Intr(CMFDriverIRQ, Regs); IF Regs.AX = 0 THEN BEGIN CMFContinueSong := TRUE; CMFSongPaused := FALSE; END ELSE BEGIN CMFContinueSong := FALSE; CMFErrStat := 530; END; END; {$F+} PROCEDURE CMFToolsExitProc; {$F-} { Resets the status byte address, allowing this program to exit.} BEGIN Regs.BX:= 1; Regs.DX:= 0; Regs.AX:= 0; Intr(CMFDriverIRQ, Regs); ExitProc := OldExitProc; END; BEGIN { Reset old ExitProc to the Tool unit proc } OldExitProc := ExitProc; ExitProc := @CMFToolsExitProc; { Initialize variables } CMFErrStat := 0; CMFSongPaused := FALSE; { Initialize driver } CMFDriverInstalled := CMFInitDriver; IF CMFDriverInstalled THEN BEGIN CMFStatusByte := 0; CMFSetStatusByte; END; END. { --------------------- DEMO PROGRAM ----------------- } Program CMFDemo; {* Demo program for CMFTOOL unit *} {$M 16384,0,65535} Uses CMFTool,Crt; VAR Check : BOOLEAN; SongName : String; SongBuffer : CMFDataTyp; PROCEDURE TextNumError; {* INPUT : None; data comes from CMFErrStat global variable * OUTPUT : None * PURPOSE : Displays SB error as text, including error number. } BEGIN Write(' Error #',CMFErrStat:3,': '); PrintCMFErrMessage; WriteLn; Halt(CMFErrStat); END; BEGIN ClrScr; { Displays error if SBFMDRV driver has not been installed } IF Not (CMFDriverInstalled) THEN TextNumError; { If no song name is included with command line parameters, program searches for the default name (here STARFM.CMF). } IF ParamCount = 0 THEN SongName := 'STARFM.CMF' ELSE SongName := ParamStr(1); { Display driver's version and subversion numbers } GotoXY(28,5); Write ('SBFMDRV Version ',Hi(CMFGetVersion):2,'.'); WriteLn(Lo(CMFGetVersion):2,' loaded'); { Display interrupt number in use } GotoXY(24,10); Write ('System interrupt (IRQ) '); WriteLn(CMFDriverIRQ:3,' in use'); GotoXY(35,15); WriteLn('Song Status'); GotoXY(31,23); WriteLn('Song name: ',SongName); { Load song file } Check := CMFGetSongBuffer(SongBuffer,SongName); IF NOT(Check) THEN TextNumError; { CMFSetTransposeOfs() controls transposition down or up of the loaded song (positive values transpose up, negative values transpose down). The value 0 plays the loaded song in its original key. } CMFSetTransposeOfs(0); { Experiment with this value } { Play song } Check := CMFPlaySong(SongBuffer); IF NOT(Check) THEN TextNumError; { During playback, display status byte } REPEAT GotoXY(41,17);Write(CMFStatusByte:3); UNTIL (KeyPressed OR (CMFStatusByte = 0)); { Stop playback if user presses a key } IF KeyPressed THEN BEGIN Check := CMFStopSong; IF NOT(Check) THEN TextNumError; END; { Re-initialize driver } Check := CMFResetDriver; IF NOT(Check) THEN TextNumError; { Free song file memory } Check := CMFFreeSongBuffer(SongBuffer); IF NOT(Check) THEN TextNumError; END.