/*
 *
 * Copyright (c) 1992-1995 Microsoft Corporation
 *
 */

/*
 * midi.c
 *
 * Midi FM Synthesis routines. converts midi messages into calls to
 * FM Synthesis functions  - currently supports base adlib (in adlib.c)
 * and opl3 synthesisers (in opl3.c).
 *
 * 15 Dec 92 Geraint Davies - based on a combination of the adlib
 *                            and WSS midi drivers.
 */


#include <windows.h>
#include <mmsystem.h>

#include "mmddk.h"
#include "driver.h"

#include "adlib.h"
#include "opl3.h"


/***********************************************************
global memory */


PORTALLOC gMidiInClient;     // input client information structure
DWORD dwRefTime;             // time when midi input was opened

DWORD dwMsgTime;             // timestamp (in ms) of current msg
DWORD dwMsg = 0L;            // short midi message
BYTE  bBytesLeft = 0;        // number of bytes needed to complete message
BYTE  bBytePos = 0;          // position in short message buffer
DWORD dwCurData = 0L;        // position in long message buffer
BOOL  fSysex = FALSE;        // are we in sysex mode?
BYTE  status = 0;
BYTE    fMidiInStarted = 0;     /* has the midi been started */
LPMIDIHDR      lpMIQueue = NULL;
BYTE    gbMidiInUse = 0;                /* if MIDI is in use */

static WORD  wMidiOutEntered = 0;    // reentrancy check
static PORTALLOC gMidiOutClient;     // client information

/* transformation of linear velocity value to
        logarithmic attenuation */
BYTE gbVelocityAtten[32] = {
        40, 36, 32, 28, 23, 21, 19, 17,
        15, 14, 13, 12, 11, 10, 9, 8,
        7, 6, 5, 5, 4, 4, 3, 3,
        2, 2, 1, 1, 1, 0, 0, 0 };

BYTE BCODE gbPercMap[53][2] =
{
   {  0, 35 },
   {  0, 35 },
   {  2, 52 },
   {  3, 48 },
   {  4, 58 },
   {  5, 60 },
   {  6, 47 },
   {  7, 43 },
   {  6, 49 },
   {  9, 43 },
   {  6, 51 },
   { 11, 43 },
   {  6, 54 },
   {  6, 57 },
   { 14, 72 },
   {  6, 60 },
   { 16, 76 },
   { 17, 84 },
   { 18, 36 },
   { 19, 76 },
   { 20, 84 },
   { 21, 83 },
   { 22, 84 },
   { 23, 24 },
   { 16, 77 },
   { 25, 60 },
   { 26, 65 },
   { 27, 59 },
   { 28, 51 },
   { 29, 45 },
   { 30, 71 },
   { 31, 60 },
   { 32, 58 },
   { 33, 53 },
   { 34, 64 },
   { 35, 71 },
   { 36, 61 },
   { 37, 61 },
   { 38, 48 },
   { 39, 48 },
   { 40, 69 },
   { 41, 68 },
   { 42, 63 },
   { 43, 74 },
   { 44, 60 },
   { 45, 80 },
   { 46, 64 },
   { 47, 69 },
   { 48, 73 },
   { 49, 75 },
   { 50, 68 },
   { 51, 48 },
   { 52, 53 }
} ;

short   giBend[NUMCHANNELS];    /* bend for each channel */
BYTE    gbPatch[NUMCHANNELS];   /* patch number mapped to */

/* --- interface functions ---------------------------------- */


/*
 * the functions in this section call out to adlib.c or opl3.c
 * depending on which device we have installed.
 */


/**************************************************************
MidiAllNotesOff - switch off all active voices.

inputs - none
returns - none
*/
VOID MidiAllNotesOff(void)
{
    switch (gMidiType) {
    case TYPE_OPL3:
        Opl3_AllNotesOff();
        break;

    case TYPE_ADLIB:
        Adlib_AllNotesOff();
        break;
    }
}


/**************************************************************
MidiNewVolume - This should be called if a volume level
        has changed. This will adjust the levels of all the playing
        voices.

inputs
        WORD    wLeft   - left attenuation (1.5 db units)
        WORD    wRight  - right attenuation (ignore if mono)
returns
        none
*/
VOID FAR PASCAL MidiNewVolume (WORD wLeft, WORD wRight)
{
    switch (gMidiType) {
    case TYPE_OPL3:
        Opl3_NewVolume(wLeft, wRight);
        break;

    case TYPE_ADLIB:
        Adlib_NewVolume(wLeft, wRight);
        break;
    }

}

/***************************************************************
MidiChannelVolume - set the volume level for an individual channel.

inputs
        BYTE    bChannel - channel number to change
        WORD    wAtten  - attenuation in 1.5 db units

returns
        none
*/
VOID FAR PASCAL MidiChannelVolume(BYTE bChannel, WORD wAtten)
{

    switch (gMidiType) {
    case TYPE_OPL3:
        Opl3_ChannelVolume(bChannel, wAtten);
        break;

    case TYPE_ADLIB:
        Adlib_ChannelVolume(bChannel, wAtten);
        break;
    }

}



/***************************************************************
MidiSetPan - set the left-right pan position.

inputs
        BYTE    bChannel  - channel number to alter
        BYTE    bPan   - 0 for left, 127 for right or somewhere in the middle.

returns - none
*/
VOID FAR PASCAL MidiSetPan(BYTE bChannel, BYTE bPan)
{
    switch (gMidiType) {
    case TYPE_OPL3:
        Opl3_SetPan(bChannel, bPan);
        break;

    case TYPE_ADLIB:
        Adlib_SetPan(bChannel, bPan);
        break;
    }

}

/***************************************************************
MidiPitchBend - This pitch bends a channel.

inputs
        BYTE    bChannel - channel
        short   iBend - Values from -32768 to 32767, being
                        -2 to +2 half steps
returns
        none
*/
VOID NEAR PASCAL MidiPitchBend (BYTE bChannel,
        short iBend)
{
    switch (gMidiType) {
    case TYPE_OPL3:
        Opl3_PitchBend(bChannel, iBend);
        break;

    case TYPE_ADLIB:
        Adlib_PitchBend(bChannel, iBend);
        break;
    }

}

/***************************************************************
MidiBoardInit - initialise board and load patches as necessary.

* inputs - none
* returns - 0 for success or the error code
*/
WORD MidiBoardInit(void)
{
    /*
     * load patch tables and reset board
     */

    switch (gMidiType) {
    case TYPE_OPL3:
        return( Opl3_BoardInit());
        break;

    case TYPE_ADLIB:
        return (Adlib_BoardInit());
        break;
    }
    return(MMSYSERR_ERROR);
}

/*
 * MidiBoardReset - silence the board and set all voices off.
 */
VOID MidiBoardReset(void)
{
    BYTE i;

    /*
     * switch off pitch bend (we own this, not the opl3/adlib code)
     */
    for (i = 0; i < NUMCHANNELS; i++)
        giBend[i] = 0;

    /*
     * set all voices off, set channel atten to default,
     * & silence board.
     */
    switch (gMidiType) {
    case TYPE_OPL3:
        Opl3_BoardReset();
        break;

    case TYPE_ADLIB:
        Adlib_BoardReset();
        break;
    }
}



/* --- midi interpretation -------------------------------------*/


/***************************************************************
MidiMessage - This handles a MIDI message. This
        does not do running status.

inputs
        DWORD dwData - up to 4 bytes of MIDI data
                depending upon the message.
returns
        none
*/
VOID NEAR PASCAL MidiMessage (DWORD dwData)
{
    BYTE    bChannel, bVelocity, bNote;
    WORD    wTemp;

    // D1("\nMidiMessage");
    bChannel = (BYTE) dwData & (BYTE)0x0f;
    bVelocity = (BYTE) (dwData >> 16) & (BYTE)0x7f;
    bNote = (BYTE) ((WORD) dwData >> 8) & (BYTE)0x7f;

    switch ((BYTE)dwData & 0xf0) {
        case 0x90:
#ifdef DEBUG
            {
                    char szTemp[4];
                    szTemp[0] = "0123456789abcdef"[bNote >> 4];
                    szTemp[1] = "0123456789abcdef"[bNote & 0x0f];
                    szTemp[2] = ' ';
                    szTemp[3] = 0;
                    if ((bChannel == 9) && bVelocity) D1(szTemp);
            }
#endif
            /* turn key on, or key off if volume == 0 */
            if (bVelocity) {
                switch(gMidiType) {
                case TYPE_OPL3:
                    if (bChannel == DRUMCHANNEL)
                    {
                       if (bNote >= 35 && bNote <= 87)
                       {
#ifdef DEBUG
                          char szDebug[ 80 ] ;

                          wsprintf( szDebug, "bChannel = %d, bNote = %d",
                                    bChannel, bNote ) ;
                          D1( szDebug ) ;
#endif

                          Opl3_NoteOn( (BYTE) (gbPercMap[ bNote - 35 ][ 0 ] + 0x80),
                                       gbPercMap[ bNote - 35 ][ 1 ],
                                       bChannel, bVelocity,
                                       (short) giBend[ bChannel ] ) ;
                       }
                    }
                    else
                        Opl3_NoteOn( (BYTE) gbPatch[ bChannel ], bNote, bChannel,
                                     bVelocity, (short) giBend[ bChannel ] ) ;
                    break;

                case TYPE_ADLIB:
                    Adlib_NoteOn (
                            (BYTE) ((bChannel == DRUMCHANNEL) ?
                                    (BYTE) (bNote + 128) : (BYTE) gbPatch[bChannel]),
                            bNote, bChannel, bVelocity, (short) giBend[bChannel]);
                    break;
                }
                break;
            }

                /* else, continue through and turn key off */
        case 0x80:
                /* turn key off */
                switch (gMidiType) {
                case TYPE_OPL3:
                    if (bChannel == DRUMCHANNEL)
                    {
                       if (bNote >= 35 && bNote <= 87)
                          Opl3_NoteOff( (BYTE) (gbPercMap[ bNote - 35 ][ 0 ] + 0x80),
                                       gbPercMap[ bNote - 35 ][ 1 ], bChannel ) ;
                    }
                    else
                       Opl3_NoteOff( (BYTE) gbPatch[bChannel], bNote, bChannel ) ;
                    break;

                case TYPE_ADLIB:
                    Adlib_NoteOff (
                            (BYTE) ((bChannel == DRUMCHANNEL) ?
                                    (BYTE) (bNote + 128) : (BYTE) gbPatch[bChannel]),
                                    bNote, bChannel);
                    break;
                }
                break;

        case 0xb0:
                // D1("\nChangeControl");
                /* change control */
                switch (bNote) {
                        case 7:
                                /* change channel volume */
                                MidiChannelVolume(
                                    bChannel,
                                    gbVelocityAtten[(bVelocity & 0x7f) >> 2]);

                                break;
                        case 8:
                        case 10:
                                /* change the pan level */
                                MidiSetPan(bChannel, bVelocity);
                                break;
                        };
                break;

        case 0xc0:
            if (bChannel != DRUMCHANNEL)
            {
               int  i ;

               // Turn off all active notes for this channel...

               if (gMidiType == TYPE_OPL3) {
                   Opl3_ChannelNotesOff(bChannel);
               }

               gbPatch[ bChannel ] = bNote ;

            }
            break;

        case 0xe0:
                // D1("\nBend");
                /* pitch bend */
                wTemp = ((WORD) bVelocity << 9) | ((WORD) bNote << 2);
                giBend[bChannel] = (short) (WORD) (wTemp + 0x7FFF);
                MidiPitchBend (bChannel, giBend[bChannel]);

                break;
    };

    return;
}

/****************************************************************************
 * @doc INTERNAL
 *
 * @api void | midiSynthCallback | This calls DriverCallback for a midi device.
 *
 * @parm NPPORTALLOC| pPort | Pointer to the PORTALLOC.
 *
 * @parm WORD | msg | The message to send.
 *
 * @parm DWORD | dw1 | Message-dependent parameter.
 *
 * @parm DWORD | dw2 | Message-dependent parameter.
 *
 * @rdesc There is no return value.
 ***************************************************************************/
void NEAR PASCAL midiSynthCallback(NPPORTALLOC pPort, WORD msg, DWORD_PTR dw1, DWORD_PTR dw2)
{

    // invoke the callback function, if it exists.  dwFlags contains driver-
    // specific flags in the LOWORD and generic driver flags in the HIWORD
    if (pPort->dwCallback)
        DriverCallback(pPort->dwCallback,       // client's callback DWORD
            HIWORD(pPort->dwFlags) | DCB_NOSWITCH,  // callback flags
            (HDRVR)pPort->hMidi,     // handle to the wave device
            msg,                     // the message
            pPort->dwInstance,       // client's instance data
            dw1,                     // first DWORD
            dw2);                    // second DWORD
}

/****************************************************************************
 * @doc INTERNAL
 *
 * @api void | midBufferWrite | This function writes a byte into the long
 *     message buffer.  If the buffer is full or a SYSEX_ERROR or
 *     end-of-sysex byte is received, the buffer is marked as 'done' and
 *     it's owner is called back.
 *
 * @parm BYTE | byte | The byte received.
 *
 * @rdesc There is no return value
 ***************************************************************************/
static void NEAR PASCAL midBufferWrite(BYTE byte)
{
    LPMIDIHDR  lpmh;
    WORD       msg;

    // if no buffers, nothing happens
    if (lpMIQueue == NULL)
        return;

    lpmh = lpMIQueue;

    if (byte == SYSEX_ERROR) {
        D2(("sysexerror"));
        msg = MIM_LONGERROR;
        }
    else {
        D2(("bufferwrite"));
        msg = MIM_LONGDATA;
        *((HPSTR)(lpmh->lpData) + dwCurData++) = byte;
        }

    // if end of sysex, buffer full or error, send them back the buffer
    if ((byte == SYSEX_ERROR) || (byte == 0xF7) || (dwCurData >= lpmh->dwBufferLength)) {
        D2(("bufferdone"));
        lpMIQueue = lpMIQueue->lpNext;
        lpmh->dwBytesRecorded = dwCurData;
        dwCurData = 0L;
        lpmh->dwFlags |= MHDR_DONE;
        lpmh->dwFlags &= ~MHDR_INQUEUE;
        midiSynthCallback(&gMidiInClient, msg, (DWORD_PTR)lpmh, dwMsgTime);
    }

    return;
}



/****************************************************************************

    This function conforms to the standard MIDI output driver message proc
    modMessage, which is documented in mmddk.d.

 ***************************************************************************/
DWORD APIENTRY modSynthMessage(UINT id,
        UINT msg, DWORD_PTR dwUser, DWORD_PTR dwParam1, DWORD_PTR dwParam2)
{
    LPMIDIHDR    lpHdr;
    LPSTR           lpBuf;          /* current spot in the long msg buf */
    DWORD           dwBytesRead;    /* how far are we in the buffer */
    DWORD           dwMsg = 0;      /* short midi message sent to synth */
    BYTE            bBytePos=0;     /* shift current byte by dwBytePos*s */
    BYTE            bBytesLeft = 0; /* how many dat bytes needed */
    BYTE            curByte;        /* current byte in long buffer */
    UINT            mRc;            /* Return code */


    // this driver only supports one device
    if (id != 0) {
        D1(("invalid midi device id"));
        return MMSYSERR_BADDEVICEID;
    }

    switch (msg) {
        case MODM_GETNUMDEVS:
            D1(("MODM_GETNUMDEVS"));
        //
        // Check if the kernel driver got loaded OK
        //
        mRc = sndGetNumDevs(SYNTH_DEVICE);
                break;

        case MODM_GETDEVCAPS:
            D1(("MODM_GETDEVCAPS"));
            mRc = midiGetDevCaps(0, SYNTH_DEVICE, (LPBYTE)dwParam1, (WORD)dwParam2);
            break;

        case MODM_OPEN:
            D1(("MODM_OPEN"));

            /* open the midi */
            if (MidiOpen())
                return MMSYSERR_ALLOCATED;

            // save client information
            gMidiOutClient.dwCallback = ((LPMIDIOPENDESC)dwParam1)->dwCallback;
            gMidiOutClient.dwInstance = ((LPMIDIOPENDESC)dwParam1)->dwInstance;
            gMidiOutClient.hMidi      = (HMIDIOUT)((LPMIDIOPENDESC)dwParam1)->hMidi;
            gMidiOutClient.dwFlags    = (DWORD)dwParam2;

            // notify client
            midiSynthCallback(&gMidiOutClient, MOM_OPEN, 0L, 0L);

            /* were in use */
            gbMidiInUse = TRUE;

            mRc = 0L;
                break;

        case MODM_CLOSE:
            D1(("MODM_CLOSE"));

            /* shut up the FM synthesizer */
            MidiClose();

            // notify client
            midiSynthCallback(&gMidiOutClient, MOM_CLOSE, 0L, 0L);

            /* were not used any more */
            gbMidiInUse = FALSE;

            mRc = 0L;
                break;

        case MODM_RESET:
            D1(("MODM_RESET"));

            //
            //  turn off FM synthesis
            //
            //  note that we increment our 're-entered' counter so that a
            //  background interrupt handler doesn't mess up our resetting
            //  of the synth by calling midiOut[Short|Long]Msg.. just
            //  practicing safe midi. NOTE: this should never be necessary
            //  if a midi app is PROPERLY written!
            //
            wMidiOutEntered++;
            {
                if (wMidiOutEntered == 1)
                {
                    MidiReset();
                    dwParam1 = 0L;
                }
                else
                {
                    D1(("MODM_RESET reentered!"));
                    dwParam1 = MIDIERR_NOTREADY;
                }
            }
            wMidiOutEntered--;
            mRc = (DWORD)dwParam1;
                break;

        case MODM_DATA:                    // message is in dwParam1
            MidiCheckVolume();             // See if the volume has changed

            // make sure we're not being reentered
            wMidiOutEntered++;
            if (wMidiOutEntered > 1) {
                        D1(("MODM_DATA reentered!"));
                        wMidiOutEntered--;
                        return MIDIERR_NOTREADY;
                    }

            /* if have repeated messages */
            if (dwParam1 & 0x00000080)  /* status byte */
                        status = LOBYTE(LOWORD(dwParam1));
            else
                        dwParam1 = (dwParam1 << 8) | ((DWORD) status);

            /* if not, have an FM synthesis message */
            MidiMessage ((DWORD)dwParam1);

            wMidiOutEntered--;
            mRc = 0L;
                break;

        case MODM_LONGDATA:      // far pointer to header in dwParam1

            MidiCheckVolume();   // See if the volume has changed

            // make sure we're not being reentered
            wMidiOutEntered++;
            if (wMidiOutEntered > 1) {
                        D1(("MODM_LONGDATA reentered!"));
                        wMidiOutEntered--;
                        return MIDIERR_NOTREADY;
                        }

            // check if it's been prepared
            lpHdr = (LPMIDIHDR)dwParam1;
            if (!(lpHdr->dwFlags & MHDR_PREPARED)) {
                        wMidiOutEntered--;
                        return MIDIERR_UNPREPARED;
                        }

            lpBuf = lpHdr->lpData;
            dwBytesRead = 0;
            curByte = *lpBuf;

            while (TRUE) {
                        /* if its a system realtime message send it and continue
                                this does not affect the running status */

                        if (curByte >= 0xf8)
                                MidiMessage (0x000000ff & curByte);
                        else if (curByte >= 0xf0) {
                                status = 0;     /* kill running status */
                                dwMsg = 0L;     /* throw away any incomplete data */
                                bBytePos = 0;   /* start at beginning of message */

                                switch (curByte) {
                                        case 0xf0:      /* sysex - ignore */
                                        case 0xf7:
                                                break;
                                        case 0xf4:      /* system common, no data */
                                        case 0xf5:
                                        case 0xf6:
                                                MidiMessage (0x000000ff & curByte);
                                                break;
                                        case 0xf1:      /* system common, one data byte */
                                        case 0xf3:
                                                dwMsg |= curByte;
                                                bBytesLeft = 1;
                                                bBytePos = 1;
                                                break;
                                        case 0xf2:      /* system common, 2 data bytes */
                                                dwMsg |= curByte;
                                                bBytesLeft = 2;
                                                bBytePos = 1;
                                                break;
                                        };
                                }
                    /* else its a channel message */
                    else if (curByte >= 0x80) {
                                status = curByte;
                                dwMsg = 0L;

                                switch (curByte & 0xf0) {
                                        case 0xc0:      /* channel message, one data */
                                        case 0xd0:
                                                dwMsg |= curByte;
                                                bBytesLeft = 1;
                                                bBytePos = 1;
                                                break;
                                        case 0x80:      /* two bytes */
                                        case 0x90:
                                        case 0xa0:
                                        case 0xb0:
                                        case 0xe0:
                                                dwMsg |= curByte;
                                                bBytesLeft = 2;
                                                bBytePos = 1;
                                                break;
                                        };
                                }

                        /* else if its an expected data byte */
                    else if (bBytePos != 0) {
                                dwMsg |= ((DWORD)curByte) << (bBytePos++ * 8);
                                if (--bBytesLeft == 0) {

                                                MidiMessage (dwMsg);

                                        if (status) {
                                                dwMsg = status;
                                                bBytesLeft = bBytePos - (BYTE)1;
                                                bBytePos = 1;
                                                }
                                        else {
                                                dwMsg = 0L;
                                                bBytePos = 0;
                                                };
                                        };
                                };

            /* read the next byte if there is one */
            /* remember we have already read and processed one byte that
             * we have not yet counted- so we need to pre-inc, not post-inc
             */
            if (++dwBytesRead >= lpHdr->dwBufferLength) break;
                curByte = *++lpBuf;
            };      /* while TRUE */

            /* return buffer to client */
            lpHdr->dwFlags |= MHDR_DONE;
            midiSynthCallback (&gMidiOutClient, MOM_DONE, dwParam1, 0L);

            wMidiOutEntered--;
            mRc = 0L;
                break;

        case MODM_SETVOLUME:
                mRc = MidiSetVolume(LOWORD(dwParam1) << 16, HIWORD(dwParam1) << 16);
                break;

        case MODM_GETVOLUME:
                mRc = MidiGetVolume((LPDWORD)dwParam1);
                break;

        default:
            return MMSYSERR_NOTSUPPORTED;
    }
    MidiFlush();

    return mRc;


// should never get here...
return MMSYSERR_NOTSUPPORTED;
}

/****************************************************************
MidiInit - Initializes the FM synthesis chip and internal
        variables. This assumes that HwInit() has been called
        and that a card location has been found. This loads in
        the patch information.

inputs
        none
returns
        WORD - 0 if successful, else error
*/
WORD FAR PASCAL MidiInit (VOID)
{
//    WORD        i;

    D1 (("\nMidiInit"));

// don't reset the patch map - it will be initialised at loadtime to 0
// (because its static data) and we should not change it after that
// since the mci sequencer will not re-send patch change messages.
//
//
//    /* reset all channels to patch 0 */
//    for (i = 0; i < NUMCHANNELS; i++) {
//      gbPatch[i] = 0;
//    }

    /* initialise the h/w specific patch tables */
    return MidiBoardInit();
}


/*****************************************************************
MidiOpen - This should be called when a midi file is opened.
        It initializes some variables and locks the patch global
        memories.

inputs
        none
returns
        UINT - 0 if succedes, else error
*/
UINT FAR PASCAL MidiOpen (VOID)
{
    MMRESULT mRc;

    D1(("MidiOpen"));


    //
    // For 32-bit we must open our kernel device
    //

    mRc = MidiOpenDevice(&MidiDeviceHandle, TRUE);

    if (mRc != MMSYSERR_NOERROR) {
            return mRc;
    }


    /*
     * reset the device (set default channel attenuation etc)
     */
    MidiBoardReset();

    return 0;
}

/***************************************************************
MidiClose - This kills the playing midi voices and closes the kernel driver

inputs
        none
returns
        none
*/
VOID FAR PASCAL MidiClose (VOID)
{

    D1(("MidiClose"));

    if (MidiDeviceHandle == NULL) {
        return;
    }

    /* make sure all notes turned off */
    MidiAllNotesOff();

    MidiCloseDevice(MidiDeviceHandle);

    MidiDeviceHandle = NULL;
}

/** void FAR PASCAL MidiReset(void)
 *
 *  DESCRIPTION:
 *
 *
 *  ARGUMENTS:
 *      (void)
 *
 *  RETURN (void FAR PASCAL):
 *
 *
 *  NOTES:
 *
 ** cjp */

void FAR PASCAL MidiReset(void)
{

    D1(("MidiReset"));

    /* make sure all notes turned off */
    MidiAllNotesOff();

    /* silence the board and reset board-specific variables */
    MidiBoardReset();


} /* MidiReset() */