UNIT MVInt94;
{$I mvInt94.def}
{-------------------------------------------------------------------------}
{ Media Vision Pro Audio Spectrum routines for Borland Pascal v7.  This   }
{ is a simple little unit designed to access the Media Vision sound board }
{ through PCM.COM's INT 94h driver.  This software has been written from  }
{ documentation supplied in the PCM.ZIP file downloaded from the Media    }
{ Vision BBS, and a lot of answers from Bart Crane at Media Vision, who   }
{ wrote the PCM.COM driver in the first place.                            }
{                                                                         }
{ Copyright 1993, Peter W. Cervasio.                                      }
{ This software may be freely used, so long as my copyright is maintained }
{-------------------------------------------------------------------------}
{ Revision Data:                                                          }
{  1.0   02 Dec 93  pwc     Initial coding                                }
{  1.1   10 Dec 93  pwc     Added SetPCMInfo and .WAV header info         }
{-------------------------------------------------------------------------}
{ Written/Translated by Peter W. Cervasio                                 }
{ Contact me via the Media Vision BBS,  (Pete Cervasio)                   }
{   or via CompuServe: 73443,1426                                         }
{-------------------------------------------------------------------------}
INTERFACE
USES Dos;

CONST
    BoardAddress : Word = $0388;                  { default board address }

    ThunderBoard : Word = 0;         { ThunderBoard bit.  Set to $4000 if }
            { using the Thunderboard or Soundblaster portion of the card. }

  { we really don't use these next two, though they're here in case we do }
                                   { something with them at a later time. }
    IRQ          : Byte = $07;                        { default board irq }
    DMA          : Byte = $03;                { default board dma channel }

{ These are the bits returned by GetHWVersionBits.  To use them, AND the  }
{ value returned with the specified constant.  For example: to find out   }
{ if we have the MVA508 mixer or the National one, do the following:      }
{                                                                         }
{    HWStuff := GetHWVersionBits;                                         }
{    IF HWStuff and bMVA508 = bMVA508 then                                }
{        Writeln ('We have the MVA508 mixer')                             }
{    else                                                                 }
{        Writeln ('We have the National mixer')';                         }
{                                                                         }

    bMVA508     = $0001;  { MVA508(1) or National(0) mixer }
    bMVPS2      = $0002;  { PS2 bus stuff }
    bMVSLAVE    = $0004;  { CDPC Slave device is present }
    bMVSCSI     = $0008;  { SCSI interface }
    bMVENHSCSI  = $0010;  { Enhanced SCSI interface }
    bMVSONY     = $0020;  { Sony 535 interface }
    bMVDAC16    = $0040;  { 16 bit DAC }
    bMVSBEMUL   = $0080;  { SB h/w emulation }
    bMVMPUEMUL  = $0100;  { MPU h/w emulation }
    bMVOPL3     = $0200;  { OPL3(1) or 3812(0) }
    bMV101      = $0400;  { MV101 ASIC }
    bMV101_REV  = $7800;  { MV101 Revision }

TYPE

{-----------------------------------------------------------------------------}
{ The "WaveHeaderType" structure holds the format of a .WAV file's header.    }
{ This is for informational purposes only, really.  There aren't any routines }
{ in this unit to open .WAV files.  Some programs stick their own junk in the }
{ middle of a .WAV's header, too.  GoldWave is one.  You just have to open    }
{ file and search for the various header markers ('RIFF', 'fmt ', 'data',     }
{ etc.) before reading the data in.  I believe all the Media Vision recording }
{ utilities create a .WAV header in this format, though, so you should be     }
{ safe reading the beginning of those as a record.                            }
{-----------------------------------------------------------------------------}
WaveHeaderType = RECORD
    Riff        : Array[0..3] of char;    { 'RIFF' }
    RiffLength  : LongInt;
    Wave        : Array[0..3] of char;    { 'WAVE' }
    Fmt         : Array[0..3] of char;    { 'fmt ' }
    FmtLength   : LongInt;                { length of the info block (16)   }
    FormatTag   : Word;                   { format tag??                    }
    Channels    : Word;                   { 1 = mono, 2 = stereo            }
    SampleRate  : LongInt;                { samples per second              }
    SampPerSec  : LongInt;                { channels * sample rate          }
    BlockAlign  : Word;                   { block alignment (1=byte)        }
    SampleSize  : Word;                   { bits per sample (8/16)          }
    DataHeader  : Array[0..3] of char;    { 'data'                          }
    WaveLength  : LongInt;                { length of PCM data in the file  }
END;


{-------------------------------------------------------------------------}
{ The "MVState" structure holds the hardware state table returned by      }
{ InitMVSound().  I don't understand this.. I just translated information }
{ that was in a C header file.                                            }
{-------------------------------------------------------------------------}
MVState = RECORD
    SysSpkrTmr  : Byte;         {   42 System Speaker Timer Address  }
    SysTmrCllr  : Byte;         {   43 System Timer Control          }
    SysSpkrReg  : Byte;         {   61 System Speaker Register       }
    Joystick    : Byte;         {  201 Joystick Register             }
    LFMAddr     : Byte;         {  388 Left  FM Synth Address        }
    LFMData     : Byte;         {  389 Left  FM Synth Data           }
    RFMAddr     : Byte;         {  38A Right FM Synth Address        }
    RFMData     : Byte;         {  38B Right FM Synth Data           }
    DFMAddr     : Byte;         {  788 Dual  FM Synth Address        }
    DFMData     : Byte;         {  789 Dual  FM Synth Data           }
    RESRVD1     : Byte;         {      reserved                      }
    PAudioMixr  : Byte;         {  78B Paralllel Audio Mixer Control }
    AudioMixr   : Byte;         {  B88 Audio Mixer Control           }
    IntrCtlrSt  : Byte;         {  B89 Interrupt Status              }
    AudioFilt   : Byte;         {  B8A Audio Filter Control          }
    IntrCtlr    : Byte;         {  B8B Interrupt Control             }
    PcmData     : Byte;         {  F88 PCM Data I/O Register         }
    RESRVD2     : Byte;         {      reserved                      }
    CrossChannel: Byte;         {  F8A Cross Channel                 }
    RESRVD3     : Byte;         {      reserved                      }
    SampleRate  : Word;         { 1388 Sample Rate Timer             }
    SampleCnt   : Word;         { 1389 Sample Count Register         }
    SpkrTmr     : Word;         { 138A Shadow Speaker Timer Count    }
    TmrCtlr     : Byte;         { 138B Shadow Speaker Timer Control  }
    MdIrqVect   : Byte;         { 1788 MIDI IRQ Vector Register      }
    MdSysCtlr   : Byte;         { 1789 MIDI System Control Register  }
    MdSysStat   : Byte;         { 178A MIDI IRQ Status Register      }
    MdIrqClr    : Byte;         { 178B MIDI IRQ Clear Register       }
    MdGroup1    : Byte;         { 1B88 MIDI Group #1 Register        }
    MdGroup2    : Byte;         { 1B89 MIDI Group #2 Register        }
    MdGroup3    : Byte;         { 1B8A MIDI Group #3 Register        }
    MdGroup4    : Byte;         { 1B8B MIDI Group #4 Register        }
END;
MVStatePtr = ^MVState;

{-------------------------------------------------------------------------}
{ The PCMINFOTYPE record is used to set the sample rate, bit size and     }
{ the mono/stereo setting.  The sample rate can be up to 88200 for mono   }
{ and 44100 for stereo.  Bit size is either 8 or 16.  StereoFlag is set   }
{ to 0 for mono, or 1 for stereo.                                         }
{-------------------------------------------------------------------------}
PCMInfoType = RECORD                                { Passed to PCMInfo() }
    SampleRate : LongInt;          { 100 - 88200 (200 - 44100 for stereo) }
    StereoFlag : Integer;                          { 0 = mono, 1 = stereo }
                 { I added these two... don't know if they're right - pwc }
    UnKnown    : Integer;                                      { set to 0 }
    BitSize    : Integer;                       { 8 or 16 bits per sample }
end;

{-------------------------------------------------------------------------}
{ The DMABUFTYPE record is used to inform the TSR of the DMA buffer's     }
{ address, it's size in kilobytes, and the number of partitions it is     }
{ to be split into.  The DMA buffer must be contained completely within   }
{ a 64k block.  i.e.: it must not go from 2xxx:xxxx into 3xxx:xxxx.  You  }
{ WILL experience lockups if it does so.  There is a function in the      }
{ unit called AllocateDMABuffer that takes a word parameter telling how   }
{ many kb you want to allocate.  It will return a pointer to a block of   }
{ memory that will satisfy the conditions.  See the comments near the     }
{ function for more information.                                          }
{-------------------------------------------------------------------------}
DMABufType = RECORD                               { Passed to DMABuffer() }
    DmaBuffer   : Pointer;                        { address of DMA buffer }
    BufKBSize   : Word;                        { size of DMA buffer in KB }
    Partitions  : Word;                            { number of partitions }
end;

{-------------------------------------------------------------------------}
{ The FDMABUFTYPE record is used with the FindDMABuffer function.  The    }
{ DMABuf element should point to a block of memory that is at least twice }
{ as large as the DMA buffer you desire, and the BufKBSize element should }
{ be set to the size (in kilobytes) of the DMA buffer you desire.  The    }
{ FindDMABuffer function will return a valid DMA buffer address out of    }
{ the block of memory you passed to it.  This functionality is in the     }
{ Int $94 driver.                                                         }
{-------------------------------------------------------------------------}
FDMABufType = RECORD                          { Passed to FindDMABuffer() }
    DmaBuf      : Pointer;                  { address of memory allocated }
    BufKBSize   : Word;                            { size of memory in KB }
end;

{ This only sucks up six bytes from your Data Segment... pretty cheap, huh? }
VAR
    DMACounter : Word;   { Used by EzDMACounter - incremented every time the }
                                           { DMA Callback routine is called. }
    EZDMA      : Pointer;             { Pointer set to point to EZDMACounter }


{ ------- Functions and Procedures defined in the unit -------- }

FUNCTION InitMVSound: Pointer;
FUNCTION InitPCM: Integer;
FUNCTION SetPCMInfo(DataSize, MonoStereo: Word; SampleRate: LongInt): Integer;
FUNCTION PCMInfo(VAR P_Info: PCMInfoType): Integer;
FUNCTION UserFunc(VAR S): Pointer;
FUNCTION DMABuffer(VAR D_Info: DMABufType): Pointer;
FUNCTION PCMRecord: Integer;
FUNCTION PCMPlay: Integer;

FUNCTION GetHWVersionBits: Word;
FUNCTION FindDMABuffer(VAR S: FDmaBufType): Pointer;
FUNCTION GetDMAAddress: Pointer;
FUNCTION GetDMASize: LongInt;

PROCEDURE ResumePCM;
PROCEDURE PausePCM;
PROCEDURE StopPCM;
PROCEDURE RemovePCM;
PROCEDURE AllocateDMABuffer(VAR TheBuffer: Pointer; Size: Word);
PROCEDURE EzDMACallback;

IMPLEMENTATION

VAR
    Int94Ptr : Pointer;

{ In Turbo Pascal, we are free to modify all the microprocessor's registers }
{ with the exception of SS, DS, BP and SP.

{ This call is not really necessary, since it is done by the PCM.COM tsr upon  }
{ loading.  Returns the address of the hardware state table (MVState above) or }
{ a nil pointer if it fails                                                    }
FUNCTION InitMVSound: Pointer; assembler;
asm
    mov     si, BoardAddress
    or      si, ThunderBoard
    shl     si, 4
    int     $94
end;

{ Initialize PCM State Tables }
{ Returns 0 if failed, otherwise the library version }
FUNCTION InitPCM: Integer; assembler;
asm
    mov     ax, 1
    mov     si, BoardAddress
    or      si, ThunderBoard
    shl     si, 4
    or      si, ax
    int     $94
end;

{ Set sample rate, size, and mono/stereo }
{ Returns 0 if okay, -1 if Sample Rate outside valid range }
FUNCTION SetPCMInfo(DataSize, MonoStereo: Word; SampleRate: LongInt): Integer; assembler;
asm
    mov     ax, datasize
    push    ax
    mov     ax, 0
    push    ax
    mov     ax, monostereo
    push    ax
    mov     ax, word ptr samplerate[2]
    push    ax
    mov     ax, word ptr samplerate[0]
    push    ax
    mov     ax, 2
    mov     bx, ss
    mov     es, bx
    mov     bx, sp
    mov     si, BoardAddress
    or      si, ThunderBoard
    shl     si, 4
    or      si, ax
    int     $94
    pop     bx
    pop     bx
    pop     bx
    pop     bx
    pop     bx
end;

{ Same as SetPCMInfo, but using a record to hold the info. }
FUNCTION PCMInfo(VAR P_Info: PCMInfoType): Integer; assembler;
asm
    les     bx, p_info
    mov     ax, 2
    mov     si, BoardAddress
    or      si, ThunderBoard
    shl     si, 4
    or      si, ax
    int     $94
end;

{ Set DMA Buffer address, size, and partitions }
{ NOTE:  The DMA buffer _CANNOT_ cross a 64k boundary.  See the sample program }
{        for an easy way of getting it to start at the beginning of one }
{ Returns nil pointer if it failed }
FUNCTION DMABuffer(VAR D_Info: DMABufType): Pointer; assembler;
asm
    les     bx, d_info
    mov     ax, 3
    mov     si, BoardAddress
    or      si, ThunderBoard
    shl     si, 4
    or      si, ax
    int     $94
end;

{ Set the address of the callback procedure.  Note that the DS register will }
{ probably not be set to our data segment when it gets called.  Using the one }
{ in this unit, EzDMACallback, and the variable DMACounter is recommended. }
{ Returns nil pointer if it failed to set the address correctly }
FUNCTION UserFunc(VAR S): Pointer; assembler;
asm
    les     bx, S
    mov     ax, 4
    mov     si, BoardAddress
    or      si, ThunderBoard
    shl     si, 4
    or      si, ax
    int     $94
end;

{ Sets the PCM routines into the Play state.  Upon the next call to ResumePCM }
{ the digital audio will start.  This function returns 0 if successful        }
FUNCTION PCMPlay: Integer; assembler;
asm
    mov     ax, 5
    mov     si, BoardAddress
    or      si, ThunderBoard
    shl     si, 4
    or      si, ax
    int     $94
end;

{ Sets the PCM routines into the Record state.  On the next call to ResumePCM }
{ the recording will start.  This function returns 0 if successful            }
FUNCTION PCMRecord: Integer; assembler;
asm
    mov     ax, 6
    mov     si, BoardAddress
    or      si, ThunderBoard
    shl     si, 4
    or      si, ax
    int     $94
end;

{ This procedure sets the PCM routines into the pause state.  Use this before  }
{ calling PCMRecord or PCMPlay to avoid the DMA handler starting until you are }
{ ready for it.  Also used while recording/playing PCM audio to provide a      }
{ pause.                                                                       }
PROCEDURE PausePCM; assembler;
asm
    mov     ax, 7
    mov     si, BoardAddress
    or      si, ThunderBoard
    shl     si, 4
    or      si, ax
    int     $94
end;


{ This procedure starts the Play / Record of PCM audio data after a call to }
{ PCMPlay or PCMRecord, or it resumes the operation that was paused with a  }
{ call to PausePCM.                                                         }
PROCEDURE ResumePCM; assembler;
asm
    mov     ax, 8
    mov     si, BoardAddress
    or      si, ThunderBoard
    shl     si, 4
    or      si, ax
    int     $94
end;

{ This procedure stops the Play / Record of PCM data. }
PROCEDURE StopPCM; assembler;
asm
    mov     ax, 9
    mov     si, BoardAddress
    or      si, ThunderBoard
    shl     si, 4
    or      si, ax
    int     $94
end;

{ Remove the PCM hook to Int $94 and reset internal DMA variables }
PROCEDURE RemovePCM; assembler;
asm
    mov     ax, 10
    mov     si, BoardAddress
    or      si, ThunderBoard
    shl     si, 4
    or      si, ax
    int     $94
end;

{ find an area from memory within a 64k block }
{ returns nil if failed, or a valid DMA buffer address }
FUNCTION FindDMABuffer(VAR S: FDmaBufType): Pointer; assembler;
asm
    les     bx, S
    mov     ax, 11
    mov     si, BoardAddress
    or      si, ThunderBoard
    shl     si, 4
    or      si, ax
    int     $94
end;

{ This function doesn't always work, according to Bart Crane of Media Vision. }
{ It is supposed to return the address of the DMA buffer that was set using   }
{ DMABuffer().  Certain versions of PCM.COM have an invalid segment override  }
{ in them, though, and return the wrong information.  Since the DMABuffer()   }
{ routine is supposed to always work, you don't really need this.             }
FUNCTION GetDMAAddress: Pointer; assembler;
asm
    mov     ax, $8000
    mov     si, BoardAddress
    or      si, ThunderBoard
    shl     si, 4
    or      si, ax
    int     $94
end;

{ This function doesn't always work, according to Bart Crane of Media Vision. }
{ It is supposed to return the size and # of partitons of the DMA buffer that }
{ was set using DMABuffer().  See GetDMAAddress above for the reasons it may  }
{ not work.  The number of partitions is returned in the high word of the     }
{ long integer while the size of the buffer is returned in the low word.      }
FUNCTION GetDMASize: LongInt; assembler;
asm
    mov     ax, $8001
    mov     si, BoardAddress
    or      si, ThunderBoard
    shl     si, 4
    or      si, ax
    int     $94
end;

{ This function returns a word describing the hardware that is detected at }
{ the current board address.  The bit meanings are described at the top of }
{ the unit.                                                                }
FUNCTION GetHWVersionBits: Word; assembler;
asm
    mov     ax, 12
    mov     si, BoardAddress
    or      si, ThunderBoard
    shl     si, 4
    or      si, ax
    int     $94
end;

{ This is a quick and easy DMA callback routine that you can use.  It will  }
{ increment the word variable DMACounter once every time a DMA partition is }
{ used.  Your main program loop should monitor this variable after calling  }
{ ResumePCM to know when a DMA partition should be filled with new data.    }
{ See the TESTPCM.PAS program that came with this unit for an example of    }
{ what I'm talking about.                                                   }
PROCEDURE EzDMACallback; assembler;
asm
    push    ds
    mov     ax, SEG @data
    mov     ds, ax
    inc     DMACounter
    pop     ds
end;

{ Allocate memory for the DMA buffer.  This memory must _NOT_ cross a 64k  }
{ boundary.  We take care of that by finding out where the heap pointer is }
{ and how far it is to the next 64k boundary.  If there is enough space to }
{ allocate the DMA buffer without crossing the boundary, we go ahead and   }
{ put it where it will go.  Otherwise, we temporarily suck up enough heap  }
{ space to put the heap pointer on the next 64k boundary and allocate the  }
{ DMA buffer there, freeing the temporary space after we're done.  GetMem  }
{ is used for the memory allocation, so you need to call FreeMem with the  }
{ correct size if you want to free the DMA buffer for some reason.         }
PROCEDURE AllocateDMABuffer(VAR TheBuffer: Pointer; Size: Word);
VAR
    J, JJ : Pointer;
    K : Array[1..2] of Word absolute J;
    M : Word;
BEGIN
    { Take the parameter as either the number of K, or the actual size of   }
    { the requested buffer.  If it's 64 or less, assume it's in kb, if more }
    { then assume it's the number of bytes requested.                       }
    if Size <= 64 then Size := Size * 1024;
    J := HeapPtr;                      { Find out where our heap pointer is }
    M := $1000 - (K[2] AND $FFF);   { Take high word of it and find out how }
                                              { far it is to the next $x000 }
    if M = $1000 then M := 0;            { deal with it being there already }
    if Size < M * 16 then M := 0;        { We have room where we are if so. }
    if M > 0 then GetMem (JJ, M * 16);   { if it isn't, suck some memory up }
    GetMem (TheBuffer, Size);               { okay, allocate our DMA buffer }
    if M > 0 then FreeMem(JJ, M * 16);       { and free our temporary stuff }
END;


{ Unit initialization code.  This just checks to see that PCM.COM has been }
{ loaded into memory and halts with a message if not.  If you want to try  }
{ and automatically load it if it isn't found, then it is up to you to get }
{ it working... I think it would be okay to do a SwapVectors and exec PCM  }
{ because I don't think SwapVectors bothers with int $94.  I don't need it }
{ working that way, so I haven't bothered with it.                         }
BEGIN
    GetIntVec($94, Int94Ptr);
    if not assigned(Int94Ptr) then
    begin
         writeln ('ERROR: Sound driver not installed properly.');
         writeln ('Please load PCM.COM and re-run this program.');
         Halt(1);
    end;
    EZDma := @EZDMACallback;
END.
