声卡数据采集

发布时间 2023-11-29 17:17:38作者: 阿风小子

Loopback 录制模式
在 loopback 模式下,WASAPI 的客户端可以捕获 rendering endpoint 设备(通常即声卡)正在播放的音频流。

客户端只能为共享模式流(AUDCLNT_SHAREMODE_SHARED)启用 loopback 模式。 独占模式(AUDCLNT_SHAREMODE_EXCLUSIVE)流不能在 loopback 模式下运行。

WASAPI 系统模块在软件中实现环回模式。在 loopback 模式下,WASAPI 将来自音频引擎的输出流复制到应用程序的捕获缓冲区中。

Windows 从 Vista 开始支持数字版权管理(DRM)。内容提供商依靠 DRM 来保护其专有音乐或其他内容免受未经授权的复制和其他非法使用。WASAPI 不允许 loopback 录制包含 DRM 保护内容的数字流。

无论音频源自哪个终端服务会话(session),WASAPI loopback 都包含正在播放的所有音频的混合。

Loopback 录制代码
以下是概要的 loopback 录制代码,省略类的具体实现和错误处理:

CWavFileHelper g_recWavFile;
void onAudioCaptured(BYTE* pData, DWORD len) 
{
    g_recWavFile.append((const char*)pData, len);
}

int _tmain(int argc, _TCHAR* argv[]) 
{
    HRESULT hr = E_FAIL;
    hr = CoInitialize(NULL);
    
    LoopackAudCap audCap;
    hr = audCap.init(onAudioCaptured);
    hr = g_recWavFile.create(argv[1], *audCap.getWavFormat());
    hr = audCap.start();
    
    _tprintf(_T("Started recording...press Enter to stop recording.\n"));   
    
    char ch = getchar(); // wait for keyboard input and then stop the recording
    hr = audCap.stop();
    
    audCap.finaize();
    g_recWavFile.close();
    CoUninitialize();
    return hr;
}


LoopackAudCap::init 函数
typedef void (*PFON_AUD_CAPTURED)(BYTE* pData, DWORD len);

HRESULT init(PFON_AUD_CAPTURED pCallback)
{
    HRESULT hr = E_FAIL;
    CComPtr<IMMDevice> pSpeaker = NULL;
    MMDeviceHelper device;
    WAVEFORMATEX *pwfx = NULL;
    
    m_pCallback = pCallback;
    m_hStartEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
    m_hStopEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
    
    hr = device.getDefaultSpeaker(&pSpeaker);
    hr = pSpeaker->Activate(__uuidof(IAudioClient), CLSCTX_ALL, NULL, (void**)&m_audioClient);
    hr = m_audioClient->GetMixFormat(&pwfx);
    hr = m_audioClient->Initialize(AUDCLNT_SHAREMODE_SHARED, AUDCLNT_STREAMFLAGS_LOOPBACK, RECORD_BUF_DURATION, 0, pwfx, NULL);
    if (hr == AUDCLNT_E_DEVICE_IN_USE) 
        DL_E0("The audio endpoint is in exclusive mode and can not be used now!");
    GOTO_LABEL_IF_FAILED(hr, OnErr);
    
    m_hThread = CreateThread(NULL, 0, _loopbackCapThread, this, 0, NULL);
    m_pWavFormat = pwfx;
    m_isDisposing = false;
    return S_OK;
OnErr:
    SAFE_CLOSE_HANDLE(m_hStartEvent);
    SAFE_CLOSE_HANDLE(m_hStopEvent);
    if (NULL != pwfx)
        CoTaskMemFree(pwfx);
    m_audioClient = NULL;
    return hr;
}


MMDeviceHelper::getDefaultSpeaker 函数
GetDefaultAudioEndpoint API 需要两个输入参数 dataFlow 和 role,用来指定要获取的 Audio Endpoint 设备。dataflow 包含两个选项 eRender 和 eCapture,role 包含三个选项 eConsole、eMultimedia 和 eCommunications,具体请参考 MSDN。

HRESULT getDefaultSpeaker(IMMDevice **ppMMDevice)
{
    HRESULT hr = S_OK;
    *ppMMDevice = NULL;

    CComPtr<IMMDeviceEnumerator> pMMDeviceEnumerator = NULL;
    hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL, CLSCTX_ALL, 
        __uuidof(IMMDeviceEnumerator), (void**)&pMMDeviceEnumerator);
    RETURN_IF_FAILED(hr);

    hr = pMMDeviceEnumerator->GetDefaultAudioEndpoint(eRender, eMultimedia, ppMMDevice);
    RETURN_IF_FAILED(hr);

    RETURN_IF_NULL_EX(*ppMMDevice, HRESULT_LAST_ERROR());
    return S_OK;
}


LoopackAudCap::_loopbackCap 函数
上面 _loopbackCapThread 线程函数调用该函数实现具体的声卡数据捕获功能。
MMCSS 的说明请看 MSDN。

HRESULT _loopbackCap()
{
    // register with MMCSS
    DWORD nTaskIndex = 0;
    HANDLE hTask = AvSetMmThreadCharacteristics(_T("Audio"), &nTaskIndex);
    
    HANDLE hWakeUp = CreateWaitableTimer(NULL, FALSE, NULL);
    
    UINT32 bufferFrameCount = 0;
    hr = m_audioClient->GetBufferSize(&bufferFrameCount);
    REFERENCE_TIME hnsActualDuration = (REFERENCE_TIME)
        ((double)RECORD_BUF_DURATION * bufferFrameCount / m_pWavFormat->nSamplesPerSec);
        
    LARGE_INTEGER liFirstFire;
    liFirstFire.QuadPart = -m_hnsDefaultDevicePeriod / 2; // negative means relative time
    LONG lTimeBetweenFires = (LONG)(hnsActualDuration / REFTIMES_PER_MILLISEC / 2);
    BOOL bOK = SetWaitableTimer(hWakeUp, &liFirstFire, lTimeBetweenFires, NULL, NULL, FALSE);
    
    DWORD dwWaitResult = WaitForSingleObject(m_hStartEvent, INFINITE);
    
    hr = m_audioClient->Start();
    
    HANDLE waitArray[] = { m_hStopEvent, hWakeUp };
    CComPtr<IAudioCaptureClient> pAudioCaptureClient = NULL;
    hr = m_audioClient->GetService(__uuidof(IAudioCaptureClient), (void**)&pAudioCaptureClient);
    
    while (true) {
        hr = _capture(pAudioCaptureClient);
        dwWaitResult = WaitForMultipleObjects(ARRAYSIZE(waitArray), waitArray, FALSE, INFINITE);
        if (m_isDisposing)
            break;
            
        if (WAIT_OBJECT_0 == dwWaitResult)
            dwWaitResult = WaitForSingleObject(m_hStartEvent, INFINITE);
    }
    return hr;
}


LoopackAudCap::_capture 函数
只要有声卡数据就榨干 (ˉ^ˉ),回调函数负责写入文件。

HRESULT _capture(IAudioCaptureClient* pAudioCaptureClient)
{
    HRESULT hr = E_FAIL;
    
    // drain data while it is available
    UINT32 nNextPacketSize = 0;
    for (hr = pAudioCaptureClient->GetNextPacketSize(&nNextPacketSize);
        SUCCEEDED(hr) && nNextPacketSize > 0;
        hr = pAudioCaptureClient->GetNextPacketSize(&nNextPacketSize))
    {
        BYTE *pData = NULL;
        UINT32 nNumFramesToRead = 0;
        DWORD dwFlags = 0;
        hr = pAudioCaptureClient->GetBuffer(&pData, &nNumFramesToRead, &dwFlags, NULL, NULL);
        RETURN_IF_FAILED(hr);
        
        LONG lBytesToWrite = nNumFramesToRead * m_pWavFormat->nBlockAlign;
        if ((dwFlags & AUDCLNT_BUFFERFLAGS_SILENT) == AUDCLNT_BUFFERFLAGS_SILENT)
            memset(pData, 0, lBytesToWrite);
            
        m_pCallback(pData, lBytesToWrite);
        
        hr = pAudioCaptureClient->ReleaseBuffer(nNumFramesToRead);
        RETURN_IF_FAILED(hr);
    }
    
    return hr;
}


LoopackAudCap::start & stop 函数
Loopback 录制是由单独线程执行的,所以外部调用方可以随时 start 或 stop 录制,且 stop 之后可以重新 start 录制。

HRESULT start()
{
    RETURN_IF_NULL(m_hStartEvent);
    RETURN_IF_NULL(m_hThread);
    SetEvent(m_hStartEvent);
    return S_OK;
}

HRESULT stop()
{
    RETURN_IF_NULL(m_hStopEvent);
    SetEvent(m_hStopEvent);
    return S_OK;
}

CWavFileHelper::close 函数
录制结束以后需要对 wav 文件头做收尾工作,即填充 wav 文件的内容长度。

void close()
{
    if (NULL != m_hFile) {
        if (m_isWrite) {
            MMRESULT mRes = mmioAscend(m_hFile, &m_chunkData, 0);
            PRINT_ERROR_LOG_IF_FALSE(mRes == MMSYSERR_NOERROR, mRes);

            mRes = mmioAscend(m_hFile, &m_chunkRIFF, 0);
            PRINT_ERROR_LOG_IF_FALSE(mRes == MMSYSERR_NOERROR, mRes);
        }

        mmioClose(m_hFile, 0);
        m_hFile = NULL;
    }

    SAFE_DELETE_ARRAY(m_data);
}