返回

调整IMFSourceReader数据块大小实现低延迟音频捕获

windows

调整 IMFSourceReader 的数据块大小

在使用 Windows Media Foundation (IMF) 进行音频录制时,IMFSourceReader::OnReadSample 方法返回的数据块大小以及调用频率,直接关系到音频数据的实时性和响应速度。开发者常常需要根据应用需求,调整这些参数来获得更好的音频处理效果。 默认情况下,IMFSourceReader返回的音频块大小通常是基于硬件和格式的一些默认设置。 这会导致回调间隔固定,数据块延迟增大。

问题剖析

在实际开发中,我们遇到如下问题:

  1. 通过IMFSourceReader读取音频数据时,每个数据块的持续时间固定为 50 毫秒。
  2. 降低采样率会影响数据块的大小,但不会改变其持续时间。
  3. 尝试调整如 MF_SAUDIO_SAMPLES-PER-BLOCK 等参数并没有产生预期效果。
  4. 需要降低数据块延迟到 10 毫秒或更高频率的回调,实现更精细化的音频操作。

这个问题的本质在于 IMFSourceReader 对数据块的管理和输出机制。它的输出,并非严格按照开发者设置来返回数据块。系统对 IMFSourceReader 读取的数据流,进行缓冲管理。 而在 IMFSourceReader 的设置上,系统会根据设备和格式来进行默认缓冲配置,而不是开发者简单调整就能立即生效。要解决这一问题,需要对 IMFSourceReader 以及其底层的媒体管道机制有深入理解。

解决方案

1. 设置更小的采样率

尽管降低采样率本身不会直接减少数据块的持续时间, 但它能缩小每次返回的数据块的字节大小。较小的数据块配合后续的手段可以带来一些优化空间。 可以通过在 IMFMediaType 上设置 MF_MT_AUDIO_SAMPLES_PER_SECOND来实现 。

操作步骤:

  1. 获取当前的 IMFMediaType
  2. 通过 SetUINT32(MF_MT_AUDIO_SAMPLES_PER_SECOND, desiredSampleRate) 方法修改采样率
  3. 重新初始化 IMFSourceReader 使用更新后的媒体类型。

代码示例:

HRESULT SetAudioSampleRate(IMFMediaType* pMediaType, UINT32 sampleRate) {
    HRESULT hr = S_OK;
    hr = pMediaType->SetUINT32(MF_MT_AUDIO_SAMPLES_PER_SECOND, sampleRate);
    return hr;
}

// Example Usage:
// 假设 pMediaType 为 IMFMediaType 的实例, newSampleRate 是新的采样率值 (例如, 24000).
// 需要在 IMFSourceReader 初始化之前,执行此操作。
HRESULT hr = SetAudioSampleRate(pMediaType, newSampleRate);

2. 设置音频设备缓存时长

IMFSourceReader 通常使用默认的音频设备缓冲区。通过调整这个缓冲时长可以影响 OnReadSample 回调的频率,通过更短的缓冲区时间能使得音频流更精细的呈现。可以使用 IPropertyStore 来控制音频捕获设备缓存属性 PKEY_AudioEngine_DeviceFormatKSAUDIO_BUFFER_LENGTH参数。 这涉及到底层 KSPROPERTY 机制。

操作步骤:

  1. 获取音频捕捉设备的 IKsControl 接口。
  2. 获取设备的 Property Store.
  3. 修改 Property Store 中的 KSAUDIO_BUFFER_LENGTH 参数。

代码示例:

HRESULT SetAudioBufferLength(IMMDevice* pDevice, DWORD bufferLengthMilliseconds) {
    HRESULT hr = S_OK;
    IKsControl* pKsControl = NULL;
    IPropertyStore* pPropertyStore = NULL;

    // 获取 IKsControl 接口.
    hr = pDevice->Activate(__uuidof(IKsControl), CLSCTX_ALL, NULL, (void**)&pKsControl);

    if(FAILED(hr))
        goto done;
    // Get the Property Store for the audio endpoint
    hr = pDevice->OpenPropertyStore(STGM_READWRITE, &pPropertyStore);
     if(FAILED(hr))
        goto done;
     
   // 创建一个新的 variant 用于储存时间数据,以十分之一毫秒为单位.
    PROPVARIANT varValue;
    PropVariantInit(&varValue);
    varValue.vt = VT_UI8; //Unsigned 64-bit integer

    ULONGLONG ullNanoseconds = (ULONGLONG)bufferLengthMilliseconds * 10000;

    varValue.uhVal.QuadPart =  ullNanoseconds;


    // Set the buffer length to PKEY_AudioEngine_DeviceFormat property
    PROPERTYKEY keyBufferLength =  PKEY_AudioEngine_DeviceFormat;
    hr = pPropertyStore->SetValue(keyBufferLength,varValue );

     if (FAILED(hr)) {
      goto done;
   }

    hr = pPropertyStore->Commit();

 done:
      if (pPropertyStore)
      {
           pPropertyStore->Release();
          pPropertyStore = nullptr;
       }
       if (pKsControl)
       {
            pKsControl->Release();
            pKsControl = nullptr;
       }
     
     return hr;

}

//Example usage
// 假设  pAudioCaptureDevice  是 IMMDevice 音频捕捉设备
DWORD desiredBufferLengthMilliseconds = 10; //设置缓冲区时长为 10 毫秒
HRESULT hr = SetAudioBufferLength(pAudioCaptureDevice,desiredBufferLengthMilliseconds);

注意: 设置过小的缓存值可能导致音频数据丢失,增加抖动或者卡顿。需根据实际情况,平衡延迟与稳定性。 另外这种修改影响系统级属性,注意做回滚操作以及异常情况处理,以及权限控制。

3.使用 WASAPI (Windows Audio Session API)

相对于 IMFSourceReader , WASAPI 提供了更底层的音频访问。 WASAPI 可以提供精确到样本级的音频数据操作,这使得对数据块的处理有更多的掌控权,更低延迟。

操作步骤:

  1. 使用 IMMDeviceEnumerator 枚举音频设备,并选择指定的音频设备.
  2. 激活 IAudioClient 并配置音频格式。
  3. 通过 IAudioCaptureClient 获取捕获的数据.

** 代码示例: (仅为演示, 具体细节需按需调整)**

// 参考 Microsoft 官方文档:https://learn.microsoft.com/en-us/windows/win32/coreaudio/capturing-a-stream
#include <mmdeviceapi.h>
#include <audioclient.h>
// 为了简化演示省略很多异常检查
void CaptureAudioStreamWASAPI(int sampleRate, int channelCount,int bufferLengthInMilliseconds) {

  IMMDeviceEnumerator* pDeviceEnumerator = NULL;
  IMMDevice* pDevice = NULL;
  IAudioClient* pAudioClient = NULL;
  IAudioCaptureClient* pCaptureClient = NULL;

  HRESULT hr = CoCreateInstance(__uuidof(MMDeviceEnumerator),NULL, CLSCTX_ALL, __uuidof(IMMDeviceEnumerator),(void**)&pDeviceEnumerator);

    hr = pDeviceEnumerator->GetDefaultAudioEndpoint(eCapture,eConsole,&pDevice);
   hr = pDevice->Activate(__uuidof(IAudioClient), CLSCTX_ALL, NULL, (void**)&pAudioClient);
   WAVEFORMATEX *pwfx=nullptr;
  hr= pAudioClient->GetMixFormat(&pwfx);


      WAVEFORMATEX  waveFormat = {};
      waveFormat.wFormatTag = WAVE_FORMAT_PCM;
        waveFormat.nChannels =  channelCount;
      waveFormat.nSamplesPerSec =sampleRate;
     waveFormat.wBitsPerSample = 16; // or 32
      waveFormat.nBlockAlign = waveFormat.nChannels *(waveFormat.wBitsPerSample/8);
      waveFormat.nAvgBytesPerSec = waveFormat.nSamplesPerSec * waveFormat.nBlockAlign;


  
    REFERENCE_TIME bufferDurationInHundredNanoseconds = (REFERENCE_TIME)bufferLengthInMilliseconds * 10000; // Convert milliseconds to 100ns units
     hr =pAudioClient->Initialize(AUDCLNT_SHAREMODE_SHARED,0,bufferDurationInHundredNanoseconds,0,&waveFormat, NULL);
  hr=pAudioClient->GetBufferSize(&bufferSize);

  hr = pAudioClient->GetService(__uuidof(IAudioCaptureClient),(void**)&pCaptureClient);
      hr = pAudioClient->Start();



   //在循环中,从CaptureClient中读取音频数据并发送,  这里只做了简单演示

}

安全建议:

  • 在涉及操作系统音频配置的时候需要额外注意权限和操作,不当的配置可能会影响系统和其他音频应用的正常使用,记得处理异常以及提供适当的回滚机制
  • 在使用 WASAPI 的时候 需要格外注意资源的分配和释放,例如线程资源。并且捕获和渲染,是相对复杂的流程,需要认真阅读 Microsoft 文档,谨慎操作。

上述方法各有优缺点,开发者需要根据自身实际情况,选择最适合的方案,或者将几种方案进行组合,以期获得最佳效果。