/*========================================================================== * * Copyright (C) 1999, 2000 Microsoft Corporation. All Rights Reserved. * * File: mixserver.cpp * Content: Implements the mixing server portion of the server class * * History: * Date By Reason * ==== == ====== * 11/01/2000 rodtoll Split out from dvsereng.cpp * 12/14/2000 rodtoll DPVOICE: [Mixing Server] Mixer may create infinite loop * 02/20/2001 rodtoll WINBUG #321297 - DPVOICE: Access violation in DPVoice.dll while running DVSalvo server * 04/09/2001 rodtoll WINBUG #364126 - DPVoice : Memory leak when Initializing 2 Voice Servers with same DPlay transport * 02/28/2002 rodtoll WINBUG #549959 - SECURITY: DPVOICE: Voice server trusts client's target list * - Update receive path to use server's copy of client target list when server controlled targetting enabled * 06/13/2002 simonpow BUG #59944 Switched over to using Threadpool based timers rather than multimedia ***************************************************************************/ #include "dxvoicepch.h" #undef DPF_MODNAME #define DPF_MODNAME "CDirectVoiceServerEngine::AddPlayerToMixingAddList" void CDirectVoiceServerEngine::AddPlayerToMixingAddList( CVoicePlayer *pPlayer ) { CDVCSPlayer *pVoicePlayer = (CDVCSPlayer *) pPlayer; ASSERT_VPLAYER( pVoicePlayer ); for( DWORD dwIndex = 0; dwIndex < m_dwNumMixingThreads; dwIndex++ ) { DNEnterCriticalSection( &m_prWorkerControl[dwIndex].m_csMixingAddList ); pVoicePlayer->AddToMixingList( dwIndex, &m_prWorkerControl[dwIndex].m_blMixingAddPlayers ); pVoicePlayer->AddRef(); DNLeaveCriticalSection( &m_prWorkerControl[dwIndex].m_csMixingAddList ); } } #undef DPF_MODNAME #define DPF_MODNAME "CDirectVoiceServerEngine::StartWorkerThreads" // StartWorkerThreads // // This function starts mixer worker threads. The number started is based on the // m_dwNumMixingThreads variable which must be initialized before this is called. // HRESULT CDirectVoiceServerEngine::StartWorkerThreads() { HRESULT hr = DV_OK; DWORD dwIndex; m_prWorkerControl = new MIXERTHREAD_CONTROL[m_dwNumMixingThreads]; if( m_prWorkerControl == NULL ) { DPFX(DPFPREP, DVF_ERRORLEVEL, "Out of memory!" ); return DVERR_OUTOFMEMORY; } // Zero memory so everything is initialized. ZeroMemory( m_prWorkerControl, sizeof( MIXERTHREAD_CONTROL )*m_dwNumMixingThreads ); for( dwIndex = 0; dwIndex < m_dwNumMixingThreads; dwIndex++ ) { m_prWorkerControl[dwIndex].dwThreadIndex = dwIndex; m_prWorkerControl[dwIndex].hThreadDone = CreateEvent( NULL, FALSE, FALSE, NULL ); m_prWorkerControl[dwIndex].hThreadDoWork = CreateEvent( NULL, FALSE, FALSE, NULL ); m_prWorkerControl[dwIndex].hThreadIdle = CreateEvent( NULL, FALSE, FALSE, NULL ); m_prWorkerControl[dwIndex].hThreadQuit = CreateEvent( NULL, FALSE, FALSE, NULL ); m_prWorkerControl[dwIndex].m_pServerObject = this; m_prWorkerControl[dwIndex].m_blMixingAddPlayers.Initialize(); m_prWorkerControl[dwIndex].m_blMixingActivePlayers.Initialize(); m_prWorkerControl[dwIndex].m_blMixingSpeakingPlayers.Initialize(); m_prWorkerControl[dwIndex].m_blMixingHearingPlayers.Initialize(); if (!DNInitializeCriticalSection( &m_prWorkerControl[dwIndex].m_csMixingAddList )) { hr = DVERR_OUTOFMEMORY; goto EXIT_ERROR; } if( m_prWorkerControl[dwIndex].hThreadDone == NULL || m_prWorkerControl[dwIndex].hThreadDoWork == NULL || m_prWorkerControl[dwIndex].hThreadIdle == NULL || m_prWorkerControl[dwIndex].hThreadQuit == NULL ) { hr = GetLastError(); DPFX(DPFPREP, DVF_ERRORLEVEL, "Error creating events hr=0x%x", hr ); hr = DVERR_GENERIC; goto EXIT_ERROR; } m_prWorkerControl[dwIndex].m_mixerBuffer = new BYTE[m_dwUnCompressedFrameSize]; m_prWorkerControl[dwIndex].m_realMixerBuffer = new LONG[m_dwMixerSize]; if( m_prWorkerControl[dwIndex].m_mixerBuffer == NULL || m_prWorkerControl[dwIndex].m_realMixerBuffer == NULL ) { DPFX(DPFPREP, DVF_ERRORLEVEL, "Error allocating memory" ); hr = DVERR_OUTOFMEMORY; goto EXIT_ERROR; } m_prWorkerControl[dwIndex].hThread = (HANDLE) CreateThread( NULL, 0, MixerWorker, &m_prWorkerControl[dwIndex], 0, &m_prWorkerControl[dwIndex].dwThreadID ); if( m_prWorkerControl[dwIndex].hThread == NULL ) { DPFX(DPFPREP, DVF_ERRORLEVEL, "Error creating events/thread" ); hr = DVERR_GENERIC; goto EXIT_ERROR; } ::SetThreadPriority( m_prWorkerControl[dwIndex].hThread, THREAD_PRIORITY_TIME_CRITICAL ); } return DV_OK; EXIT_ERROR: ShutdownWorkerThreads(); return hr; } HRESULT CDirectVoiceServerEngine::ShutdownWorkerThreads() { DWORD dwIndex; if( m_prWorkerControl ) { for( dwIndex = 0; dwIndex < m_dwNumMixingThreads; dwIndex++ ) { if( m_prWorkerControl[dwIndex].hThread ) { DPFX(DPFPREP, DVF_MIXER_DEBUG_LEVEL, "[%d]: Shutting down ID=[0x%x]", dwIndex, m_prWorkerControl[dwIndex].dwThreadID ); SetEvent( m_prWorkerControl[dwIndex].hThreadQuit ); WaitForSingleObject( m_prWorkerControl[dwIndex].hThreadDone, INFINITE ); DPFX(DPFPREP, DVF_MIXER_DEBUG_LEVEL, "[%d]: Shutting down complete", dwIndex ); CloseHandle( m_prWorkerControl[dwIndex].hThread ); m_prWorkerControl[dwIndex].hThread = NULL; DNDeleteCriticalSection( &m_prWorkerControl[dwIndex].m_csMixingAddList ); } if( m_prWorkerControl[dwIndex].hThreadDone ) CloseHandle( m_prWorkerControl[dwIndex].hThreadDone ); if( m_prWorkerControl[dwIndex].hThreadDoWork ) CloseHandle( m_prWorkerControl[dwIndex].hThreadDoWork ); if( m_prWorkerControl[dwIndex].hThreadIdle ) CloseHandle( m_prWorkerControl[dwIndex].hThreadIdle ); if( m_prWorkerControl[dwIndex].hThreadQuit ) CloseHandle( m_prWorkerControl[dwIndex].hThreadQuit ); if( m_prWorkerControl[dwIndex].m_mixerBuffer ) delete [] m_prWorkerControl[dwIndex].m_mixerBuffer; if( m_prWorkerControl[dwIndex].m_realMixerBuffer ) delete [] m_prWorkerControl[dwIndex].m_realMixerBuffer; DNASSERT( m_prWorkerControl[dwIndex].m_blMixingAddPlayers.IsEmpty() ); DNASSERT( m_prWorkerControl[dwIndex].m_blMixingActivePlayers.IsEmpty() ); } delete [] m_prWorkerControl; m_prWorkerControl = NULL; } return 0; } #undef DPF_MODNAME #define DPF_MODNAME "CDirectVoiceServerEngine::StartupClientServer" // // StartupClientServer // // This function is called to initialize the Mixer portion of the server object. // Only called for Mixing Sessions. Initialization includes the startup of // the mixing thread and startup of the mixer multimedia timer. // // Called By: // - StartSession // // Locks Required: // - None // HRESULT CDirectVoiceServerEngine::StartupClientServer() { HRESULT hr; HANDLE tmpThreadHandle; SYSTEM_INFO sysInfo; DWORD dwIndex; m_pFramePool = NULL; m_dwCompressedFrameSize = m_lpdvfCompressionInfo->dwFrameLength; m_dwUnCompressedFrameSize = DVCDB_CalcUnCompressedFrameSize( m_lpdvfCompressionInfo, s_lpwfxMixerFormat ); m_dwNumPerBuffer = m_lpdvfCompressionInfo->dwFramesPerBuffer; m_pFramePool = new CFramePool( m_dwCompressedFrameSize ); if( m_pFramePool == NULL ) { DPFX(DPFPREP, DVF_ERRORLEVEL, "Unable to allocate frame pool" ); return DVERR_OUTOFMEMORY; } if (!m_pFramePool->Init()) { delete m_pFramePool; m_pFramePool = NULL; return DVERR_OUTOFMEMORY; } m_mixerEightBit = (s_lpwfxMixerFormat->wBitsPerSample==8) ? TRUE : FALSE; GetSystemInfo( &sysInfo ); m_dwNumMixingThreads = sysInfo.dwNumberOfProcessors; DPFX(DPFPREP, DVF_MIXER_DEBUG_LEVEL, "MIXER: There will be %d worker threads", m_dwNumMixingThreads ); if( m_mixerEightBit ) { m_dwMixerSize = m_dwUnCompressedFrameSize; } else { // Mixer size is / 2 because 16-bit samples, only need 1 LONG for // each 16-bit sample = 2 * 8bit. m_dwMixerSize = m_dwUnCompressedFrameSize / 2; } m_pStats->m_dwNumMixingThreads = m_dwNumMixingThreads; hr = StartWorkerThreads(); if( FAILED( hr ) ) { DPFX(DPFPREP, DVF_ERRORLEVEL, "Failed starting worker threads hr=0x%x", hr ); goto EXIT_CLIENTSERVERSTARTUP; } // General info m_pTimer = new DvTimer; if( m_pTimer == NULL ) { DPFX(DPFPREP, DVF_ERRORLEVEL, "Out of memory!" ); hr = DVERR_OUTOFMEMORY; goto EXIT_CLIENTSERVERSTARTUP; } m_hTickSemaphore = CreateSemaphore( NULL, 0, 0xFFFFFF, NULL ); if( m_hTickSemaphore == NULL ) { DPFX(DPFPREP, DVF_ERRORLEVEL, "Unable to create semaphore" ); hr = DVERR_GENERIC; goto EXIT_CLIENTSERVERSTARTUP; } m_hShutdownMixerEvent = CreateEvent( NULL, FALSE, FALSE, NULL ); m_hMixerDoneEvent = CreateEvent( NULL, FALSE, FALSE, NULL ); if( m_hShutdownMixerEvent == NULL || m_hMixerDoneEvent == NULL ) { DPFX(DPFPREP, DVF_ERRORLEVEL, "Unable to create events" ); hr = DVERR_GENERIC; goto EXIT_CLIENTSERVERSTARTUP; } m_hMixerControlThread = CreateThread( NULL, 0, MixerControl, this, 0, &m_dwMixerControlThreadID ); DPFX(DPFPREP, DVF_MIXER_DEBUG_LEVEL, "MIXER: Controller Started: ID=0x%x", m_dwMixerControlThreadID ); if( m_hMixerControlThread == NULL ) { DPFX(DPFPREP, DVF_ERRORLEVEL, "Error creating events/thread" ); hr = DVERR_GENERIC; goto EXIT_CLIENTSERVERSTARTUP; } ::SetThreadPriority( m_hMixerControlThread, THREAD_PRIORITY_TIME_CRITICAL ); if( !m_pTimer->Create( m_lpdvfCompressionInfo->dwTimeout, &m_hTickSemaphore, MixingServerWakeupProc ) ) { DPFX(DPFPREP, DVF_ERRORLEVEL, "Unable to create multimedia timer" ); hr = DVERR_GENERIC; goto EXIT_CLIENTSERVERSTARTUP; } return DV_OK; EXIT_CLIENTSERVERSTARTUP: ShutdownClientServer(); return hr; } #undef DPF_MODNAME #define DPF_MODNAME "CDirectVoiceServerEngine::ShutdownClientServer" // // ShutdownClientServer // // This function is responsible for shutting down the mixer portion of the // server object. This function should only be called for mixing sessions. // // This function will stop the mixer thread and the mixer multimedia timer. // // Called By: // - StopSession // // Locks Required: // - None // HRESULT CDirectVoiceServerEngine::ShutdownClientServer() { if( m_hMixerControlThread ) { SetEvent( m_hShutdownMixerEvent ); WaitForSingleObject( m_hMixerDoneEvent, INFINITE ); CloseHandle( m_hMixerControlThread ); m_hMixerControlThread = NULL; // Cleanup the mixing list CleanupMixingList(); } if( m_hShutdownMixerEvent ) { CloseHandle( m_hShutdownMixerEvent ); m_hShutdownMixerEvent = NULL; } if( m_hMixerDoneEvent ) { CloseHandle( m_hMixerDoneEvent ); m_hMixerDoneEvent = NULL; } ShutdownWorkerThreads(); if( m_pTimer ) { delete m_pTimer; m_pTimer = NULL; } if( m_hTickSemaphore ) { CloseHandle( m_hTickSemaphore ); m_hTickSemaphore = NULL; } return DV_OK; } #undef DPF_MODNAME #define DPF_MODNAME "CDirectVoiceServerEngine::Mixer_Buffer_Reset" // Mixer_Buffer_Reset // // This function resets the mixer buffer back to silence. void CDirectVoiceServerEngine::Mixer_Buffer_Reset( DWORD dwThreadIndex ) { FillBufferWithSilence( m_prWorkerControl[dwThreadIndex].m_realMixerBuffer, m_prWorkerControl[dwThreadIndex].m_pServerObject->m_mixerEightBit, m_prWorkerControl[dwThreadIndex].m_pServerObject->m_dwUnCompressedFrameSize ); } #undef DPF_MODNAME #define DPF_MODNAME "CDirectVoiceServerEngine::Mixer_Buffer_MixBuffer" // Mixer_Buffer_MixBuffer // // This function mixes the speech pointed to by the source parameter // into the mixer buffer. // // Parameters: // unsigned char *source - // Pointer to source data in uncompressed format void CDirectVoiceServerEngine::Mixer_Buffer_MixBuffer( DWORD dwThreadIndex, const unsigned char *source ) { MixInBuffer( m_prWorkerControl[dwThreadIndex].m_realMixerBuffer, source, m_prWorkerControl[dwThreadIndex].m_pServerObject->m_mixerEightBit, m_prWorkerControl[dwThreadIndex].m_pServerObject->m_dwUnCompressedFrameSize ); } #undef DPF_MODNAME #define DPF_MODNAME "CDirectVoiceServerEngine::Mixer_Buffer_Normalize" // Mixer_Buffer_Normalize // // This function takes the mixed audio data from the mixer // buffer and transfers it back to the mixer format // and places it into the m_mixerBuffer buffer. // void CDirectVoiceServerEngine::Mixer_Buffer_Normalize( DWORD dwThreadIndex ) { NormalizeBuffer( m_prWorkerControl[dwThreadIndex].m_mixerBuffer, m_prWorkerControl[dwThreadIndex].m_realMixerBuffer, m_prWorkerControl[dwThreadIndex].m_pServerObject->m_mixerEightBit, m_prWorkerControl[dwThreadIndex].m_pServerObject->m_dwUnCompressedFrameSize ); } #undef DPF_MODNAME #define DPF_MODNAME "CDirectVoiceServerEngine::HandleMixerThreadError" // // HandleMixerThreadError // // This function is called by the mixer when an unrecoverable error // occurs. // void CDirectVoiceServerEngine::HandleMixerThreadError( HRESULT hr ) { DPFX(DPFPREP, DVF_ERRORLEVEL, "Mixer Thread Encountered an error. hr=0x%x", hr ); SetEvent( m_hMixerDoneEvent ); StopSession( 0, FALSE, hr ); } #undef DPF_MODNAME #define DPF_MODNAME "CDirectVoiceServerEngine::MixerControl" DWORD WINAPI CDirectVoiceServerEngine::MixerControl( void *pvContext ) { CDirectVoiceServerEngine *This = (CDirectVoiceServerEngine *) pvContext; HANDLE hEvents[3]; HANDLE *hIdleEvents = new HANDLE[This->m_dwNumMixingThreads+1]; DWORD dwIndex = 0; LONG lFreeThreadIndex = 0; DWORD dwNumToMix = 0; DWORD dwTickCountStart; LONG lWaitResult; if( !hIdleEvents ) { DPFX(DPFPREP, DVF_ERRORLEVEL, "MIXCTRL: Error allocating array" ); DNASSERT( FALSE ); SetEvent( This->m_hMixerDoneEvent ); return 0; } hEvents[0] = This->m_hShutdownMixerEvent; hEvents[1] = This->m_hTickSemaphore; hEvents[2] = (HANDLE) ((DWORD_PTR) 0xFFFFFFFF); DPFX(DPFPREP, DVF_MIXER_DEBUG_LEVEL, "MIXCTRL: Starting up" ); for( dwIndex = 0; dwIndex < This->m_dwNumMixingThreads; dwIndex++ ) { hIdleEvents[dwIndex] = This->m_prWorkerControl[dwIndex].hThreadIdle; } hIdleEvents[This->m_dwNumMixingThreads] = (HANDLE) ((DWORD_PTR) 0xFFFFFFFF); // Wait for tick or for quit command while( (lWaitResult = WaitForMultipleObjects( 2, hEvents, FALSE, INFINITE )) != WAIT_OBJECT_0 ) { // On Win9X we may occationally over run the end of the wait list // and the result is we hit the FFFFFFFFF which will cause // a failure. if( lWaitResult == WAIT_FAILED ) continue; // Update statistics block InterlockedIncrement( &This->m_pStats->m_dwNumMixingPasses ); dwTickCountStart = GetTickCount(); // On Win95 you may occasionally encounter a situation where the waitformultiple runs // off the end of the list and ends up with the invalid handle above. Just continue // in this case. lFreeThreadIndex = WAIT_FAILED; while( lFreeThreadIndex == WAIT_FAILED ) { // Wait for a single mixing thread to be free lFreeThreadIndex = WaitForMultipleObjects( This->m_dwNumMixingThreads, hIdleEvents, FALSE, INFINITE ); //// TODO: Error checking! } lFreeThreadIndex -= WAIT_OBJECT_0; DPFX(DPFPREP, DVF_MIXER_DEBUG_LEVEL, "MIXCTRL: Worker [%d] is elected to do work", lFreeThreadIndex ); This->SpinWorkToThread( lFreeThreadIndex ); } delete [] hIdleEvents; SetEvent( This->m_hMixerDoneEvent ); DPFX(DPFPREP, DVF_MIXER_DEBUG_LEVEL, "MIXCTRL: Shutting down" ); return 0; } #undef DPF_MODNAME #define DPF_MODNAME "CDirectVoiceServerEngine::MixerWorker" DWORD WINAPI CDirectVoiceServerEngine::MixerWorker( void *pvContext ) { PMIXERTHREAD_CONTROL This = (PMIXERTHREAD_CONTROL) pvContext; HANDLE hEvents[3]; CBilink *pblSearch, *pblSubSearch; CDVCSPlayer *pCurrentPlayer = NULL, *pTmpPlayer = NULL; DWORD dwNumTargets = 0; DWORD dwTargetIndex = 0; DWORD dwResultSize = 0; DWORD dwIndex = 0; DWORD dwThreadIndex = This->dwThreadIndex; HRESULT hr; CDVCSPlayer **ppThreadHearList = NULL; PDVPROTOCOLMSG_SPEECHHEADER pdvmSpeechHeader = NULL; PDVTRANSPORT_BUFFERDESC pdvbTransmitBufferDesc = NULL; PVOID pvSendContext = NULL; DVID dvidSendTarget; DWORD dwTickCountStart; DWORD dwTickCountDecStart; DWORD dwTickCountMixStart; DWORD dwTickCountDupStart; DWORD dwTickCountRetStart; DWORD dwStatIndex; DWORD dwTickCountEnd; DWORD dwTotalMix, dwForwardMix, dwReuseMix, dwOriginalMix; LONG lWaitResult; MixingServerStats *pStats = This->m_pServerObject->m_pStats; DPFX(DPFPREP, DVF_MIXER_DEBUG_LEVEL, "MIXWORKER: [%d] Started [0x%x] Thread", This->dwThreadIndex, GetCurrentThreadId() ); hEvents[0] = This->hThreadQuit; hEvents[1] = This->hThreadDoWork; hEvents[2] = (HANDLE) ((DWORD_PTR) 0xFFFFFFFF); SetEvent( This->hThreadIdle ); while( (lWaitResult = WaitForMultipleObjects( 2, hEvents, FALSE, INFINITE )) != WAIT_OBJECT_0 ) { // On Win95 it may occationally move off the end of the list and hit the guard value if( lWaitResult == WAIT_FAILED ) continue; // Statistics update dwTickCountStart = GetTickCount(); InterlockedIncrement( &pStats->m_dwNumMixingThreadsActive ); pStats->m_dwNumMixingPassesPerThread[dwThreadIndex]++; if( pStats->m_dwNumMixingThreadsActive > pStats->m_dwMaxMixingThreadsActive ) { pStats->m_dwMaxMixingThreadsActive = pStats->m_dwNumMixingThreadsActive; } dwStatIndex = pStats->m_dwCurrentMixingHistoryLoc[dwThreadIndex]; pStats->m_lCurrentPlayerCount[dwThreadIndex][dwStatIndex] = This->dwNumToMix; DPFX(DPFPREP, DVF_MIXER_DEBUG_LEVEL, "MIXWORKER: [%d] Starting work", This->dwThreadIndex ); if( This->dwNumToMix == 0 ) { DPFX(DPFPREP, DVF_MIXER_DEBUG_LEVEL, "MIXWORKER: No players to process!" ); goto WORK_COMPLETE; } dwTickCountDecStart = GetTickCount(); pStats->m_lCurrentDecCountHistory[dwThreadIndex][dwStatIndex] = 0; // Pass through player list and decompress those who need decompression // pblSearch = This->m_blMixingSpeakingPlayers.GetNext(); while( pblSearch != &This->m_blMixingSpeakingPlayers ) { CBilinkPlusObject* pRealBilink = CONTAINING_OBJECT( pblSearch, CBilinkPlusObject, m_bl ); pCurrentPlayer = pRealBilink->m_pPlayer; ASSERT_VPLAYER( pCurrentPlayer ); // Dereference the array of can hear players for this player ppThreadHearList = pCurrentPlayer->m_pppCanHear[dwThreadIndex]; // Player needs to have their voice decompressed if( pCurrentPlayer->m_pfNeedsDecompression[dwThreadIndex] ) { DNASSERT( pCurrentPlayer ); DNASSERT( pCurrentPlayer->m_pSourceFrame[dwThreadIndex] ); DNASSERT( !pCurrentPlayer->m_pSourceFrame[dwThreadIndex]->GetIsSilence() ); dwResultSize = This->m_pServerObject->m_dwUnCompressedFrameSize; hr = pCurrentPlayer->DeCompressInBound( pCurrentPlayer->m_pSourceFrame[dwThreadIndex], &pCurrentPlayer->m_sourceUnCompressed[pCurrentPlayer->m_pdwUnCompressedBufferOffset[dwThreadIndex]], &dwResultSize ); pStats->m_lCurrentDecCountHistory[dwThreadIndex][dwStatIndex]++; if( FAILED( hr ) ) { DNASSERT( FALSE ); // TODO: ERROR Handling for failed decompression } else { pCurrentPlayer->m_pfDecompressed[dwThreadIndex] = TRUE; } DNASSERT( dwResultSize == This->m_pServerObject->m_dwUnCompressedFrameSize ); } // Integrity checks // // Check to ensure that each player who this person can hear is supposed to be decompressed #ifdef _DEBUG DNASSERT( pCurrentPlayer->m_pdwHearCount[dwThreadIndex] < This->dwNumToMix ); if( pCurrentPlayer->m_pdwHearCount[dwThreadIndex] > 1 ) { for( dwIndex; dwIndex < pCurrentPlayer->m_pdwHearCount[dwThreadIndex]; dwIndex++ ) { DNASSERT( ppThreadHearList[dwIndex] ); DNASSERT( ppThreadHearList[dwIndex]->m_pfNeedsDecompression[dwThreadIndex] ); } } #endif pblSearch = pblSearch->GetNext(); } dwTickCountDupStart = GetTickCount(); pStats->m_lCurrentDecTimeHistory[dwThreadIndex][dwStatIndex] = dwTickCountDupStart - dwTickCountDecStart; // Check for duplicates in the sending. If there is duplicates then we need // to setup the reuse pblSearch = This->m_blMixingHearingPlayers.GetNext(); while( pblSearch != &This->m_blMixingHearingPlayers ) { CBilinkPlusObject* pRealBilink = CONTAINING_OBJECT( pblSearch, CBilinkPlusObject, m_bl ); pCurrentPlayer = pRealBilink->m_pPlayer; ASSERT_VPLAYER( pCurrentPlayer ); // If we don't hear anybody, this step is irrelevant if( pCurrentPlayer->m_pdwHearCount[dwThreadIndex] < 2 ) goto DUPLICATE_CHECK_LOOP_DONE; // Dereference the array of can hear players for this player ppThreadHearList = pCurrentPlayer->m_pppCanHear[dwThreadIndex]; pblSubSearch = This->m_blMixingHearingPlayers.GetNext(); // Only do the people who come before them. while( pblSubSearch != pblSearch ) { CBilinkPlusObject* pRealSubBilink = CONTAINING_OBJECT( pblSubSearch, CBilinkPlusObject, m_bl ); pTmpPlayer = pRealSubBilink->m_pPlayer; ASSERT_VPLAYER( pTmpPlayer ); // This person's mix is the same, re-use it! if( pTmpPlayer->ComparePlayerMix( dwThreadIndex, pCurrentPlayer ) ) { pCurrentPlayer->m_pReuseMixFromThisPlayer[dwThreadIndex] = pTmpPlayer; pTmpPlayer->m_pfMixToBeReused[dwThreadIndex] = TRUE; break; } pblSubSearch = pblSubSearch->GetNext(); } DUPLICATE_CHECK_LOOP_DONE: pblSearch = pblSearch->GetNext(); } dwTickCountMixStart = GetTickCount(); pStats->m_lCurrentDupTimeHistory[dwThreadIndex][dwStatIndex] = dwTickCountMixStart - dwTickCountDupStart; dwTotalMix = 0; dwForwardMix = 0; dwReuseMix = 0; dwOriginalMix = 0; // Pass through player list and compress and send mixes as appropriate pblSearch = This->m_blMixingHearingPlayers.GetNext(); while( pblSearch != &This->m_blMixingHearingPlayers ) { CBilinkPlusObject* pRealBilink = CONTAINING_OBJECT( pblSearch, CBilinkPlusObject, m_bl ); pCurrentPlayer = pRealBilink->m_pPlayer; ASSERT_VPLAYER( pCurrentPlayer ); // Dereference the array of can hear players for this player ppThreadHearList = pCurrentPlayer->m_pppCanHear[dwThreadIndex]; // Pre-set next so we can continue() below and still go to next item pblSearch = pblSearch->GetNext(); if( !pCurrentPlayer->m_pdwHearCount[dwThreadIndex] ) { continue; } dwTotalMix++; // Get a transmission buffer and description pdvbTransmitBufferDesc = This->m_pServerObject->GetTransmitBuffer( This->m_pServerObject->m_dwCompressedFrameSize+sizeof(DVPROTOCOLMSG_SPEECHHEADER)+COMPRESSION_SLUSH, &pvSendContext ); if( pdvbTransmitBufferDesc == NULL ) { // TODO: Error handling for out of memory condition DNASSERT( FALSE ); } // Setup the packet header pdvmSpeechHeader = (PDVPROTOCOLMSG_SPEECHHEADER) pdvbTransmitBufferDesc->pBufferData; pdvmSpeechHeader->dwType = DVMSGID_SPEECHBOUNCE; pdvmSpeechHeader->bMsgNum = pCurrentPlayer->m_pbMsgNumToSend[dwThreadIndex]; pdvmSpeechHeader->bSeqNum = pCurrentPlayer->m_pbSeqNumToSend[dwThreadIndex]; DPFX(DPFPREP, DVF_MIXER_DEBUG_LEVEL, "MIXWORKER: [%d] Sending Packet to 0x%x Msg=0x%x Seq=0x%x", dwThreadIndex, pCurrentPlayer->GetPlayerID(), pdvmSpeechHeader->bMsgNum, pdvmSpeechHeader->bSeqNum ); // If this player hears something they will be getting a packet // // Only hear one person -- forward the packet // if( pCurrentPlayer->m_pdwHearCount[dwThreadIndex] == 1) { dwResultSize = ppThreadHearList[0]->m_pSourceFrame[dwThreadIndex]->GetFrameLength(); memcpy( &pdvmSpeechHeader[1], ppThreadHearList[0]->m_pSourceFrame[dwThreadIndex]->GetDataPointer(), dwResultSize ); DPFX(DPFPREP, DVF_MIXER_DEBUG_LEVEL, "MIXWORKER: [%d] Forwarding already compressed packet", dwThreadIndex ); pCurrentPlayer->m_pfMixed[dwThreadIndex] = TRUE; dwForwardMix++; } else if( pCurrentPlayer->m_pdwHearCount[dwThreadIndex] > 1) { pTmpPlayer = pCurrentPlayer->m_pReuseMixFromThisPlayer[dwThreadIndex]; // We are re-using a previous player's mix if( pTmpPlayer ) { DPFX(DPFPREP, DVF_MIXER_DEBUG_LEVEL, "MIXERWORKER: [%d] Forwarding pre-built mix", dwThreadIndex ); ASSERT_VPLAYER( pTmpPlayer ); DNASSERT( pTmpPlayer->m_pfMixed[dwThreadIndex] ); DNASSERT( pTmpPlayer->m_pfMixToBeReused[dwThreadIndex] ); DNASSERT( pTmpPlayer->m_pdwResultLength[dwThreadIndex] ); dwResultSize = pTmpPlayer->m_pdwResultLength[dwThreadIndex]; memcpy( &pdvmSpeechHeader[1], &pTmpPlayer->m_targetCompressed[pTmpPlayer->m_pdwCompressedBufferOffset[dwThreadIndex]], dwResultSize ); dwReuseMix++; } else { DPFX(DPFPREP, DVF_MIXER_DEBUG_LEVEL, "MIXERWORKER: [%d] Creating original mix", dwThreadIndex ); dwOriginalMix++; dwResultSize = This->m_pServerObject->m_dwCompressedFrameSize; // Reset the high resolution mixer buffer This->m_pServerObject->Mixer_Buffer_Reset(dwThreadIndex); // Mix in specified player's audio. for( dwIndex = 0; dwIndex < pCurrentPlayer->m_pdwHearCount[dwThreadIndex]; dwIndex++ ) { DNASSERT( !ppThreadHearList[dwIndex]->m_pfSilence[dwThreadIndex] ); This->m_pServerObject->Mixer_Buffer_MixBuffer(dwThreadIndex,ppThreadHearList[dwIndex]->m_sourceUnCompressed ); } // Normalize the buffer back to the thread's mix buffer This->m_pServerObject->Mixer_Buffer_Normalize(dwThreadIndex); hr = pCurrentPlayer->CompressOutBound( This->m_mixerBuffer, This->m_pServerObject->m_dwUnCompressedFrameSize, (BYTE *) &pdvmSpeechHeader[1], &dwResultSize ); if( FAILED( hr ) ) { DNASSERT( FALSE ); DPFX(DPFPREP, DVF_ERRORLEVEL, "Failed compressing outbound audio" ); } pCurrentPlayer->m_pfMixed[dwThreadIndex] = TRUE; pCurrentPlayer->m_pdwResultLength[dwThreadIndex] = dwResultSize; // This player's mix will be re-used, ensure that we cache it if( pCurrentPlayer->m_pfMixToBeReused[dwThreadIndex] ) { memcpy( &pCurrentPlayer->m_targetCompressed[pCurrentPlayer->m_pdwCompressedBufferOffset[dwThreadIndex]], &pdvmSpeechHeader[1], pCurrentPlayer->m_pdwResultLength[dwThreadIndex] ); } } } else { DNASSERT(FALSE); } dvidSendTarget = pCurrentPlayer->GetPlayerID(); pdvbTransmitBufferDesc->dwBufferSize= dwResultSize + sizeof( DVPROTOCOLMSG_SPEECHHEADER ); hr = This->m_pServerObject->m_lpSessionTransport->SendToIDS( &dvidSendTarget, 1, pdvbTransmitBufferDesc, pvSendContext, 0 ); if( hr == DVERR_PENDING ) hr = DV_OK; if( FAILED( hr ) ) { DPFX(DPFPREP, DVF_ERRORLEVEL, "MIXWORKER: [%d] Unable to transmit to target [0x%x]", pCurrentPlayer->GetPlayerID() ); } } dwTickCountRetStart = GetTickCount(); pStats->m_lCurrentMixTimeHistory[dwThreadIndex][dwStatIndex] = dwTickCountRetStart - dwTickCountMixStart; pStats->m_lCurrentMixCountTotalHistory[dwThreadIndex][dwStatIndex] = dwTotalMix; pStats->m_lCurrentMixCountFwdHistory[dwThreadIndex][dwStatIndex] = dwForwardMix; pStats->m_lCurrentMixCountReuseHistory[dwThreadIndex][dwStatIndex] = dwReuseMix; pStats->m_lCurrentMixCountOriginalHistory[dwThreadIndex][dwStatIndex] = dwOriginalMix; WORK_COMPLETE: // Pass through player list and return frames pblSearch = This->m_blMixingActivePlayers.GetNext(); while( pblSearch != &This->m_blMixingActivePlayers ) { CBilinkPlusObject* pRealBilink = CONTAINING_OBJECT( pblSearch, CBilinkPlusObject, m_bl ); pCurrentPlayer = pRealBilink->m_pPlayer; ASSERT_VPLAYER( pCurrentPlayer ); DNASSERT( pCurrentPlayer->m_pSourceFrame[dwThreadIndex] ); pCurrentPlayer->CompleteRun( dwThreadIndex ); pblSearch = pblSearch->GetNext(); } dwTickCountEnd = GetTickCount(); pStats->m_lCurrentRetTimeHistory[dwThreadIndex][dwStatIndex] = dwTickCountEnd - dwTickCountRetStart; DPFX(DPFPREP, DVF_MIXER_DEBUG_LEVEL, "MIXWORKER: [%d] Work complete", This->dwThreadIndex ); // Statistics update InterlockedDecrement( &This->m_pServerObject->m_pStats->m_dwNumMixingThreadsActive ); SetEvent( This->hThreadIdle ); // Statistics update pStats->m_dwMixingPassesTimeHistory[dwThreadIndex][dwStatIndex] = dwTickCountEnd - dwTickCountStart; pStats->m_dwCurrentMixingHistoryLoc[dwThreadIndex]++; pStats->m_dwCurrentMixingHistoryLoc[dwThreadIndex] %= MIXING_HISTORY; } DPFX(DPFPREP, DVF_MIXER_DEBUG_LEVEL, "MIXWORKER: [%d] Shutting down", This->dwThreadIndex ); SetEvent( This->hThreadDone ); return 0; } #undef DPF_MODNAME #define DPF_MODNAME "CDirectVoiceServerEngine::SpinWorkToThread" // // SpinWorkToThread // // This function performs the first step of a mixing server pass and then // passes the work off to the specified thread // // Responsible for: // 1. Updating the // 2. running the list of players, determinging who they can hear // void CDirectVoiceServerEngine::SpinWorkToThread( LONG lThreadIndex ) { CBilink *pblSearch = NULL, *pblSubSearch = NULL; CDVCSPlayer *pCurrentPlayer = NULL, *pTmpPlayer = NULL, *pComparePlayer = NULL; HRESULT hr; PDVID pdvidTargets = NULL; DWORD dwNumTargets = 0; DWORD dwTargetIndex = 0; DWORD dwTickCountStart = GetTickCount(); // Update the list of players from the pending lists to the individual bilinks UpdateActiveMixingPendingList( lThreadIndex, &m_prWorkerControl[lThreadIndex].dwNumToMix ); // Pass 1 through player list. // // Reset state variables for specified thread, create any converters that need creating pblSearch = m_prWorkerControl[lThreadIndex].m_blMixingActivePlayers.GetNext(); while( pblSearch != &m_prWorkerControl[lThreadIndex].m_blMixingActivePlayers ) { CBilinkPlusObject* pRealBilink = CONTAINING_OBJECT( pblSearch, CBilinkPlusObject, m_bl ); pTmpPlayer = pRealBilink->m_pPlayer; ASSERT_VPLAYER( pTmpPlayer ); pblSearch = pblSearch->GetNext(); // Reset for the next pass pTmpPlayer->ResetForNextRun(lThreadIndex,TRUE); // Resize the can hear array pTmpPlayer->ResizeIfRequired( lThreadIndex, m_prWorkerControl[lThreadIndex].dwNumToMix ); // Lock the player -- only one person should be creating converter at a time pTmpPlayer->Lock(); // Create outbound converter if required if( !pTmpPlayer->IsOutBoundConverterInitialized() ) { hr = pTmpPlayer->CreateOutBoundConverter( s_lpwfxMixerFormat, m_dvSessionDesc.guidCT ); if( FAILED( hr ) ) { DPFX(DPFPREP, DVF_ERRORLEVEL, "Failed to create outbound converter hr=0x%x", hr ); DNASSERT( FALSE ); } } // Create inbound converter if required if( !pTmpPlayer->IsInBoundConverterInitialized() ) { hr = pTmpPlayer->CreateInBoundConverter( m_dvSessionDesc.guidCT, s_lpwfxMixerFormat ); if( FAILED( hr ) ) { DPFX(DPFPREP, DVF_ERRORLEVEL, "Failed to create converter" ); DNASSERT( FALSE ); } } pTmpPlayer->UnLock(); } DNASSERT(m_prWorkerControl[lThreadIndex].m_blMixingSpeakingPlayers.IsEmpty()); DNASSERT(m_prWorkerControl[lThreadIndex].m_blMixingHearingPlayers.IsEmpty()); m_prWorkerControl[lThreadIndex].m_blMixingSpeakingPlayers.Initialize(); m_prWorkerControl[lThreadIndex].m_blMixingHearingPlayers.Initialize(); DPFX(DPFPREP, DVF_MIXER_DEBUG_LEVEL, "SST: 2" ); // Pass 2. // // For each player: // 1. Figure out who they hear. // 2. TODO: If they hear anyone, add them to the "to send to" list of people // 3. TODO: If they hear > 1, add the people they hear to the "to decompress" list of people. // 4. Setup the appropriate sequence # / msg # for the transmission // pblSearch = m_prWorkerControl[lThreadIndex].m_blMixingActivePlayers.GetNext(); while( pblSearch != &m_prWorkerControl[lThreadIndex].m_blMixingActivePlayers ) { CBilinkPlusObject* pRealBilink = CONTAINING_OBJECT( pblSearch, CBilinkPlusObject, m_bl ); pCurrentPlayer = pRealBilink->m_pPlayer; ASSERT_VPLAYER( pCurrentPlayer ); DPFX(DPFPREP, DVF_MIXER_DEBUG_LEVEL, "0x%x Can hear: ", pCurrentPlayer->GetPlayerID() ); pblSearch = pblSearch->GetNext(); pblSubSearch = m_prWorkerControl[lThreadIndex].m_blMixingActivePlayers.GetNext(); // Search the list of people in the session while( pblSubSearch != &m_prWorkerControl[lThreadIndex].m_blMixingActivePlayers ) { CBilinkPlusObject* pRealSubBilink = CONTAINING_OBJECT( pblSubSearch, CBilinkPlusObject, m_bl ); pComparePlayer = pRealSubBilink->m_pPlayer; ASSERT_VPLAYER( pComparePlayer ); pblSubSearch = pblSubSearch->GetNext(); // This record contains a silent record -- ignore if( pComparePlayer->m_pfSilence[lThreadIndex] ) continue; // If this isn't the player themselves if( pblSearch != pblSubSearch ) { DNASSERT( pComparePlayer->m_pSourceFrame[lThreadIndex] ); pdvidTargets = pComparePlayer->m_pSourceFrame[lThreadIndex]->GetTargetList(); dwNumTargets = pComparePlayer->m_pSourceFrame[lThreadIndex]->GetNumTargets(); // The target of the subIndex user's frame is this user OR // The user is in the group which is target of subIndex user's frame for( dwTargetIndex = 0; dwTargetIndex < dwNumTargets; dwTargetIndex++ ) { if( pCurrentPlayer->GetPlayerID() == pdvidTargets[dwTargetIndex] || m_lpSessionTransport->IsPlayerInGroup( pdvidTargets[dwTargetIndex], pCurrentPlayer->GetPlayerID() ) ) { *((*(pCurrentPlayer->m_pppCanHear+lThreadIndex))+pCurrentPlayer->m_pdwHearCount[lThreadIndex]) = pComparePlayer; pCurrentPlayer->m_pdwHearCount[lThreadIndex]++; DPFX(DPFPREP, DVF_MIXER_DEBUG_LEVEL, "0x%x", pComparePlayer->GetPlayerID() ); // Setup the appropriate msg num / sequence number so when it's sent // we ensure it gets re-assembled on the other side in the right order if( pCurrentPlayer->m_pdwHearCount[lThreadIndex] == 1 ) { if( pCurrentPlayer->m_bLastSent == FALSE ) { pCurrentPlayer->m_bMsgNum++; pCurrentPlayer->m_bSeqNum = 0; pCurrentPlayer->m_bLastSent = TRUE; } else { pCurrentPlayer->m_bSeqNum++; } pCurrentPlayer->m_pbMsgNumToSend[lThreadIndex] = pCurrentPlayer->m_bMsgNum; pCurrentPlayer->m_pbSeqNumToSend[lThreadIndex] = pCurrentPlayer->m_bSeqNum; pCurrentPlayer->AddToHearingList( lThreadIndex, &m_prWorkerControl[lThreadIndex].m_blMixingHearingPlayers ); } // We can hear > 1 person, we need to mark each person as needing decompression else if( pCurrentPlayer->m_pdwHearCount[lThreadIndex] > 1 ) { if( !pComparePlayer->m_pfNeedsDecompression[lThreadIndex] ) { // Add this player to the list of people who need to be decompressed pComparePlayer->AddToSpeakingList( lThreadIndex, &m_prWorkerControl[lThreadIndex].m_blMixingSpeakingPlayers ); pComparePlayer->m_pfNeedsDecompression[lThreadIndex] = TRUE; } // Special case, we just transitioned to having > 1 people heard by this player, // we should mark the first person we can hear for decompression as well if( pCurrentPlayer->m_pdwHearCount[lThreadIndex] == 2 ) { pTmpPlayer = (pCurrentPlayer->m_pppCanHear[lThreadIndex])[0]; ASSERT_VPLAYER( pTmpPlayer ); if( !pTmpPlayer->m_pfNeedsDecompression[lThreadIndex] ) { pTmpPlayer->AddToSpeakingList( lThreadIndex, &m_prWorkerControl[lThreadIndex].m_blMixingSpeakingPlayers ); pTmpPlayer->m_pfNeedsDecompression[lThreadIndex] = TRUE; } } } // We need to break out of the loop as we only need to add an individual player once to the // list of people a player can hear. break; } } } } if( !pCurrentPlayer->m_pdwHearCount[lThreadIndex] ) { pCurrentPlayer->m_bLastSent = FALSE; } else { pCurrentPlayer->m_bLastSent = TRUE; } } m_pStats->m_dwPreMixingPassTimeHistoryLoc++; m_pStats->m_dwPreMixingPassTimeHistoryLoc %= MIXING_HISTORY; m_pStats->m_dwPreMixingPassTimeHistory[m_pStats->m_dwPreMixingPassTimeHistoryLoc] = GetTickCount() - dwTickCountStart; SetEvent( m_prWorkerControl[lThreadIndex].hThreadDoWork ); } #undef DPF_MODNAME #define DPF_MODNAME "CDirectVoiceServerEngine::MixingServerWakeupProc" // MixingServerWakeupProc // // This function is called by the windows timer used by this // class each time the timer goes off. The function signals // a semaphore provided by the creator of the timer. // // Parameters: // DWORD param - A recast pointer to a HANDLE void CDirectVoiceServerEngine::MixingServerWakeupProc( void * pvUserData ) { ReleaseSemaphore( *((HANDLE * ) pvUserData), 1, NULL ); } #undef DPF_MODNAME #define DPF_MODNAME "CDirectVoiceServerEngine::HandleMixingReceive" HRESULT CDirectVoiceServerEngine::HandleMixingReceive( CDVCSPlayer *pTargetPlayer, PDVPROTOCOLMSG_SPEECHWITHTARGET pdvSpeechWithtarget, DWORD dwSpeechSize, PBYTE pSourceSpeech ) { HRESULT hr; DPFX(DPFPREP, DVF_MIXER_DEBUG_LEVEL, "Mixing Server Speech Handler" ); ASSERT_VPLAYER(pTargetPlayer); hr = pTargetPlayer->HandleMixingReceive( &pdvSpeechWithtarget->dvHeader, pSourceSpeech, dwSpeechSize, (PDVID) &pdvSpeechWithtarget[1], pdvSpeechWithtarget->dwNumTargets, m_dvSessionDesc.dwFlags & DVSESSION_SERVERCONTROLTARGET ); DPFX(DPFPREP, DVF_CLIENT_SEQNUM_DEBUG_LEVEL, "SEQ: Receive: Msg [%d] Seq [%d]", pdvSpeechWithtarget->dvHeader.bMsgNum, pdvSpeechWithtarget->dvHeader.bSeqNum ); return hr; } #undef DPF_MODNAME #define DPF_MODNAME "CDirectVoiceClientEngine::UpdateActiveMixingPendingList" void CDirectVoiceServerEngine::UpdateActiveMixingPendingList( DWORD dwThreadIndex, DWORD *pdwNumActive) { CBilink *pblSearch; CDVCSPlayer *pVoicePlayer; DNEnterCriticalSection( &m_prWorkerControl[dwThreadIndex].m_csMixingAddList ); // Add players who are pending pblSearch = m_prWorkerControl[dwThreadIndex].m_blMixingAddPlayers.GetNext(); while( pblSearch != &m_prWorkerControl[dwThreadIndex].m_blMixingAddPlayers ) { CBilinkPlusObject* pRealBilink = CONTAINING_OBJECT( pblSearch, CBilinkPlusObject, m_bl ); pVoicePlayer = pRealBilink->m_pPlayer; ASSERT_VPLAYER(pVoicePlayer); pVoicePlayer->RemoveFromMixingList(dwThreadIndex); pVoicePlayer->AddToMixingList( dwThreadIndex, &m_prWorkerControl[dwThreadIndex].m_blMixingActivePlayers ); pblSearch = m_prWorkerControl[dwThreadIndex].m_blMixingAddPlayers.GetNext(); } DNASSERT( m_prWorkerControl[dwThreadIndex].m_blMixingAddPlayers.IsEmpty() ); DNLeaveCriticalSection( &m_prWorkerControl[dwThreadIndex].m_csMixingAddList ); *pdwNumActive = 0; // Remove players who have disconnected pblSearch = m_prWorkerControl[dwThreadIndex].m_blMixingActivePlayers.GetNext(); while( pblSearch != &m_prWorkerControl[dwThreadIndex].m_blMixingActivePlayers ) { CBilinkPlusObject* pRealBilink = CONTAINING_OBJECT( pblSearch, CBilinkPlusObject, m_bl ); pVoicePlayer = pRealBilink->m_pPlayer; ASSERT_VPLAYER(pVoicePlayer); pblSearch = pblSearch->GetNext(); // If current player has disconnected, remove them from active list // and release the reference the list has if( pVoicePlayer->IsDisconnected() ) { // Because players are removed from active list here, but not from hearing and/or speaking // list you may end up in a situation where a player is talking, drops and during removal // ends up in a state where they are not on the mixing list, but they are on the hearing // and speaking lists. Except, the hearing and speaking lists are initialized for the // next run in the spinworktothread. The result is when you go to delete the player // object and attempt to release the player it will assert and/or crash because player // still points to next person in the list (even though they don't point back). Causes // bilink corruption. pVoicePlayer->RemoveFromMixingList(dwThreadIndex); pVoicePlayer->RemoveFromHearingList(dwThreadIndex); pVoicePlayer->RemoveFromSpeakingList(dwThreadIndex); pVoicePlayer->Release(); } else { (*pdwNumActive)++; } } } #undef DPF_MODNAME #define DPF_MODNAME "CDirectVoiceClientEngine::CleanupMixingList" void CDirectVoiceServerEngine::CleanupMixingList() { CBilink *pblSearch; CDVCSPlayer *pVoicePlayer; for( DWORD dwIndex = 0; dwIndex < m_dwNumMixingThreads; dwIndex++ ) { DNEnterCriticalSection( &m_prWorkerControl[dwIndex].m_csMixingAddList ); // Add players who are pending pblSearch = m_prWorkerControl[dwIndex].m_blMixingAddPlayers.GetNext(); while( pblSearch != &m_prWorkerControl[dwIndex].m_blMixingAddPlayers ) { CBilinkPlusObject* pRealBilink = CONTAINING_OBJECT( pblSearch, CBilinkPlusObject, m_bl ); pVoicePlayer = pRealBilink->m_pPlayer; ASSERT_VPLAYER(pVoicePlayer); pblSearch = pblSearch->GetNext(); pVoicePlayer->RemoveFromMixingList(dwIndex); pVoicePlayer->Release(); } DNLeaveCriticalSection( &m_prWorkerControl[dwIndex].m_csMixingAddList ); DNASSERT( m_prWorkerControl[dwIndex].m_blMixingAddPlayers.IsEmpty() ); pblSearch = m_prWorkerControl[dwIndex].m_blMixingActivePlayers.GetNext(); while( pblSearch != &m_prWorkerControl[dwIndex].m_blMixingActivePlayers ) { CBilinkPlusObject* pRealBilink = CONTAINING_OBJECT( pblSearch, CBilinkPlusObject, m_bl ); pVoicePlayer = pRealBilink->m_pPlayer; ASSERT_VPLAYER(pVoicePlayer); pblSearch = pblSearch->GetNext(); pVoicePlayer->RemoveFromMixingList(dwIndex); pVoicePlayer->RemoveFromHearingList(dwIndex); pVoicePlayer->RemoveFromSpeakingList(dwIndex); pVoicePlayer->Release(); } } }