/* Copyright (c) 1998-1999 Microsoft Corporation */ /* * @Doc DMusic16 * * @Module MIDIIn.c - Legacy MIDI capture emulation for DirectMusic | */ #pragma warning(disable:4704) /* Inline assembly */ #include #include #include #include "dmusic16.h" #include "debug.h" #define IS_STATUS_BYTE(x) ((x) & 0x80) #define IS_CHANNEL_MSG(x) (((x) & 0xF0) != 0xF0) #define IS_SYSEX(x) ((x) == 0xF0) #define SYSEX_SIZE 4096 /* (65535 - sizeof(MIDIHDR) - sizeof(EVENT) - sizeof(SEGHDR)) */ #define SYSEX_BUFFERS 8 /* Keep 2 buffers outstanding */ static unsigned cbChanMsg[16] = { 0, 0, 0, 0, 0, 0, 0, 0, /* Running status */ 3, 3, 3, 3, 2, 2, 3, 0 }; static unsigned cbSysCommData[16] = { 1, 2, 3, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 }; VOID CALLBACK _loadds midiInProc(HMIDIIN hMidiIn, UINT wMsg, DWORD dwInstance, DWORD dwParam1, DWORD dwParam2); STATIC BOOL NEAR PASCAL RecordShortEvent(NPOPENHANDLE poh, DWORD dwMessage, DWORD dwTime); STATIC BOOL NEAR PASCAL RecordSysExEvent(NPOPENHANDLE poh, LPMIDIHDR lpmh, DWORD dwTime); STATIC VOID NEAR PASCAL NotifyClientList(LPOPENHANDLE poh); STATIC VOID NEAR PASCAL ThruClientList(LPOPENHANDLE poh, DWORD dwMessage); STATIC VOID NEAR PASCAL RefillFreeEventList(NPOPENHANDLE poh); STATIC VOID NEAR PASCAL MidiInFlushQueues(NPOPENHANDLE poh); #pragma alloc_text(INIT_TEXT, MidiOutOnLoad) #pragma alloc_text(FIX_IN_TEXT, midiInProc) #pragma alloc_text(FIX_IN_TEXT, RecordShortEvent) #pragma alloc_text(FIX_IN_TEXT, RecordSysExEvent) #pragma alloc_text(FIX_IN_TEXT, NotifyClientList) #pragma alloc_text(FIX_IN_TEXT, ThruClientList) /* @func Called at DLL * * @comm * * Currently does nothing. * */ VOID PASCAL MidiInOnLoad(VOID) { } /* @func Called at DLL * * @comm * * Currently does nothing */ VOID PASCAL MidiInOnExit() { } /* @func Open a MIDI in device * * @rdesc Returns one of the following: * @flag MMSYSERR_NOERROR | on success * @flag MMSYSERR_NOMEM | on out of memory * * @comm * * Makes sure only one client is opening the device. * * Opens the device and starts MIDI input on it, noting the time of the start for timestamp calculations. */ MMRESULT PASCAL MidiInOnOpen( NPOPENHANDLEINSTANCE pohi) /* @parm The open handle instance to fulfill */ { NPOPENHANDLE poh = pohi->pHandle; int iChannel; MMRESULT mmr; /* Protect here against more than one client opening an input device. */ if (poh->uReferenceCount > 1) { return MMSYSERR_ALLOCATED; } /* Per client initialize thruing to NULL. */ pohi->pThru = (NPTHRUCHANNEL)LocalAlloc(LPTR, MIDI_CHANNELS * sizeof(THRUCHANNEL)); if (pohi->pThru == NULL) { return MMSYSERR_NOMEM; } DPF(2, "MidiInOnOpen: pohi %04X pThru %04X", pohi, pohi->pThru); for (iChannel = 0; iChannel < MIDI_CHANNELS; iChannel++) { pohi->pThru[iChannel].pohi = (HANDLE)NULL; } return MMSYSERR_NOERROR; } /* @func Close a MIDI in device * * @comm * * Close the device using the API. */ VOID PASCAL MidiInOnClose( NPOPENHANDLEINSTANCE pohi) /* @parm The open handle instance to close */ { } /* @func Activate a MIDI in device * * @rdesc Returns one of the following: * @flag MMSYSERR_NOERROR | on success * @flag MMSYSERR_ALLOCATED | if the device is already in use * * May also return any of the possible return codes from the API call. * * @comm * * Opens the device and starts MIDI input on it, noting the time of the start for timestamp calculations. */ MMRESULT PASCAL MidiInOnActivate( NPOPENHANDLEINSTANCE pohi) { NPOPENHANDLE poh = pohi->pHandle; MMRESULT mmr; if (1 == poh->uActiveCount) { poh->wFlags &= ~OH_F_CLOSING; mmr = midiInOpen(&poh->hmi, poh->id, (DWORD)midiInProc, (DWORD)(LPOPENHANDLE)poh, CALLBACK_FUNCTION); if (mmr) { return mmr; } mmr = midiInStart(poh->hmi); poh->msStartTime = timeGetTime(); if (mmr) { midiInClose(poh->hmi); } /* NOTE: poh memory is guaranteed zeroed by allocator, so we have * no event count and NULL pointers right now. */ RefillFreeEventList(poh); } return MMSYSERR_NOERROR; } /* @func Deactivate a MIDI in device * * @comm * * Close the device using the API. */ MMRESULT PASCAL MidiInOnDeactivate( NPOPENHANDLEINSTANCE pohi) { NPOPENHANDLE poh = pohi->pHandle; MMRESULT mmr; if (0 == poh->uActiveCount) { poh->wFlags |= OH_F_CLOSING; mmr = midiInStop(poh->hmi); if (mmr) { return mmr; } if (MMSYSERR_NOERROR == midiInReset(poh->hmi)) { while (poh->wPostedSysExBuffers) { } } midiInClose(poh->hmi); MidiInFlushQueues(poh); } return MMSYSERR_NOERROR; } /* @func Set the event handle to signal * * @rdesc Always returns MMSYSERR_NOERROR. * * @comm * * This function is exported through the thunk layer to DMusic32.DLL * * This handle is already the VxD handle that can be passed to VWin32 via MMDEVLDR using * . * * Input notification is delivered to the Win32 application using events. The application creates * an event using the API and gives it to the DirectMusic port. The port code * for legacy emulation calls the undocumented Win9x kernel API to retrieve * an equivalent event handle that is valid in any kernel context. That handle is passed to * this function. * * The event handle is stored in our per-client data (). When MIDI data * arrives, the event will be set. This is done using MMDEVLDR, which already has semantics * in place to do the same sort of notification for WinMM event callbacks. * */ MMRESULT WINAPI MidiInSetEventHandle( HANDLE hMidiIn, /* @parm The handle of the input device which desires notification */ DWORD dwEvent) /* @parm The VxD handle of the event to set when new data arrives */ { NPOPENHANDLEINSTANCE pohi; if (!IsValidHandle(hMidiIn, VA_F_INPUT, &pohi)) { return MMSYSERR_INVALHANDLE; } pohi->dwVxDEventHandle = dwEvent; return MMSYSERR_NOERROR; } /* @func Read MIDI input data into a buffer * * @rdesc Returns one of the following * * @comm * * This function is thunked to the 32-bit DLL * * Take as much data from the given event list as will fit and put it into the buffer. */ MMRESULT WINAPI MidiInRead( HANDLE hMidiIn, /* @parm The handle of the input device to read */ LPBYTE lpBuffer, /* @parm A pointer to memory to pack, in DMEVENT format */ LPDWORD pcbData, /* @parm On input, the max size of

in bytes. On return, will contain the number of bytes of data packed into the buffer */ LPDWORD pmsTime) /* @parm On return, will contain the starting time of the buffer */ { NPOPENHANDLEINSTANCE pohi; NPOPENHANDLE poh; WORD wCSID; LPEVENT pEvent; LPEVENT pEventRemoved; LPBYTE pbEventData; DWORD cbLength; DWORD cbPaddedLength; DWORD cbLeft; LPBYTE lpNextEvent; LPDMEVENT pdm; DWORD msFirst; MMRESULT mmr; LPMIDIHDR lpmh; if (!IsValidHandle(hMidiIn, VA_F_INPUT, &pohi)) { return MMSYSERR_INVALHANDLE; } poh = pohi->pHandle; lpNextEvent = lpBuffer; cbLeft = *pcbData; wCSID = EnterCriticalSection(&poh->wCritSect, CS_BLOCKING); assert(wCSID); msFirst = 0; while (NULL != (pEvent = QueuePeek(&poh->qDone))) { lpmh = NULL; if (cbLeft < sizeof(DMEVENT)) { break; } if (pEvent->wFlags & EVENT_F_MIDIHDR) { /* This event is a SysEx message starting with a MIDIHDR, which contains * the recorded length of the message. */ lpmh = (LPMIDIHDR)(&pEvent->abEvent[0]); cbLength = lpmh->dwBytesRecorded - lpmh->dwOffset; pbEventData = lpmh->lpData + lpmh->dwOffset; cbPaddedLength = DMEVENT_SIZE(cbLength); /* For SysEx, split out as much as will fit if the whole message can't. */ if (cbPaddedLength > cbLeft) { cbLength = DMEVENT_DATASIZE(cbLeft); cbPaddedLength = DMEVENT_SIZE(cbLength); } } else { /* The data for this event is directly contained in the event. */ cbLength = pEvent->cbEvent; pbEventData = &pEvent->abEvent[0]; cbPaddedLength = DMEVENT_SIZE(cbLength); if (cbPaddedLength > cbLeft) { break; } } assert(cbPaddedLength <= cbLeft); pdm = (LPDMEVENT)lpNextEvent; pdm->cbEvent = cbLength; pdm->dwChannelGroup = 1; pdm->dwFlags = 0; if (msFirst) { QuadwordMul( pEvent->msTime - msFirst, REFTIME_TO_MS, &pdm->rtDelta); } else { *pmsTime = pEvent->msTime; msFirst = pEvent->msTime; pdm->rtDelta.dwLow = 0; pdm->rtDelta.dwHigh = 0; } hmemcpy(pdm->abEvent, pbEventData, cbLength); lpNextEvent += cbPaddedLength; cbLeft -= cbPaddedLength; if (lpmh) { lpmh->dwOffset += cbLength; assert(lpmh->dwOffset <= lpmh->dwBytesRecorded); if (lpmh->dwOffset == lpmh->dwBytesRecorded) { pEventRemoved = QueueRemoveFromFront(&poh->qDone); assert(pEventRemoved == pEvent); InterlockedIncrement(&poh->wPostedSysExBuffers); lpmh->dwOffset = 0; mmr = midiInAddBuffer(poh->hmi, (LPMIDIHDR)(&pEvent->abEvent[0]), sizeof(MIDIHDR)); if (mmr) { InterlockedDecrement(&poh->wPostedSysExBuffers); DPF(0, "midiInAddBuffer failed with mmr=%d", mmr); mmr = midiInUnprepareHeader(poh->hmi, (LPMIDIHDR)(&pEvent->abEvent[0]), sizeof(MIDIHDR)); if (mmr) { DPF(0, "...midiInUnprepareHeader failed too %d, memory leak!", mmr); } else { FreeEvent(pEvent); } } } } else { pEventRemoved = QueueRemoveFromFront(&poh->qDone); assert(pEventRemoved == pEvent); QueueAppend(&poh->qFree, pEvent); } } *pcbData = lpNextEvent - lpBuffer; DPF(1, "MidiInRead: Returning %ld bytes", (DWORD)*pcbData); LeaveCriticalSection(&poh->wCritSect); return MMSYSERR_NOERROR; } /* @func Enable thruing to a MIDI output port * * @comm For the given channel group and channel, enable (or disable, if the * output handle is NULL) thruing to the given output handle, channel group, and * channel. */ MMRESULT WINAPI MidiInThru( HANDLE hMidiIn, /* @parm The handle of the input device to thru */ DWORD dwFrom, /* @parm The channel of the input stream to thru */ DWORD dwTo, /* @parm Desination channel */ HANDLE hMidiOut) /* The output handle to receive the thru'ed data. */ { NPOPENHANDLEINSTANCE pohiInput; NPOPENHANDLEINSTANCE pohiOutput; if (!IsValidHandle(hMidiIn, VA_F_INPUT, &pohiInput) || ((hMidiOut != NULL) && !IsValidHandle(hMidiOut, VA_F_OUTPUT, &pohiOutput))) { return MMSYSERR_INVALHANDLE; } /* Note that since only 1 channel group is supported on legacy drivers, * we don't need any channel group information. */ if (dwFrom > 15 || dwTo > 15) { return MMSYSERR_INVALPARAM; } DPF(1, "Thru: Sending <%04X,%u> to <%04X,%u>", (WORD)hMidiIn, (UINT)dwFrom, (WORD)hMidiOut, (UINT)dwTo); pohiInput->pThru[(WORD)dwFrom].wChannel = (WORD)dwTo; pohiInput->pThru[(WORD)dwFrom].pohi = hMidiOut ? pohiOutput : NULL; return MMSYSERR_NOERROR; } /* @func MIDI in data callback * * @comm * * This is a standard MIDI input callback from MMSYSYTEM. It calls the correct record routine * and notifies the client that data has arrived. * * For a description of event notification of clients, see . */ VOID CALLBACK _loadds midiInProc( HMIDIIN hMidiIn, /* @parm The MMSYSTEM handle of the device which received data */ UINT wMsg, /* @parm The type of callback */ DWORD dwInstance, /* @parm Instance data; in our case, a pointer to an matching

*/ DWORD dwParam1, /* @parm Message-specific parameters */ DWORD dwParam2) /* @parm Message-specific parameters */ { NPOPENHANDLE poh = (NPOPENHANDLE)(WORD)dwInstance; BOOL bIsNewData = FALSE; WORD wCSID; /* If we can get the critical section we can do all sorts of fun stuff like * transfer the lists over. */ wCSID = EnterCriticalSection(&poh->wCritSect, CS_NONBLOCKING); if (wCSID) { /* We now have exclusive access to all the queues. * * Move any new free events into our internal free list. */ QueueCat(&poh->qFreeCB, &poh->qFree); } switch(wMsg) { case MIM_DATA: DPF(1, "MIM_DATA %08lX %08lX", dwParam1, dwParam2); bIsNewData = RecordShortEvent(poh, dwParam1, dwParam2); break; case MIM_LONGDATA: DPF(1, "MIM_LONGDATA %08lX %08lX", dwParam1, dwParam2); bIsNewData = RecordSysExEvent(poh, (LPMIDIHDR)dwParam1, dwParam2); break; default: break; } if (wCSID) { /* It's safe to move events over to the shared list. */ QueueCat(&poh->qDone, &poh->qDoneCB); LeaveCriticalSection(&poh->wCritSect); } /* Let clients know there is new data */ if (bIsNewData && (!(poh->wFlags & OH_F_CLOSING))) { NotifyClientList(poh); } } /* @func Record a short message (channel messsage or system message). * * @comm * * Queue the incoming data as quickly as possible. * * For a description of the queues used for incoming data, see the struct. * * @rdesc * Returns TRUE if the data was successfully recorded; FALSE otherwise. */ STATIC BOOL NEAR PASCAL RecordShortEvent( NPOPENHANDLE poh, /* @parm The handle to record this data to */ DWORD dwMessage, /* @parm The short message to record */ DWORD dwTime) /* @parm The time stamp of the message */ { LPEVENT pEvent; LPBYTE pb; BYTE b; pEvent = QueueRemoveFromFront(&poh->qFreeCB); if (pEvent == NULL) { DPF(0, "midiInProc: Missed a short event!!!"); return FALSE; } pEvent->msTime = poh->msStartTime + dwTime; pEvent->wFlags = 0; /* Now we have to parse and rebuild the channel message. * * NOTE: Endian specific code ahead */ pb = (LPBYTE)&dwMessage; assert(!IS_SYSEX(*pb)); /* This should *always* be in MIM_LONGDATA */ assert(IS_STATUS_BYTE(*pb)); /* API guarantees no running status */ /* Copying over all the bytes is harmless (we have a DWORD in both * source and dest) and is faster than checking to see if we have to. */ b = pEvent->abEvent[0] = *pb++; pEvent->abEvent[1] = *pb++; pEvent->abEvent[2] = *pb++; if (IS_CHANNEL_MSG(b)) { /* 8x, 9x, Ax, Bx, Cx, Dx, Ex */ /* 0x..7x invalid, that would need running status */ /* Fx handled below */ pEvent->cbEvent = cbChanMsg[(b >> 4) & 0x0F]; /* This is also our criteria for thruing */ ThruClientList(poh, dwMessage); } else { /* F1..FF */ /* F0 is sysex, should never see it here */ pEvent->cbEvent = cbSysCommData[b & 0x0F]; } /* Now we have something to save */ QueueAppend(&poh->qDoneCB, pEvent); return TRUE; } /* @func Record a SysEx message. * * @comm * * Queue the incoming data as quickly as possible. * * For a description of the queues used for incoming data, see the struct. * * @rdesc * Returns TRUE if the data was successfully recorded; FALSE otherwise. */ STATIC BOOL NEAR PASCAL RecordSysExEvent( NPOPENHANDLE poh, /* @parm The handle to record this data to */ LPMIDIHDR lpmh, /* @parm The SysEx message to record */ DWORD dwTime) /* @parm The time stamp of the message */ { LPEVENT pEvent; /* Get back the event header for this MIDIHDR. While buffers are in MMSYSTEM, they are not * in any queue. */ InterlockedDecrement(&poh->wPostedSysExBuffers); /* dwOffset in the MIDIHDR is used to indicate the start of data to send * up to Win32. It is incremented by MidiInRead until the buffer has been * emptied, at which time it will be put back into the pool. */ lpmh->dwOffset = 0; pEvent = (LPEVENT)(lpmh->dwUser); pEvent->msTime = poh->msStartTime + dwTime; QueueAppend(&poh->qDoneCB, pEvent); return TRUE; } /* @func Notify all clients of a device that data has arrived. * * @comm * * Walks the list of clients for the device and sets the notification event for each one. * * This function is now overkill since we no longer support multiple input clients per device. */ STATIC VOID NEAR PASCAL NotifyClientList( LPOPENHANDLE poh) /* @parm The handle of the device that has received data */ { NPLINKNODE plink; NPOPENHANDLEINSTANCE pohi; for (plink = poh->pInstanceList; plink; plink = plink->pNext) { pohi = (NPOPENHANDLEINSTANCE)(((PBYTE)plink) - offsetof(OPENHANDLEINSTANCE, linkHandleList)); if (!pohi->dwVxDEventHandle) { /* No notification event registered for this handle yet. */ continue; } SetWin32Event(pohi->dwVxDEventHandle); } } /* @func Thru this message based on the settings of all clients of a device. * * @comm * * Walks the list of clients for the device and looks at the thru settings of each one. * * This function is now overkill since we no longer support multiple input clients per device. */ STATIC VOID NEAR PASCAL ThruClientList( LPOPENHANDLE poh, DWORD dwMessage) { NPLINKNODE plink; NPOPENHANDLEINSTANCE pohi; NPOPENHANDLEINSTANCE pohiDest; int iChannel; iChannel = (int)(dwMessage & 0x0000000Fl); dwMessage &= 0xFFFFFFF0l; for (plink = poh->pInstanceList; plink; plink = plink->pNext) { pohi = (NPOPENHANDLEINSTANCE)(((PBYTE)plink) - offsetof(OPENHANDLEINSTANCE, linkHandleList)); pohiDest = pohi->pThru[iChannel].pohi; if (pohiDest == NULL || !pohiDest->fActive) { continue; } MidiOutThru(pohiDest, dwMessage & 0xFFFFFFF0l | pohi->pThru[iChannel].wChannel); } } /* @func Refill the free lists * * @comm * * This function is called periodically from user mode to ensure that there are enough free * events available for the input callback. */ VOID PASCAL MidiInRefillFreeLists(VOID) { NPLINKNODE plink; NPOPENHANDLE poh; for (plink = gOpenHandleList; (poh = (NPOPENHANDLE)plink) != NULL; plink = plink->pNext) { /* Only refill MIDI in devices which are not in the process of closing */ if ((poh->wFlags & (OH_F_MIDIIN | OH_F_CLOSING)) != OH_F_MIDIIN) { continue; } RefillFreeEventList(poh); } } /* @func Terminate thruing to this output handle * * @comm * * This function is called before the given output handle is closed. */ VOID PASCAL MidiInUnthruToInstance( NPOPENHANDLEINSTANCE pohiClosing) /* @parm NPOPENHANDLE | pohClosing | The handle which is closing. */ { NPLINKNODE plink; NPOPENHANDLE poh; NPLINKNODE plinkInstance; NPOPENHANDLEINSTANCE pohiInstance; int iChannel; for (plink = gOpenHandleList; (poh = (NPOPENHANDLE)plink) != NULL; plink = plink->pNext) { DPF(2, "Unthru: poh <%04X>", (WORD)poh); if (!(poh->wFlags & OH_F_MIDIIN)) { DPF(2, "...not input"); continue; } for (plinkInstance = poh->pInstanceList; plinkInstance; plinkInstance = plinkInstance->pNext) { pohiInstance = (NPOPENHANDLEINSTANCE)(((PBYTE)plinkInstance) - offsetof(OPENHANDLEINSTANCE, linkHandleList)); DPF(2, "pohiInstance <%04X>", (WORD)pohiInstance); for (iChannel = 0; iChannel < MIDI_CHANNELS; iChannel++) { DPF(2, "Channel 0 @ <%04X>", (WORD)&pohiInstance->pThru[iChannel]); if (pohiInstance->pThru[iChannel].pohi == pohiClosing) { DPF(1, "Thru: Closing output handle %04X which is in use!", (WORD)pohiClosing); pohiInstance->pThru[iChannel].pohi = NULL; } } } } } /* @func Allocate enough free events to refill the pool to CAP_HIGHWATERMARK * * @comm * * BUGBUG call this on a window timer callback * */ STATIC VOID NEAR PASCAL RefillFreeEventList( NPOPENHANDLE poh) /* @parm The device to refill the free list of */ { int idx; LPEVENT pEvent; UINT cFree; WORD wCSID; QUADWORD rt = {0, 0}; int cNewBuffers; LPMIDIHDR lpmh; MMRESULT mmr; WORD wIntStat; wCSID = EnterCriticalSection(&poh->wCritSect, CS_BLOCKING); assert(wCSID); /* NOTE: Technically not allowed to access qFreeCB here, but this is an approximation */ cFree = poh->qFree.cEle + poh->qFreeCB.cEle; if (cFree < CAP_HIGHWATERMARK) { DPF(1, "RefillFreeEventList poh %.4x free %u highwater %u", (WORD)poh, (UINT)cFree, (UINT)CAP_HIGHWATERMARK); for (idx = CAP_HIGHWATERMARK - cFree; idx; --idx) { pEvent = AllocEvent(0, rt, sizeof(DWORD)); if (NULL == pEvent) { DPF(0, "AllocEvent returned NULL in RefillFreeEventList"); break; } QueueAppend(&poh->qFree, pEvent); } } LeaveCriticalSection(&poh->wCritSect); if (poh->wPostedSysExBuffers < SYSEX_BUFFERS) { for (idx = SYSEX_BUFFERS - cFree; idx; --idx) { pEvent = AllocEvent(0, rt, sizeof(MIDIHDR) + SYSEX_SIZE); if (NULL == pEvent) { break; } pEvent->wFlags |= EVENT_F_MIDIHDR; lpmh = (LPMIDIHDR)(&pEvent->abEvent[0]); lpmh->lpData = (LPSTR)(lpmh + 1); lpmh->dwBufferLength = SYSEX_SIZE; lpmh->dwUser = (DWORD)pEvent; mmr = midiInPrepareHeader(poh->hmi, lpmh, sizeof(MIDIHDR)); if (mmr) { DPF(0, "midiInPrepareHeader: %u\n", mmr); FreeEvent(pEvent); break; } InterlockedIncrement(&poh->wPostedSysExBuffers); mmr = midiInAddBuffer(poh->hmi, lpmh, sizeof(MIDIHDR)); if (mmr) { InterlockedDecrement(&poh->wPostedSysExBuffers); DPF(0, "midiInAddBuffer: %u\n", mmr); midiInUnprepareHeader(poh->hmi, lpmh, sizeof(MIDIHDR)); FreeEvent(pEvent); break; } } } } /* @func Return all memory from all queues to the free event list. * * @comm * */ STATIC VOID NEAR PASCAL MidiInFlushQueues( NPOPENHANDLE poh) { WORD wCSID; wCSID = EnterCriticalSection(&poh->wCritSect, CS_BLOCKING); assert(wCSID); FreeAllQueueEvents(&poh->qDone); FreeAllQueueEvents(&poh->qDoneCB); FreeAllQueueEvents(&poh->qFree); FreeAllQueueEvents(&poh->qFreeCB); LeaveCriticalSection(&poh->wCritSect); } /* @func Free all events in the given event queue. * * @comm * * Assumes that the queue's critical section has already been taken by the caller. * */ VOID PASCAL FreeAllQueueEvents( NPEVENTQUEUE peq) { LPEVENT lpCurr; LPEVENT lpNext; lpCurr = peq->pHead; while (lpCurr) { lpNext = lpCurr->lpNext; FreeEvent(lpCurr); lpCurr = lpNext; } peq->pHead = peq->pTail = NULL; peq->cEle = 0; }