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);
}