/*==========================================================================
 *
 *  Copyright (C) 1995-1996 Microsoft Corporation. All Rights Reserved.
 *
 *  File:   mid2strm.c
 *  Content:    Converts a MIDI file into a MDS (MidiStream) File.
 *
 ***************************************************************************/
#include <stdio.h>
#include <windows.h>
#include <windowsx.h>
#include <mmsystem.h>
#include <assert.h>

// MIDI file constants
//
#define MThd            0x6468544D              // Start of file
#define MTrk            0x6B72544D              // Start of track

#define MIDI_SYSEX      ((BYTE)0xF0)            // SysEx begin
#define MIDI_SYSEXEND   ((BYTE)0xF7)            // SysEx begin
#define MIDI_META       ((BYTE)0xFF)            // Meta event begin
#define MIDI_META_TEMPO ((BYTE)0x51)
#define MIDI_META_EOT   ((BYTE)0x2F)            // End-of-track

#define MIDI_NOTEOFF    ((BYTE)0x80)            // + note + velocity
#define MIDI_NOTEON     ((BYTE)0x90)            // + note + velocity
#define MIDI_POLYPRESS  ((BYTE)0xA0)            // + pressure (2 bytes)
#define MIDI_CTRLCHANGE ((BYTE)0xB0)            // + ctrlr + value
#define MIDI_PRGMCHANGE ((BYTE)0xC0)            // + new patch
#define MIDI_CHANPRESS  ((BYTE)0xD0)            // + pressure (1 byte)
#define MIDI_PITCHBEND  ((BYTE)0xE0)            // + pitch bend (2 bytes)

#define CB_STREAMBUF    (4096)                  // Size of each stream buffer

#define MIDS_SHORTMSG   (0x00000000)
#define MIDS_TEMPO      (0x01000000)

// Macros for swapping hi/lo-endian data
//
#define WORDSWAP(w)     (((w) >> 8) | \
                        (((w) << 8) & 0xFF00))

#define DWORDSWAP(dw)   (((dw) >> 24) |                 \
                        (((dw) >> 8) & 0x0000FF00) |    \
                        (((dw) << 8) & 0x00FF0000) |    \
                        (((dw) << 24) & 0xFF000000))

// In debug builds, TRACKERR will show us where the parser died
//
#ifdef _DEBUG
#define TRACKERR(p,sz) ShowTrackError(p,sz);
#else
#define TRACKERR(p,sz)
#endif


// These structures are stored in MIDI files; they need to be byte
// aligned.
//
#pragma pack(1)

// Chunk header. dwTag is either MTrk or MThd.
//
typedef struct
{
    DWORD           dwTag;                  // Type
    DWORD           cbChunk;                // Length (hi-lo)
} MIDICHUNK;

// Contents of MThd chunk.
typedef struct
{
    WORD            wFormat;                // Format (hi-lo)
    WORD            cTrack;                 // # tracks (hi-lo)
    WORD            wTimeDivision;          // Time division (hi-lo)
} MIDIFILEHDR;

#pragma pack()

// One event we're reading or writing to a track
//
typedef struct
{
    DWORD           tkEvent;            // Absolute time of event
    BYTE            abEvent[4];         // Event type and parameters if channel msg
    DWORD           cbEvent;            // Of data which follows if meta or sysex
    LPBYTE          pEvent;             // -> Event data if applicable
} MEVENT;   

// Description of a track open for read
//
#define ITS_F_ENDOFTRK  0x00000001

typedef struct
{
    DWORD           fdwTrack;           // Track status
    DWORD           cbTrack;            // Total bytes in track
    DWORD           cbLeft;             // Bytes left unread in track
    LPBYTE          pTrack;             // -> start of track data
    LPBYTE          pTrackPointer;      // -> next byte to read
    DWORD           tkNextEventDue;     // Absolute time of next event in track
    BYTE            bRunningStatus;     // Running status from last channel msg
#ifdef _DEBUG
    DWORD           nTrack;             // # of this track for debugging
#endif
} INTRACKSTATE;

// Description of the input MIDI file
//
typedef struct
{
    DWORD           cbFile;             // Total bytes in file
    LPBYTE          pFile;              // -> entire file in memory
    DWORD           cbLeft;             // Bytes left unread
    LPBYTE          pFilePointer;       // -> next byte to read

    DWORD           dwTimeDivision;     // Original time division
    DWORD           dwFormat;           // Original format
    DWORD           cTrack;             // Track count (specifies apIts size)
    INTRACKSTATE*   apIts;              // -> array of tracks in this file
} INFILESTATE;

// Description of a stream buffer on the output side
//
typedef struct STREAMBUF *PSTREAMBUF;
typedef struct STREAMBUF
{
    LPBYTE          pBuffer;            // -> Start of actual buffer
    DWORD           tkStart;            // Tick time just before first event
    LPBYTE          pbNextEvent;        // Where to write next event
    DWORD           cbLeft;             // bytes left in buffer
    DWORD           cbLeftUncompressed; // bytes left when uncompressed
    PSTREAMBUF      pNext;              // Next buffer
} STREAMBUF;

// Description of output stream open for write
//
typedef struct
{
    DWORD           tkTrack;            // Current tick position in track   
    PSTREAMBUF      pFirst;             // First stream buffer
    PSTREAMBUF      pLast;              // Last (current) stream buffer
} OUTSTREAMSTATE;

// Format of structs within a MSD file
//
// 'fmt ' chunk
//

#define MDS_F_NOSTREAMID    0x00000001  // Stream ID's skipped; reader inserts
typedef struct
{
    DWORD           dwTimeFormat;       // Low word == time format in SMF format
    DWORD           cbMaxBuffer;        // Guaranteed max buffer size
    DWORD           dwFlags;            // Format flags
} MIDSFMT;

// 'data' chunk buffer header
//
typedef struct
{
    DWORD           tkStart;            // Absolute tick offset at start of buffer
    DWORD           cbBuffer;           // Bytes in this buffer
} MIDSBUFFER;

// A few globals
//
static HANDLE       hInFile             = INVALID_HANDLE_VALUE;
static HANDLE       hOutFile            = INVALID_HANDLE_VALUE;
static INFILESTATE  ifs;
static OUTSTREAMSTATE ots;
static BOOL         fCompress           = FALSE;

// Messages
//
static char szInitErrMem[]          = "Out of memory.\n";
static char szInitErrInFile[]       = "Read error on input file or file is corrupt.\n";

#ifdef _DEBUG
static char gteBadRunStat[] =       "Reference to missing running status.";
static char gteRunStatMsgTrunc[] =  "Running status message truncated";
static char gteChanMsgTrunc[] =     "Channel message truncated";
static char gteSysExLenTrunc[] =    "SysEx event truncated (length)";
static char gteSysExTrunc[] =       "SysEx event truncated";
static char gteMetaNoClass[] =      "Meta event truncated (no class byte)";
static char gteMetaLenTrunc[] =     "Meta event truncated (length)";
static char gteMetaTrunc[] =        "Meta event truncated";
#endif

// Prototypes
//
static BOOL         Init(LPSTR szInFile, LPSTR szOutFile);
static LPBYTE       GetInFileData(DWORD cbToGet);
static void         Cleanup(void);
static BOOL         BuildNewTracks(void);
static BOOL         WriteStreamBuffers(void);
static BOOL         GetTrackVDWord(INTRACKSTATE* pTs, LPDWORD lpdw);
static BOOL         GetTrackEvent(INTRACKSTATE* pTs, MEVENT *pMe);
static BOOL         AddEventToStream(MEVENT *pMe);
static LPBYTE       GetOutStreamBytes(DWORD tkNow, DWORD cbNeeded, DWORD cbUncompressed);
#ifdef _DEBUG
static void         ShowTrackError(INTRACKSTATE* pTs, char* szErr);
#endif

void main(int argc, char* argv[])
{
    UINT            idxFnames;

    if (argc < 3)
    {
        fprintf(stderr, "Format is mid2strm [-c] infile outfile\n");
        fprintf(stderr, "-c\tNo-stream-id compression\n");
        exit(1);
    }

    idxFnames = 1;
    if (argv[1][0] == '-')
    {
        ++idxFnames;
        if (argv[1][1] == 'c')
            fCompress = TRUE;
    }

    if (!Init(argv[idxFnames], argv[idxFnames+1]))
        exit(1);

    if (!BuildNewTracks())
        exit(1);

    if (!WriteStreamBuffers())
        exit(1);

    // Add cleanup code!!!
    //
    Cleanup();

    exit(0);
}

// Init
// 
// Open the input and output files
// Allocate and read the entire input file into memory
// Validate the input file structure
// Allocate the input track structures and initialize them
// Initialize the output track structures
//
// Return TRUE on success
// Prints its own error message if something goes wrong
//
static BOOL Init(LPSTR szInFile, LPSTR szOutFile)
{
    BOOL            fRet = FALSE;
    DWORD           cbRead;
    LPDWORD         lpdwTag;
    LPDWORD         lpcbHeader;
    DWORD           cbHeader;
    MIDIFILEHDR*    pHeader;
    INTRACKSTATE*   pTs;
    UINT            idx;
    
    // Initialize things we'll try to free later if we fail
    //
    ifs.cbFile = 0;
    ifs.pFile = NULL;
    ifs.apIts = NULL;

    // Attempt to open the input and output files
    //
    hInFile = CreateFile(szInFile, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    if (INVALID_HANDLE_VALUE == hInFile)
    {
        fprintf(stderr, "Could not open \"%s\" for read.\n", szInFile);
        goto Init_Cleanup;
    }

    hOutFile = CreateFile(szOutFile, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
    if (INVALID_HANDLE_VALUE == hOutFile)
    {
        fprintf(stderr, "Could not open \"%s\" for write.\n", szOutFile);
        goto Init_Cleanup;
    }

    // Figure out how big the input file is and allocate a chunk of memory big enough
    // to hold the whole thing. Read the whole file in at once.
    //
    if (((UINT)-1) == (ifs.cbFile = GetFileSize(hInFile, NULL)))
    {
        fprintf(stderr, "File system error on input file.\n");
        goto Init_Cleanup;
    }

    if (NULL == (ifs.pFile = GlobalAllocPtr(GPTR, ifs.cbFile)))
    {
        fprintf(stderr, szInitErrMem);
        goto Init_Cleanup;
    }

    if ((!ReadFile(hInFile, ifs.pFile, ifs.cbFile, &cbRead, NULL)) ||
        cbRead != ifs.cbFile)
    {
        fprintf(stderr, szInitErrInFile);
        goto Init_Cleanup;
    }
    
    // Set up to read from the memory buffer. Read and validate
    // - MThd header
    // - size of file header chunk
    // - file header itself
    //  
    ifs.cbLeft = ifs.cbFile;
    ifs.pFilePointer = ifs.pFile;

    if (NULL == (lpdwTag = (LPDWORD)GetInFileData(sizeof(*lpdwTag))) ||
        *lpdwTag != MThd ||
        NULL == (lpcbHeader = (LPDWORD)GetInFileData(sizeof(*lpcbHeader))) ||
        (cbHeader = DWORDSWAP(*lpcbHeader)) < sizeof(MIDIFILEHDR) ||
        NULL == (pHeader = (MIDIFILEHDR*)GetInFileData(cbHeader)))
    {
        fprintf(stderr, szInitErrInFile);
        goto Init_Cleanup;
    }

    // File header is stored in hi-lo order. Swap this into Intel order and save
    // parameters in our native int size (32 bits)
    //
    ifs.dwFormat = (DWORD)WORDSWAP(pHeader->wFormat);
    ifs.cTrack = (DWORD)WORDSWAP(pHeader->cTrack);
    ifs.dwTimeDivision = (DWORD)WORDSWAP(pHeader->wTimeDivision);

    // We know how many tracks there are; allocate the structures for them and parse
    // them. The parse merely looks at the MTrk signature and track chunk length
    // in order to skip to the next track header.
    //
    ifs.apIts = (INTRACKSTATE*)GlobalAllocPtr(GPTR, ifs.cTrack*sizeof(INTRACKSTATE));
    if (NULL == ifs.apIts)
    {
        fprintf(stderr, szInitErrMem);
        goto Init_Cleanup;
    }

    for (idx = 0, pTs = ifs.apIts; idx < ifs.cTrack; ++idx, ++pTs)
    {
        if (NULL == (lpdwTag = (LPDWORD)GetInFileData(sizeof(*lpdwTag))) ||
            *lpdwTag != MTrk ||
            NULL == (lpcbHeader = (LPDWORD)GetInFileData(sizeof(*lpcbHeader))))
        {
            fprintf(stderr, szInitErrInFile);
            goto Init_Cleanup;
        }

        cbHeader = DWORDSWAP(*lpcbHeader);
        pTs->cbTrack = cbHeader;
        pTs->cbLeft = cbHeader;
        pTs->pTrack = GetInFileData(cbHeader);
        if (NULL == pTs->pTrack)
        {
            fprintf(stderr, szInitErrInFile);
            goto Init_Cleanup;
        }

#ifdef _DEBUG
        pTs->nTrack = idx;
#endif
        pTs->pTrackPointer = pTs->pTrack;
        pTs->cbLeft = pTs->cbTrack;
        pTs->fdwTrack = 0;
        pTs->bRunningStatus = 0;

        // Handle bozo MIDI files which contain empty track chunks
        //
        if (!pTs->cbLeft)
        {
            pTs->fdwTrack |= ITS_F_ENDOFTRK;
            continue;
        }


        // We always preread the time from each track so the mixer code can
        // determine which track has the next event with a minimum of work
        //
        if (!GetTrackVDWord(pTs, &pTs->tkNextEventDue))
        {
            fprintf(stderr, szInitErrInFile);
            goto Init_Cleanup;
        }
    }

    ots.tkTrack = 0;
    ots.pFirst = NULL;
    ots.pLast = NULL;

    fRet = TRUE;
        
Init_Cleanup:
    if (!fRet)
        Cleanup();

    return fRet;
}

//
// GetInFileData
//
// Gets the requested number of bytes of data from the input file and returns
// a pointer to them.
// 
// Returns a pointer to the data or NULL if we'd read more than is
// there.
//
static LPBYTE       GetInFileData(DWORD cbToGet)
{
    LPBYTE              pRet;

    if (ifs.cbLeft < cbToGet)
        return NULL;

    pRet = ifs.pFilePointer;

    ifs.cbLeft -= cbToGet;
    ifs.pFilePointer += cbToGet;

    return pRet;
}

//
// Cleanup
//
// Free anything we ever allocated
//
static void         Cleanup(void)
{
    PSTREAMBUF          pCurr;
    PSTREAMBUF          pNext;

    if (hInFile != INVALID_HANDLE_VALUE)    CloseHandle(hInFile);
    if (hOutFile != INVALID_HANDLE_VALUE)   CloseHandle(hOutFile);
    if (ifs.pFile)                          GlobalFreePtr(ifs.pFile);
    if (ifs.apIts)                          GlobalFreePtr(ifs.apIts);
    

    pCurr = ots.pFirst;
    while (pCurr)
    {
        pNext = pCurr->pNext;
        GlobalFreePtr(pCurr);
        pCurr = pNext;
    }
}

//
// BuildNewTracks
//
// This is where the actual work gets done.
//
// Until all tracks are done,
//  Scan the tracks to find the next due event
//  Figure out where the event belongs in the new mapping
//  Put it there
// Add end of track metas to all new tracks that now have any data
//
// Return TRUE on success
// Prints its own error message if something goes wrong
//
static BOOL         BuildNewTracks(void)
{
    INTRACKSTATE*       pTs;
    INTRACKSTATE*       pTsFound;
    UINT                idx;
    DWORD               tkNext;
    MEVENT          me;
    
    for(;;)
    {
        // Find nearest event due
        //
        pTsFound = NULL;
        tkNext = 0xFFFFFFFFL;

        for (idx = 0, pTs = ifs.apIts; idx < ifs.cTrack; ++idx, ++pTs)
            if ((!(pTs->fdwTrack & ITS_F_ENDOFTRK)) && (pTs->tkNextEventDue < tkNext))
            {
                tkNext = pTs->tkNextEventDue;
                pTsFound = pTs;
            }

        // None found? We must be done
        //
        if (!pTsFound)
            break;

        // Ok, get the event header from that track
        //

        if (!GetTrackEvent(pTsFound, &me))
        {
            fprintf(stderr, "MIDI file is corrupt!\n");
            return FALSE;
        }

        // Don't add end of track event 'til we're done
        //
        if (me.abEvent[0] == MIDI_META && me.abEvent[1] == MIDI_META_EOT)
            continue;
        
        if (!AddEventToStream(&me))
        {
            fprintf(stderr, "Out of memory building tracks.\n");
            return FALSE;
        }
    }   

    return TRUE;
}

//
// WriteStreamBuffers
//
// Write stream buffers into an MDS file (RIFF MIDS format)
//
// Return TRUE on success
// Prints its own error message if something goes wrong
//
#define FOURCC_MIDS mmioFOURCC('M','I','D','S')
#define FOURCC_fmt  mmioFOURCC('f','m','t',' ')
#define FOURCC_data mmioFOURCC('d','a','t','a')

static BOOL         WriteStreamBuffers(void)
{
    DWORD           cbFmt;
    DWORD           cbData;
    DWORD           cbRiff;
    PSTREAMBUF      psb;
    FOURCC          fcc;
    FOURCC          fcc2;
    MIDSFMT         fmt;
    MIDSBUFFER      data;
    DWORD           cb;
    DWORD           cBuffers;

    // Walk buffer list to find entire size of data chunk
    //
    cbData = sizeof(cBuffers);
    cBuffers = 0;
    for (psb = ots.pFirst; psb; psb = psb->pNext, ++cBuffers)
        cbData += sizeof(MIDSBUFFER) + (CB_STREAMBUF - psb->cbLeft);

    cbFmt = sizeof(fmt);

    // Figure size of entire RIFF chunk
    //
    cbRiff = 
        sizeof(FOURCC) +            // RIFF form type ('MIDS')
        sizeof(FOURCC) +            // Format chunk type ('fmt ')
        sizeof(DWORD) +             // Format chunk size
        sizeof(MIDSFMT) +           // Format chunk contents
        sizeof(FOURCC) +            // Data chunk type ('data')
        sizeof(DWORD) +             // Data chunk size
        cbData;                     // Data chunk contents
        
    fcc = FOURCC_RIFF;
    fcc2 = FOURCC_MIDS;
    if ((!WriteFile(hOutFile, &fcc, sizeof(fcc), &cb, NULL)) ||
        (!WriteFile(hOutFile, &cbRiff, sizeof(cbRiff), &cb, NULL)) ||
        (!WriteFile(hOutFile, &fcc2, sizeof(fcc2), &cb, NULL)))
        return FALSE;


    fmt.dwTimeFormat    = ifs.dwTimeDivision;
    fmt.cbMaxBuffer     = CB_STREAMBUF;
    fmt.dwFlags         = 0;
    if (fCompress)
        fmt.dwFlags     |= MDS_F_NOSTREAMID;

    fcc = FOURCC_fmt;
    if ((!WriteFile(hOutFile, &fcc, sizeof(fcc), &cb, NULL)) ||
        (!WriteFile(hOutFile, &cbFmt, sizeof(cbFmt), &cb, NULL)) ||
        (!WriteFile(hOutFile, &fmt, sizeof(fmt), &cb, NULL)))
        return FALSE;

    fcc = FOURCC_data;
    if ((!WriteFile(hOutFile, &fcc, sizeof(fcc), &cb, NULL)) ||
        (!WriteFile(hOutFile, &cbData, sizeof(cbData), &cb, NULL)) ||
        (!WriteFile(hOutFile, &cBuffers, sizeof(cBuffers), &cb, NULL)))
        return FALSE;

    for (psb = ots.pFirst; psb; psb = psb->pNext)
    {
        data.tkStart = psb->tkStart;
        data.cbBuffer = CB_STREAMBUF - psb->cbLeft;

        if ((!WriteFile(hOutFile, &data, sizeof(data), &cb, NULL)) ||
            (!WriteFile(hOutFile, psb->pBuffer, data.cbBuffer, &cb, NULL)))
            return FALSE;
    }
            
    return TRUE;
}

//
// GetTrackVDWord
//
// Attempts to parse a variable length DWORD from the given track. A VDWord
// in a MIDI file
//  (a) is in lo-hi format 
//  (b) has the high bit set on every byte except the last
//
// Returns the DWORD in *lpdw and TRUE on success; else
// FALSE if we hit end of track first. Sets ITS_F_ENDOFTRK
// if we hit end of track.
//
static BOOL         GetTrackVDWord(INTRACKSTATE* pTs, LPDWORD lpdw)
{
    BYTE                b;
    DWORD               dw = 0;

    if (pTs->fdwTrack & ITS_F_ENDOFTRK)
        return FALSE;

    do
    {
        if (!pTs->cbLeft)
        {
            pTs->fdwTrack |= ITS_F_ENDOFTRK;
            return FALSE;
        }

        b = *pTs->pTrackPointer++;
        --pTs->cbLeft;

        dw = (dw << 7) | (b & 0x7F);
    } while (b & 0x80);

    *lpdw = dw;

    return TRUE;
}

//
// GetTrackEvent
//
// Fills in the event struct with the next event from the track
//
// pMe->tkEvent will contain the absolute tick time of the event
// pMe->abEvent[0] will contain
//  MIDI_META if the event is a meta event;
//   in this case pMe->abEvent[1] will contain the meta class
//  MIDI_SYSEX or MIDI_SYSEXEND if the event is a SysEx event
//  Otherwise, the event is a channel message and pMe->abEvent[1]
//   and pMe->abEvent[2] will contain the rest of the event.
//
// pMe->cbEvent will contain
//  The total length of the channel message in pMe->abEvent if
//   the event is a channel message
//  The total length of the paramter data pointed to by
//   pMe->pEvent otherwise
//
// pMe->pEvent will point at any additional paramters if the 
//  event is a SysEx or meta event with non-zero length; else
//  it will contain NULL
//
// Returns TRUE on success or FALSE on any kind of parse error
// Prints its own error message ONLY in the debug version
//
// Maintains the state of the input track (i.e. pTs->cbLeft,
// pTs->pTrackPointers, and pTs->bRunningStatus).
//
static BOOL         GetTrackEvent(INTRACKSTATE* pTs, MEVENT *pMe)
{
    BYTE                b;
    UINT                cbEvent;

    pMe->pEvent = NULL;

    // Already at end of track? There's nothing to read.
    //
    if ((pTs->fdwTrack & ITS_F_ENDOFTRK) || !pTs->cbLeft)
        return FALSE;

    // Get the first byte, which determines the type of event.
    //
    b = *pTs->pTrackPointer++;
    --pTs->cbLeft;

    // If the high bit is not set, then this is a channel message
    // which uses the status byte from the last channel message
    // we saw. NOTE: We do not clear running status across SysEx or
    // meta events even though the spec says to because there are
    // actually files out there which contain that sequence of data.
    //
    if (!(b & 0x80))
    {
        // No previous status byte? We're hosed.
        //
        if (!pTs->bRunningStatus)
        {
            TRACKERR(pTs, gteBadRunStat);
            return FALSE;
        }

        pMe->abEvent[0] = pTs->bRunningStatus;
        pMe->abEvent[1] = b;

        b = pMe->abEvent[0] & 0xF0;
        pMe->cbEvent = 2;

        // Only program change and channel pressure events are 2 bytes long;
        // the rest are 3 and need another byte
        //
        if (b != MIDI_PRGMCHANGE && b != MIDI_CHANPRESS)
        {
            if (!pTs->cbLeft)
            {
                TRACKERR(pTs, gteRunStatMsgTrunc);
                pTs->fdwTrack |= ITS_F_ENDOFTRK;
                return FALSE;
            }

            pMe->abEvent[2] = *pTs->pTrackPointer++;
            --pTs->cbLeft;
            ++pMe->cbEvent;
        }
    }
    else if ((b & 0xF0) != MIDI_SYSEX)
    {
        // Not running status, not in SysEx range - must be
        // normal channel message (0x80-0xEF)
        //
        pMe->abEvent[0] = b;
        pTs->bRunningStatus = b;
        
        // Strip off channel and just keep message type
        //
        b &= 0xF0;

        cbEvent = (b == MIDI_PRGMCHANGE || b == MIDI_CHANPRESS) ? 1 : 2;
        pMe->cbEvent = cbEvent + 1;

        if (pTs->cbLeft < cbEvent)
        {
            TRACKERR(pTs, gteChanMsgTrunc);
            pTs->fdwTrack |= ITS_F_ENDOFTRK;
            return FALSE;
        }

        pMe->abEvent[1] = *pTs->pTrackPointer++;
        if (cbEvent == 2)
            pMe->abEvent[2] = *pTs->pTrackPointer++;

        pTs->cbLeft -= cbEvent;
    } 
    else if (b == MIDI_SYSEX || b == MIDI_SYSEXEND)
    {
        // One of the SysEx types. (They are the same as far as we're concerned;
        // there is only a semantic difference in how the data would actually
        // get sent when the file is played. We must take care to put the correct
        // event type back on the output track, however.)
        //
        // Parse the general format of:
        //  BYTE    bEvent (MIDI_SYSEX or MIDI_SYSEXEND)
        //  VDWORD  cbParms
        //  BYTE    abParms[cbParms]
        //
        pMe->abEvent[0] = b;
        if (!GetTrackVDWord(pTs, &pMe->cbEvent))
        {
            TRACKERR(pTs, gteSysExLenTrunc);
            return FALSE;
        }
        
        if (pTs->cbLeft < pMe->cbEvent)
        {
            TRACKERR(pTs, gteSysExTrunc);
            pTs->fdwTrack |= ITS_F_ENDOFTRK;
            return FALSE;
        }

        pMe->pEvent = pTs->pTrackPointer;
        pTs->pTrackPointer += pMe->cbEvent;
        pTs->cbLeft -= pMe->cbEvent;
    } 
    else if (b == MIDI_META)
    {
        // It's a meta event. Parse the general form:
        //  BYTE    bEvent  (MIDI_META)
        //  BYTE    bClass
        //  VDWORD  cbParms
        //  BYTE    abParms[cbParms]
        //
        pMe->abEvent[0] = b;

        if (!pTs->cbLeft)
        {
            TRACKERR(pTs, gteMetaNoClass);
            pTs->fdwTrack |= ITS_F_ENDOFTRK;
            return FALSE;
        }

        pMe->abEvent[1] = *pTs->pTrackPointer++;
        --pTs->cbLeft;

        if (!GetTrackVDWord(pTs, &pMe->cbEvent))
        {   
            TRACKERR(pTs, gteMetaLenTrunc);
            return FALSE;
        }

        // NOTE: Perfectly valid to have a meta with no data
        // In this case, cbEvent == 0 and pEvent == NULL
        //
        if (pMe->cbEvent)
        {       
            if (pTs->cbLeft < pMe->cbEvent)
            {
                TRACKERR(pTs, gteMetaTrunc);
                pTs->fdwTrack |= ITS_F_ENDOFTRK;
                return FALSE;
            }

            pMe->pEvent = pTs->pTrackPointer;
            pTs->pTrackPointer += pMe->cbEvent;
            pTs->cbLeft -= pMe->cbEvent;
        }

        if (pMe->abEvent[1] == MIDI_META_EOT)
            pTs->fdwTrack |= ITS_F_ENDOFTRK;
    }
    else
    {
        // Messages in this range are system messages and aren't supposed to
        // be in a normal MIDI file. If they are, we've misparsed or the
        // authoring software is stpuid.
        //
        return FALSE;
    }

    // Event time was already stored as the current track time
    //
    pMe->tkEvent = pTs->tkNextEventDue;

    // Now update to the next event time. The code above MUST properly
    // maintain the end of track flag in case the end of track meta is
    // missing. 
    //
    if (!(pTs->fdwTrack & ITS_F_ENDOFTRK))
    {
        DWORD               tkDelta;

        if (!GetTrackVDWord(pTs, &tkDelta))
            return FALSE;

        pTs->tkNextEventDue += tkDelta;
    }

    return TRUE;
}

//
// AddEventToStream
//
// Put the given event onto the given output track.
// pMe must point to an event filled out in accordance with the
// description given in GetTrackEvent
//
// Returns TRUE on sucess or FALSE if we're out of memory
//
static BOOL         AddEventToStream(MEVENT *pMe)
{
    PDWORD                  pdw;        
    DWORD                   tkNow;
    DWORD                   tkDelta;    
    UINT                    cdw;

    tkNow = ots.tkTrack;

    // Delta time is absolute event time minus absolute time
    // already gone by on this track
    //
    tkDelta = pMe->tkEvent - ots.tkTrack;

    // Event time is now current time on this track
    //
    ots.tkTrack = pMe->tkEvent;

    if (pMe->abEvent[0] < MIDI_SYSEX)
    {
        // Channel message. We know how long it is, just copy it. Need 3 DWORD's: delta-t, 
        // stream-ID, event
        // 
        // TODO: Compress with running status
        //

        cdw = (fCompress ? 2 : 3);
        if (NULL == (pdw = (PDWORD)GetOutStreamBytes(tkNow, cdw * sizeof(DWORD), 3 * sizeof(DWORD))))
            return FALSE;

        *pdw++ = tkDelta;
        if (!fCompress)
            *pdw++ = 0;
        *pdw =  (pMe->abEvent[0]) |
                (((DWORD)pMe->abEvent[1]) << 8) |
                (((DWORD)pMe->abEvent[2]) << 16) |
                MIDS_SHORTMSG;

    }
    else if (pMe->abEvent[0] == MIDI_SYSEX || pMe->abEvent[0] == MIDI_SYSEXEND)
    {
        fprintf(stderr, "NOTE: Ignoring SysEx for now.\n");
    }
    else
    {
        // Better be a meta event.
        //  BYTE        bEvent
        //  BYTE        bClass
        //  VDWORD      cbParms
        //  BYTE        abParms[cbParms]
        //
        assert(pMe->abEvent[0] == MIDI_META);

        // The only meta-event we care about is change tempo
        //
        if (pMe->abEvent[1] != MIDI_META_TEMPO)
            return TRUE;

        assert(pMe->cbEvent == 3);

        cdw = (fCompress ? 2 : 3);
        pdw = (PDWORD)GetOutStreamBytes(tkNow, cdw * sizeof(DWORD), 3 * sizeof(DWORD));
        if (NULL == pdw)
            return FALSE;

        *pdw++ = tkDelta;
        if (!fCompress)
            *pdw++ = (DWORD)-1;
        *pdw =  (pMe->pEvent[2]) |
                (((DWORD)pMe->pEvent[1]) << 8) |
                (((DWORD)pMe->pEvent[0]) << 16) |
                MIDS_TEMPO;
    }

    return TRUE;    
}

//
// GetOutStreamBytes
//
// This function performs the memory management and pseudo-file I/O for output
// tracks.
// 
// We build a linked list of stream buffers as they would exist if they were
// about to be played. Each buffer is CB_STREAMBUF bytes long maximum. They are
// filled as full as possible; events are not allowed to cross buffers.
//
// Returns a pointer to the number of requested bytes or NULL if we're out of memory
//
static LPBYTE       GetOutStreamBytes(DWORD tkNow, DWORD cbNeeded, DWORD cbUncompressed)
{
    LPBYTE                  pb;

    // Round request up to the next DWORD boundry. This aligns the final output buffer correctly
    // and allows the above routines to deal with byte-aligned data
    //
    cbNeeded = (cbNeeded + 3) & ~3;
    cbUncompressed = (cbUncompressed + 3) & ~3;

    assert(cbUncompressed >= cbNeeded);

    if (NULL == ots.pLast || cbUncompressed > ots.pLast->cbLeftUncompressed)
    {
        PSTREAMBUF          pNew;

        pNew = GlobalAllocPtr(GHND, sizeof(*pNew) + CB_STREAMBUF);
        if (NULL == pNew)
            return NULL;
            
        pNew->pBuffer       = (LPBYTE)(pNew + 1);
        pNew->tkStart       = tkNow;
        pNew->pbNextEvent   = pNew->pBuffer;
        pNew->cbLeft        = CB_STREAMBUF;
        pNew->cbLeftUncompressed = CB_STREAMBUF;
        pNew->pNext         = NULL;

        if (!ots.pLast)
        {
            ots.pFirst = pNew;
            ots.pLast  = pNew;
        }
        else
        {
            ots.pLast->pNext    = pNew;
            ots.pLast           = pNew;
        }
    }

    // If there's STILL not enough room for the requested block, then an event is bigger than 
    // the buffer size -- this is unacceptable. 
    //
    if (cbNeeded > ots.pLast->cbLeft)
    {
        fprintf(stderr, "NOTE: An event requested %lu bytes of memory; the\n", cbNeeded);
        fprintf(stderr, "      maximum configured buffer size is %lu.\n", (DWORD)CB_STREAMBUF);

        return NULL;
    }

    pb = ots.pLast->pbNextEvent;

    ots.pLast->pbNextEvent += cbNeeded;
    ots.pLast->cbLeft -= cbNeeded;
    ots.pLast->cbLeftUncompressed -= cbUncompressed;

    return pb;
}

#ifdef _DEBUG
static void             ShowTrackError(INTRACKSTATE* pTs, char* szErr)
{
    fprintf(stderr, "Track %u: %s\n", pTs->nTrack, szErr);      
    fprintf(stderr, "Track offset %lu\n", (DWORD)(pTs->pTrackPointer - pTs->pTrack));
    fprintf(stderr, "Track total %lu  Track left %lu\n", pTs->cbTrack, pTs->cbLeft);
}
#endif
