├── AudioLoopback.cpp ├── AudioLoopbackFilter.cpp ├── AudioLoopbackFilter.def ├── AudioLoopbackFilter.h ├── AudioLoopbackFilter.sln ├── AudioLoopbackFilter.suo ├── AudioLoopbackFilter.vcproj ├── AudioLoopbackFilter.vcxproj ├── AudioLoopbackFilter.vcxproj.filters ├── AudioLoopbackFilter.vcxproj.user ├── IAudioLoopbackFilter.h ├── README.md ├── dynsrc.h ├── log.h └── resource.h /AudioLoopback.cpp: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // File: AudioLoopback.cpp 3 | // 4 | // 5 | // Copyright (c) Corey Auger. All rights reserved. 6 | //------------------------------------------------------------------------------ 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | 22 | #include 23 | #if (1100 > _MSC_VER) 24 | #include 25 | #else 26 | #include 27 | #endif 28 | 29 | #define _AUDIOSYNTH_IMPLEMENTATION_ 30 | 31 | #include "DynSrc.h" 32 | #include "IAudioLoopbackFilter.h" 33 | #include "AudioLoopbackFilter.h" 34 | 35 | #include 36 | #include 37 | 38 | // Quick and dirty debug.. 39 | void Log( const char* frmt, ... ){ 40 | #ifdef _DEBUG 41 | char buf[2048]; 42 | va_list ptr; 43 | va_start(ptr,frmt); 44 | vsprintf_s(buf,frmt,ptr); 45 | OutputDebugStringA(buf); 46 | OutputDebugStringA("\n"); 47 | 48 | // static FILE *f = fopen("C:\\tmp\\_aloop.txt","a"); 49 | // fprintf(f,"%s",buf); 50 | // fflush(f); 51 | #endif 52 | } 53 | 54 | 55 | 56 | // setup data 57 | 58 | const AMOVIESETUP_MEDIATYPE sudOpPinTypes = 59 | { &MEDIATYPE_Audio // clsMajorType 60 | , &MEDIASUBTYPE_NULL }; // clsMinorType 61 | 62 | const AMOVIESETUP_PIN sudOpPin = 63 | { L"Output" // strName 64 | , FALSE // bRendered 65 | , TRUE // bOutput 66 | , FALSE // bZero 67 | , FALSE // bMany 68 | , &CLSID_NULL // clsConnectsToFilter 69 | , L"Input" // strConnectsToPin 70 | , 1 // nTypes 71 | , &sudOpPinTypes }; // lpTypes 72 | 73 | const AMOVIESETUP_FILTER sudSynth = 74 | { &CLSID_AudioLoopbackFilter // clsID 75 | , L"Audio Loopback" // strName 76 | , MERIT_UNLIKELY // dwMerit 77 | , 1 // nPins 78 | , &sudOpPin }; // lpPin 79 | 80 | // ------------------------------------------------------------------------- 81 | // g_Templates 82 | // ------------------------------------------------------------------------- 83 | // COM global table of objects in this dll 84 | 85 | CFactoryTemplate g_Templates[] = { 86 | 87 | { L"Audio Loopback" 88 | , &CLSID_AudioLoopbackFilter 89 | , CAudioLoopbackFilter::CreateInstance 90 | , NULL 91 | , &sudSynth } 92 | , 93 | //{ L"Audio Loopback Property Page" 94 | //, &CLSID_AudioLoopbackPropertyPage 95 | //, CAudioLoopbackProperties::CreateInstance } 96 | 97 | }; 98 | int g_cTemplates = sizeof(g_Templates) / sizeof(g_Templates[0]); 99 | 100 | // There are 8 bits in a byte. 101 | const DWORD BITS_PER_BYTE = 8; 102 | 103 | 104 | // ------------------------------------------------------------------------- 105 | // CSynthFilter, the main filter object 106 | // ------------------------------------------------------------------------- 107 | // 108 | // CreateInstance 109 | // 110 | // The only allowed way to create Synthesizers 111 | 112 | CUnknown * WINAPI CAudioLoopbackFilter::CreateInstance(LPUNKNOWN lpunk, HRESULT *phr) 113 | { 114 | ASSERT(phr); 115 | 116 | CUnknown *punk = new CAudioLoopbackFilter(lpunk, phr); 117 | if (punk == NULL) { 118 | if (phr) 119 | *phr = E_OUTOFMEMORY; 120 | } 121 | 122 | return punk; 123 | } 124 | 125 | 126 | CAudioLoopbackFilter::CAudioLoopbackFilter(LPUNKNOWN lpunk, HRESULT *phr) 127 | : CDynamicSource(NAME("Audio Loopback Filter"),lpunk, CLSID_AudioLoopbackFilter, phr) 128 | , CPersistStream(lpunk, phr) 129 | { 130 | m_paStreams = (CDynamicSourceStream **) new CSynthStream*[1]; 131 | if (m_paStreams == NULL) { 132 | if (phr) 133 | *phr = E_OUTOFMEMORY; 134 | return; 135 | } 136 | 137 | m_paStreams[0] = new CAudioLoopbackPin(phr, this, L"Audio Loopback Pin"); 138 | if (m_paStreams[0] == NULL) { 139 | if (phr) 140 | *phr = E_OUTOFMEMORY; 141 | return; 142 | } 143 | 144 | } 145 | 146 | 147 | CAudioLoopbackFilter::~CAudioLoopbackFilter(void) 148 | { 149 | // 150 | // Base class will free our pins 151 | // 152 | } 153 | 154 | 155 | // 156 | // NonDelegatingQueryInterface 157 | // 158 | // Reveal our property page, persistance, and control interfaces 159 | STDMETHODIMP CAudioLoopbackFilter::NonDelegatingQueryInterface(REFIID riid, void **ppv) 160 | { 161 | if (riid == IID_IAudioLoopbackFilter) { 162 | return GetInterface((IAudioLoopbackFilter *) this, ppv); 163 | } 164 | else if (riid == IID_IPersistStream) { 165 | return GetInterface((IPersistStream *) this, ppv); 166 | } 167 | else if (riid == IID_ISpecifyPropertyPages) { 168 | return GetInterface((ISpecifyPropertyPages *) this, ppv); 169 | } 170 | else { 171 | return CDynamicSource::NonDelegatingQueryInterface(riid, ppv); 172 | } 173 | } 174 | 175 | 176 | // 177 | // GetPages 178 | // 179 | STDMETHODIMP CAudioLoopbackFilter::GetPages(CAUUID * pPages) 180 | { 181 | CheckPointer(pPages,E_POINTER); 182 | 183 | pPages->cElems = 1; 184 | pPages->pElems = (GUID *) CoTaskMemAlloc(sizeof(GUID)); 185 | if (pPages->pElems == NULL) { 186 | return E_OUTOFMEMORY; 187 | } 188 | 189 | *(pPages->pElems) = CLSID_AudioLoopbackPropertyPage; 190 | 191 | return NOERROR; 192 | } 193 | 194 | 195 | // ------------------------------------------------------------------------- 196 | // --- IPersistStream --- 197 | // ------------------------------------------------------------------------- 198 | #define WRITEOUT(var) hr = pStream->Write(&var, sizeof(var), NULL); \ 199 | if (FAILED(hr)) return hr; 200 | 201 | #define READIN(var) hr = pStream->Read(&var, sizeof(var), NULL); \ 202 | if (FAILED(hr)) return hr; 203 | 204 | 205 | STDMETHODIMP CAudioLoopbackFilter::GetClassID(CLSID *pClsid) 206 | { 207 | return CBaseFilter::GetClassID(pClsid); 208 | } 209 | 210 | 211 | int CAudioLoopbackFilter::SizeMax () 212 | { 213 | return sizeof (int) * 8; 214 | } 215 | 216 | 217 | HRESULT CAudioLoopbackFilter::WriteToStream(IStream *pStream) 218 | { 219 | CheckPointer(pStream,E_POINTER); 220 | HRESULT hr; 221 | int i, k; 222 | return hr; 223 | } 224 | 225 | 226 | HRESULT CAudioLoopbackFilter::ReadFromStream(IStream *pStream) 227 | { 228 | CheckPointer(pStream,E_POINTER); 229 | if (GetSoftwareVersion() != mPS_dwFileVersion) 230 | return E_FAIL; 231 | 232 | HRESULT hr; 233 | int i, k; 234 | 235 | return hr; 236 | } 237 | 238 | 239 | DWORD CAudioLoopbackFilter::GetSoftwareVersion(void) 240 | { 241 | return 1; 242 | } 243 | 244 | 245 | // ------------------------------------------------------------------------- 246 | // IAudioLoopbackFilter, the control interface for the synthesizer 247 | // ------------------------------------------------------------------------- 248 | 249 | 250 | // ------------------------------------------------------------------------- 251 | // CAudioLoopbackPin, the output pin 252 | // ------------------------------------------------------------------------- 253 | 254 | // 255 | // CAudioLoopbackPin::Constructor 256 | // 257 | CAudioLoopbackPin::CAudioLoopbackPin(HRESULT *phr, CAudioLoopbackFilter *pParent, LPCWSTR pName) 258 | : CDynamicSourceStream(NAME("AudioLoopback Pin"),phr, pParent, pName) 259 | , m_hPCMToMSADPCMConversionStream(NULL) 260 | , m_dwTempPCMBufferSize(0) 261 | , m_fFirstSampleDelivered(FALSE) 262 | , m_llSampleMediaTimeStart(0) 263 | , m_dwAdviseToken(NULL) 264 | { 265 | ASSERT(phr); 266 | 267 | m_Loopback = new CAudioLoopback(pParent->pStateLock()); 268 | 269 | pParent->m_Loopback = m_Loopback; 270 | if (m_Loopback == NULL) { 271 | *phr = E_OUTOFMEMORY; 272 | return; 273 | } 274 | 275 | m_pParent = pParent; 276 | } 277 | 278 | 279 | // 280 | // CAudioLoopbackPin::Destructor 281 | CAudioLoopbackPin::~CAudioLoopbackPin(void) 282 | { 283 | delete m_Loopback; 284 | } 285 | 286 | 287 | // 288 | // FillBuffer 289 | // 290 | // Stuffs the buffer with data 291 | HRESULT CAudioLoopbackPin::FillBuffer(IMediaSample *pms) 292 | { 293 | // Try to enter the semaphore gate. 294 | DWORD dwWaitResult = WaitForSingleObject( m_hSemaphore, INFINITE); 295 | 296 | CheckPointer(pms,E_POINTER); 297 | 298 | BYTE *pData; 299 | 300 | HRESULT hr = pms->GetPointer(&pData); 301 | if (FAILED(hr)) { 302 | return hr; 303 | } 304 | 305 | // This function must hold the state lock because it calls 306 | // FillPCMAudioBuffer(). 307 | CAutoLock lStateLock(m_pParent->pStateLock()); 308 | 309 | // This lock must be held because this function uses 310 | // m_dwTempPCMBufferSize, m_hPCMToMSADPCMConversionStream, 311 | // m_rtSampleTime, m_fFirstSampleDelivered and 312 | // m_llSampleMediaTimeStart. 313 | CAutoLock lShared(&m_cSharedState); 314 | 315 | WAVEFORMATEX* pwfexCurrent = (WAVEFORMATEX*)m_mt.Format(); 316 | 317 | if (WAVE_FORMAT_PCM == pwfexCurrent->wFormatTag) 318 | { 319 | //m_Synth->FillPCMAudioBuffer(*pwfexCurrent, pData, pms->GetSize()); 320 | long size = pms->GetSize(); 321 | m_Loopback->FillPCMAudioBuffer(*pwfexCurrent, pData, size); 322 | 323 | //if( size == 0 )return NOERROR; 324 | 325 | hr = pms->SetActualDataLength(size); 326 | if (FAILED(hr)) 327 | return hr; 328 | 329 | } 330 | else 331 | { 332 | // This filter only supports two audio formats: PCM and ADPCM. 333 | /* 334 | ASSERT(WAVE_FORMAT_ADPCM == pwfexCurrent->wFormatTag); 335 | 336 | // Create PCM audio samples and then compress them. We use the 337 | // Audio Compression Manager (ACM) API to convert the samples to 338 | // the ADPCM format. 339 | 340 | ACMSTREAMHEADER ACMStreamHeader; 341 | 342 | ACMStreamHeader.cbStruct = sizeof(ACMStreamHeader); 343 | ACMStreamHeader.fdwStatus = 0; 344 | ACMStreamHeader.dwUser = 0; 345 | ACMStreamHeader.cbSrcLength = m_dwTempPCMBufferSize; 346 | ACMStreamHeader.cbSrcLengthUsed = 0; 347 | ACMStreamHeader.dwSrcUser = 0; 348 | ACMStreamHeader.pbDst = pData; 349 | ACMStreamHeader.cbDstLength = pms->GetSize(); 350 | ACMStreamHeader.cbDstLengthUsed = 0; 351 | ACMStreamHeader.dwDstUser = 0; 352 | ACMStreamHeader.pbSrc = new BYTE[m_dwTempPCMBufferSize]; 353 | if (NULL == ACMStreamHeader.pbSrc) { 354 | return E_OUTOFMEMORY; 355 | } 356 | 357 | WAVEFORMATEX wfexPCMAudio; 358 | 359 | DerivePCMFormatFromADPCMFormatStructure(*pwfexCurrent, &wfexPCMAudio); 360 | 361 | m_Synth->FillPCMAudioBuffer(wfexPCMAudio, 362 | ACMStreamHeader.pbSrc, 363 | ACMStreamHeader.cbSrcLength); 364 | 365 | MMRESULT mmr = acmStreamPrepareHeader(m_hPCMToMSADPCMConversionStream, 366 | &ACMStreamHeader, 367 | 0); 368 | 369 | // acmStreamPrepareHeader() returns 0 if no errors occur. 370 | if (mmr != 0) { 371 | delete [] ACMStreamHeader.pbSrc; 372 | return E_FAIL; 373 | } 374 | 375 | mmr = acmStreamConvert(m_hPCMToMSADPCMConversionStream, 376 | &ACMStreamHeader, 377 | ACM_STREAMCONVERTF_BLOCKALIGN); 378 | 379 | MMRESULT mmrUnprepare = acmStreamUnprepareHeader(m_hPCMToMSADPCMConversionStream, 380 | &ACMStreamHeader, 381 | 0); 382 | 383 | delete [] ACMStreamHeader.pbSrc; 384 | 385 | // acmStreamConvert() andacmStreamUnprepareHeader() returns 0 if no errors occur. 386 | if ((mmr != 0) || (mmrUnprepare != 0)) { 387 | return E_FAIL; 388 | } 389 | 390 | hr = pms->SetActualDataLength(ACMStreamHeader.cbDstLengthUsed); 391 | if (FAILED(hr)) { 392 | return hr; 393 | } 394 | */ 395 | } 396 | 397 | // Set the sample's time stamps. 398 | CRefTime rtStart = m_rtSampleTime; 399 | 400 | m_rtSampleTime = rtStart + (REFERENCE_TIME)(UNITS * pms->GetActualDataLength()) / 401 | (REFERENCE_TIME)pwfexCurrent->nAvgBytesPerSec; 402 | 403 | hr = pms->SetTime((REFERENCE_TIME*)&rtStart, (REFERENCE_TIME*)&m_rtSampleTime); 404 | 405 | if (FAILED(hr)) { 406 | return hr; 407 | } 408 | 409 | // Set the sample's properties. 410 | hr = pms->SetPreroll(FALSE); 411 | if (FAILED(hr)) { 412 | return hr; 413 | } 414 | 415 | hr = pms->SetMediaType(NULL); 416 | if (FAILED(hr)) { 417 | return hr; 418 | } 419 | 420 | hr = pms->SetDiscontinuity(!m_fFirstSampleDelivered); 421 | if (FAILED(hr)) { 422 | return hr; 423 | } 424 | 425 | hr = pms->SetSyncPoint(!m_fFirstSampleDelivered); 426 | if (FAILED(hr)) { 427 | return hr; 428 | } 429 | 430 | LONGLONG llMediaTimeStart = m_llSampleMediaTimeStart; 431 | 432 | DWORD dwNumAudioSamplesInPacket = (pms->GetActualDataLength() * BITS_PER_BYTE) / 433 | (pwfexCurrent->nChannels * pwfexCurrent->wBitsPerSample); 434 | 435 | LONGLONG llMediaTimeStop = m_llSampleMediaTimeStart + dwNumAudioSamplesInPacket; 436 | 437 | hr = pms->SetMediaTime(&llMediaTimeStart, &llMediaTimeStop); 438 | if (FAILED(hr)) { 439 | return hr; 440 | } 441 | 442 | m_llSampleMediaTimeStart = llMediaTimeStop; 443 | m_fFirstSampleDelivered = TRUE; 444 | 445 | return NOERROR; 446 | } 447 | 448 | 449 | // 450 | // Format Support 451 | // 452 | 453 | // 454 | // GetMediaType 455 | // 456 | HRESULT CAudioLoopbackPin::GetMediaType(CMediaType *pmt) 457 | { 458 | CheckPointer(pmt,E_POINTER); 459 | 460 | // The caller must hold the state lock because this function 461 | // calls get_OutputFormat() and GetPCMFormatStructure(). 462 | // The function assumes that the state of the m_Synth 463 | // object does not change between the two calls. The 464 | // m_Synth object's state will not change if the 465 | // state lock is held. 466 | ASSERT(CritCheckIn(m_pParent->pStateLock())); 467 | 468 | //WAVEFORMATEX *pwfex; 469 | WAVEFORMATEX *pwfex = m_pParent->m_Loopback->Format(); 470 | 471 | SYNTH_OUTPUT_FORMAT ofCurrent = SYNTH_OF_PCM; 472 | if(SYNTH_OF_PCM == ofCurrent) 473 | { 474 | pwfex = (WAVEFORMATEX *) pmt->AllocFormatBuffer(sizeof(WAVEFORMATEX)); 475 | if(NULL == pwfex) 476 | { 477 | return E_OUTOFMEMORY; 478 | } 479 | 480 | m_Loopback->GetPCMFormatStructure(pwfex); 481 | } 482 | else if(SYNTH_OF_MS_ADPCM == ofCurrent) 483 | { 484 | DWORD dwMaxWAVEFORMATEXSize; 485 | 486 | MMRESULT mmr = acmMetrics(NULL, ACM_METRIC_MAX_SIZE_FORMAT, 487 | (void*)&dwMaxWAVEFORMATEXSize); 488 | 489 | // acmMetrics() returns 0 if no errors occur. 490 | if(0 != mmr) 491 | { 492 | return E_FAIL; 493 | } 494 | 495 | pwfex = (WAVEFORMATEX *) pmt->AllocFormatBuffer(dwMaxWAVEFORMATEXSize); 496 | if(NULL == pwfex) 497 | { 498 | return E_OUTOFMEMORY; 499 | } 500 | 501 | WAVEFORMATEX wfexSourceFormat; 502 | m_Loopback->GetPCMFormatStructure(&wfexSourceFormat); 503 | 504 | ZeroMemory(pwfex, dwMaxWAVEFORMATEXSize); 505 | pwfex->wFormatTag = WAVE_FORMAT_ADPCM; 506 | pwfex->cbSize = (USHORT)(dwMaxWAVEFORMATEXSize - sizeof(WAVEFORMATEX)); 507 | pwfex->nChannels = wfexSourceFormat.nChannels; 508 | pwfex->nSamplesPerSec = wfexSourceFormat.nSamplesPerSec; 509 | 510 | mmr = acmFormatSuggest(NULL, 511 | &wfexSourceFormat, 512 | pwfex, 513 | dwMaxWAVEFORMATEXSize, 514 | ACM_FORMATSUGGESTF_WFORMATTAG | 515 | ACM_FORMATSUGGESTF_NSAMPLESPERSEC | 516 | ACM_FORMATSUGGESTF_NCHANNELS); 517 | // acmFormatSuggest() returns 0 if no errors occur. 518 | if(0 != mmr) 519 | { 520 | return E_FAIL; 521 | } 522 | 523 | } 524 | else 525 | { 526 | return E_UNEXPECTED; 527 | } 528 | 529 | return CreateAudioMediaType(pwfex, pmt, FALSE); 530 | } 531 | 532 | 533 | HRESULT CAudioLoopbackPin::CompleteConnect(IPin *pReceivePin) 534 | { 535 | // This lock must be held because this function uses 536 | // m_hPCMToMSADPCMConversionStream, m_fFirstSampleDelivered 537 | // and m_llSampleMediaTimeStart. 538 | CAutoLock lShared(&m_cSharedState); 539 | 540 | HRESULT hr; 541 | WAVEFORMATEX *pwfexCurrent = (WAVEFORMATEX*)m_mt.Format(); 542 | 543 | if(WAVE_FORMAT_PCM == pwfexCurrent->wFormatTag) 544 | { 545 | 546 | } 547 | else if(WAVE_FORMAT_ADPCM == pwfexCurrent->wFormatTag) 548 | { 549 | WAVEFORMATEX wfexSourceFormat; 550 | 551 | DerivePCMFormatFromADPCMFormatStructure(*pwfexCurrent, &wfexSourceFormat); 552 | 553 | MMRESULT mmr = acmStreamOpen(&m_hPCMToMSADPCMConversionStream, 554 | NULL, 555 | &wfexSourceFormat, 556 | pwfexCurrent, 557 | NULL, 558 | 0, 559 | 0, 560 | ACM_STREAMOPENF_NONREALTIME); 561 | // acmStreamOpen() returns 0 if an no errors occur. 562 | if(mmr != 0) 563 | { 564 | return E_FAIL; 565 | } 566 | } 567 | else 568 | { 569 | ASSERT(NULL == m_hPCMToMSADPCMConversionStream); 570 | 571 | } 572 | 573 | hr = CDynamicSourceStream::CompleteConnect(pReceivePin); 574 | if(FAILED(hr)) 575 | { 576 | if(WAVE_FORMAT_ADPCM == pwfexCurrent->wFormatTag) 577 | { 578 | // acmStreamClose() should never fail because m_hPCMToMSADPCMConversionStream 579 | // holds a valid ACM stream handle and all operations using the handle are 580 | // synchronous. 581 | EXECUTE_ASSERT(0 == acmStreamClose(m_hPCMToMSADPCMConversionStream, 0)); 582 | m_hPCMToMSADPCMConversionStream = NULL; 583 | } 584 | 585 | return hr; 586 | } 587 | 588 | m_fFirstSampleDelivered = FALSE; 589 | m_llSampleMediaTimeStart = 0; 590 | 591 | return S_OK; 592 | } 593 | 594 | 595 | void CAudioLoopbackPin::DerivePCMFormatFromADPCMFormatStructure(const WAVEFORMATEX& wfexADPCM, 596 | WAVEFORMATEX* pwfexPCM) 597 | { 598 | ASSERT(pwfexPCM); 599 | if (!pwfexPCM) 600 | return; 601 | 602 | pwfexPCM->wFormatTag = WAVE_FORMAT_PCM; 603 | pwfexPCM->wBitsPerSample = 16; 604 | pwfexPCM->cbSize = 0; 605 | 606 | pwfexPCM->nChannels = wfexADPCM.nChannels; 607 | pwfexPCM->nSamplesPerSec = wfexADPCM.nSamplesPerSec; 608 | 609 | pwfexPCM->nBlockAlign = (WORD)((pwfexPCM->nChannels * pwfexPCM->wBitsPerSample) / BITS_PER_BYTE); 610 | pwfexPCM->nAvgBytesPerSec = pwfexPCM->nBlockAlign * pwfexPCM->nSamplesPerSec; 611 | } 612 | 613 | 614 | HRESULT CAudioLoopbackPin::BreakConnect(void) 615 | { 616 | // This lock must be held because this function uses 617 | // m_hPCMToMSADPCMConversionStream and m_dwTempPCMBufferSize. 618 | CAutoLock lShared(&m_cSharedState); 619 | 620 | HRESULT hr = CDynamicSourceStream::BreakConnect(); 621 | if(FAILED(hr)) 622 | { 623 | return hr; 624 | } 625 | 626 | if(NULL != m_hPCMToMSADPCMConversionStream) 627 | { 628 | // acmStreamClose() should never fail because m_hPCMToMSADPCMConversionStream 629 | // holds a valid ACM stream handle and all operations using the handle are 630 | // synchronous. 631 | EXECUTE_ASSERT(0 == acmStreamClose(m_hPCMToMSADPCMConversionStream, 0)); 632 | m_hPCMToMSADPCMConversionStream = NULL; 633 | m_dwTempPCMBufferSize = 0; 634 | } 635 | if(m_dwAdviseToken){ 636 | m_pClock->Unadvise(m_dwAdviseToken); 637 | m_dwAdviseToken = NULL; 638 | } 639 | 640 | return S_OK; 641 | } 642 | 643 | 644 | // 645 | // DecideBufferSize 646 | // 647 | // This will always be called after the format has been sucessfully 648 | // negotiated. So we have a look at m_mt to see what format we agreed to. 649 | // Then we can ask for buffers of the correct size to contain them. 650 | HRESULT CAudioLoopbackPin::DecideBufferSize(IMemAllocator *pAlloc, 651 | ALLOCATOR_PROPERTIES *pProperties) 652 | { 653 | // The caller should always hold the shared state lock 654 | // before calling this function. This function must hold 655 | // the shared state lock because it uses m_hPCMToMSADPCMConversionStream 656 | // m_dwTempPCMBufferSize. 657 | ASSERT(CritCheckIn(&m_cSharedState)); 658 | 659 | CheckPointer(pAlloc,E_POINTER); 660 | CheckPointer(pProperties,E_POINTER); 661 | 662 | WAVEFORMATEX *pwfexCurrent = (WAVEFORMATEX*)m_mt.Format(); 663 | //WAVEFORMATEX *pwfexCurrent = m_pParent->m_Loopback->Format(); 664 | 665 | 666 | if(WAVE_FORMAT_PCM == pwfexCurrent->wFormatTag) 667 | { 668 | pProperties->cbBuffer = WaveBufferSize; 669 | } 670 | else 671 | { 672 | // This filter only supports two formats: PCM and ADPCM. 673 | ASSERT(WAVE_FORMAT_ADPCM == pwfexCurrent->wFormatTag); 674 | 675 | pProperties->cbBuffer = pwfexCurrent->nBlockAlign; 676 | 677 | MMRESULT mmr = acmStreamSize(m_hPCMToMSADPCMConversionStream, 678 | pwfexCurrent->nBlockAlign, 679 | &m_dwTempPCMBufferSize, 680 | ACM_STREAMSIZEF_DESTINATION); 681 | 682 | // acmStreamSize() returns 0 if no error occurs. 683 | if(0 != mmr) 684 | { 685 | return E_FAIL; 686 | } 687 | } 688 | 689 | int nBitsPerSample = pwfexCurrent->wBitsPerSample; 690 | int nSamplesPerSec = pwfexCurrent->nSamplesPerSec; 691 | int nChannels = pwfexCurrent->nChannels; 692 | 693 | pProperties->cBuffers = (nChannels * nSamplesPerSec * nBitsPerSample) / 694 | (pProperties->cbBuffer * BITS_PER_BYTE); 695 | 696 | // Get 1/2 second worth of buffers 697 | pProperties->cBuffers /= 2; 698 | if(pProperties->cBuffers < 1) 699 | pProperties->cBuffers = 1 ; 700 | 701 | // Ask the allocator to reserve us the memory 702 | ALLOCATOR_PROPERTIES Actual; 703 | HRESULT hr = pAlloc->SetProperties(pProperties,&Actual); 704 | if(FAILED(hr)) 705 | { 706 | return hr; 707 | } 708 | 709 | // Is this allocator unsuitable 710 | if(Actual.cbBuffer < pProperties->cbBuffer) 711 | { 712 | return E_FAIL; 713 | } 714 | 715 | return NOERROR; 716 | } 717 | 718 | 719 | // 720 | // Active 721 | // 722 | HRESULT CAudioLoopbackPin::Active(void) 723 | { 724 | // This lock must be held because the function 725 | // uses m_rtSampleTime, m_fFirstSampleDelivered 726 | // and m_llSampleMediaTimeStart. 727 | CAutoLock lShared(&m_cSharedState); 728 | 729 | HRESULT hr = CDynamicSourceStream::Active(); 730 | if(FAILED(hr)) 731 | { 732 | return hr; 733 | } 734 | 735 | m_rtSampleTime = 0; 736 | m_fFirstSampleDelivered = FALSE; 737 | m_llSampleMediaTimeStart = 0; 738 | 739 | return NOERROR; 740 | } 741 | 742 | 743 | HRESULT CAudioLoopbackPin::Run(REFERENCE_TIME tStart) 744 | { 745 | if( !m_dwAdviseToken ){ 746 | HRESULT hr = S_OK; 747 | m_hSemaphore = CreateSemaphore(NULL, 0, 0x7FFFFFFF, NULL); 748 | const REFERENCE_TIME rtPeriodTime = m_Loopback->m_hnsDefaultDevicePeriod; 749 | 750 | hr = m_pClock->AdvisePeriodic(tStart+rtPeriodTime, rtPeriodTime, (HSEMAPHORE)m_hSemaphore, &m_dwAdviseToken); 751 | //hr = m_pClock->AdvisePeriodic(1, rtPeriodTime, (HSEMAPHORE)m_hSemaphore, &m_dwAdviseToken); 752 | if( hr != S_OK ){ 753 | // TODO: log this. 754 | return hr; 755 | } 756 | } 757 | //return CBaseOutputPin::Run(0); 758 | return CBaseOutputPin::Run(tStart); 759 | } 760 | 761 | 762 | HRESULT CAudioLoopbackPin::SetSyncSource(IReferenceClock *pClock) 763 | { 764 | m_pClock = pClock; 765 | return S_OK; 766 | } 767 | 768 | 769 | // ------------------------------------------------------------------------- 770 | // CAudioLoopback 771 | // ------------------------------------------------------------------------- 772 | HRESULT get_default_device(IMMDevice **ppMMDevice) { 773 | HRESULT hr = S_OK; 774 | IMMDeviceEnumerator *pMMDeviceEnumerator; 775 | 776 | // activate a device enumerator 777 | hr = CoCreateInstance( 778 | __uuidof(MMDeviceEnumerator), NULL, CLSCTX_ALL, 779 | __uuidof(IMMDeviceEnumerator), 780 | (void**)&pMMDeviceEnumerator 781 | ); 782 | if (FAILED(hr)) { 783 | Log( "CoCreateInstance(IMMDeviceEnumerator) failed: hr = 0x%08x",hr); 784 | return hr; 785 | } 786 | 787 | // get the default render endpoint 788 | hr = pMMDeviceEnumerator->GetDefaultAudioEndpoint(eRender, eConsole, ppMMDevice); 789 | pMMDeviceEnumerator->Release(); 790 | if (FAILED(hr)) { 791 | Log("IMMDeviceEnumerator::GetDefaultAudioEndpoint failed: hr = 0x%08x",hr); 792 | return hr; 793 | } 794 | 795 | return S_OK; 796 | } 797 | 798 | 799 | 800 | CAudioLoopback::CAudioLoopback(CCritSec* pStateLock) 801 | :pnFrames(0) 802 | ,bFirstPacket(true) 803 | { 804 | 805 | Initialize(); // should take out of constructor 806 | 807 | } 808 | CAudioLoopback::~CAudioLoopback(){ 809 | AvRevertMmThreadCharacteristics(m_hTask); 810 | m_pAudioClient->Release(); 811 | m_pAudioCaptureClient->Release(); 812 | CoTaskMemFree(m_pwfx); 813 | delete[] m_silenceBuf; 814 | } 815 | 816 | void CAudioLoopback::GetPCMFormatStructure(WAVEFORMATEX* pwfex) 817 | { 818 | ASSERT(pwfex); 819 | if (!pwfex) 820 | return; 821 | pwfex->wFormatTag = WAVE_FORMAT_PCM; 822 | pwfex->nChannels = m_pwfx->nChannels; 823 | pwfex->nSamplesPerSec = m_pwfx->nSamplesPerSec; 824 | pwfex->wBitsPerSample = m_pwfx->wBitsPerSample; 825 | pwfex->nBlockAlign = m_pwfx->nBlockAlign; 826 | pwfex->nAvgBytesPerSec = pwfex->nBlockAlign * pwfex->nSamplesPerSec; 827 | pwfex->cbSize = m_pwfx->cbSize; 828 | } 829 | 830 | HRESULT CAudioLoopback::Initialize() 831 | { 832 | 833 | IMMDevice *pMMDevice; 834 | HRESULT hr; 835 | 836 | hr = get_default_device(&pMMDevice); 837 | if (FAILED(hr)) { 838 | return hr; 839 | } 840 | 841 | // activate an IAudioClient 842 | hr = pMMDevice->Activate( 843 | __uuidof(IAudioClient), 844 | CLSCTX_ALL, NULL, 845 | (void**)&m_pAudioClient 846 | ); 847 | if (FAILED(hr)) { 848 | Log("IMMDevice::Activate(IAudioClient) failed: hr = 0x%08x",hr); 849 | return hr; 850 | } 851 | 852 | // get the default device periodicity 853 | 854 | hr = m_pAudioClient->GetDevicePeriod(&m_hnsDefaultDevicePeriod, NULL); 855 | if (FAILED(hr)) { 856 | Log("IAudioClient::GetDevicePeriod failed: hr = 0x%08x", hr); 857 | m_pAudioClient->Release(); 858 | return hr; 859 | } 860 | Log("GetDevicePeriod set %d\n", m_hnsDefaultDevicePeriod); 861 | Log("Milliseconds beween fire %d\n", (int)(m_hnsDefaultDevicePeriod / 2 / (10 * 1000))); // convert to milliseconds 862 | 863 | // get the default device format 864 | 865 | hr = m_pAudioClient->GetMixFormat(&m_pwfx); 866 | if (FAILED(hr)) { 867 | Log("IAudioClient::GetMixFormat failed: hr = 0x%08x",hr); 868 | return hr; 869 | } 870 | 871 | //if (bInt16) { 872 | if( true ){ 873 | // coerce int-16 wave format 874 | // can do this in-place since we're not changing the size of the format 875 | // also, the engine will auto-convert from float to int for us 876 | switch (m_pwfx->wFormatTag) { 877 | case WAVE_FORMAT_IEEE_FLOAT: 878 | m_pwfx->wFormatTag = WAVE_FORMAT_PCM; 879 | m_pwfx->wBitsPerSample = 16; 880 | m_pwfx->nBlockAlign = m_pwfx->nChannels * m_pwfx->wBitsPerSample / 8; 881 | m_pwfx->nAvgBytesPerSec = m_pwfx->nBlockAlign * m_pwfx->nSamplesPerSec; 882 | break; 883 | 884 | case WAVE_FORMAT_EXTENSIBLE: 885 | { 886 | // naked scope for case-local variable 887 | PWAVEFORMATEXTENSIBLE pEx = reinterpret_cast(m_pwfx); 888 | if (IsEqualGUID(KSDATAFORMAT_SUBTYPE_IEEE_FLOAT, pEx->SubFormat)) { 889 | pEx->SubFormat = KSDATAFORMAT_SUBTYPE_PCM; 890 | pEx->Samples.wValidBitsPerSample = 16; 891 | m_pwfx->wBitsPerSample = 16; 892 | m_pwfx->nBlockAlign = m_pwfx->nChannels * m_pwfx->wBitsPerSample / 8; 893 | m_pwfx->nAvgBytesPerSec = m_pwfx->nBlockAlign * m_pwfx->nSamplesPerSec; 894 | } else { 895 | Log("Don't know how to coerce mix format to int-16\n"); 896 | return E_UNEXPECTED; 897 | } 898 | } 899 | break; 900 | 901 | default: 902 | Log("Don't know how to coerce WAVEFORMATEX with wFormatTag = 0x%08x to int-16",m_pwfx->wFormatTag); 903 | return E_UNEXPECTED; 904 | } 905 | } 906 | 907 | Log("wFormatTag: %d\n",m_pwfx->wFormatTag); 908 | Log("wBitsPerSampl: %d\n",m_pwfx->wBitsPerSample); 909 | Log("nChannels: %d\n",m_pwfx->nChannels); 910 | Log("wBitsPerSample: %d\n",m_pwfx->wBitsPerSample); 911 | Log("nBlockAlign: %d\n",m_pwfx->nBlockAlign); 912 | Log("nSamplesPerSec: %d\n",m_pwfx->nSamplesPerSec); 913 | Log("nAvgBytesPerSec: %d\n",m_pwfx->nAvgBytesPerSec); 914 | 915 | m_silenceBuf = new BYTE[SILENCE_BUF_SIZE]; 916 | memset(m_silenceBuf, 128, SILENCE_BUF_SIZE); 917 | 918 | 919 | UINT32 nBlockAlign = m_pwfx->nBlockAlign; 920 | 921 | // call IAudioClient::Initialize 922 | // note that AUDCLNT_STREAMFLAGS_LOOPBACK and AUDCLNT_STREAMFLAGS_EVENTCALLBACK 923 | // do not work together... 924 | // the "data ready" event never gets set 925 | // so we're going to do a timer-driven loop 926 | hr = m_pAudioClient->Initialize( 927 | AUDCLNT_SHAREMODE_SHARED, 928 | AUDCLNT_STREAMFLAGS_LOOPBACK, 929 | 0, 0, m_pwfx, 0 930 | ); 931 | if (FAILED(hr)) { 932 | Log( "IAudioClient::Initialize failed: hr = 0x%08x", hr); 933 | m_pAudioClient->Release(); 934 | return hr; 935 | } 936 | 937 | // activate an IAudioCaptureClient 938 | hr = m_pAudioClient->GetService( 939 | __uuidof(IAudioCaptureClient), 940 | (void**)&m_pAudioCaptureClient 941 | ); 942 | if (FAILED(hr)) { 943 | Log( "IAudioClient::GetService(IAudioCaptureClient) failed: hr 0x%08x", hr); 944 | return hr; 945 | } 946 | 947 | // register with MMCSS 948 | DWORD nTaskIndex = 0; 949 | m_hTask = AvSetMmThreadCharacteristicsW(L"Capture", &nTaskIndex); 950 | if (NULL == m_hTask) { 951 | DWORD dwErr = GetLastError(); 952 | Log("AvSetMmThreadCharacteristics failed: last error = %u\n", dwErr); 953 | return HRESULT_FROM_WIN32(dwErr); 954 | } 955 | 956 | // call IAudioClient::Start 957 | hr = m_pAudioClient->Start(); 958 | if (FAILED(hr)) { 959 | Log("IAudioClient::Start failed: hr = 0x%08x\n", hr); 960 | return hr; 961 | } 962 | } 963 | 964 | 965 | 966 | 967 | HRESULT CAudioLoopback::FillPCMAudioBuffer(const WAVEFORMATEX& wfex, BYTE pBuf[], long& iSize) 968 | { 969 | HRESULT hr = S_OK; 970 | // grab next audio chunk... 971 | UINT32 nNextPacketSize; 972 | hr = m_pAudioCaptureClient->GetNextPacketSize(&nNextPacketSize); // get next packet, if one is ready... 973 | if (FAILED(hr)) { 974 | Log("IAudioCaptureClient::GetNextPacketSize failed after %u frames: hr = 0x%08x\n", pnFrames, hr); 975 | m_pAudioClient->Stop(); 976 | return hr; 977 | } 978 | 979 | // get the captured data 980 | BYTE *pData; 981 | UINT32 nNumFramesToRead; 982 | DWORD dwFlags; 983 | 984 | hr = m_pAudioCaptureClient->GetBuffer( 985 | &pData, 986 | &nNumFramesToRead, 987 | &dwFlags, 988 | NULL, 989 | NULL 990 | ); 991 | 992 | if (FAILED(hr)) { 993 | Log("IAudioCaptureClient::GetBuffer failed after %u frames: hr = 0x%08x\n", pnFrames, hr); 994 | m_pAudioClient->Stop(); 995 | return hr; 996 | } 997 | 998 | if (0 == nNumFramesToRead) { 999 | // We have silence in this case... 1000 | iSize = SILENCE_BUF_SIZE; 1001 | memcpy(pBuf, m_silenceBuf, iSize); // (CA) - inject silence buffer.. 1002 | return S_OK; 1003 | } 1004 | pnFrames += nNumFramesToRead; // increment total count... 1005 | 1006 | // lBytesToWrite typically 1792 bytes... 1007 | LONG lBytesToWrite = nNumFramesToRead * m_pwfx->nBlockAlign; // nBlockAlign is "audio block size" or frame size, for one audio segment... 1008 | iSize = min(iSize, lBytesToWrite); // (CA) NOTE: this sets the size of the refrence var 1009 | memcpy(pBuf, pData, iSize); 1010 | 1011 | hr = m_pAudioCaptureClient->ReleaseBuffer(nNumFramesToRead); 1012 | if (FAILED(hr)) { 1013 | Log("IAudioCaptureClient::ReleaseBuffer failed after %u frames: hr = 0x%08x\n", pnFrames, hr); 1014 | m_pAudioClient->Stop(); 1015 | return hr; 1016 | } 1017 | return S_OK; 1018 | } 1019 | 1020 | 1021 | //////////////////////////////////////////////////////////////////////// 1022 | // 1023 | // Exported entry points for registration and unregistration 1024 | // (in this case they only call through to default implementations). 1025 | // 1026 | //////////////////////////////////////////////////////////////////////// 1027 | 1028 | STDAPI DllRegisterServer() 1029 | { 1030 | return AMovieDllRegisterServer2(TRUE); 1031 | } 1032 | 1033 | STDAPI DllUnregisterServer() 1034 | { 1035 | return AMovieDllRegisterServer2(FALSE); 1036 | } 1037 | 1038 | // 1039 | // DllEntryPoint 1040 | // 1041 | extern "C" BOOL WINAPI DllEntryPoint(HINSTANCE, ULONG, LPVOID); 1042 | 1043 | BOOL APIENTRY DllMain(HANDLE hModule, 1044 | DWORD dwReason, 1045 | LPVOID lpReserved) 1046 | { 1047 | return DllEntryPoint((HINSTANCE)(hModule), dwReason, lpReserved); 1048 | } 1049 | 1050 | 1051 | -------------------------------------------------------------------------------- /AudioLoopbackFilter.cpp: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // File: AudioLoopbackFilter.cpp 3 | // 4 | // Desc: (CA) Mode from DirectShow sample code 5 | // - implements CDynamicSource, which is a 6 | // Quartz source filter. 7 | // 8 | // Copyright (c) Corey Auger. All rights reserved. 9 | //------------------------------------------------------------------------------ 10 | 11 | #include 12 | #include 13 | 14 | #include "DynSrc.h" 15 | #include "log.h" 16 | 17 | 18 | CDynamicSource::CDynamicSource(TCHAR *pName, LPUNKNOWN lpunk, CLSID clsid, HRESULT *phr) 19 | : CBaseFilter(pName, lpunk, &m_cStateLock, clsid), 20 | m_iPins(0), 21 | m_paStreams(NULL), 22 | m_evFilterStoppingEvent(TRUE) 23 | { 24 | ASSERT(phr); 25 | 26 | // Make sure the event was successfully created. 27 | if( NULL == (HANDLE)m_evFilterStoppingEvent ) { 28 | if (phr) 29 | *phr = E_FAIL; 30 | } 31 | } 32 | 33 | 34 | #ifdef UNICODE 35 | CDynamicSource::CDynamicSource(CHAR *pName, LPUNKNOWN lpunk, CLSID clsid, HRESULT *phr) 36 | : CBaseFilter(pName, lpunk, &m_cStateLock, clsid), 37 | m_iPins(0), 38 | m_paStreams(NULL), 39 | m_evFilterStoppingEvent(TRUE) 40 | { 41 | ASSERT(phr); 42 | 43 | // Make sure the event was successfully created. 44 | if( NULL == (HANDLE)m_evFilterStoppingEvent ) { 45 | if (phr) 46 | *phr = E_FAIL; 47 | } 48 | } 49 | #endif 50 | 51 | 52 | // 53 | // CDynamicSource::Destructor 54 | // 55 | CDynamicSource::~CDynamicSource() 56 | { 57 | /* Free our pins and pin array */ 58 | while (m_iPins != 0) 59 | { 60 | // deleting the pins causes them to be removed from the array... 61 | delete m_paStreams[m_iPins - 1]; 62 | } 63 | 64 | ASSERT(m_paStreams == NULL); 65 | } 66 | 67 | 68 | // 69 | // Add a new pin 70 | // 71 | HRESULT CDynamicSource::AddPin(CDynamicSourceStream *pStream) 72 | { 73 | // This function holds the filter state lock because 74 | // it changes the number of pins. The number of pins 75 | // cannot be change while another thread is calling 76 | // IMediaFilter::Run(), IMediaFilter::Stop(), 77 | // IMediaFilter::Pause() or IBaseFilter::FindPin(). 78 | CAutoLock lock(&m_cStateLock); 79 | 80 | // This function holds the pin state lock because it 81 | // uses m_iPins and m_paStreams. 82 | CAutoLock alPinStateLock(&m_csPinStateLock); 83 | 84 | /* Allocate space for this pin and the old ones */ 85 | CDynamicSourceStream **paStreams = new CDynamicSourceStream *[m_iPins + 1]; 86 | if (paStreams == NULL) 87 | return E_OUTOFMEMORY; 88 | 89 | if (m_paStreams != NULL) 90 | { 91 | CopyMemory((PVOID)paStreams, (PVOID)m_paStreams, 92 | m_iPins * sizeof(m_paStreams[0])); 93 | 94 | paStreams[m_iPins] = pStream; 95 | delete [] m_paStreams; 96 | } 97 | 98 | m_paStreams = paStreams; 99 | m_paStreams[m_iPins] = pStream; 100 | m_iPins++; 101 | 102 | return S_OK; 103 | } 104 | 105 | 106 | // 107 | // Remove a pin - pStream is NOT deleted 108 | // 109 | HRESULT CDynamicSource::RemovePin(CDynamicSourceStream *pStream) 110 | { 111 | // This function holds the filter state lock because 112 | // it changes the number of pins. The number of pins 113 | // cannot be change while another thread is calling 114 | // IMediaFilter::Run(), IMediaFilter::Stop(), 115 | // IMediaFilter::Pause() or IBaseFilter::FindPin(). 116 | CAutoLock lock(&m_cStateLock); 117 | 118 | // This function holds the pin state lock because it 119 | // uses m_iPins and m_paStreams. 120 | CAutoLock alPinStateLock(&m_csPinStateLock); 121 | 122 | for(int i = 0; i < m_iPins; i++) 123 | { 124 | if(m_paStreams[i] == pStream) 125 | { 126 | if(m_iPins == 1) 127 | { 128 | delete [] m_paStreams; 129 | m_paStreams = NULL; 130 | } 131 | else 132 | { 133 | /* no need to reallocate */ 134 | while(++i < m_iPins) 135 | m_paStreams[i - 1] = m_paStreams[i]; 136 | } 137 | 138 | m_iPins--; 139 | return S_OK; 140 | } 141 | } 142 | 143 | return S_FALSE; 144 | } 145 | 146 | 147 | // 148 | // FindPin 149 | // 150 | // Set *ppPin to the IPin* that has the id Id. 151 | // or to NULL if the Id cannot be matched. 152 | STDMETHODIMP CDynamicSource::FindPin(LPCWSTR Id, IPin **ppPin) 153 | { 154 | CAutoLock alPinStateLock(&m_csPinStateLock); 155 | 156 | CheckPointer(ppPin,E_POINTER); 157 | ValidateReadWritePtr(ppPin,sizeof(IPin *)); 158 | 159 | // The -1 undoes the +1 in QueryId and ensures that totally invalid 160 | // strings (for which WstrToInt delivers 0) give a deliver a NULL pin. 161 | int i = WstrToInt(Id) -1; 162 | 163 | *ppPin = GetPin(i); 164 | if (*ppPin!=NULL) 165 | { 166 | (*ppPin)->AddRef(); 167 | return NOERROR; 168 | } 169 | 170 | return VFW_E_NOT_FOUND; 171 | } 172 | 173 | 174 | // 175 | // FindPinNumber 176 | // 177 | // return the number of the pin with this IPin* or -1 if none 178 | int CDynamicSource::FindPinNumber(IPin *iPin) 179 | { 180 | 181 | // This function holds the pin state lock because it 182 | // uses m_iPins and m_paStreams. 183 | CAutoLock alPinStateLock(&m_csPinStateLock); 184 | 185 | for (int i=0; in && n>=0 it follows that m_iPins>0 224 | // which is what used to be checked (i.e. checking that we have a pin) 225 | if((n >= 0) && (n < m_iPins)) 226 | { 227 | ASSERT(m_paStreams[n]); 228 | return m_paStreams[n]; 229 | } 230 | 231 | return NULL; 232 | } 233 | 234 | 235 | STDMETHODIMP CDynamicSource::Stop(void) 236 | { 237 | m_evFilterStoppingEvent.Set(); 238 | 239 | HRESULT hr = CBaseFilter::Stop(); 240 | 241 | // The following code ensures that a pins thread will be destroyed even 242 | // if the pin is disconnected when CBaseFilter::Stop() is called. 243 | int nCurrentPin; 244 | CDynamicSourceStream* pOutputPin; 245 | { 246 | // This code holds the pin state lock because it 247 | // does not want the number of pins to change 248 | // while it executes. 249 | CAutoLock alPinStateLock(&m_csPinStateLock); 250 | 251 | for(nCurrentPin = 0; nCurrentPin < GetPinCount(); nCurrentPin++) 252 | { 253 | pOutputPin = (CDynamicSourceStream*)GetPin(nCurrentPin); 254 | if(pOutputPin->ThreadExists()) 255 | { 256 | pOutputPin->DestroySourceThread(); 257 | } 258 | } 259 | } 260 | 261 | if(FAILED(hr)) 262 | { 263 | return hr; 264 | } 265 | 266 | return NOERROR; 267 | } 268 | 269 | 270 | STDMETHODIMP CDynamicSource::Pause(void) 271 | { 272 | m_evFilterStoppingEvent.Reset(); 273 | 274 | return CBaseFilter::Pause(); 275 | } 276 | 277 | 278 | 279 | STDMETHODIMP CDynamicSource::SetSyncSource(IReferenceClock *pClock) 280 | { 281 | ((CDynamicSourceStream*) GetPin(0))->SetSyncSource(pClock); 282 | return CBaseFilter::SetSyncSource(pClock); 283 | } 284 | 285 | 286 | STDMETHODIMP CDynamicSource::JoinFilterGraph(IFilterGraph* pGraph, LPCWSTR pName) 287 | { 288 | CAutoLock lock(&m_cStateLock); 289 | 290 | HRESULT hr; 291 | int nCurrentPin; 292 | CDynamicSourceStream* pOutputPin; 293 | 294 | // The filter is joining the filter graph. 295 | if(NULL != pGraph) 296 | { 297 | IGraphConfig* pGraphConfig = NULL; 298 | 299 | hr = pGraph->QueryInterface(IID_IGraphConfig, (void**)&pGraphConfig); 300 | if(FAILED(hr)) 301 | { 302 | return hr; 303 | } 304 | 305 | hr = CBaseFilter::JoinFilterGraph(pGraph, pName); 306 | if(FAILED(hr)) 307 | { 308 | pGraphConfig->Release(); 309 | return hr; 310 | } 311 | 312 | for(nCurrentPin = 0; nCurrentPin < GetPinCount(); nCurrentPin++) 313 | { 314 | pOutputPin = (CDynamicSourceStream*) GetPin(nCurrentPin); 315 | pOutputPin->SetConfigInfo(pGraphConfig, m_evFilterStoppingEvent); 316 | } 317 | 318 | pGraphConfig->Release(); 319 | } 320 | else 321 | { 322 | hr = CBaseFilter::JoinFilterGraph(pGraph, pName); 323 | if(FAILED(hr)) 324 | { 325 | return hr; 326 | } 327 | 328 | for(nCurrentPin = 0; nCurrentPin < GetPinCount(); nCurrentPin++) 329 | { 330 | pOutputPin = (CDynamicSourceStream*)GetPin(nCurrentPin); 331 | pOutputPin->SetConfigInfo(NULL, NULL); 332 | } 333 | } 334 | 335 | return S_OK; 336 | } 337 | 338 | 339 | // * 340 | // * --- CDynamicSourceStream ---- 341 | // * 342 | 343 | // 344 | // Set Id to point to a CoTaskMemAlloc'd 345 | STDMETHODIMP CDynamicSourceStream::QueryId(LPWSTR *pId) 346 | { 347 | CheckPointer(pId,E_POINTER); 348 | ValidateReadWritePtr(pId,sizeof(LPWSTR)); 349 | 350 | // We give the pins id's which are 1,2,... 351 | // FindPinNumber returns -1 for an invalid pin 352 | int i = 1+ m_pFilter->FindPinNumber(this); 353 | 354 | if(i<1) 355 | return VFW_E_NOT_FOUND; 356 | 357 | *pId = (LPWSTR)CoTaskMemAlloc(4*sizeof(WCHAR)); 358 | if(*pId==NULL) 359 | { 360 | return E_OUTOFMEMORY; 361 | } 362 | 363 | IntToWstr(i, *pId); 364 | return NOERROR; 365 | } 366 | 367 | 368 | // 369 | // CDynamicSourceStream::Constructor 370 | // 371 | // increments the number of pins present on the filter 372 | CDynamicSourceStream::CDynamicSourceStream( 373 | TCHAR *pObjectName, 374 | HRESULT *phr, 375 | CDynamicSource*ps, 376 | LPCWSTR pPinName) 377 | : CDynamicOutputPin(pObjectName, ps, ps->pStateLock(), phr, pPinName), 378 | m_pFilter(ps), 379 | m_fReconnectOutputPin(false) 380 | { 381 | ASSERT(phr); 382 | *phr = m_pFilter->AddPin(this); 383 | } 384 | 385 | 386 | #ifdef UNICODE 387 | 388 | CDynamicSourceStream::CDynamicSourceStream( 389 | char *pObjectName, 390 | HRESULT *phr, 391 | CDynamicSource*ps, 392 | LPCWSTR pPinName) 393 | : CDynamicOutputPin(pObjectName, ps, ps->pStateLock(), phr, pPinName), 394 | m_pFilter(ps), 395 | m_fReconnectOutputPin(false) 396 | { 397 | ASSERT(phr); 398 | *phr = m_pFilter->AddPin(this); 399 | } 400 | 401 | #endif 402 | 403 | 404 | // 405 | // CDynamicSourceStream::Destructor 406 | // 407 | // Decrements the number of pins on this filter 408 | CDynamicSourceStream::~CDynamicSourceStream(void) 409 | { 410 | m_pFilter->RemovePin(this); 411 | } 412 | 413 | 414 | // 415 | // CheckMediaType 416 | // 417 | // Do we support this type? Provides the default support for 1 type. 418 | HRESULT CDynamicSourceStream::CheckMediaType(const CMediaType *pMediaType) 419 | { 420 | CheckPointer(pMediaType,E_POINTER); 421 | CAutoLock lock(m_pFilter->pStateLock()); 422 | 423 | CMediaType mt; 424 | GetMediaType(&mt); 425 | 426 | if(mt == *pMediaType) 427 | { 428 | return NOERROR; 429 | } 430 | 431 | return E_FAIL; 432 | } 433 | 434 | 435 | // 436 | // GetMediaType 437 | // 438 | // By default we support only one type 439 | // iPosition indexes are 0-n 440 | HRESULT CDynamicSourceStream::GetMediaType(int iPosition, CMediaType *pMediaType) 441 | { 442 | CAutoLock lock(m_pFilter->pStateLock()); 443 | 444 | if(iPosition<0) 445 | { 446 | return E_INVALIDARG; 447 | } 448 | if(iPosition>0) 449 | { 450 | return VFW_S_NO_MORE_ITEMS; 451 | } 452 | 453 | return GetMediaType(pMediaType); 454 | } 455 | 456 | 457 | // 458 | // Active 459 | // 460 | // The pin is active - start up the worker thread 461 | HRESULT CDynamicSourceStream::Active(void) 462 | { 463 | CAutoLock lock(m_pFilter->pStateLock()); 464 | 465 | HRESULT hr; 466 | 467 | if(m_pFilter->IsActive()) 468 | { 469 | return S_FALSE; // succeeded, but did not allocate resources (they already exist...) 470 | } 471 | 472 | // do nothing if not connected - its ok not to connect to 473 | // all pins of a source filter 474 | if(!IsConnected()) 475 | { 476 | return NOERROR; 477 | } 478 | 479 | hr = CDynamicOutputPin::Active(); 480 | if(FAILED(hr)) 481 | { 482 | return hr; 483 | } 484 | 485 | ASSERT(!ThreadExists()); 486 | 487 | // start the thread 488 | if(!Create()) 489 | { 490 | return E_FAIL; 491 | } 492 | 493 | // Tell thread to initialize. If OnThreadCreate Fails, so does this. 494 | hr = Init(); 495 | if(FAILED(hr)) 496 | return hr; 497 | 498 | return Pause(); 499 | } 500 | 501 | 502 | HRESULT CDynamicSourceStream::BreakConnect(void) 503 | { 504 | HRESULT hr = CDynamicOutputPin::BreakConnect(); 505 | if(FAILED(hr)) 506 | { 507 | return hr; 508 | } 509 | 510 | m_fReconnectOutputPin = false; 511 | 512 | return S_OK; 513 | } 514 | 515 | 516 | HRESULT CDynamicSourceStream::DestroySourceThread(void) 517 | { 518 | // The pin's thread cannot be destroyed if the thread does not exist. 519 | ASSERT(ThreadExists()); 520 | 521 | HRESULT hr = Stop(); 522 | if(FAILED(hr)) 523 | { 524 | return hr; 525 | } 526 | 527 | hr = Exit(); 528 | if(FAILED(hr)) 529 | { 530 | return hr; 531 | } 532 | 533 | Close(); // Wait for the thread to exit, then tidy up. 534 | 535 | return NOERROR; 536 | } 537 | 538 | 539 | // 540 | // ThreadProc 541 | // 542 | // When this returns the thread exits 543 | // Return codes > 0 indicate an error occured 544 | DWORD CDynamicSourceStream::ThreadProc(void) 545 | { 546 | HRESULT hr; // the return code from calls 547 | Command com; 548 | 549 | do 550 | { 551 | com = GetRequest(); 552 | if(com != CMD_INIT) 553 | { 554 | DbgLog((LOG_ERROR, 1, TEXT("Thread expected init command"))); 555 | Reply((DWORD) E_UNEXPECTED); 556 | } 557 | 558 | } while(com != CMD_INIT); 559 | 560 | DbgLog((LOG_TRACE, 1, TEXT("CDynamicSourceStream worker thread initializing"))); 561 | 562 | hr = OnThreadCreate(); // perform set up tasks 563 | if(FAILED(hr)) 564 | { 565 | DbgLog((LOG_ERROR, 1, TEXT("CDynamicSourceStream::OnThreadCreate failed. Aborting thread."))); 566 | 567 | OnThreadDestroy(); 568 | Reply(hr); // send failed return code from OnThreadCreate 569 | return 1; 570 | } 571 | 572 | // Initialisation suceeded 573 | Reply(NOERROR); 574 | 575 | Command cmd; 576 | do 577 | { 578 | cmd = GetRequest(); 579 | 580 | switch(cmd) 581 | { 582 | case CMD_EXIT: 583 | Reply(NOERROR); 584 | break; 585 | 586 | case CMD_RUN: 587 | DbgLog((LOG_ERROR, 1, TEXT("CMD_RUN received before a CMD_PAUSE???"))); 588 | // !!! fall through 589 | 590 | case CMD_PAUSE: 591 | Reply(NOERROR); 592 | DoBufferProcessingLoop(); 593 | break; 594 | 595 | case CMD_STOP: 596 | Reply(NOERROR); 597 | break; 598 | 599 | default: 600 | DbgLog((LOG_ERROR, 1, TEXT("Unknown command %d received!"), cmd)); 601 | Reply((DWORD) E_NOTIMPL); 602 | break; 603 | } 604 | 605 | } while(cmd != CMD_EXIT); 606 | 607 | hr = OnThreadDestroy(); // tidy up. 608 | if(FAILED(hr)) 609 | { 610 | DbgLog((LOG_ERROR, 1, TEXT("CDynamicSourceStream::OnThreadDestroy failed. Exiting thread."))); 611 | return 1; 612 | } 613 | 614 | DbgLog((LOG_TRACE, 1, TEXT("CDynamicSourceStream worker thread exiting"))); 615 | return 0; 616 | } 617 | 618 | 619 | // 620 | // DoBufferProcessingLoop 621 | // 622 | // Grabs a buffer and calls the users processing function. 623 | // Overridable, so that different delivery styles can be catered for. 624 | HRESULT CDynamicSourceStream::DoBufferProcessingLoop(void) 625 | { 626 | Command com; 627 | bool fOutputFormatChanged = false; 628 | 629 | OnThreadStartPlay(); 630 | 631 | do 632 | { 633 | while(!CheckRequest(&com)) 634 | { 635 | // CAutoUsingOutputPin::CAutoUsingOutputPin() only changes the value of hr 636 | // if an error occurs. 637 | HRESULT hr = S_OK; 638 | 639 | CAutoUsingOutputPin auopUsingOutputPin(this, &hr); 640 | if(FAILED(hr)) 641 | { 642 | FatalError(hr); 643 | return hr; 644 | } 645 | 646 | if(m_fReconnectOutputPin) 647 | { 648 | hr = DynamicReconnect(NULL); 649 | 650 | m_fReconnectOutputPin = false; 651 | 652 | if(FAILED(hr)) 653 | { 654 | FatalError(hr); 655 | return hr; 656 | } 657 | 658 | fOutputFormatChanged = true; 659 | } 660 | 661 | IMediaSample *pSample; 662 | 663 | hr = GetDeliveryBuffer(&pSample,NULL,NULL,0); 664 | if(FAILED(hr)) 665 | { 666 | Sleep(1); 667 | continue; // go round again. Perhaps the error will go away 668 | // or the allocator is decommited & we will be asked to 669 | // exit soon. 670 | } 671 | 672 | if(fOutputFormatChanged) 673 | { 674 | pSample->SetDiscontinuity(TRUE); 675 | fOutputFormatChanged = false; 676 | } 677 | 678 | // Virtual function user will override. 679 | hr = FillBuffer(pSample); 680 | 681 | if(hr == S_OK) 682 | { 683 | hr = Deliver(pSample); 684 | pSample->Release(); 685 | 686 | // downstream filter returns S_FALSE if it wants us to 687 | // stop or an error if it's reporting an error. 688 | if(hr != S_OK) 689 | { 690 | DbgLog((LOG_TRACE, 2, TEXT("Deliver() returned %08x; stopping"), hr)); 691 | return S_OK; 692 | } 693 | 694 | } 695 | else if(hr == S_FALSE) 696 | { 697 | // derived class wants us to stop pushing data 698 | pSample->Release(); 699 | DeliverEndOfStream(); 700 | return S_OK; 701 | } 702 | else 703 | { 704 | // derived class encountered an error 705 | pSample->Release(); 706 | DbgLog((LOG_ERROR, 1, TEXT("Error %08lX from FillBuffer!!!"), hr)); 707 | 708 | FatalError(hr); 709 | return hr; 710 | } 711 | // all paths release the sample 712 | } 713 | 714 | // For all commands sent to us there must be a Reply call! 715 | if(com == CMD_RUN || com == CMD_PAUSE) 716 | { 717 | Reply(NOERROR); 718 | } 719 | else if(com != CMD_STOP) 720 | { 721 | Reply((DWORD) E_UNEXPECTED); 722 | DbgLog((LOG_ERROR, 1, TEXT("Unexpected command!!!"))); 723 | } 724 | } while(com != CMD_STOP); 725 | 726 | return S_FALSE; 727 | } 728 | 729 | 730 | void CDynamicSourceStream::FatalError(HRESULT hr) 731 | { 732 | // Make sure the user is reporting a failure. 733 | ASSERT(FAILED(hr)); 734 | 735 | m_bRunTimeError = TRUE; 736 | DeliverEndOfStream(); 737 | m_pFilter->NotifyEvent(EC_ERRORABORT, hr, 0); 738 | } 739 | 740 | 741 | void CDynamicSourceStream::OutputPinNeedsToBeReconnected(void) 742 | { 743 | m_fReconnectOutputPin = true; 744 | } 745 | 746 | -------------------------------------------------------------------------------- /AudioLoopbackFilter.def: -------------------------------------------------------------------------------- 1 | LIBRARY AudioLoopbackFilter.ax 2 | 3 | EXPORTS 4 | DllMain PRIVATE 5 | DllGetClassObject PRIVATE 6 | DllCanUnloadNow PRIVATE 7 | DllRegisterServer PRIVATE 8 | DllUnregisterServer PRIVATE 9 | 10 | 11 | -------------------------------------------------------------------------------- /AudioLoopbackFilter.h: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // File: AudioLoopbackFilter.h 3 | // 4 | // Desc: DirectShow 5 | // 6 | // Copyright (c) Corey Auger. All rights reserved. 7 | //------------------------------------------------------------------------------ 8 | 9 | 10 | #ifndef __AUDIOLOOPBACKFILTER__ 11 | #define __AUDIOLOOPBACKFILTER__ 12 | 13 | //CLSID_AudioLoopbackFilter 14 | //{3DDE0EEE-479E-4157-A57D-1F13C1D0E352} 15 | DEFINE_GUID(CLSID_AudioLoopbackFilter, 16 | 0x3DDE0EEE, 0x479E, 0x4157, 0xA5, 0x7D, 0x1F, 0x13, 0xC1, 0xD0, 0xE3, 0x52); 17 | 18 | //CLSID_SynthFilterPropertyPage 19 | //{79A98DE1-BC00-11ce-AC2E-444553541111} 20 | DEFINE_GUID(CLSID_AudioLoopbackPropertyPage, 21 | 0x79a98de1, 0xbc00, 0x11ce, 0xac, 0x2e, 0x44, 0x45, 0x53, 0x54, 0x11, 0x11); 22 | 23 | const int WaveBufferSize = 16*1024; // Size of each allocated buffer 24 | // Originally used to be 2K, but at 25 | // 44khz/16bit/stereo you would get 26 | // audio breaks with a transform in the 27 | // middle. 28 | 29 | const int SILENCE_BUF_SIZE = 1792; // (CA) need to calc this.. 30 | 31 | 32 | #define WM_PROPERTYPAGE_ENABLE (WM_USER + 100) 33 | 34 | // below stuff is implementation-only.... 35 | #ifdef _AUDIOSYNTH_IMPLEMENTATION_ 36 | 37 | HRESULT get_default_device(IMMDevice **ppMMDevice); 38 | 39 | // ------------------------------------------------------------------------- 40 | // CAudioLoopback 41 | // ------------------------------------------------------------------------- 42 | class CAudioLoopback { 43 | public: 44 | CAudioLoopback(CCritSec* pStateLock); 45 | ~CAudioLoopback(); 46 | 47 | WAVEFORMATEX* Format(){ return m_pwfx; } 48 | 49 | HRESULT Initialize(); 50 | HRESULT FillPCMAudioBuffer(const WAVEFORMATEX& wfex, BYTE pBuf[], long& iSize); 51 | void GetPCMFormatStructure(WAVEFORMATEX* pwfex); 52 | 53 | REFERENCE_TIME m_hnsDefaultDevicePeriod; 54 | 55 | protected: 56 | UINT32 pnFrames; 57 | bool bFirstPacket; 58 | HANDLE m_hTask; 59 | IAudioClient* m_pAudioClient; 60 | IAudioCaptureClient* m_pAudioCaptureClient; 61 | WAVEFORMATEX* m_pwfx; 62 | 63 | 64 | BYTE* m_silenceBuf; 65 | }; 66 | 67 | 68 | class CSynthStream; 69 | class CAudioLoopback; 70 | 71 | // ------------------------------------------------------------------------- 72 | // CAudioLoopbackFilter 73 | // ------------------------------------------------------------------------- 74 | // CAudioLoopbackFilter manages filter level stuff 75 | 76 | class CAudioLoopbackFilter : public IAudioLoopbackFilter, 77 | public CPersistStream, 78 | public ISpecifyPropertyPages, 79 | public CDynamicSource { 80 | 81 | public: 82 | 83 | static CUnknown * WINAPI CreateInstance(LPUNKNOWN lpunk, HRESULT *phr); 84 | ~CAudioLoopbackFilter(); 85 | 86 | DECLARE_IUNKNOWN; 87 | 88 | // override this to reveal our property interface 89 | STDMETHODIMP NonDelegatingQueryInterface(REFIID riid, void ** ppv); 90 | 91 | // --- ISpecifyPropertyPages --- 92 | 93 | // return our property pages 94 | STDMETHODIMP GetPages(CAUUID * pPages); 95 | 96 | // --- IPersistStream Interface 97 | 98 | STDMETHODIMP GetClassID(CLSID *pClsid); 99 | int SizeMax(); 100 | HRESULT WriteToStream(IStream *pStream); 101 | HRESULT ReadFromStream(IStream *pStream); 102 | DWORD GetSoftwareVersion(void); 103 | 104 | CAudioLoopback *m_Loopback; 105 | 106 | private: 107 | 108 | // it is only allowed to to create these objects with CreateInstance 109 | CAudioLoopbackFilter(LPUNKNOWN lpunk, HRESULT *phr); 110 | 111 | // When the format changes, reconnect... 112 | void ReconnectWithNewFormat(void); 113 | 114 | }; 115 | 116 | 117 | // ------------------------------------------------------------------------- 118 | // CAudioLoopbackPin 119 | // ------------------------------------------------------------------------- 120 | // CAudioLoopbackPin manages the data flow from the output pin. 121 | 122 | class CAudioLoopbackPin : public CDynamicSourceStream { 123 | 124 | public: 125 | 126 | CAudioLoopbackPin(HRESULT *phr, CAudioLoopbackFilter *pParent, LPCWSTR pPinName); 127 | ~CAudioLoopbackPin(); 128 | 129 | BOOL ReadyToStop(void) {return FALSE;} 130 | 131 | // stuff an audio buffer with the current format 132 | HRESULT FillBuffer(IMediaSample *pms); 133 | 134 | // ask for buffers of the size appropriate to the agreed media type. 135 | HRESULT DecideBufferSize(IMemAllocator *pIMemAlloc, 136 | ALLOCATOR_PROPERTIES *pProperties); 137 | 138 | HRESULT GetMediaType(CMediaType *pmt); 139 | 140 | HRESULT CompleteConnect(IPin *pReceivePin); 141 | HRESULT BreakConnect(void); 142 | 143 | // resets the stream time to zero. 144 | HRESULT Active(void); 145 | HRESULT Run(REFERENCE_TIME tStart); 146 | 147 | HRESULT SetSyncSource(IReferenceClock *pClock); 148 | 149 | private: 150 | 151 | void DerivePCMFormatFromADPCMFormatStructure(const WAVEFORMATEX& wfexADPCM, WAVEFORMATEX* pwfexPCM); 152 | 153 | // Access to this state information should be serialized with the filters 154 | // critical section (m_pFilter->pStateLock()) 155 | 156 | // This lock protects: m_dwTempPCMBufferSize, m_hPCMToMSADPCMConversionStream, 157 | // m_rtSampleTime, m_fFirstSampleDelivered and m_llSampleMediaTimeStart 158 | CCritSec m_cSharedState; 159 | 160 | CRefTime m_rtSampleTime; // The time to be stamped on each sample 161 | HACMSTREAM m_hPCMToMSADPCMConversionStream; 162 | 163 | DWORD m_dwTempPCMBufferSize; 164 | bool m_fFirstSampleDelivered; 165 | LONGLONG m_llSampleMediaTimeStart; 166 | DWORD_PTR m_dwAdviseToken; 167 | HANDLE m_hSemaphore; 168 | 169 | CAudioLoopback *m_Loopback; 170 | CAudioLoopbackFilter *m_pParent; 171 | IReferenceClock *m_pClock; 172 | }; 173 | 174 | #endif // _AUDIOSYNTH_IMPLEMENTATION_ implementation only.... 175 | 176 | #endif /* __AUDIOSYNTH__ */ 177 | 178 | 179 | 180 | -------------------------------------------------------------------------------- /AudioLoopbackFilter.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 11.00 3 | # Visual Studio 2010 4 | Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "AudioLoopbackFilter", "AudioLoopbackFilter.vcxproj", "{BF20BC76-A330-4857-830B-80C85660478B}" 5 | EndProject 6 | Global 7 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 8 | Debug|Win32 = Debug|Win32 9 | Debug|x64 = Debug|x64 10 | Release|Win32 = Release|Win32 11 | Release|x64 = Release|x64 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {BF20BC76-A330-4857-830B-80C85660478B}.Debug|Win32.ActiveCfg = Debug|Win32 15 | {BF20BC76-A330-4857-830B-80C85660478B}.Debug|Win32.Build.0 = Debug|Win32 16 | {BF20BC76-A330-4857-830B-80C85660478B}.Debug|x64.ActiveCfg = Debug|x64 17 | {BF20BC76-A330-4857-830B-80C85660478B}.Debug|x64.Build.0 = Debug|x64 18 | {BF20BC76-A330-4857-830B-80C85660478B}.Release|Win32.ActiveCfg = Release|Win32 19 | {BF20BC76-A330-4857-830B-80C85660478B}.Release|Win32.Build.0 = Release|Win32 20 | {BF20BC76-A330-4857-830B-80C85660478B}.Release|x64.ActiveCfg = Release|x64 21 | {BF20BC76-A330-4857-830B-80C85660478B}.Release|x64.Build.0 = Release|x64 22 | EndGlobalSection 23 | GlobalSection(SolutionProperties) = preSolution 24 | HideSolutionNode = FALSE 25 | EndGlobalSection 26 | EndGlobal 27 | -------------------------------------------------------------------------------- /AudioLoopbackFilter.suo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreyauger/AudioLoopbackFilter/4aa0f9b5400f28f870a767ca7aa268cf0aa6e900/AudioLoopbackFilter.suo -------------------------------------------------------------------------------- /AudioLoopbackFilter.vcproj: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 14 | 17 | 18 | 19 | 20 | 21 | 28 | 31 | 34 | 37 | 40 | 43 | 56 | 59 | 62 | 65 | 75 | 78 | 81 | 84 | 87 | 90 | 93 | 96 | 99 | 100 | 107 | 110 | 113 | 116 | 119 | 122 | 133 | 136 | 139 | 142 | 154 | 157 | 160 | 163 | 166 | 169 | 172 | 175 | 178 | 179 | 186 | 189 | 192 | 195 | 198 | 202 | 215 | 218 | 221 | 224 | 235 | 238 | 241 | 244 | 247 | 250 | 253 | 256 | 259 | 260 | 267 | 270 | 273 | 276 | 279 | 283 | 294 | 297 | 300 | 303 | 316 | 319 | 322 | 325 | 328 | 331 | 334 | 337 | 340 | 341 | 342 | 343 | 344 | 345 | 350 | 353 | 354 | 357 | 358 | 361 | 362 | 365 | 366 | 367 | 372 | 375 | 376 | 379 | 380 | 383 | 384 | 387 | 388 | 391 | 392 | 393 | 398 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | -------------------------------------------------------------------------------- /AudioLoopbackFilter.vcxproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | Win32 7 | 8 | 9 | Debug 10 | x64 11 | 12 | 13 | Release 14 | Win32 15 | 16 | 17 | Release 18 | x64 19 | 20 | 21 | 22 | {BF20BC76-A330-4857-830B-80C85660478B} 23 | AudioLoopback 24 | Win32Proj 25 | AudioLoopbackFilter 26 | 27 | 28 | 29 | DynamicLibrary 30 | Unicode 31 | 32 | 33 | DynamicLibrary 34 | Unicode 35 | 36 | 37 | DynamicLibrary 38 | Unicode 39 | 40 | 41 | DynamicLibrary 42 | Unicode 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | <_ProjectFileVersion>10.0.30319.1 62 | Debug\ 63 | Debug\ 64 | true 65 | Release\ 66 | Release\ 67 | false 68 | $(Platform)\$(Configuration)\ 69 | $(Platform)\$(Configuration)\ 70 | true 71 | $(Platform)\$(Configuration)\ 72 | $(Platform)\$(Configuration)\ 73 | false 74 | .ax 75 | .ax 76 | 77 | 78 | 79 | Disabled 80 | C:\Program Files\Microsoft SDKs\Windows\v7.1\Samples\multimedia\directshow\baseclasses;%(AdditionalIncludeDirectories) 81 | WIN32;_DEBUG;_WINDOWS;_USRDLL;SYNTH_EXPORTS;%(PreprocessorDefinitions) 82 | true 83 | EnableFastChecks 84 | MultiThreadedDebugDLL 85 | 86 | 87 | Level3 88 | EditAndContinue 89 | StdCall 90 | 91 | 92 | C:\Program Files\Microsoft SDKs\Windows\v7.1\Samples\multimedia\directshow\baseclasses\Debug\strmbasd.lib;winmm.lib;msvcrtd.lib;Msacm32.lib;comctl32.lib;Avrt.lib;%(AdditionalDependencies) 93 | true 94 | AudioLoopbackFilter.def 95 | true 96 | Windows 97 | MachineX86 98 | 99 | 100 | 101 | 102 | MaxSpeed 103 | C:\Program Files\Microsoft SDKs\Windows\v7.1\Samples\multimedia\directshow\baseclasses;%(AdditionalIncludeDirectories) 104 | WIN32;NDEBUG;_WINDOWS;_USRDLL;SYNTH_EXPORTS;%(PreprocessorDefinitions) 105 | MultiThreadedDLL 106 | 107 | 108 | Level3 109 | ProgramDatabase 110 | StdCall 111 | 112 | 113 | C:\Program Files\Microsoft SDKs\Windows\v7.1\Samples\multimedia\directshow\baseclasses\Release\strmbase.lib;winmm.lib;msvcrt.lib;Msacm32.lib;comctl32.lib;Avrt.lib;%(AdditionalDependencies) 114 | true 115 | AudioLoopbackFilter.def 116 | true 117 | Windows 118 | true 119 | true 120 | MachineX86 121 | 122 | 123 | 124 | 125 | X64 126 | 127 | 128 | Disabled 129 | ..\..\BaseClasses\;%(AdditionalIncludeDirectories) 130 | WIN32;_DEBUG;_WINDOWS;_USRDLL;SYNTH_EXPORTS;%(PreprocessorDefinitions) 131 | true 132 | EnableFastChecks 133 | MultiThreadedDebugDLL 134 | 135 | 136 | Level3 137 | ProgramDatabase 138 | StdCall 139 | 140 | 141 | strmbasd.lib;winmm.lib;msvcrtd.lib;Msacm32.lib;comctl32.lib;%(AdditionalDependencies) 142 | ..\..\BaseClasses\x64\Debug\;%(AdditionalLibraryDirectories) 143 | true 144 | synth.def 145 | true 146 | Windows 147 | MachineX64 148 | 149 | 150 | 151 | 152 | X64 153 | 154 | 155 | MaxSpeed 156 | ..\..\BaseClasses\;%(AdditionalIncludeDirectories) 157 | WIN32;NDEBUG;_WINDOWS;_USRDLL;SYNTH_EXPORTS;%(PreprocessorDefinitions) 158 | MultiThreadedDLL 159 | 160 | 161 | Level3 162 | ProgramDatabase 163 | StdCall 164 | 165 | 166 | strmbase.lib;winmm.lib;msvcrt.lib;Msacm32.lib;comctl32.lib;%(AdditionalDependencies) 167 | ..\..\BaseClasses\x64\Release\;%(AdditionalLibraryDirectories) 168 | true 169 | synth.def 170 | true 171 | Windows 172 | true 173 | true 174 | MachineX64 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | -------------------------------------------------------------------------------- /AudioLoopbackFilter.vcxproj.filters: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | {4FC737F1-C7A5-4376-A066-2A32D752A2FF} 6 | cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx 7 | 8 | 9 | {93995380-89BD-4b04-88EB-625FBE52EBFB} 10 | h;hpp;hxx;hm;inl;inc;xsd 11 | 12 | 13 | {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} 14 | rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav 15 | 16 | 17 | 18 | 19 | Source Files 20 | 21 | 22 | Source Files 23 | 24 | 25 | 26 | 27 | Header Files 28 | 29 | 30 | Header Files 31 | 32 | 33 | Header Files 34 | 35 | 36 | 37 | 38 | Source Files 39 | 40 | 41 | -------------------------------------------------------------------------------- /AudioLoopbackFilter.vcxproj.user: -------------------------------------------------------------------------------- 1 |  2 | 3 | -------------------------------------------------------------------------------- /IAudioLoopbackFilter.h: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // File: IAudioLoopbackFilter.h 3 | // 4 | // Desc: DirectShow 5 | // 6 | // Copyright (c) Corey Auger. All rights reserved. 7 | //------------------------------------------------------------------------------ 8 | 9 | 10 | #ifndef __IAUDIOLOOPBACKFILTER__ 11 | #define __IAUDIOLOOPBACKFILTER__ 12 | 13 | #ifdef __cplusplus 14 | extern "C" { 15 | #endif 16 | 17 | 18 | // 19 | // IAudioLoopbackFilter's GUID 20 | // 21 | // {4E48A6E8-481D-45CE-AAAE-7557BE49F5FB} 22 | DEFINE_GUID(IID_IAudioLoopbackFilter, 23 | 0x4E48A6E8, 0x481D, 0x45CE, 0xAA, 0xAE, 0x75, 0x57, 0xBE, 0x49, 0xF5, 0xFB); 24 | 25 | enum SYNTH_OUTPUT_FORMAT 26 | { 27 | SYNTH_OF_PCM, 28 | SYNTH_OF_MS_ADPCM 29 | }; 30 | 31 | // 32 | // IAudioLoopbackFilter 33 | // 34 | DECLARE_INTERFACE_(IAudioLoopbackFilter, IUnknown) { 35 | 36 | 37 | }; 38 | 39 | 40 | #ifdef __cplusplus 41 | } 42 | #endif 43 | 44 | #endif // __ISYNTH2__ 45 | 46 | 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | AudioLoopbackFilter 2 | =================== 3 | 4 | Directshow Audio Loopback Capture Filter 5 | 6 | ## Dependencies 7 | Microsoft SDK (Direct Show base classes) [http://www.microsoft.com/en-ca/download/details.aspx?id=8279](http://www.microsoft.com/en-ca/download/details.aspx?id=8279) 8 | 9 | ## Overview 10 | This project provides an efficient working audio capture source filter. A “what you hear is what you get”. The code is based on Microsofts [IAudioCaptureClient](http://msdn.microsoft.com/en-us/library/dd370858.aspx) running in “loopback” mode. A feature that has been added since Windows Vista. 11 | 12 | ## Past Work 13 | This code is based on the initial work done by “Maurits” at Microsoft. His work and all the comments containing some of the issues with the code are located [here](http://blogs.msdn.com/b/matthew_van_eerde/archive/2008/12/16/sample-wasapi-loopback-capture-record-what-you-hear.aspx). 14 | 15 | 16 | Additionally “Roger Pack” wrote the original directshow filter wrapper for Maurits code. Roger’s work can be found here: [https://github.com/rdp/virtual-audio-capture-grabber-device](https://github.com/rdp/virtual-audio-capture-grabber-device). I found that in Windows 8 the filter no longer worked on this project. I also submit a patch that fixed the audio in windows 8.. but the filter graph would pause if there was silence (no audio). 17 | 18 | The main issue with these works, is the inability to generate “silence”. This problem is magnified in directshow by not allowing your filter graph to process any video unless there is valid audio (ie silence will pause your graph). 19 | 20 | ## My Solution 21 | I based my filter off the Synth Audio Source, from the directshow example projects. I have removed most of the source that was not needed for my filter to make it easier to understand. 22 | 23 | There are a few major differences in my filter from the one I was using [before](https://github.com/rdp/virtual-audio-capture-grabber-device). 24 | 25 | * I generate silence values into the buffer when there is “No” data available from the AudioCaptureClient. This will allow the filter graph to continue to process video frames/mux even if there is no audio. 26 | 27 | * I sync the Reference clock to use the clock time specified by the AudioCaptureClient. This eliminates the usage of the extra threading and BAD sleep statements in the code. The result is a significantly lighter CPU footprint. 28 | 29 | ## Finally 30 | My solution has only undergone a very small amount of testing. Which did include both a Windows 7 and Windows 8 machine. However, if you have any trouble with the filter please contact me and I will be glad to help. Thanks. 31 | 32 | 33 | [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/coreyauger/audioloopbackfilter/trend.png)](https://bitdeli.com/free "Bitdeli Badge") 34 | 35 | -------------------------------------------------------------------------------- /dynsrc.h: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // File: DynSrc.h 3 | // 4 | // 5 | // Copyright (c) Corey Auger. All rights reserved. 6 | //------------------------------------------------------------------------------ 7 | 8 | 9 | #ifndef __CDYNAMICSOURCE__ 10 | #define __CDYNAMICSOURCE__ 11 | 12 | class CDynamicSourceStream; // The class that will handle each pin 13 | 14 | class CDynamicSource: public CBaseFilter { 15 | public: 16 | 17 | CDynamicSource(TCHAR *pName, LPUNKNOWN lpunk, CLSID clsid, HRESULT *phr); 18 | #ifdef UNICODE 19 | CDynamicSource(CHAR *pName, LPUNKNOWN lpunk, CLSID clsid, HRESULT *phr); 20 | #endif 21 | ~CDynamicSource(); 22 | 23 | int GetPinCount(void); 24 | CBasePin *GetPin(int n); 25 | 26 | // -- Utilities -- 27 | 28 | CCritSec* pStateLock(void) { return &m_cStateLock; } // provide our critical section 29 | 30 | HRESULT AddPin(CDynamicSourceStream *); 31 | HRESULT RemovePin(CDynamicSourceStream *); 32 | 33 | STDMETHODIMP FindPin( 34 | LPCWSTR Id, 35 | IPin ** ppPin 36 | ); 37 | 38 | int FindPinNumber(IPin *iPin); 39 | 40 | STDMETHODIMP JoinFilterGraph(IFilterGraph* pGraph, LPCWSTR pName); 41 | STDMETHODIMP Stop(void); 42 | STDMETHODIMP Pause(void); 43 | 44 | STDMETHODIMP SetSyncSource(IReferenceClock *pClock); 45 | 46 | protected: 47 | 48 | CAMEvent m_evFilterStoppingEvent; 49 | 50 | int m_iPins; // The number of pins on this filter. Updated by 51 | // CDynamicSourceStream constructors & destructors. 52 | 53 | CDynamicSourceStream **m_paStreams; // the pins on this filter. 54 | 55 | CCritSec m_csPinStateLock; 56 | 57 | CCritSec m_cStateLock; 58 | 59 | }; 60 | 61 | 62 | 63 | class CDynamicSourceStream : public CAMThread, public CDynamicOutputPin { 64 | public: 65 | 66 | CDynamicSourceStream(TCHAR *pObjectName, 67 | HRESULT *phr, 68 | CDynamicSource*pms, 69 | LPCWSTR pName); 70 | #ifdef UNICODE 71 | CDynamicSourceStream(CHAR *pObjectName, 72 | HRESULT *phr, 73 | CDynamicSource*pms, 74 | LPCWSTR pName); 75 | #endif 76 | virtual ~CDynamicSourceStream(void); // virtual destructor ensures derived 77 | // class destructors are called too 78 | 79 | HRESULT DestroySourceThread(void); 80 | 81 | virtual HRESULT SetSyncSource(IReferenceClock *pClock) PURE; // CA - added this.. 82 | 83 | protected: 84 | 85 | CDynamicSource*m_pFilter; // The parent of this stream 86 | 87 | virtual HRESULT FillBuffer(IMediaSample *pSamp) PURE; 88 | 89 | 90 | // Called as the thread is created/destroyed - use to perform 91 | // jobs such as start/stop streaming mode 92 | // If OnThreadCreate returns an error the thread will exit. 93 | virtual HRESULT OnThreadCreate(void) {return NOERROR;}; 94 | virtual HRESULT OnThreadDestroy(void) {return NOERROR;}; 95 | virtual HRESULT OnThreadStartPlay(void) {return NOERROR;}; 96 | 97 | HRESULT Active(void); // Starts up the worker thread 98 | 99 | HRESULT BreakConnect(void); 100 | 101 | public: 102 | // thread commands 103 | enum Command {CMD_INIT, CMD_PAUSE, CMD_RUN, CMD_STOP, CMD_EXIT}; 104 | 105 | HRESULT Init(void) { return CallWorker(CMD_INIT); } 106 | HRESULT Exit(void) { return CallWorker(CMD_EXIT); } 107 | HRESULT Run(void) { return CallWorker(CMD_RUN); } 108 | HRESULT Pause(void) { return CallWorker(CMD_PAUSE); } 109 | HRESULT Stop(void) { return CallWorker(CMD_STOP); } 110 | 111 | void OutputPinNeedsToBeReconnected(void); 112 | 113 | protected: 114 | Command GetRequest(void) { return (Command) CAMThread::GetRequest(); } 115 | BOOL CheckRequest(Command *pCom) { return CAMThread::CheckRequest( (DWORD *) pCom); } 116 | 117 | // override these if you want to add thread commands 118 | virtual DWORD ThreadProc(void); // the thread function 119 | 120 | virtual HRESULT DoBufferProcessingLoop(void); // the loop executed whilst running 121 | 122 | void FatalError(HRESULT hr); 123 | 124 | // * 125 | // * AM_MEDIA_TYPE support 126 | // * 127 | 128 | // If you support more than one media type then override these 2 functions 129 | virtual HRESULT CheckMediaType(const CMediaType *pMediaType); 130 | virtual HRESULT GetMediaType(int iPosition, CMediaType *pMediaType); // List pos. 0-n 131 | 132 | // If you support only one type then override this fn. 133 | // This will only be called by the default implementations 134 | // of CheckMediaType and GetMediaType(int, CMediaType*) 135 | // You must override this fn. or the above 2! 136 | virtual HRESULT GetMediaType(CMediaType *pMediaType) {return E_UNEXPECTED;} 137 | 138 | STDMETHODIMP QueryId( 139 | LPWSTR * Id 140 | ); 141 | 142 | bool m_fReconnectOutputPin; 143 | }; 144 | 145 | #endif // __CDYNAMICSOURCE__ 146 | 147 | -------------------------------------------------------------------------------- /log.h: -------------------------------------------------------------------------------- 1 | #ifndef __LOG_H__ 2 | #define __LOG_H__ 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | inline std::string NowTime(); 9 | 10 | enum TLogLevel {logERROR, logWARNING, logINFO, logDEBUG, logDEBUG1, logDEBUG2, logDEBUG3, logDEBUG4}; 11 | 12 | template 13 | class Log 14 | { 15 | public: 16 | Log(); 17 | virtual ~Log(); 18 | std::ostringstream& Get(TLogLevel level = logINFO); 19 | public: 20 | static TLogLevel& ReportingLevel(); 21 | static std::string ToString(TLogLevel level); 22 | static TLogLevel FromString(const std::string& level); 23 | protected: 24 | std::ostringstream os; 25 | private: 26 | Log(const Log&); 27 | Log& operator =(const Log&); 28 | }; 29 | 30 | template 31 | Log::Log() 32 | { 33 | } 34 | 35 | template 36 | std::ostringstream& Log::Get(TLogLevel level) 37 | { 38 | os << "- " << NowTime(); 39 | os << " " << ToString(level) << ": "; 40 | os << std::string(level > logDEBUG ? level - logDEBUG : 0, '\t'); 41 | return os; 42 | } 43 | 44 | template 45 | Log::~Log() 46 | { 47 | os << std::endl; 48 | T::Output(os.str()); 49 | } 50 | 51 | template 52 | TLogLevel& Log::ReportingLevel() 53 | { 54 | static TLogLevel reportingLevel = logDEBUG4; 55 | return reportingLevel; 56 | } 57 | 58 | template 59 | std::string Log::ToString(TLogLevel level) 60 | { 61 | static const char* const buffer[] = {"ERROR", "WARNING", "INFO", "DEBUG", "DEBUG1", "DEBUG2", "DEBUG3", "DEBUG4"}; 62 | return buffer[level]; 63 | } 64 | 65 | template 66 | TLogLevel Log::FromString(const std::string& level) 67 | { 68 | if (level == "DEBUG4") 69 | return logDEBUG4; 70 | if (level == "DEBUG3") 71 | return logDEBUG3; 72 | if (level == "DEBUG2") 73 | return logDEBUG2; 74 | if (level == "DEBUG1") 75 | return logDEBUG1; 76 | if (level == "DEBUG") 77 | return logDEBUG; 78 | if (level == "INFO") 79 | return logINFO; 80 | if (level == "WARNING") 81 | return logWARNING; 82 | if (level == "ERROR") 83 | return logERROR; 84 | Log().Get(logWARNING) << "Unknown logging level '" << level << "'. Using INFO level as default."; 85 | return logINFO; 86 | } 87 | 88 | class Output2FILE 89 | { 90 | public: 91 | static FILE*& Stream(); 92 | static void Output(const std::string& msg); 93 | }; 94 | 95 | inline FILE*& Output2FILE::Stream() 96 | { 97 | static FILE* pStream = stderr; 98 | return pStream; 99 | } 100 | 101 | inline void Output2FILE::Output(const std::string& msg) 102 | { 103 | FILE* pStream = Stream(); 104 | if (!pStream) 105 | return; 106 | fprintf(pStream, "%s", msg.c_str()); 107 | fflush(pStream); 108 | } 109 | 110 | #if defined(WIN32) || defined(_WIN32) || defined(__WIN32__) 111 | # if defined (BUILDING_FILELOG_DLL) 112 | # define FILELOG_DECLSPEC __declspec (dllexport) 113 | # elif defined (USING_FILELOG_DLL) 114 | # define FILELOG_DECLSPEC __declspec (dllimport) 115 | # else 116 | # define FILELOG_DECLSPEC 117 | # endif // BUILDING_DBSIMPLE_DLL 118 | #else 119 | # define FILELOG_DECLSPEC 120 | #endif // _WIN32 121 | 122 | class FILELOG_DECLSPEC FILELog : public Log {}; 123 | //typedef Log FILELog; 124 | 125 | #ifndef FILELOG_MAX_LEVEL 126 | #define FILELOG_MAX_LEVEL logDEBUG4 127 | #endif 128 | 129 | #define FILE_LOG(level) \ 130 | if (level > FILELOG_MAX_LEVEL) ;\ 131 | else if (level > FILELog::ReportingLevel() || !Output2FILE::Stream()) ; \ 132 | else FILELog().Get(level) 133 | 134 | #if defined(WIN32) || defined(_WIN32) || defined(__WIN32__) 135 | 136 | #include 137 | 138 | inline std::string NowTime() 139 | { 140 | const int MAX_LEN = 200; 141 | char buffer[MAX_LEN]; 142 | if (GetTimeFormatA(LOCALE_USER_DEFAULT, 0, 0, 143 | "HH':'mm':'ss", buffer, MAX_LEN) == 0) 144 | return "Error in NowTime()"; 145 | 146 | char result[100] = {0}; 147 | static DWORD first = GetTickCount(); 148 | std::sprintf(result, "%s.%03ld", buffer, (long)(GetTickCount() - first) % 1000); 149 | return result; 150 | } 151 | 152 | #else 153 | 154 | #include 155 | 156 | inline std::string NowTime() 157 | { 158 | char buffer[11]; 159 | time_t t; 160 | time(&t); 161 | tm r = {0}; 162 | strftime(buffer, sizeof(buffer), "%X", localtime_r(&t, &r)); 163 | struct timeval tv; 164 | gettimeofday(&tv, 0); 165 | char result[100] = {0}; 166 | std::sprintf(result, "%s.%03ld", buffer, (long)tv.tv_usec / 1000); 167 | return result; 168 | } 169 | 170 | #endif //WIN32 171 | 172 | #endif //__LOG_H__ 173 | -------------------------------------------------------------------------------- /resource.h: -------------------------------------------------------------------------------- 1 | // 2 | // Microsoft Visual C++ generated include file. 3 | // Used by synth.rc 4 | // 5 | 6 | #define IDD_BALLPROP 101 7 | #define IDD_SYNTHPROP1 101 8 | 9 | #define IDS_SYNTHPROPNAME 1024 10 | #define IDC_OUTPUTFORMAT 1025 11 | #define IDC_OF_PCM 1026 12 | #define IDC_OF_MSADPCM 1027 13 | 14 | #define IDS_STATIC -1 15 | #define IDC_CHANNELSGROUP -1 16 | 17 | // Next default values for new objects 18 | // 19 | #ifdef APSTUDIO_INVOKED 20 | #ifndef APSTUDIO_READONLY_SYMBOLS 21 | #define _APS_NO_MFC 1 22 | #define _APS_3D_CONTROLS 1 23 | #define _APS_NEXT_RESOURCE_VALUE 102 24 | #define _APS_NEXT_COMMAND_VALUE 40001 25 | #define _APS_NEXT_CONTROL_VALUE 1027 26 | #define _APS_NEXT_SYMED_VALUE 101 27 | #endif 28 | #endif 29 | --------------------------------------------------------------------------------