///////////////////////////////////////////////////////////////////////////// // Copyright (C) 1998 by Jörg König // All rights reserved // // This file is part of the completely free tetris clone "CGTetris". // // This is free software. // You may redistribute it by any means providing it is not sold for profit // without the authors written consent. // // No warrantee of any kind, expressed or implied, is included with this // software; use at your own risk, responsibility for damages (if any) to // anyone resulting from the use of this software rests entirely with the // user. // // Send bug reports, bug fixes, enhancements, requests, flames, etc., and // I'll try to keep a version up to date. I can be reached as follows: // J.Koenig@adg.de (company site) // Joerg.Koenig@rhein-neckar.de (private site) ///////////////////////////////////////////////////////////////////////////// // Midi.cpp // // The CMIDI class is based on a sample in the DirectX SDK (mstream) #include "stdafx.h" #include "Midi.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif #define MThd 0x6468544D // Start of file #define MTrk 0x6B72544D // Start of track #define BUFFER_TIME_LENGTH 60 // Amount to fill in milliseconds // These structures are stored in MIDI files; they need to be byte aligned. // #pragma pack(1) // Contents of MThd chunk. struct MIDIFILEHDR { WORD wFormat; // Format (hi-lo) WORD wTrackCount; // # tracks (hi-lo) WORD wTimeDivision; // Time division (hi-lo) }; #pragma pack() // End of need for byte-aligned structures // 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)) 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"; static char gteNoMem[] = "Out of memory during malloc call"; ////////////////////////////////////////////////////////////////////// // CMIDI -- Construction/Destruction ////////////////////////////////////////////////////////////////////// CMIDI::CMIDI() : m_dwSoundSize(0) , m_pSoundData(0) , m_dwFormat(0) , m_dwTrackCount(0) , m_dwTimeDivision(0) , m_bPlaying(FALSE) , m_hStream(0) , m_dwProgressBytes(0) , m_bLooped(FALSE) , m_tkCurrentTime(0) , m_dwBufferTickLength(0) , m_dwCurrentTempo(0) , m_dwTempoMultiplier(100) , m_bInsertTempo(FALSE) , m_bBuffersPrepared(FALSE) , m_nCurrentBuffer(0) , m_uMIDIDeviceID(MIDI_MAPPER) , m_nEmptyBuffers(0) , m_bPaused(FALSE) , m_uCallbackStatus(0) , m_hBufferReturnEvent(0) , m_ptsTrack(0) , m_ptsFound(0) , m_dwStatus(0) , m_tkNext(0) , m_dwMallocBlocks(0) { m_hBufferReturnEvent = ::CreateEvent(0, FALSE, FALSE, TEXT("Wait For Buffer Return")); ASSERT(m_hBufferReturnEvent != 0); } CMIDI::~CMIDI() { Stop(FALSE); if(m_hBufferReturnEvent) ::CloseHandle(m_hBufferReturnEvent); } BOOL CMIDI::Create(UINT uResID, CWnd * pWndParent /* = NULL */) { return Create(MAKEINTRESOURCE(uResID), pWndParent); } BOOL CMIDI::Create(LPCTSTR pszResID, CWnd * pWndParent /* = NULL */) { ////////////////////////////////////////////////////////////////// // load resource HINSTANCE hApp = ::GetModuleHandle(0); ASSERT(hApp); HRSRC hResInfo = ::FindResource(hApp, pszResID, TEXT("MIDI")); if(hResInfo == 0) return FALSE; HGLOBAL hRes = ::LoadResource(hApp, hResInfo); if(hRes == 0) return FALSE; LPVOID pTheSound = ::LockResource(hRes); if(pTheSound == 0) return FALSE; DWORD dwTheSound = ::SizeofResource(hApp, hResInfo); return Create(pTheSound, dwTheSound, pWndParent); } BOOL CMIDI::Create(LPVOID pSoundData, DWORD dwSize, CWnd * pWndParent /* = NULL */) { if( m_pSoundData ) { // already created ASSERT(FALSE); return FALSE; } ASSERT(pSoundData != 0); ASSERT(dwSize > 0); register LPBYTE p = LPBYTE(pSoundData); // check header of MIDI if(*(DWORD*)p != MThd) { ASSERT(FALSE); return FALSE; } p += sizeof(DWORD); // check header size DWORD dwHeaderSize = DWORDSWAP(*(DWORD*)p); if( dwHeaderSize != sizeof(MIDIFILEHDR) ) { ASSERT(FALSE); return FALSE; } p += sizeof(DWORD); // get header MIDIFILEHDR hdr; ::CopyMemory(&hdr, p, dwHeaderSize); m_dwFormat = DWORD(WORDSWAP(hdr.wFormat)); m_dwTrackCount = DWORD(WORDSWAP(hdr.wTrackCount)); m_dwTimeDivision = DWORD(WORDSWAP(hdr.wTimeDivision)); p += dwHeaderSize; // create the array of tracks m_Tracks.resize(m_dwTrackCount); for(register DWORD i = 0; i < m_dwTrackCount; ++i) { // check header of track if(*(DWORD*)p != MTrk) { ASSERT(FALSE); return FALSE; } p += sizeof(DWORD); m_Tracks[i].dwTrackLength = DWORDSWAP(*(DWORD*)p); p += sizeof(DWORD); m_Tracks[i].pTrackStart = m_Tracks[i].pTrackCurrent = p; p += m_Tracks[i].dwTrackLength; // Handle bozo MIDI files which contain empty track chunks if( !m_Tracks[i].dwTrackLength ) { m_Tracks[i].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( &m_Tracks[i], &m_Tracks[i].tkNextEventDue )) { TRACE0("Error in MIDI data\n"); ASSERT(FALSE); return FALSE; } } m_pSoundData = pSoundData; m_dwSoundSize = dwSize; m_pWndParent = pWndParent; // allocate volume channels and initialise them m_Volumes.resize(NUM_CHANNELS, VOLUME_INIT); if( ! StreamBufferSetup() ) { ASSERT(FALSE); return FALSE; } return TRUE; } BOOL CMIDI :: Play(BOOL bInfinite /* = FALSE */) { if( IsPaused() ) { Continue(); return TRUE; } // calling Play() while it is already playing will restart from scratch if( IsPlaying() ) Stop(); // Clear the status of our callback so it will handle // MOM_DONE callbacks once more m_uCallbackStatus = 0; if( !m_bLooped ) m_bInsertTempo = TRUE; MMRESULT mmResult; if( (mmResult = midiStreamRestart(m_hStream)) != MMSYSERR_NOERROR ) { MidiError(mmResult); return FALSE; } m_bPlaying = TRUE; m_bLooped = bInfinite; return m_bPlaying; } BOOL CMIDI :: Stop(BOOL bReOpen /*=TRUE*/) { MMRESULT mmrRetVal; if( IsPlaying() || (m_uCallbackStatus != STATUS_CALLBACKDEAD) ) { m_bPlaying = m_bPaused = FALSE; if( m_uCallbackStatus != STATUS_CALLBACKDEAD && m_uCallbackStatus != STATUS_WAITINGFOREND ) m_uCallbackStatus = STATUS_KILLCALLBACK; if( (mmrRetVal = midiStreamStop(m_hStream) ) != MMSYSERR_NOERROR ) { MidiError(mmrRetVal); return FALSE; } if( (mmrRetVal = midiOutReset((HMIDIOUT)m_hStream)) != MMSYSERR_NOERROR ) { MidiError(mmrRetVal); return FALSE; } // Wait for the callback thread to release this thread, which it will do by // calling SetEvent() once all buffers are returned to it if( WaitForSingleObject( m_hBufferReturnEvent, DEBUG_CALLBACK_TIMEOUT ) == WAIT_TIMEOUT ) { // Note, this is a risky move because the callback may be genuinely busy, but // when we're debugging, it's safer and faster than freezing the application, // which leaves the MIDI device locked up and forces a system reset... TRACE0("Timed out waiting for MIDI callback\n"); m_uCallbackStatus = STATUS_CALLBACKDEAD; } } if( m_uCallbackStatus == STATUS_CALLBACKDEAD ) { m_uCallbackStatus = 0; FreeBuffers(); if( m_hStream ) { if( (mmrRetVal = midiStreamClose(m_hStream) ) != MMSYSERR_NOERROR ) { MidiError(mmrRetVal); } m_hStream = 0; } if( bReOpen ) { if( !StreamBufferSetup() ) { // Error setting up for MIDI file // Notification is already taken care of... return FALSE; } if( ! m_bLooped ) { Rewind(); m_dwProgressBytes = 0; m_dwStatus = 0; } } } return TRUE; } BOOL CMIDI :: Pause() { if( ! m_bPaused && m_bPlaying && m_pSoundData && m_hStream ) { midiStreamPause(m_hStream); m_bPaused = TRUE; } return FALSE; } BOOL CMIDI :: Continue() { if( m_bPaused && m_bPlaying && m_pSoundData && m_hStream ) { midiStreamRestart(m_hStream); m_bPaused = FALSE; } return FALSE; } BOOL CMIDI :: Rewind() { if( ! m_pSoundData ) return FALSE; for(register DWORD i = 0; i < m_dwTrackCount; ++i) { m_Tracks[i].pTrackCurrent = m_Tracks[i].pTrackStart; m_Tracks[i].byRunningStatus = 0; m_Tracks[i].tkNextEventDue = 0; m_Tracks[i].fdwTrack = 0; // Handle bozo MIDI files which contain empty track chunks if( !m_Tracks[i].dwTrackLength ) { m_Tracks[i].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( &m_Tracks[i], &m_Tracks[i].tkNextEventDue )) { TRACE0("Error in MIDI data\n"); ASSERT(FALSE); return FALSE; } } return TRUE; } DWORD CMIDI :: GetChannelCount() const { return m_Volumes.size(); } void CMIDI :: SetVolume(DWORD dwPercent) { const DWORD dwSize = m_Volumes.size(); for( register DWORD i = 0; i < dwSize; ++i ) SetChannelVolume(i, dwPercent); } DWORD CMIDI :: GetVolume() const { DWORD dwVolume = 0; const DWORD dwSize = m_Volumes.size(); for( register DWORD i = 0; i < dwSize; ++i ) dwVolume += GetChannelVolume(i); return dwVolume / GetChannelCount(); } void CMIDI :: SetChannelVolume(DWORD dwChannel, DWORD dwPercent) { ASSERT(dwChannel < m_Volumes.size()); if( !m_bPlaying ) return; m_Volumes[dwChannel] = (dwPercent > 100) ? 100 : dwPercent; DWORD dwEvent = MIDI_CTRLCHANGE | dwChannel | ((DWORD)MIDICTRL_VOLUME << 8) | ((DWORD)(m_Volumes[dwChannel]*VOLUME_MAX/100) << 16); MMRESULT mmrRetVal; if(( mmrRetVal = midiOutShortMsg((HMIDIOUT)m_hStream, dwEvent)) != MMSYSERR_NOERROR ) { MidiError(mmrRetVal); return; } } DWORD CMIDI :: GetChannelVolume(DWORD dwChannel) const { ASSERT(dwChannel < GetChannelCount()); return m_Volumes[dwChannel]; } void CMIDI :: SetTempo(DWORD dwPercent) { m_dwTempoMultiplier = dwPercent ? dwPercent : 1; m_bInsertTempo = TRUE; } DWORD CMIDI :: GetTempo() const { return m_dwTempoMultiplier; } void CMIDI :: SetInfinitePlay(BOOL bSet) { m_bLooped = bSet; } ////////////////////////////////////////////////////////////////////// // CMIDI -- implementation ////////////////////////////////////////////////////////////////////// // This function converts MIDI data from the track buffers setup by a // previous call to ConverterInit(). It will convert data until an error is // encountered or the output buffer has been filled with as much event data // as possible, not to exceed dwMaxLength. This function can take a couple // bit flags, passed through dwFlags. Information about the success/failure // of this operation and the number of output bytes actually converted will // be returned in the CONVERTINFO structure pointed at by lpciInfo. int CMIDI :: ConvertToBuffer(DWORD dwFlags, CONVERTINFO * lpciInfo) { int nChkErr; lpciInfo->dwBytesRecorded = 0; if( dwFlags & CONVERTF_RESET ) { m_dwProgressBytes = 0; m_dwStatus = 0; memset( &m_teTemp, 0, sizeof(TEMPEVENT)); m_ptsTrack = m_ptsFound = 0; } // If we were already done, then return with a warning... if( m_dwStatus & CONVERTF_STATUS_DONE ) { if( m_bLooped ) { Rewind(); m_dwProgressBytes = 0; m_dwStatus = 0; } else return CONVERTERR_DONE; } else if( m_dwStatus & CONVERTF_STATUS_STUCK ) { // The caller is asking us to continue, but we're already hosed because we // previously identified something as corrupt, so complain louder this time. return( CONVERTERR_STUCK ); } else if( m_dwStatus & CONVERTF_STATUS_GOTEVENT ) { // Turn off this bit flag m_dwStatus ^= CONVERTF_STATUS_GOTEVENT; // The following code for this case is duplicated from below, and is // designed to handle a "straggler" event, should we have one left over // from previous processing the last time this function was called. // Don't add end of track event 'til we're done if( m_teTemp.byShortData[0] == MIDI_META && m_teTemp.byShortData[1] == MIDI_META_EOT ) { if( m_dwMallocBlocks ) { delete [] m_teTemp.pLongData; --m_dwMallocBlocks; } } else if(( nChkErr = AddEventToStreamBuffer( &m_teTemp, lpciInfo )) != CONVERTERR_NOERROR ) { if( nChkErr == CONVERTERR_BUFFERFULL ) { // Do some processing and tell caller that this buffer's full m_dwStatus |= CONVERTF_STATUS_GOTEVENT; return CONVERTERR_NOERROR; } else if( nChkErr == CONVERTERR_METASKIP ) { // We skip by all meta events that aren't tempo changes... } else { TRACE0("Unable to add event to stream buffer.\n"); if( m_dwMallocBlocks ) { delete [] m_teTemp.pLongData; m_dwMallocBlocks--; } return( TRUE ); } } } for(;;) { m_ptsFound = 0; m_tkNext = 0xFFFFFFFFL; // Find nearest event due for( register DWORD idx = 0; idx < m_Tracks.size(); ++idx ) { m_ptsTrack = &m_Tracks[idx]; if( !(m_ptsTrack->fdwTrack & ITS_F_ENDOFTRK) && (m_ptsTrack->tkNextEventDue < m_tkNext) ) { m_tkNext = m_ptsTrack->tkNextEventDue; m_ptsFound = m_ptsTrack; } } // None found? We must be done, so return to the caller with a smile. if( !m_ptsFound ) { m_dwStatus |= CONVERTF_STATUS_DONE; // Need to set return buffer members properly return CONVERTERR_NOERROR; } // Ok, get the event header from that track if( !GetTrackEvent( m_ptsFound, &m_teTemp )) { // Warn future calls that this converter is stuck at a corrupt spot // and can't continue m_dwStatus |= CONVERTF_STATUS_STUCK; return CONVERTERR_CORRUPT; } // Don't add end of track event 'til we're done if( m_teTemp.byShortData[0] == MIDI_META && m_teTemp.byShortData[1] == MIDI_META_EOT ) { if( m_dwMallocBlocks ) { delete [] m_teTemp.pLongData; --m_dwMallocBlocks; } continue; } if(( nChkErr = AddEventToStreamBuffer( &m_teTemp, lpciInfo )) != CONVERTERR_NOERROR ) { if( nChkErr == CONVERTERR_BUFFERFULL ) { // Do some processing and tell somebody this buffer is full... m_dwStatus |= CONVERTF_STATUS_GOTEVENT; return CONVERTERR_NOERROR; } else if( nChkErr == CONVERTERR_METASKIP ) { // We skip by all meta events that aren't tempo changes... } else { TRACE0("Unable to add event to stream buffer.\n"); if( m_dwMallocBlocks ) { delete [] m_teTemp.pLongData; m_dwMallocBlocks--; } return TRUE; } } } return CONVERTERR_NOERROR; } // GetTrackEvent // // Fills in the event struct with the next event from the track // // pteTemp->tkEvent will contain the absolute tick time of the event // pteTemp->byShortData[0] will contain // MIDI_META if the event is a meta event; // in this case pteTemp->byShortData[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 pteTemp->byShortData[1] // and pteTemp->byShortData[2] will contain the rest of the event. // // pteTemp->dwEventLength will contain // The total length of the channel message in pteTemp->byShortData if // the event is a channel message // The total length of the paramter data pointed to by // pteTemp->pLongData otherwise // // pteTemp->pLongData 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. // ptsTrack->pTrackPointers, and ptsTrack->byRunningStatus). // BOOL CMIDI :: GetTrackEvent(TRACK * ptsTrack, TEMPEVENT * pteTemp) { DWORD idx; UINT dwEventLength; // Clear out the temporary event structure to get rid of old data... memset( pteTemp, 0, sizeof(TEMPEVENT)); // Already at end of track? There's nothing to read. if( ptsTrack->fdwTrack & ITS_F_ENDOFTRK ) return FALSE; // Get the first byte, which determines the type of event. BYTE byByte; if( !GetTrackByte(ptsTrack, &byByte) ) return FALSE; // 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( !(byByte & 0x80) ) { // No previous status byte? We're hosed. if( !ptsTrack->byRunningStatus ) { TrackError(ptsTrack, gteBadRunStat); return FALSE; } pteTemp->byShortData[0] = ptsTrack->byRunningStatus; pteTemp->byShortData[1] = byByte; byByte = pteTemp->byShortData[0] & 0xF0; pteTemp->dwEventLength = 2; // Only program change and channel pressure events are 2 bytes long; // the rest are 3 and need another byte if(( byByte != MIDI_PRGMCHANGE ) && ( byByte != MIDI_CHANPRESS )) { if( !GetTrackByte( ptsTrack, &pteTemp->byShortData[2] )) return FALSE; ++pteTemp->dwEventLength; } } else if(( byByte & 0xF0 ) != MIDI_SYSEX ) { // Not running status, not in SysEx range - must be // normal channel message (0x80-0xEF) pteTemp->byShortData[0] = byByte; ptsTrack->byRunningStatus = byByte; // Strip off channel and just keep message type byByte &= 0xF0; dwEventLength = ( byByte == MIDI_PRGMCHANGE || byByte == MIDI_CHANPRESS ) ? 1 : 2; pteTemp->dwEventLength = dwEventLength + 1; if( !GetTrackByte( ptsTrack, &pteTemp->byShortData[1] )) return FALSE; if( dwEventLength == 2 ) if( !GetTrackByte( ptsTrack, &pteTemp->byShortData[2] )) return FALSE; } else if(( byByte == MIDI_SYSEX ) || ( byByte == 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 proper // 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] pteTemp->byShortData[0] = byByte; if( !GetTrackVDWord( ptsTrack, &pteTemp->dwEventLength )) { TrackError( ptsTrack, gteSysExLenTrunc ); return FALSE; } // Malloc a temporary memory block to hold the parameter data pteTemp->pLongData = new BYTE [pteTemp->dwEventLength]; if( pteTemp->pLongData == 0 ) { TrackError( ptsTrack, gteNoMem ); return FALSE; } // Increment our counter, which tells the program to look around for // a malloc block to free, should it need to exit or reset before the // block would normally be freed ++m_dwMallocBlocks; // Copy from the input buffer to the parameter data buffer for( idx = 0; idx < pteTemp->dwEventLength; idx++ ) if( !GetTrackByte( ptsTrack, pteTemp->pLongData + idx )) { TrackError( ptsTrack, gteSysExTrunc ); return FALSE; } } else if( byByte == MIDI_META ) { // It's a meta event. Parse the general form: // BYTE bEvent (MIDI_META) // BYTE bClass // VDWORD cbParms // BYTE abParms[cbParms] pteTemp->byShortData[0] = byByte; if( !GetTrackByte( ptsTrack, &pteTemp->byShortData[1] )) return FALSE; if( !GetTrackVDWord( ptsTrack, &pteTemp->dwEventLength )) { TrackError( ptsTrack, gteMetaLenTrunc ); return FALSE; } // NOTE: It's perfectly valid to have a meta with no data // In this case, dwEventLength == 0 and pLongData == NULL if( pteTemp->dwEventLength ) { // Malloc a temporary memory block to hold the parameter data pteTemp->pLongData = new BYTE [pteTemp->dwEventLength]; if( pteTemp->pLongData == 0 ) { TrackError( ptsTrack, gteNoMem ); return FALSE; } // Increment our counter, which tells the program to look around for // a malloc block to free, should it need to exit or reset before the // block would normally be freed ++m_dwMallocBlocks; // Copy from the input buffer to the parameter data buffer for( idx = 0; idx < pteTemp->dwEventLength; idx++ ) if( !GetTrackByte( ptsTrack, pteTemp->pLongData + idx )) { TrackError( ptsTrack, gteMetaTrunc ); return FALSE; } } if( pteTemp->byShortData[1] == MIDI_META_EOT ) ptsTrack->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 either misparsed or the // authoring software is stupid. return FALSE; } // Event time was already stored as the current track time pteTemp->tkEvent = ptsTrack->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. NOTE: This code is a continuation of the track event // time pre-read which is done at the end of track initialization. if( !( ptsTrack->fdwTrack & ITS_F_ENDOFTRK )) { DWORD tkDelta; if( !GetTrackVDWord( ptsTrack, &tkDelta )) return FALSE; ptsTrack->tkNextEventDue += tkDelta; } 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. BOOL CMIDI :: GetTrackVDWord(TRACK * ptsTrack, LPDWORD lpdw) { ASSERT(ptsTrack != 0); ASSERT(lpdw != 0); if( ptsTrack->fdwTrack & ITS_F_ENDOFTRK ) return FALSE; BYTE byByte; DWORD dw = 0; do { if( !GetTrackByte( ptsTrack, &byByte )) return FALSE; dw = ( dw << 7 ) | ( byByte & 0x7F ); } while( byByte & 0x80 ); *lpdw = dw; return TRUE; } // AddEventToStreamBuffer // // Put the given event into the given stream buffer at the given location // pteTemp must point to an event filled out in accordance with the // description given in GetTrackEvent // // Handles its own error notification by displaying to the appropriate // output device (either our debugging window, or the screen). int CMIDI :: AddEventToStreamBuffer( TEMPEVENT * pteTemp, CONVERTINFO *lpciInfo ) { MIDIEVENT * pmeEvent = (MIDIEVENT *)( lpciInfo->mhBuffer.lpData + lpciInfo->dwStartOffset + lpciInfo->dwBytesRecorded ); // When we see a new, empty buffer, set the start time on it... if( !lpciInfo->dwBytesRecorded ) lpciInfo->tkStart = m_tkCurrentTime; // Use the above set start time to figure out how much longer we should fill // this buffer before officially declaring it as "full" if( m_tkCurrentTime - lpciInfo->tkStart > m_dwBufferTickLength ) if( lpciInfo->bTimesUp ) { lpciInfo->bTimesUp = FALSE; return CONVERTERR_BUFFERFULL; } else lpciInfo->bTimesUp = TRUE; DWORD tkNow = m_tkCurrentTime; // Delta time is absolute event time minus absolute time // already gone by on this track DWORD tkDelta = pteTemp->tkEvent - m_tkCurrentTime; // Event time is now current time on this track m_tkCurrentTime = pteTemp->tkEvent; if( m_bInsertTempo ) { m_bInsertTempo = FALSE; if( lpciInfo->dwMaxLength-lpciInfo->dwBytesRecorded < 3*sizeof(DWORD)) { // Cleanup from our write operation return CONVERTERR_BUFFERFULL; } if( m_dwCurrentTempo ) { pmeEvent->dwDeltaTime = 0; pmeEvent->dwStreamID = 0; pmeEvent->dwEvent = ( m_dwCurrentTempo * 100 ) / m_dwTempoMultiplier; pmeEvent->dwEvent |= (((DWORD)MEVT_TEMPO ) << 24 ) | MEVT_F_SHORT; lpciInfo->dwBytesRecorded += 3 * sizeof(DWORD); pmeEvent += 3 * sizeof(DWORD); } } if( pteTemp->byShortData[0] < MIDI_SYSEX ) { // Channel message. We know how long it is, just copy it. // Need 3 DWORD's: delta-t, stream-ID, event if( lpciInfo->dwMaxLength-lpciInfo->dwBytesRecorded < 3*sizeof(DWORD)) { // Cleanup from our write operation return CONVERTERR_BUFFERFULL; } pmeEvent->dwDeltaTime = tkDelta; pmeEvent->dwStreamID = 0; pmeEvent->dwEvent = ( pteTemp->byShortData[0] ) | (((DWORD)pteTemp->byShortData[1] ) << 8 ) | (((DWORD)pteTemp->byShortData[2] ) << 16 ) | MEVT_F_SHORT; if((( pteTemp->byShortData[0] & 0xF0) == MIDI_CTRLCHANGE ) && ( pteTemp->byShortData[1] == MIDICTRL_VOLUME )) { // If this is a volume change, generate a callback so we can grab // the new volume for our cache pmeEvent->dwEvent |= MEVT_F_CALLBACK; } lpciInfo->dwBytesRecorded += 3 *sizeof(DWORD); } else if(( pteTemp->byShortData[0] == MIDI_SYSEX ) || ( pteTemp->byShortData[0] == MIDI_SYSEXEND )) { TRACE0("AddEventToStreamBuffer: Ignoring SysEx event.\n"); if( m_dwMallocBlocks ) { delete [] pteTemp->pLongData; --m_dwMallocBlocks; } } else { // Better be a meta event. // BYTE byEvent // BYTE byEventType // VDWORD dwEventLength // BYTE pLongEventData[dwEventLength] ASSERT( pteTemp->byShortData[0] == MIDI_META ); // The only meta-event we care about is change tempo if( pteTemp->byShortData[1] != MIDI_META_TEMPO ) { if( m_dwMallocBlocks ) { delete [] pteTemp->pLongData; --m_dwMallocBlocks; } return CONVERTERR_METASKIP; } // We should have three bytes of parameter data... ASSERT(pteTemp->dwEventLength == 3); // Need 3 DWORD's: delta-t, stream-ID, event data if( lpciInfo->dwMaxLength - lpciInfo->dwBytesRecorded < 3 *sizeof(DWORD)) { // Cleanup the temporary event if necessary and return if( m_dwMallocBlocks ) { delete [] pteTemp->pLongData; --m_dwMallocBlocks; } return CONVERTERR_BUFFERFULL; } pmeEvent->dwDeltaTime = tkDelta; pmeEvent->dwStreamID = 0; // Note: this is backwards from above because we're converting a single // data value from hi-lo to lo-hi format... pmeEvent->dwEvent = ( pteTemp->pLongData[2] ) | (((DWORD)pteTemp->pLongData[1] ) << 8 ) | (((DWORD)pteTemp->pLongData[0] ) << 16 ); // This next step has absolutely nothing to do with the conversion of a // MIDI file to a stream, it's simply put here to add the functionality // of the tempo slider. If you don't need this, be sure to remove the // next two lines. m_dwCurrentTempo = pmeEvent->dwEvent; pmeEvent->dwEvent = (pmeEvent->dwEvent * 100 ) / m_dwTempoMultiplier; pmeEvent->dwEvent |= (((DWORD)MEVT_TEMPO ) << 24 ) | MEVT_F_SHORT; m_dwBufferTickLength = (m_dwTimeDivision * 1000 * BUFFER_TIME_LENGTH) / m_dwCurrentTempo; TRACE1("m_dwBufferTickLength = %lu\n", m_dwBufferTickLength); if( m_dwMallocBlocks ) { delete [] pteTemp->pLongData; --m_dwMallocBlocks; } lpciInfo->dwBytesRecorded += 3 *sizeof(DWORD); } return CONVERTERR_NOERROR; } // StreamBufferSetup() // // Opens a MIDI stream. Then it goes about converting the data into a midiStream buffer for playback. BOOL CMIDI :: StreamBufferSetup() { int nChkErr; BOOL bFoundEnd = FALSE; MMRESULT mmrRetVal; if( !m_hStream ) if(( mmrRetVal = midiStreamOpen( &m_hStream, &m_uMIDIDeviceID, DWORD(1), DWORD(MidiProc), DWORD(this), CALLBACK_FUNCTION )) != MMSYSERR_NOERROR ) { MidiError(mmrRetVal); return FALSE; } // allocate stream buffers and initialise them m_StreamBuffers.resize(NUM_STREAM_BUFFERS); MIDIPROPTIMEDIV mptd; mptd.cbStruct = sizeof(mptd); mptd.dwTimeDiv = m_dwTimeDivision; if(( mmrRetVal = midiStreamProperty( m_hStream, (LPBYTE)&mptd, MIDIPROP_SET | MIDIPROP_TIMEDIV )) != MMSYSERR_NOERROR ) { MidiError( mmrRetVal ); return FALSE; } m_nEmptyBuffers = 0; DWORD dwConvertFlag = CONVERTF_RESET; for( m_nCurrentBuffer = 0; m_nCurrentBuffer < NUM_STREAM_BUFFERS; m_nCurrentBuffer++ ) { m_StreamBuffers[m_nCurrentBuffer].mhBuffer.dwBufferLength = OUT_BUFFER_SIZE; m_StreamBuffers[m_nCurrentBuffer].mhBuffer.lpData = new char [OUT_BUFFER_SIZE]; if( m_StreamBuffers[m_nCurrentBuffer].mhBuffer.lpData == 0 ) return FALSE; // Tell the converter to convert up to one entire buffer's length of output // data. Also, set a flag so it knows to reset any saved state variables it // may keep from call to call. m_StreamBuffers[m_nCurrentBuffer].dwStartOffset = 0; m_StreamBuffers[m_nCurrentBuffer].dwMaxLength = OUT_BUFFER_SIZE; m_StreamBuffers[m_nCurrentBuffer].tkStart = 0; m_StreamBuffers[m_nCurrentBuffer].bTimesUp = FALSE; if(( nChkErr = ConvertToBuffer( dwConvertFlag, &m_StreamBuffers[m_nCurrentBuffer] )) != CONVERTERR_NOERROR ) { if( nChkErr == CONVERTERR_DONE ) { bFoundEnd = TRUE; } else { TRACE0("Initial conversion pass failed\n"); return FALSE; } } m_StreamBuffers[m_nCurrentBuffer].mhBuffer.dwBytesRecorded = m_StreamBuffers[m_nCurrentBuffer].dwBytesRecorded; if( !m_bBuffersPrepared ) if(( mmrRetVal = midiOutPrepareHeader( (HMIDIOUT)m_hStream, &m_StreamBuffers[m_nCurrentBuffer].mhBuffer, sizeof(MIDIHDR))) != MMSYSERR_NOERROR ) { MidiError( mmrRetVal ); return FALSE; } if(( mmrRetVal = midiStreamOut( m_hStream, &m_StreamBuffers[m_nCurrentBuffer].mhBuffer, sizeof(MIDIHDR))) != MMSYSERR_NOERROR ) { MidiError(mmrRetVal); break; } dwConvertFlag = 0; if( bFoundEnd ) break; } m_bBuffersPrepared = TRUE; m_nCurrentBuffer = 0; return TRUE; } // This function unprepares and frees all our buffers -- something we must // do to work around a bug in MMYSYSTEM that prevents a device from playing // back properly unless it is closed and reopened after each stop. void CMIDI :: FreeBuffers() { DWORD idx; MMRESULT mmrRetVal; if( m_bBuffersPrepared ) { for( idx = 0; idx < NUM_STREAM_BUFFERS; idx++ ) if(( mmrRetVal = midiOutUnprepareHeader( (HMIDIOUT)m_hStream, &m_StreamBuffers[idx].mhBuffer, sizeof(MIDIHDR))) != MMSYSERR_NOERROR ) { MidiError(mmrRetVal); } m_bBuffersPrepared = FALSE; } // Free our stream buffers... for( idx = 0; idx < NUM_STREAM_BUFFERS; idx++ ) if( m_StreamBuffers[idx].mhBuffer.lpData ) { delete [] m_StreamBuffers[idx].mhBuffer.lpData; m_StreamBuffers[idx].mhBuffer.lpData = 0; } } ////////////////////////////////////////////////////////////////////// // CMIDI -- error handling ////////////////////////////////////////////////////////////////////// void CMIDI :: MidiError(MMRESULT mmResult) { #ifdef _DEBUG char chText[512]; midiOutGetErrorText(mmResult, chText, sizeof(chText)); TRACE1("Midi error: %hs\n", chText); #endif } void CMIDI :: TrackError(TRACK * ptsTrack, LPSTR lpszErr ) { TRACE1("Track buffer offset %lu\n", (DWORD)(ptsTrack->pTrackCurrent - ptsTrack->pTrackStart)); TRACE1("Track total length %lu\n", ptsTrack->dwTrackLength); TRACE1("%hs\n", lpszErr); } ////////////////////////////////////////////////////////////////////// // CMIDI -- overridables ////////////////////////////////////////////////////////////////////// void CMIDI :: OnMidiOutOpen() { } void CMIDI :: OnMidiOutDone(MIDIHDR & rHdr) { if( m_uCallbackStatus == STATUS_CALLBACKDEAD ) return; ++m_nEmptyBuffers; if( m_uCallbackStatus == STATUS_WAITINGFOREND ) { if( m_nEmptyBuffers < NUM_STREAM_BUFFERS ) return; else { m_uCallbackStatus = STATUS_CALLBACKDEAD; Stop(); SetEvent(m_hBufferReturnEvent); return; } } // This flag is set whenever the callback is waiting for all buffers to // come back. if( m_uCallbackStatus == STATUS_KILLCALLBACK ) { // Count NUM_STREAM_BUFFERS-1 being returned for the last time if( m_nEmptyBuffers < NUM_STREAM_BUFFERS ) return; else { // Change the status to callback dead m_uCallbackStatus = STATUS_CALLBACKDEAD; SetEvent(m_hBufferReturnEvent); return; } } m_dwProgressBytes += m_StreamBuffers[m_nCurrentBuffer].mhBuffer.dwBytesRecorded; /////////////////////////////////////////////////////////////////////////////// // Fill an available buffer with audio data again... if( m_bPlaying && m_nEmptyBuffers ) { m_StreamBuffers[m_nCurrentBuffer].dwStartOffset = 0; m_StreamBuffers[m_nCurrentBuffer].dwMaxLength = OUT_BUFFER_SIZE; m_StreamBuffers[m_nCurrentBuffer].tkStart = 0; m_StreamBuffers[m_nCurrentBuffer].dwBytesRecorded = 0; m_StreamBuffers[m_nCurrentBuffer].bTimesUp = FALSE; int nChkErr; if(( nChkErr = ConvertToBuffer( 0, &m_StreamBuffers[m_nCurrentBuffer] )) != CONVERTERR_NOERROR ) { if( nChkErr == CONVERTERR_DONE ) { m_uCallbackStatus = STATUS_WAITINGFOREND; return; } else { TRACE0("MidiProc() conversion pass failed!\n"); return; } } m_StreamBuffers[m_nCurrentBuffer].mhBuffer.dwBytesRecorded = m_StreamBuffers[m_nCurrentBuffer].dwBytesRecorded; MMRESULT mmrRetVal; if( (mmrRetVal = midiStreamOut(m_hStream, &m_StreamBuffers[m_nCurrentBuffer].mhBuffer, sizeof(MIDIHDR))) != MMSYSERR_NOERROR ) { MidiError(mmrRetVal); return; } m_nCurrentBuffer = ( m_nCurrentBuffer + 1 ) % NUM_STREAM_BUFFERS; m_nEmptyBuffers--; } } void CMIDI :: OnMidiOutPositionCB(MIDIHDR & rHdr, MIDIEVENT & rEvent) { if( MIDIEVENT_TYPE(rEvent.dwEvent) == MIDI_CTRLCHANGE ) { if( MIDIEVENT_DATA1(rEvent.dwEvent) == MIDICTRL_VOLUME ) { // Mask off the channel number and cache the volume data byte m_Volumes[MIDIEVENT_CHANNEL(rEvent.dwEvent)] = DWORD(MIDIEVENT_VOLUME(rEvent.dwEvent)*100/VOLUME_MAX); if( m_pWndParent && ::IsWindow(m_pWndParent->GetSafeHwnd()) ) // Do not use SendMessage(), because a change of the midi stream has no effect // during callback handling, so if the owner wants to adjust the volume, as a // result of the windows message, (s)he will not hear that change. m_pWndParent->PostMessage( WM_MIDI_VOLUMECHANGED, WPARAM(this), LPARAM( MAKELONG( WORD(MIDIEVENT_CHANNEL(rEvent.dwEvent)), WORD(MIDIEVENT_VOLUME(rEvent.dwEvent)*100/VOLUME_MAX) ) ) ); } } } void CMIDI :: OnMidiOutClose() { } ////////////////////////////////////////////////////////////////////// // CMIDI -- static members ////////////////////////////////////////////////////////////////////// void CMIDI :: MidiProc(HMIDIOUT hMidi, UINT uMsg, DWORD dwInstanceData, DWORD dwParam1, DWORD dwParam2) { CMIDI * pMidi = (CMIDI *) dwInstanceData; ASSERT(pMidi != 0); MIDIHDR * pHdr = (MIDIHDR*) dwParam1; switch(uMsg) { case MOM_OPEN: pMidi->OnMidiOutOpen(); break; case MOM_CLOSE: pMidi->OnMidiOutClose(); break; case MOM_DONE: ASSERT(pHdr != 0); pMidi->OnMidiOutDone(*pHdr); break; case MOM_POSITIONCB: ASSERT(pHdr != 0); pMidi->OnMidiOutPositionCB(*pHdr, *((MIDIEVENT*)(pHdr->lpData + pHdr->dwOffset))); break; default: break; } }