You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

WinAPI下从SampleGrabber获取字节转音频原始数据问题咨询

问题描述

实现目标

  • 从「麦克风->SampleGrabber->Null Renderer」链路抓取原始音频数据
  • 将捕获的原始数据写入MP3/AVI/WAVE等标准音频文件

疑问

  1. 为SampleGrabber设置的MediaType是否正确?
  2. 如何将音频数据压缩为标准音频文件?

当前代码

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设置问题

方向正确,但存在细节缺失和代码错误:

  1. 补充格式块信息:仅指定主类型和子类型不足以让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);
  1. 修复回调数据采集错误:你的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;
}
  1. 补全错误检查: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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.12 18:05:45