/*========================================================================== * * Copyright (C) 1995-1996 Microsoft Corporation. All Rights Reserved. * * File: mid2strm.c * Content: Converts a MIDI file into a MDS (MidiStream) File. * * [RH] Adapted this code from the DirectX 5 SDK for use with ZDoom * ***************************************************************************/ #include #include #include #include #include "doomtype.h" #include "mid2strm.h" #include "m_alloc.h" // 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() // 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; // A few globals // DWORD midTimeDiv; byte UsedPatches[256]; static INFILESTATE ifs; OUTSTREAMSTATE ots; // 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 (LPBYTE in, DWORD insize); static PSTREAMBUF BuildNewTracks(void); static LPBYTE GetInFileData(DWORD cbToGet); static BOOL GetTrackVDWord(INTRACKSTATE* pTs, LPDWORD lpdw); static BOOL GetTrackEvent(INTRACKSTATE* pTs, MEVENT *pMe); #ifdef _DEBUG static void ShowTrackError(INTRACKSTATE* pTs, char* szErr); #endif // Init // // 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 (LPBYTE in, DWORD insize) { BOOL fRet = FALSE; LPDWORD lpdwTag; LPDWORD lpcbHeader; DWORD cbHeader; MIDIFILEHDR* pHeader; INTRACKSTATE* pTs; UINT idx; memset (UsedPatches, 0, sizeof(UsedPatches)); // Initialize things we'll try to free later if we fail // ifs.cbFile = insize; ifs.pFile = in; ifs.apIts = NULL; // 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))) { Printf (PRINT_HIGH, 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*)malloc(ifs.cTrack*sizeof(INTRACKSTATE)); if (NULL == ifs.apIts) { Printf (PRINT_HIGH, 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)))) { Printf (PRINT_HIGH, szInitErrInFile); goto Init_Cleanup; } cbHeader = DWORDSWAP(*lpcbHeader); pTs->cbTrack = cbHeader; pTs->cbLeft = cbHeader; pTs->pTrack = GetInFileData(cbHeader); if (NULL == pTs->pTrack) { Printf (PRINT_HIGH, 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)) { Printf (PRINT_HIGH, szInitErrInFile); goto Init_Cleanup; } } ots.tkTrack = 0; ots.pFirst = NULL; ots.pLast = NULL; fRet = TRUE; Init_Cleanup: if (!fRet) mid2strmCleanup(); 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 // void mid2strmCleanup(void) { PSTREAMBUF pCurr; PSTREAMBUF pNext; if (ifs.apIts) free(ifs.apIts); pCurr = ots.pFirst; while (pCurr) { pNext = pCurr->pNext; free(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 PSTREAMBUF 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)) { Printf (PRINT_HIGH, "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)) { Printf (PRINT_HIGH, "Out of memory building tracks.\n"); return NULL; } } midTimeDiv = ifs.dwTimeDivision; return ots.pFirst; } // // 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 stupid. // 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 // 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 // if (NULL == (pdw = (PDWORD)GetOutStreamBytes(tkNow, 3 * sizeof(DWORD)))) return FALSE; *pdw++ = tkDelta; *pdw++ = 0; *pdw = (pMe->abEvent[0]) | (((DWORD)pMe->abEvent[1]) << 8) | (((DWORD)pMe->abEvent[2]) << 16) | MIDS_SHORTMSG; if ((pMe->abEvent[0] & 0xf0) == MIDI_PRGMCHANGE) UsedPatches[pMe->abEvent[1] & 127] = 1; else if (pMe->abEvent[0] == (MIDI_NOTEON | 10)) UsedPatches[128+(pMe->abEvent[1] & 127)] = 1; } else if (pMe->abEvent[0] == MIDI_SYSEX || pMe->abEvent[0] == MIDI_SYSEXEND) { Printf (PRINT_HIGH, "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 = 3; pdw = (PDWORD)GetOutStreamBytes(tkNow, 3 * sizeof(DWORD)); if (NULL == pdw) return FALSE; *pdw++ = tkDelta; *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) { 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; if (NULL == ots.pLast || cbNeeded > ots.pLast->cbLeft) { PSTREAMBUF pNew; pNew = malloc(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->pNext = NULL; pNew->prepared = false; 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) { Printf (PRINT_HIGH, "NOTE: An event requested %lu bytes of memory; the\n", cbNeeded); Printf (PRINT_HIGH, " maximum configured buffer size is %lu.\n", (DWORD)CB_STREAMBUF); return NULL; } pb = ots.pLast->pbNextEvent; ots.pLast->pbNextEvent += cbNeeded; ots.pLast->cbLeft -= cbNeeded; return pb; } PSTREAMBUF mid2strmConvert (BYTE *inFile, DWORD inSize) { if (Init (inFile, inSize)) { PSTREAMBUF buf = BuildNewTracks (); if (!buf) mid2strmCleanup (); return buf; } return NULL; } #ifdef _DEBUG static void ShowTrackError(INTRACKSTATE* pTs, char* szErr) { Printf (PRINT_HIGH, "Track %u: %s\n", pTs->nTrack, szErr); Printf (PRINT_HIGH, "Track offset %lu\n", (DWORD)(pTs->pTrackPointer - pTs->pTrack)); Printf (PRINT_HIGH, "Track total %lu Track left %lu\n", pTs->cbTrack, pTs->cbLeft); } #endif