WinAPI下从SampleGrabber获取字节转音频原始数据问题咨询
问题描述
实现目标
- 从「麦克风->SampleGrabber->Null Renderer」链路抓取原始音频数据
- 将捕获的原始数据写入MP3/AVI/WAVE等标准音频文件
疑问
- 为SampleGrabber设置的MediaType是否正确?
- 如何将音频数据压缩为标准音频文件?
当前代码
struct sRecortdingData { std::vector<BYTE> recordingData; double totalRecordingTime; }; class CFakeCallback : public ISampleGrabberCB { public: sRecortdingData sRecording; STDMETHODIMP_(ULONG) AddRef() { return 2; } STDMETHODIMP_(ULONG) Release() { return 1; } STDMETHODIMP_(HRESULT __stdcall) QueryInterface(REFIID riid, void** ppv) { if (riid == IID_ISampleGrabberCB || riid == IID_IUnknown) { *ppv = static_cast<ISampleGrabberCB*>(this); return NOERROR; } return E_NOINTERFACE; } STDMETHODIMP_(HRESULT __stdcall) SampleCB(double SampleTime, IMediaSample* pSample) { return S_OK; } STDMETHODIMP_(HRESULT __stdcall) BufferCB(double SampleTime, BYTE* pBuffer, long BufferLen) { sRecording.recordingData.push_back(*pBuffer); sRecording.totalRecordingTime = SampleTime; return S_OK; } }; void Microphone::RecordAudio() { HRESULT hr; AM_MEDIA_TYPE mt; long recordingEventCode; IMediaControl* pMediaControl = NULL; ISampleGrabber* pISampleGrabber = NULL; IBaseFilter* pSampleGrabberFilter; IGraphBuilder* pGraphBuilder = NULL; IMediaEventEx* pEvent = NULL; IBaseFilter* pMicrophoneFilter = NULL; ISampleGrabber* pSampleGrabber = NULL; ICreateDevEnum* pDeviceEnum = NULL; IBaseFilter* pNullRender = NULL; hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); if (FAILED(hr)) { HR_Failed(hr); return; } // Filter graph hr = CoCreateInstance(CLSID_FilterGraph, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pGraphBuilder)); if (FAILED(hr)) { HR_Failed(hr); return; } // Device enum hr = CoCreateInstance(CLSID_SystemDeviceEnum, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pDeviceEnum)); if (FAILED(hr)) { HR_Failed(hr); return; } // Null renderer hr = CoCreateInstance(CLSID_NullRenderer, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pNullRender)); if (FAILED(hr)) { HR_Failed(hr); return; } // Get the even control hr = pGraphBuilder->QueryInterface(IID_PPV_ARGS(&pEvent)); if (FAILED(hr)) { HR_Failed(hr); return; } // Sample Grabber hr = CoCreateInstance(CLSID_SampleGrabber, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pSampleGrabberFilter)); if (FAILED(hr)) { HR_Failed(hr); return; } // Get Media Control hr = pGraphBuilder->QueryInterface(IID_PPV_ARGS(&pMediaControl)); if (FAILED(hr)) { HR_Failed(hr); return; } // Setup input device filter pMicrophoneFilter = Microphone::SetupDeviceFilter(CLSID_AudioInputDeviceCategory, pGraphBuilder, pDeviceEnum, Utils::s2ws("My Microphone")); if (pMicrophoneFilter == NULL) { HR_Failed(hr); return; } // Setup sample grabber filter pSampleGrabberFilter = Microphone::SetupDeviceFilter(CLSID_LegacyAmFilterCategory, pGraphBuilder, pDeviceEnum, L"SampleGrabber"); if (pSampleGrabberFilter == NULL) { HR_Failed(hr); return; } // Connect both pins together Device_Connect(pMicrophoneFilter, pSampleGrabberFilter); // Setup null renderer filter pNullRender = Microphone::SetupDeviceFilter(CLSID_LegacyAmFilterCategory, pGraphBuilder, pDeviceEnum, L"Null Renderer"); if (pNullRender == NULL) { HR_Failed(hr); return; } // Connect both pins together Device_Connect(pSampleGrabberFilter, pNullRender); // Get the ISampleGranner interface hr = pSampleGrabberFilter->QueryInterface(IID_ISampleGrabber, (LPVOID*)&pISampleGrabber); if (FAILED(hr)) { HR_Failed(hr); return; } hr = pISampleGrabber->SetCallback(&CB, 1); ZeroMemory(&mt, sizeof(mt)); mt.majortype = MEDIATYPE_Audio; mt.subtype = MEDIASUBTYPE_PCM; // Set the media type hr = pISampleGrabber->SetMediaType(&mt); if (FAILED(hr)) { HR_Failed(hr); return; } pISampleGrabber->SetBufferSamples(FALSE); if (FAILED(hr)) { HR_Failed(hr); return; } hr = pMediaControl->Run(); if (FAILED(hr)) { HR_Failed(hr); return; } DWORD recordingTime = 20000; hr = pEvent->WaitForCompletion(recordingTime, &recordingEventCode); std::string str(CB.sRecording.recordingData.begin(), CB.sRecording.recordingData.end()); // Do something with the data from callback and write it to audio file }
解答
一、SampleGrabber的MediaType设置问题
方向正确,但存在细节缺失和代码错误:
- 补充格式块信息:仅指定主类型和子类型不足以让SampleGrabber正确协商格式,需要填充
WAVEFORMATEX结构:
ZeroMemory(&mt, sizeof(mt)); mt.majortype = MEDIATYPE_Audio; mt.subtype = MEDIASUBTYPE_PCM; mt.formattype = FORMAT_WaveFormatEx; WAVEFORMATEX* pWfx = (WAVEFORMATEX*)CoTaskMemAlloc(sizeof(WAVEFORMATEX)); ZeroMemory(pWfx, sizeof(WAVEFORMATEX)); pWfx->wFormatTag = WAVE_FORMAT_PCM; pWfx->nChannels = 1; // 可根据麦克风配置设为1(单声道)或2(立体声) pWfx->nSamplesPerSec = 44100; pWfx->wBitsPerSample = 16; pWfx->nBlockAlign = (pWfx->nChannels * pWfx->wBitsPerSample) / 8; pWfx->nAvgBytesPerSec = pWfx->nSamplesPerSec * pWfx->nBlockAlign; mt.pbFormat = (PBYTE)pWfx; mt.cbFormat = sizeof(WAVEFORMATEX); hr = pISampleGrabber->SetMediaType(&mt); // 使用后记得释放格式块 CoTaskMemFree(pWfx);
- 修复回调数据采集错误:你的
BufferCB只插入了单个字节,正确做法是拷贝整个缓冲区的所有数据:
STDMETHODIMP_(HRESULT __stdcall) BufferCB(double SampleTime, BYTE* pBuffer, long BufferLen) { sRecording.recordingData.insert(sRecording.recordingData.end(), pBuffer, pBuffer + BufferLen); sRecording.totalRecordingTime = SampleTime; return S_OK; }
- 补全错误检查:
SetBufferSamples调用后未检查返回值,容易忽略潜在错误:
hr = pISampleGrabber->SetBufferSamples(FALSE); if (FAILED(hr)) { HR_Failed(hr); return; }
二、将原始数据写入标准音频文件
1. WAVE文件(最直接)
WAVE是PCM数据加固定格式的文件头,步骤如下:
- 先获取SampleGrabber协商后的实际格式(在
pMediaControl->Run()后调用pISampleGrabber->GetConnectedMediaType(&mt)) - 构造WAVE文件头并写入:
typedef struct { char riff[4] = {'R','I','F','F'}; DWORD fileSize; char wave[4] = {'W','A','V','E'}; char fmt[4] = {'f','m','t',' '}; DWORD fmtSize = sizeof(WAVEFORMATEX); WAVEFORMATEX wfx; char data[4] = {'d','a','t','a'}; DWORD dataSize; } WaveFileHeader; // 填充头信息 WaveFileHeader header; WAVEFORMATEX* pActualWfx = (WAVEFORMATEX*)mt.pbFormat; header.wfx = *pActualWfx; header.dataSize = CB.sRecording.recordingData.size(); header.fileSize = sizeof(WaveFileHeader) - 8 + header.dataSize; // 写入文件 FILE* pFile = fopen("output.wav", "wb"); fwrite(&header, sizeof(WaveFileHeader), 1, pFile); fwrite(CB.sRecording.recordingData.data(), 1, header.dataSize, pFile); fclose(pFile);
2. MP3文件
有两种可行方案:
- 方案一:DirectShow压缩链路:修改filter graph为「麦克风->MP3压缩Filter(如LAME MP3 Encoder)->SampleGrabber->Null Renderer」,SampleGrabber将直接获取压缩后的MP3数据,写入文件即可。需要先注册对应的MP3压缩Filter,再通过枚举或
CoCreateInstance添加到graph中。 - 方案二:第三方库编码:用LAME、FDK-AAC等开源库将抓取到的PCM数据编码为MP3。需确保PCM格式(采样率、位深、通道数)符合库的要求,调用库的编码接口生成MP3流后写入文件。
3. AVI文件
AVI是容器格式,可通过DirectShow快速实现:
- 修改graph链路为「麦克风->音频压缩Filter->AVI Mux->File Writer」,直接让DirectShow完成音频数据的封装和写入。若需自行写入,需了解AVI文件结构,依次写入AVI主头、索引、音频数据流。
其他代码优化提示
- 重复创建filter:你既用
CoCreateInstance创建了pNullRender和pSampleGrabberFilter,又调用SetupDeviceFilter重新获取,会导致COM对象泄漏,需删除重复的SetupDeviceFilter调用。 - COM对象释放:所有创建的COM接口(如
pGraphBuilder、pMediaControl)使用完毕后必须调用Release(),避免内存泄漏。
内容的提问来源于stack exchange,提问作者TomP
相关产品推荐
相关产品推荐

