mirror of
https://github.com/libretro/ppsspp.git
synced 2024-12-14 12:40:11 +00:00
ce873ccbeb
Conflicts: Core/HW/audioPlayer.cpp
596 lines
14 KiB
C++
596 lines
14 KiB
C++
#ifdef _USE_DSHOW_
|
|
|
|
#include "audioPlayer.h"
|
|
#include "Core/Config.h"
|
|
|
|
#include <dshow.h>
|
|
#include "qeditsimple.h"
|
|
#pragma comment(lib, "Strmiids.lib")
|
|
#pragma comment(lib, "Quartz.lib")
|
|
|
|
#include "OMAConvert.h"
|
|
#include <map>
|
|
#include "StdMutex.h"
|
|
|
|
#include "../Core/System.h"
|
|
|
|
#define JIF(x) if (FAILED(hr=(x))) \
|
|
{return false;}
|
|
#define KIF(x) if (FAILED(hr=(x))) \
|
|
{return hr;}
|
|
#define LIF(x) if (FAILED(hr=(x))) \
|
|
{}
|
|
|
|
// Add filter from a dll/ax library directly
|
|
#include <comdef.h>
|
|
typedef HRESULT (STDAPICALLTYPE* FN_DLLGETCLASSOBJECT)(REFCLSID clsid, REFIID iid, void** ppv);
|
|
|
|
static const int importlibnum = 1;
|
|
static HMODULE lib[importlibnum] = {0};
|
|
static FN_DLLGETCLASSOBJECT fn[importlibnum] = {0};
|
|
|
|
HRESULT LoadFilterLibrary(int index, const char *pPath)
|
|
{
|
|
if (index < 0 || index >= importlibnum)
|
|
return E_FAIL;
|
|
// load the target DLL directly
|
|
lib[index] = LoadLibrary(pPath);
|
|
if (!lib[index])
|
|
{
|
|
return HRESULT_FROM_WIN32(GetLastError());
|
|
}
|
|
|
|
// the entry point is an exported function
|
|
fn[index] = (FN_DLLGETCLASSOBJECT)GetProcAddress(lib[index], "DllGetClassObject");
|
|
if (fn[index] == NULL)
|
|
{
|
|
return HRESULT_FROM_WIN32(GetLastError());
|
|
}
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
HRESULT FreeFilterLibrary(int index)
|
|
{
|
|
if (index < 0 || index >= importlibnum)
|
|
return E_FAIL;
|
|
if (lib[index])
|
|
FreeLibrary(lib[index]);
|
|
lib[index] = 0;
|
|
return S_OK;
|
|
}
|
|
|
|
HRESULT CreateFilterFromLibrary(int index, REFCLSID clsid, REFIID iid, void** ppUnk)
|
|
{
|
|
if (index < 0 || index >= importlibnum)
|
|
return E_FAIL;
|
|
if (!fn[index])
|
|
return E_FAIL;
|
|
// create a class factory
|
|
IUnknownPtr pUnk;
|
|
HRESULT hr = fn[index](clsid, IID_IUnknown, (void**)&pUnk);
|
|
if (SUCCEEDED(hr))
|
|
{
|
|
IClassFactoryPtr pCF = pUnk;
|
|
if (pCF == NULL)
|
|
{
|
|
hr = E_NOINTERFACE;
|
|
}
|
|
else
|
|
{
|
|
// ask the class factory to create the object
|
|
hr = pCF->CreateInstance(NULL, iid, (void**)ppUnk);
|
|
}
|
|
}
|
|
return hr;
|
|
}
|
|
|
|
// {2AE44C10-B451-4B01-9BBE-A5FBEF68C9D4}
|
|
static const GUID CLSID_AsyncStreamSource =
|
|
{ 0x2ae44c10, 0xb451, 0x4b01, { 0x9b, 0xbe, 0xa5, 0xfb, 0xef, 0x68, 0xc9, 0xd4 } };
|
|
|
|
// {268424D1-B6E9-4B28-8751-B7774F5ECF77}
|
|
static const GUID IID_IStreamSourceFilter =
|
|
{ 0x268424d1, 0xb6e9, 0x4b28, { 0x87, 0x51, 0xb7, 0x77, 0x4f, 0x5e, 0xcf, 0x77 } };
|
|
|
|
// We define the interface the app can use to program us
|
|
MIDL_INTERFACE("268424D1-B6E9-4B28-8751-B7774F5ECF77")
|
|
IStreamSourceFilter : public IFileSourceFilter
|
|
{
|
|
public:
|
|
virtual STDMETHODIMP LoadStream(void *stream, LONGLONG readSize, LONGLONG streamSize, AM_MEDIA_TYPE *pmt ) = 0;
|
|
virtual STDMETHODIMP AddStreamData(LONGLONG offset, void *stream, LONGLONG addSize) = 0;
|
|
virtual STDMETHODIMP GetStreamInfo(LONGLONG *readSize, LONGLONG *streamSize) = 0;
|
|
virtual STDMETHODIMP SetReadSize(LONGLONG readSize) = 0;
|
|
virtual BOOL STDMETHODCALLTYPE IsReadPassEnd() = 0;
|
|
};
|
|
|
|
HRESULT GetPin(IBaseFilter *pFilter, PIN_DIRECTION PinDir, IPin **ppPin)
|
|
{
|
|
IEnumPins *pEnum;
|
|
IPin *pPin;
|
|
pFilter->EnumPins(&pEnum);
|
|
while(pEnum->Next(1, &pPin, 0) == S_OK)
|
|
{
|
|
PIN_DIRECTION PinDirThis;
|
|
pPin->QueryDirection(&PinDirThis);
|
|
if (PinDir == PinDirThis)
|
|
{
|
|
pEnum->Release();
|
|
*ppPin = pPin;
|
|
return S_OK;
|
|
}
|
|
pPin->Release();
|
|
}
|
|
pEnum->Release();
|
|
return E_FAIL;
|
|
}
|
|
|
|
HRESULT ConnectFilters(IGraphBuilder *pGraph, IBaseFilter *pFirst, IBaseFilter *pSecond)
|
|
{
|
|
IPin *pOut = NULL, *pIn = NULL;
|
|
HRESULT hr = GetPin(pFirst, PINDIR_OUTPUT, &pOut);
|
|
if (FAILED(hr)) return hr;
|
|
hr = GetPin(pSecond, PINDIR_INPUT, &pIn);
|
|
if (FAILED(hr))
|
|
{
|
|
pOut->Release();
|
|
return E_FAIL;
|
|
}
|
|
hr = pGraph->Connect(pOut, pIn);
|
|
pIn->Release();
|
|
pOut->Release();
|
|
return hr;
|
|
}
|
|
|
|
class CSampleGrabberCallback : public ISampleGrabberCB
|
|
{
|
|
public:
|
|
CSampleGrabberCallback(audioPlayer *player)
|
|
{
|
|
m_player = player;
|
|
// 1MB round buffer size
|
|
m_bufsize = 0x100000;
|
|
m_buf = new u8[m_bufsize];
|
|
isNeedReset = true;
|
|
m_readPos = m_writePos = 0;
|
|
}
|
|
~CSampleGrabberCallback(){ if (m_buf) delete [] m_buf; }
|
|
STDMETHODIMP_(ULONG) AddRef() { return 2; }
|
|
STDMETHODIMP_(ULONG) Release() { return 1; }
|
|
|
|
STDMETHODIMP QueryInterface(REFIID riid, void **ppvObject)
|
|
{
|
|
return S_OK;
|
|
}
|
|
|
|
STDMETHODIMP SampleCB(double Time, IMediaSample *pSample)
|
|
{
|
|
return S_OK;
|
|
}
|
|
|
|
STDMETHODIMP BufferCB(double Time, BYTE *pBuffer, long buflen)
|
|
{
|
|
m_mutex.lock();
|
|
if (isNeedReset) {
|
|
isNeedReset = false;
|
|
m_readPos = 0;
|
|
m_writePos = 0;
|
|
}
|
|
if (m_writePos + buflen <= m_bufsize) {
|
|
memcpy(m_buf + m_writePos, pBuffer, buflen);
|
|
} else {
|
|
int size = m_bufsize - m_writePos;
|
|
memcpy(m_buf + m_writePos, pBuffer, size);
|
|
memcpy(m_buf, pBuffer + size, buflen - size);
|
|
}
|
|
m_writePos = (m_writePos + buflen) % m_bufsize;
|
|
|
|
// check how much space left to write
|
|
int space = (m_readPos - m_writePos + m_bufsize) % m_bufsize;
|
|
m_mutex.unlock();
|
|
if (space < buflen * 3) {
|
|
m_player->pause();
|
|
}
|
|
return S_OK;
|
|
}
|
|
|
|
int getNextSamples(u8* buf, int wantedbufsize) {
|
|
int timecount = 0;
|
|
while (isNeedReset) {
|
|
Sleep(1);
|
|
timecount++;
|
|
if (timecount >= 10)
|
|
return 0;
|
|
}
|
|
m_mutex.lock();
|
|
// check how much space left to read
|
|
int space = (m_writePos - m_readPos + m_bufsize) % m_bufsize;
|
|
if (m_readPos + wantedbufsize <= m_bufsize) {
|
|
memcpy(buf, m_buf + m_readPos, wantedbufsize);
|
|
} else {
|
|
int size = m_bufsize - m_readPos;
|
|
memcpy(buf, m_buf + m_readPos, size);
|
|
memcpy(buf + size, m_buf, wantedbufsize - size);
|
|
}
|
|
int bytesgot = min(wantedbufsize, space);
|
|
m_readPos = (m_readPos + bytesgot) % m_bufsize;
|
|
|
|
// check how much space left to read
|
|
space = (m_writePos - m_readPos + m_bufsize) % m_bufsize;
|
|
m_mutex.unlock();
|
|
if (space < wantedbufsize * 3) {
|
|
m_player->play();
|
|
}
|
|
return bytesgot;
|
|
}
|
|
|
|
void setResetflag(bool reset) { isNeedReset = reset; }
|
|
private:
|
|
audioPlayer *m_player;
|
|
u8* m_buf;
|
|
int m_bufsize;
|
|
int m_readPos;
|
|
int m_writePos;
|
|
bool isNeedReset;
|
|
std::recursive_mutex m_mutex;
|
|
};
|
|
|
|
bool addSampleGrabber(IGraphBuilder *pGB, IBaseFilter *pSrc,
|
|
ISampleGrabberCB *callback, void **outgrabber)
|
|
{
|
|
HRESULT hr;
|
|
ISampleGrabber *pGrabber = 0;
|
|
IBaseFilter *pGrabberF = 0;
|
|
JIF(CoCreateInstance(CLSID_SampleGrabber, NULL, CLSCTX_INPROC_SERVER,
|
|
IID_IBaseFilter, (void **)&pGrabberF));
|
|
JIF(pGB->AddFilter(pGrabberF, L"Sample Grabber"));
|
|
JIF(pGrabberF->QueryInterface(IID_ISampleGrabber, (void**)&pGrabber));
|
|
|
|
AM_MEDIA_TYPE mt;
|
|
ZeroMemory(&mt, sizeof(AM_MEDIA_TYPE));
|
|
mt.majortype = MEDIATYPE_Audio;
|
|
mt.subtype = MEDIASUBTYPE_PCM;
|
|
JIF(pGrabber->SetMediaType(&mt));
|
|
|
|
JIF(ConnectFilters(pGB, pSrc, pGrabberF));
|
|
pGrabberF->Release();
|
|
|
|
IBaseFilter *pNull = NULL;
|
|
JIF(CoCreateInstance(CLSID_NullRenderer, NULL, CLSCTX_INPROC_SERVER,
|
|
IID_IBaseFilter, (void**)&pNull));
|
|
JIF(pGB->AddFilter(pNull, L"NullRenderer"));
|
|
JIF(ConnectFilters(pGB, pGrabberF, pNull));
|
|
|
|
// Set one-shot mode and buffering.
|
|
JIF(pGrabber->SetOneShot(FALSE));
|
|
JIF(pGrabber->SetBufferSamples(TRUE));
|
|
|
|
JIF(pGrabber->SetCallback(callback, 1));
|
|
|
|
// close the clock to run as fast as possible
|
|
IMediaFilter *pMediaFilter = 0;
|
|
pGB->QueryInterface(IID_IMediaFilter, (void**)&pMediaFilter);
|
|
pMediaFilter->SetSyncSource(0);
|
|
pMediaFilter->Release();
|
|
|
|
*outgrabber = (void*)pGrabber;
|
|
|
|
return true;
|
|
}
|
|
|
|
static volatile int g_volume = 60;
|
|
|
|
audioPlayer::audioPlayer(void)
|
|
{
|
|
m_playmode = -1;
|
|
m_volume = g_volume;
|
|
m_pMC = 0;
|
|
m_pGB = 0;
|
|
m_pMS = 0;
|
|
m_pGrabber = 0;
|
|
m_pGrabberCB = 0;
|
|
m_pStreamReader = 0;
|
|
}
|
|
|
|
|
|
audioPlayer::~audioPlayer(void)
|
|
{
|
|
closeMedia();
|
|
}
|
|
|
|
bool audioPlayer::load(const char* filename, u8* stream, int readSize, int streamSize, bool samplebuffermode, bool isWave)
|
|
{
|
|
if (m_playmode == 1)
|
|
return false;
|
|
WCHAR wstrfilename[MAX_PATH + 1];
|
|
MultiByteToWideChar( CP_ACP, 0, filename ? filename : "stream", -1,
|
|
wstrfilename, MAX_PATH );
|
|
wstrfilename[MAX_PATH] = 0;
|
|
|
|
HRESULT hr;
|
|
JIF(CoCreateInstance(CLSID_FilterGraph, NULL, CLSCTX_INPROC_SERVER,
|
|
IID_IGraphBuilder, (void **)&m_pGB));
|
|
IGraphBuilder *pGB=(IGraphBuilder*)m_pGB;
|
|
JIF(pGB->QueryInterface(IID_IMediaControl, (void **)&m_pMC));
|
|
JIF(pGB->QueryInterface(IID_IMediaSeeking, (void **)&m_pMS));
|
|
|
|
IBaseFilter *pSrc = 0;
|
|
JIF(CreateFilterFromLibrary(0, CLSID_AsyncStreamSource, IID_IBaseFilter, (void**)&pSrc));
|
|
JIF(pGB->AddFilter(pSrc,wstrfilename));
|
|
JIF(pSrc->QueryInterface(IID_IStreamSourceFilter,(void**)&m_pStreamReader));
|
|
IStreamSourceFilter* pStreamReader = (IStreamSourceFilter*)m_pStreamReader;
|
|
AM_MEDIA_TYPE mt;
|
|
ZeroMemory(&mt, sizeof(mt));
|
|
mt.majortype = MEDIATYPE_Stream;
|
|
mt.subtype = isWave ? MEDIASUBTYPE_WAVE : MEDIASUBTYPE_NULL;
|
|
if (filename) {
|
|
JIF(pStreamReader->Load(wstrfilename, &mt));
|
|
} else {
|
|
JIF(pStreamReader->LoadStream(stream, readSize, streamSize, &mt));
|
|
}
|
|
if (samplebuffermode) {
|
|
m_pGrabberCB = new CSampleGrabberCallback(this);
|
|
addSampleGrabber(pGB, pSrc, (ISampleGrabberCB*)m_pGrabberCB, &m_pGrabber);
|
|
pSrc->Release();
|
|
m_playmode = 0;
|
|
play();
|
|
} else {
|
|
IPin *pOut;
|
|
JIF(GetPin(pSrc, PINDIR_OUTPUT, &pOut));
|
|
pSrc->Release();
|
|
JIF(pGB->Render(pOut));
|
|
pOut->Release();
|
|
setVolume(m_volume);
|
|
m_playmode = 0;
|
|
}
|
|
|
|
IMediaSeeking *pMS=(IMediaSeeking*)m_pMS;
|
|
m_startpos = 0;
|
|
JIF(pMS->GetStopPosition(&m_endpos));
|
|
return true;
|
|
}
|
|
|
|
bool audioPlayer::play()
|
|
{
|
|
if ((!m_pMC) || (m_playmode == -1))
|
|
return false;
|
|
IMediaControl *pMC = (IMediaControl*)m_pMC;
|
|
HRESULT hr;
|
|
JIF(pMC->Run());
|
|
m_playmode = 1;
|
|
return true;
|
|
}
|
|
|
|
bool audioPlayer::pause()
|
|
{
|
|
if ((!m_pMC) || (m_playmode == -1))
|
|
return false;
|
|
IMediaControl *pMC = (IMediaControl*)m_pMC;
|
|
HRESULT hr;
|
|
JIF(pMC->Pause());
|
|
m_playmode = 2;
|
|
return true;
|
|
}
|
|
|
|
bool audioPlayer::stop()
|
|
{
|
|
if ((!m_pMC) || (m_playmode <= 0))
|
|
return true;
|
|
IMediaControl *pMC = (IMediaControl*)m_pMC;
|
|
HRESULT hr;
|
|
JIF(pMC->Stop());
|
|
m_playmode = 0;
|
|
return true;
|
|
}
|
|
|
|
bool audioPlayer::closeMedia()
|
|
{
|
|
if (m_pGrabber) {
|
|
ISampleGrabber *pGrabber = (ISampleGrabber *)m_pGrabber;
|
|
pGrabber->SetCallback(0, 1);
|
|
pGrabber->Release();
|
|
}
|
|
if (m_pGrabberCB)
|
|
delete ((CSampleGrabberCallback*)m_pGrabberCB);
|
|
m_pGrabber = 0;
|
|
m_pGrabberCB = 0;
|
|
|
|
stop();
|
|
if (m_pMS)
|
|
((IMediaSeeking*)m_pMS)->Release();
|
|
if (m_pMC)
|
|
((IMediaControl*)m_pMC)->Release();
|
|
if (m_pStreamReader)
|
|
((IStreamSourceFilter*)m_pStreamReader)->Release();
|
|
if (m_pGB)
|
|
((IGraphBuilder*)m_pGB)->Release();
|
|
m_pMS = 0;
|
|
m_pMC = 0;
|
|
m_pStreamReader = 0;
|
|
m_pGB = 0;
|
|
m_playmode = -1;
|
|
return true;
|
|
}
|
|
|
|
bool audioPlayer::setVolume(int volume)
|
|
{
|
|
if ((volume < 0) || (volume > 100))
|
|
return false;
|
|
m_volume = volume;
|
|
if (!m_pGB)
|
|
return true;
|
|
IBasicAudio *pBA = NULL;
|
|
HRESULT hr;
|
|
int now = -(int)(exp(log((double)10001)/100*(100-volume))-1+0.5);
|
|
JIF(((IGraphBuilder*)m_pGB)->QueryInterface(IID_IBasicAudio, (void**)&pBA));
|
|
pBA->put_Volume(now);
|
|
pBA->Release();
|
|
return true;
|
|
}
|
|
|
|
bool audioPlayer::isEnd(long *mstimetoend)
|
|
{
|
|
if (!m_pMS)
|
|
return false;
|
|
IMediaSeeking *pMS=(IMediaSeeking*)m_pMS;
|
|
LONGLONG curpos;
|
|
HRESULT hr;
|
|
JIF(pMS->GetCurrentPosition(&curpos));
|
|
if (curpos >= m_endpos)
|
|
return true;
|
|
if (mstimetoend)
|
|
*mstimetoend = (m_endpos - curpos) / 10000;
|
|
return false;
|
|
}
|
|
|
|
bool audioPlayer::setPlayPos(long ms)
|
|
{
|
|
if (!m_pGB)
|
|
return false;
|
|
HRESULT hr;
|
|
IMediaSeeking *pMS = (IMediaSeeking*)m_pMS;
|
|
LONGLONG pos = ((LONGLONG)ms)*10000;
|
|
if (!m_pGrabberCB) {
|
|
JIF(pMS->SetPositions(&pos, AM_SEEKING_AbsolutePositioning,
|
|
NULL, AM_SEEKING_NoPositioning));
|
|
} else {
|
|
pause();
|
|
JIF(pMS->SetPositions(&pos, AM_SEEKING_AbsolutePositioning,
|
|
NULL, AM_SEEKING_NoPositioning));
|
|
((CSampleGrabberCallback*)m_pGrabberCB)->setResetflag(true);
|
|
play();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool audioPlayer::getPlayPos(long *ms)
|
|
{
|
|
if (!m_pGB)
|
|
return false;
|
|
HRESULT hr;
|
|
IMediaSeeking *pMS = (IMediaSeeking*)m_pMS;
|
|
LONGLONG curpos;
|
|
JIF(pMS->GetCurrentPosition(&curpos));
|
|
if (ms)
|
|
*ms = curpos / 10000;
|
|
return true;
|
|
}
|
|
|
|
int audioPlayer::getNextSamples(u8* buf, int wantedbufsize)
|
|
{
|
|
if (!m_pGrabberCB || !m_pMC) {
|
|
memset(buf, 0, wantedbufsize);
|
|
return wantedbufsize;
|
|
}
|
|
return ((CSampleGrabberCallback*)m_pGrabberCB)->getNextSamples(buf, wantedbufsize);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////
|
|
// audioEngine
|
|
|
|
bool audioEngine::loadRIFFStream(u8* stream, int streamsize, int atracID)
|
|
{
|
|
u8 *oma = 0;
|
|
m_ID = atracID;
|
|
m_channel = OMAConvert::getRIFFChannels(stream, streamsize);
|
|
bool bResult = false;
|
|
if (m_channel != 1) {
|
|
int readsize = 0;
|
|
int omasize = OMAConvert::convertRIFFtoOMA(stream, streamsize, &oma, &readsize);
|
|
if (omasize > 0){
|
|
bResult = load(0, oma, readsize, omasize, true);
|
|
OMAConvert::releaseStream(&oma);
|
|
}
|
|
}
|
|
return bResult;
|
|
}
|
|
|
|
bool audioEngine::closeStream()
|
|
{
|
|
bool bResult = closeMedia();
|
|
m_ID = -1;
|
|
return bResult;
|
|
}
|
|
|
|
bool audioEngine::setPlaySample(int sample)
|
|
{
|
|
return setPlayPos(((s64)sample) * 1000 / 44100);
|
|
}
|
|
|
|
void audioEngine::addStreamData(int offset, u8* buf, int size, int cursample)
|
|
{
|
|
if ((!m_pGB) || (m_channel == 1))
|
|
return;
|
|
IStreamSourceFilter* pStreamReader = (IStreamSourceFilter*)m_pStreamReader;
|
|
|
|
pStreamReader->AddStreamData(offset, buf, size);
|
|
|
|
bool bsetpos = pStreamReader->IsReadPassEnd();
|
|
if (bsetpos)
|
|
setPlaySample(cursample);
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
|
|
std::map<int, audioEngine*> audioMap;
|
|
std::recursive_mutex atracsection;
|
|
|
|
void addAtrac3Audio(u8* stream, int streamsize, int atracID)
|
|
{
|
|
if (audioMap.find(atracID) != audioMap.end())
|
|
return;
|
|
audioEngine *temp = new audioEngine();
|
|
bool bResult = temp->loadRIFFStream(stream, streamsize, atracID);
|
|
atracsection.lock();
|
|
audioMap[atracID] = temp;
|
|
atracsection.unlock();
|
|
if (!bResult)
|
|
temp->closeMedia();
|
|
}
|
|
|
|
audioEngine* getaudioEngineByID(int atracID)
|
|
{
|
|
if (audioMap.find(atracID) == audioMap.end()) {
|
|
return NULL;
|
|
}
|
|
return audioMap[atracID];
|
|
}
|
|
|
|
void deleteAtrac3Audio(int atracID)
|
|
{
|
|
atracsection.lock();
|
|
if (audioMap.find(atracID) != audioMap.end()) {
|
|
delete audioMap[atracID];
|
|
audioMap.erase(atracID);
|
|
}
|
|
atracsection.unlock();
|
|
}
|
|
|
|
void initaudioEngine()
|
|
{
|
|
if (g_Config.bAutoLoadDShow) {
|
|
if (LoadFilterLibrary(0, "filter\\lib\\AsyncStreamflt.ax") != S_OK) {
|
|
WARN_LOG(HLE, "AsyncStreamflt.ax failed to load");
|
|
} else {
|
|
NOTICE_LOG(HLE, "AsyncStreamflt.ax loading completed successfully");
|
|
}
|
|
}
|
|
CoInitialize(0);
|
|
}
|
|
|
|
void shutdownEngine()
|
|
{
|
|
atracsection.lock();
|
|
for (auto it = audioMap.begin(); it != audioMap.end(); ++it) {
|
|
delete it->second;
|
|
}
|
|
audioMap.clear();
|
|
atracsection.unlock();
|
|
CoUninitialize();
|
|
FreeFilterLibrary(0);
|
|
}
|
|
|
|
#endif // _USE_DSHOW_
|