Directshow的音频捕获和回放测试
2007-6-5
想了很久才定了这个题目,因为所做的工作确实非常有限,只能算是一个小小的测试。以前带学生做过一个采用Windows Media Format 9SDK捕获声音和视频祯,通过网络传输后回放的毕业设计的题目,不过播放的时候有些卡。查阅了一些资料,发现采用directshow做的比较多,并且还可以配套RTP和RTCP协议,达到实时播放的目的。于是决定采用directshow来做一套类似的东西,然后再套上P2P的功能,最后形成一个完整的基于P2P和Directshow技术的网络电视收看平台。关于音频的采集、传输和回放是其中的一个重要的组成部分,本文只是一个小小的测试,主要完成的内容是采用Grabber Filter捕获音频祯,将其保存为一个本地文件,然后再建立一个memplay的source Filter,在graph Edit中或者自己编写程序播放这个本地文件,达到回放的目的。
本文主要包括以下几个方面的内容:
1、Directshow9.0简介
关于Directshow9 SDK的简介随便在网络上一搜,便会有一大堆,我也就不罗唆了。主要说一下它的主要部分就是有一些filter组成的,其中包括source Filter、transform Filter 和render Filter。如果以我们的音频采集和播放为例,source filter对应的就是麦克风的功能,负责采集原始的音频数据;而render filter对应的就是你的音箱的功能,负责音频数据的回放。
安装完DirectX SDK9以后,在开始-程序-Microsoft DirectX 9.0 SDK-DirectX Utilities中会有一个Graph Edit的程序,这个程序非常有用。下面就是利用它完成的一个Filter的连接图。
具体的做法是选择Graph-Insert Filters,选择Audio Capture Sources,双击其中的第一个选项。然后右键单击输出针脚,即capture旁边的小凸起,选择Render Pin。这种方法采用的是智能连接的方法,系统自动地为你搜寻可以连接到的Filter。也可以自己选择合适的filter,然后通过鼠标拖拽的方式连接Filter,形成自己的Graph。建立好Graph以后,单击上面的播放按钮就可以运行这个graph了,效果是自己对着麦克风说的话会在音箱中有一个回放的动作。
自己编写好的filter注册后也可以在GraphEdit中使用,一般自己编写的filter都是显示在Directshow filters子项下的。可以通过这个工具测试自己编写的Filter是否能够正常运行。
2、音频祯的捕获
先说一下大体的思路,我没有编写专门的程序来捕获音频祯,而是借用了Directshow自带的一个grabber的例子,稍微改写了一下,将其捕获到的数据祯(既可以是音频也可以是视频)按照一定的格式存放在c盘的audiodata文件中,供以后播放时调用。这种方法并不是特别好,因为额外的写文件操作会严重影响系统的效率,这一点用到视频的时候特别明显。但是作为一个测试来说,尽量从简单易行的角度出发,也就够了。
首先打开C:/DXSDK/Samples/C++/DirectShow/Filters/Grabber(假设安装在c盘的根目录下)下的grabber.dsw,找到grabber.cpp下的Receive函数,在checkpointer(pms, E_POINTER)下输入以下内容:
//
BYTE *pBuffer = NULL;
pms->GetPointer(&pBuffer);
if(pBuffer == NULL) return E_UNEXPECTED;
REFERENCE_TIME StartTime, StopTime;
pms->GetTime( &StartTime, &StopTime);
long LBufferSize = pms->GetSize();
BYTE * buffer = NULL;
buffer = new BYTE[LBufferSize];
if(buffer == NULL) return E_UNEXPECTED;
memcpy(buffer, pBuffer, LBufferSize);
FILE * fp = NULL;
fp = fopen("c://audiodata", "ab+");
if(fp == NULL) return E_UNEXPECTED;
fwrite(&LBufferSize, sizeof(long), 1, fp);
fwrite(&StartTime, sizeof(REFERENCE_TIME), 1, fp);
fwrite(&StopTime, sizeof(REFERENCE_TIME), 1, fp);
fwrite(buffer, sizeof(BYTE), LBufferSize, fp);
fclose(fp);
delete[] buffer;
buffer = NULL;
//
这段代码非常简单,大家一看就都明白了,就是把sample的长度,开始时间,结束时间以及sample的内容存放到c盘的audiodata文件中。我习惯于用fopen,而不习惯于用CFile,我觉得fopen更简单,更好使。
编译一下这个文件,最好编译成win32 debug而不是unicode-debug版本,可能会提示说有一个../../BaseClasses/debug/strmbasd.lib找不到,因为你可能还没有编译baseclasses中的文件。找到baseclasses所在的文件夹,按照debug版本编译,然后再重新编译grabber就可以了。
在开始-运行中输入
regsvr32 C:/DXSDK/Samples/C++/DirectShow/Filters/Grabber/Debug/grabber.ax
注册该filter,现在就可以在GraphEdit中的insertFilters-Directshow Filters中找到sample Grabber Example这个Filter了,这就是我们需要用到的捕获音频祯的filter。
在GraphEdit中建立以下Graph:
运行这个Graph,对着麦克风说几句话,然后单击停止按钮。好的,现在在C盘的根目录下可以找到audiodata这个文件了,这当中就是我们按照长度、开始时间、结束时间、祯内容存放的音频文件。
3、编写source filter
Directshow提供了两种模式来进行播放,一种是推模式,一种是拉模式。前者对应于有摄像头、麦克风等音视频采集Filter的应用,而后者对应于文件的播放形式。前者的source filter可以自己产生数据,然后将数据主动推送到与之相连的下一个filter,而后者则是相反的一套动作。前者基于IMemInputPin接口,而后者基于IAsyncReader接口。曾经见到过一个用拉模式写的在网络上传输视频文件的例子,效果不是很好,卡得厉害。总以为可能是模式的原因,也没有深究其中的原委,就采用了推模式的方法。
其实所作的工作是自己设计完成了一个source filter的制作,然后利用程序或者在graph Edit中进行测试(后者更为简单),程序还有许多不足的地方,待以后日臻完善。directshow中自带了一些很好的例子,可以拿来修改后使用,非常方便。我研究是其中的filters/ ball程序,这个程序可以自行产生一组跳动的小球的画面播放。由于从它的基础上修改,我甚至连文件的名称都没有改。
fball.h
class CMemPlayStream;
// {A7BD9B8E-36C7-4d7a-9925-A20246DAFD23}
DEFINE_GUID(CLSID_MemPlay,
0xa7bd9b8e, 0x36c7, 0x4d7a, 0x99, 0x25, 0xa2, 0x2, 0x46, 0xda, 0xfd, 0x23);
class CMemPlay : public CSource
{
public:
static CUnknown * WINAPI CreateInstance(LPUNKNOWN lpunk, HRESULT *phr);
private:
CMemPlay(LPUNKNOWN lpunk, HRESULT *phr);
}; // CMemPlay
class CMemPlayStream : public CSourceStream
{
public:
CMemPlayStream(HRESULT *phr, CMemPlay *pParent, LPCWSTR pPinName);
~CMemPlayStream();
HRESULT FillBuffer(IMediaSample *pms);
HRESULT DecideBufferSize(IMemAllocator *pIMemAlloc,
ALLOCATOR_PROPERTIES *pProperties);
HRESULT SetMediaType(const CMediaType *pMediaType);
HRESULT CheckMediaType(const CMediaType *pMediaType);
HRESULT GetMediaType(CMediaType *pmt);
HRESULT OnThreadCreate(void);
private:
int m_iRepeatTime; // Time in msec between frames
const int m_iDefaultRepeatTime; // Initial m_iRepeatTime
CCritSec m_cSharedState; // Lock on m_rtSampleTime and m_Ball
CRefTime m_rtSampleTime; // The time stamp for each sample
BYTE * m_DataList[1024];
long m_SampleSize[1024];
REFERENCE_TIME m_StartTimeList[1024];
REFERENCE_TIME m_StopTimeList[1024];
int m_SampleCount;
int m_SampleReaded;
}; // CMemPlayStream
其中包括两个类,CMemPlay类是Filter的一个壳子,里面没有什么实质性的内容,无非就是调用了一下CMemPlayStream类,好像都是这么来用的,现在还说不好为什么一定要这么做。CMemPlayStream中主要派生了几个方法:FillBuffer用来填充实际的sample的内容,DecideBufferSize用来设置buffer的大小,GetMediaType、SetMediaType和CheckMediaType用来设置和匹配媒体的类型。好像不用完全覆盖这三个函数,但我没有试过。
fball.cpp
#include <streams.h>
#include <olectl.h>
#include <initguid.h>
#include <stdio.h>
#include "fball.h"
#pragma warning(disable:4710) // 'function': function not inlined (optimzation)
#pragma warning(disable:4628)
EXTERN_GUID(WMFORMAT_WaveFormatEx,
0x05589f81, 0xc356, 0x11ce, 0xbf, 0x01, 0x00, 0xaa, 0x00, 0x55, 0x59, 0x5a);
const AMOVIESETUP_MEDIATYPE sudOpPinTypes =
{
&MEDIATYPE_Audio, // Major type
&MEDIASUBTYPE_NULL // Minor type
};
const AMOVIESETUP_PIN sudOpPin =
{
L"Output", // Pin string name
FALSE, // Is it rendered
TRUE, // Is it an output
FALSE, // Can we have none
FALSE, // Can we have many
&CLSID_NULL, // Connects to filter
NULL, // Connects to pin
1, // Number of types
&sudOpPinTypes }; // Pin details
const AMOVIESETUP_FILTER sudBallax =
{
&CLSID_MemPlay, // Filter CLSID
L"Memory play", // String name
MERIT_DO_NOT_USE, // Filter merit
1, // Number pins
&sudOpPin // Pin details
};
CFactoryTemplate g_Templates[] = {
{ L"Memory play"
, &CLSID_MemPlay
, CMemPlay::CreateInstance
, NULL
, &sudBallax }
};
int g_cTemplates = sizeof(g_Templates) / sizeof(g_Templates[0]);
STDAPI DllRegisterServer()
{
return AMovieDllRegisterServer2(TRUE);
} // DllRegisterServer
STDAPI DllUnregisterServer()
{
return AMovieDllRegisterServer2(FALSE);
} // DllUnregisterServer
extern "C" BOOL WINAPI DllEntryPoint(HINSTANCE, ULONG, LPVOID);
BOOL APIENTRY DllMain(HANDLE hModule,
DWORD dwReason,
LPVOID lpReserved)
{
return DllEntryPoint((HINSTANCE)(hModule), dwReason, lpReserved);
}
CUnknown * WINAPI CMemPlay::CreateInstance(LPUNKNOWN lpunk, HRESULT *phr)
{
ASSERT(phr);
CUnknown *punk = new CMemPlay(lpunk, phr);
if(punk == NULL)
{
if(phr)
*phr = E_OUTOFMEMORY;
}
return punk;
} // CreateInstance
CMemPlay::CMemPlay(LPUNKNOWN lpunk, HRESULT *phr) :
CSource(NAME("Memory Play"), lpunk, CLSID_MemPlay)
{
ASSERT(phr);
CAutoLock cAutoLock(&m_cStateLock);
m_paStreams = (CSourceStream **) new CMemPlayStream*[1];
if(m_paStreams == NULL)
{
if(phr)
*phr = E_OUTOFMEMORY;
return;
}
m_paStreams[0] = new CMemPlayStream(phr, this, L"Memory Play!");
if(m_paStreams[0] == NULL)
{
if(phr)
*phr = E_OUTOFMEMORY;
return;
}
} // (Constructor)