Windows 设备音频录制 | WASAPI 音频数据采集 / 环回录制
注:本文为 “Windows 设备音频录制” 相关合辑。
中文引文,略作重排。
如有内容异常,请看原文。
声卡数据采集
posted @ 2023-11-29 17:13 阿风小子
在 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 recordinghr = 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 MMCSSDWORD 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 timeLONG 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 availableUINT32 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);
}
Windows 平台下使用 WASAPI 进行音频数据采集
原创于 2020-05-29 14:06:51 发布
在 Windows 平台开发音视频的时候,常常需要对麦克风和扬声器的数据进行音频采集,这里简单记录一下大概流程和在实际过程中遇到的一些坑,如有表述错误地方请各位大佬在评论区指正。
MMDevice API 获取设备
The Windows Multimedia Device (MMDevice) API enables audio clients to discover audio endpoint devices, determine their capabilities, and create driver instances for those devices. The header file Mmdeviceapi.h
defines the interfaces in the MMDevice API.
音频 client 利用 MMDevice API 来 发现 audio endpoint devices,为 devices 创建驱动实例 等。头文件:
#include <MMDeviceAPI.h>
1.1 创建 IMMDeviceEnumerator interface
ComPtr<IMMDeviceEnumerator> enumerator;HRESULT res;res = CoCreateInstance(__uuidof(MMDeviceEnumerator),nullptr, CLSCTX_ALL,__uuidof(IMMDeviceEnumerator),(void**)enumerator.Assign());if (FAILED(res))throw HRError("Failed to create enumerator", res);
1.2 获取默认的设备 GetDefaultAudioEndpoint
微软 msdn 地址 https://msdn.microsoft.com/en-us/library/windows/desktop/dd371401(v=vs.85).aspx
ComPtr<IMMDevice> device;ComPtr<IAudioClient> client;ComPtr<IAudioCaptureClient> capture; // 采集音频数据对象ComPtr<IAudioRenderClient> render; // 声音渲染对象HRESULT res;if (isDefaultDevice) {res = enumerator->GetDefaultAudioEndpoint(isInputDevice ? eCapture : eRender,//isInputDevice ? eCommunications : eConsole,isInputDevice ? eMultimedia : eConsole,device.Assign());} else {wchar_t *w_id;os_utf8_to_wcs_ptr(device_id.c_str(), device_id.size(), &w_id);res = enumerator->GetDevice(w_id, device.Assign());bfree(w_id);}
其中 eCapture
表示麦克风,eRender
表示扬声器;在 GetDefaultAudioEndpoint 中第二个参数在 msdn 上也有说明,但是在 win7/8 里面如果麦克风设置成 eCommunications 角色,在你进行采集数据的时候系统会认为你正在通讯,所以会把音量降低 80%,这是一个很操蛋的角色,但是如果实际场景需要还是试着用这个角色来看效果。其他角色 MSDN 上都有具体说明。在多设备的时候也可以自行选择哪个设备。
1.3 获取设备名称
string device_name;ComPtr<IPropertyStore> store;HRESULT res;if (SUCCEEDED(device->OpenPropertyStore(STGM_READ, store.Assign()))) {PROPVARIANT nameVar;PropVariantInit(&nameVar);res = store->GetValue(PKEY_Device_FriendlyName, &nameVar);if (SUCCEEDED(res) && nameVar.pwszVal && *nameVar.pwszVal) {size_t len = wcslen(nameVar.pwszVal);size_t size;size = os_wcs_to_utf8(nameVar.pwszVal, len,nullptr, 0) + 1;device_name.resize(size);os_wcs_to_utf8(nameVar.pwszVal, len, &device_name[0], size);}}
WASAPI 进行音频数据采集
The Windows Audio Session API (WASAPI) enables client applications to manage the flow of audio data between the application and an audio endpoint device. The header files Audioclient.h
and Audiopolicy.h
define the WASAPI interfaces.
头文件:
#include <AudioClient.h>
程序可通过 audio engine,以共享模式访问 audio endpoint device(比如麦克风 或 Speakers)。audio engine 在 endpoint buffer 和 endpoint device 之间传输数据。当播放音频数据时,程序向 rendering endpoint buffer 周期性写入数据。当采集音频数据时,程序从 capture endpoint buffer 周期性读取数据。
使用 WASAPI 的几个重要函数:
1. IMMDevice::Activate
IMMDevice::Activate
来获取 an audio endpoint device 的 IAudioClient interface 引用。
1)先获取一个 device,比如麦克风设备
2)调用 Activate 激活该麦克风的音频采集接口
device->Activate(__uuidof(IAudioClient), CLSCTX_ALL,nullptr, (void**)client.Assign());
2. IAudioClient::Initialize
IAudioClient::Initialize
用来在 endpoint device 初始化流。通用格式:
CoTaskMemPtr<WAVEFORMATEX> wfex;HRESULT res;DWORD flags = AUDCLNT_STREAMFLAGS_EVENTCALLBACK;res = client->GetMixFormat(&wfex);if (FAILED(res))throw HRError("Failed to get mix format", res);InitFormat(wfex);if (!isInputDevice)flags |= AUDCLNT_STREAMFLAGS_LOOPBACK;res = client->Initialize(AUDCLNT_SHAREMODE_SHARED, flags,BUFFER_TIME_100NS, 0, wfex, nullptr);
我们先来看看 Windows 下的音频框架关系图:
在 Render 设备进行 Initialize 第一个参数是分为独占模式和共享模式,如上图可知 Exclusive Mode 直接和音频驱动直连,而 Shared Mode 需要一个 Audio Engine 这样做的好处是可以把好多应用的声音采集进行 Mix,这样你就可以采集到多处声音。当然在 Mix 会做重采样动作,在高采样率转低采样的时候会有精度的丢失。
在 initialize 中的第三个参数是 100 ns(nanosecond)为单位,纳秒:时间单位。1 秒 = 1000 毫秒;1 毫秒 = 1000 微秒;1 微秒 = 1000 纳秒。
其中程序设置的 BUFFER_TIME_100NS = (5 * 10000000)
其中:
AUDCLNT_STREAMFLAGS_LOOPBACK
表示音频 engine 会将 rending 设备正在播放的音频流,拷贝一份到音频的 endpoint buffer,这样的话,WASAPI client 可以采集到 the stream.如果 AUDCLNT_STREAMFLAGS_LOOPBACK
被设置,IAudioClient::Initialize
会尝试在 rending 设备开辟一块 capture buffer.AUDCLNT_STREAMFLAGS_LOOPBACK
只对 rending
设备有效,Initialize 仅在 AUDCLNT_SHAREMODE_SHARED
时才可以使用,否则 Initialize 会失败。
AUDCLNT_STREAMFLAGS_EVENTCALLBACK
表示当 audio buffer 数据就绪时,会给系统发个信号,也就是事件触发。
在 wsapi 中采集到的 PCM 数据总是 float
obs 在采集声卡声音对 render 对象做了一次初始化:
CoTaskMemPtr<WAVEFORMATEX> wfex;HRESULT res;LPBYTE buffer;UINT32 frames;ComPtr<IAudioClient> client;res = device->Activate(__uuidof(IAudioClient), CLSCTX_ALL,nullptr, (void**)client.Assign());if (FAILED(res))throw HRError("Failed to activate client context", res);res = client->GetMixFormat(&wfex);if (FAILED(res))throw HRError("Failed to get mix format", res);res = client->Initialize(AUDCLNT_SHAREMODE_SHARED, 0,BUFFER_TIME_100NS, 0, wfex, nullptr);if (FAILED(res))throw HRError("Failed to get initialize audio client", res);/* Silent loopback fix. Prevents audio stream from stopping and *//* messing up timestamps and other weird glitches during silence *//* by playing a silent sample all over again. */res = client->GetBufferSize(&frames);if (FAILED(res))throw HRError("Failed to get buffer size", res);res = client->GetService(__uuidof(IAudioRenderClient),(void**)render.Assign());if (FAILED(res))throw HRError("Failed to get render client", res);res = render->GetBuffer(frames, &buffer);if (FAILED(res))throw HRError("Failed to get buffer", res);memset(buffer, 0, frames*wfex->nBlockAlign);render->ReleaseBuffer(frames, 0);
3. IAudioClient::GetService
初始化流之后,可调用 IAudioClient::GetService
来获取其它 WASAPI interfaces 的引用。
HRESULT res = client->GetService(__uuidof(IAudioCaptureClient),(void**)capture.Assign());if (FAILED(res))throw HRError("Failed to create capture context", res);res = client->SetEventHandle(receiveSignal); // 设置信号if (FAILED(res))throw HRError("Failed to set event handle", res);captureThread = CreateThread(nullptr, 0,WASAPISource::CaptureThread, this,0, nullptr);if (!captureThread.Valid())throw "Failed to create capture thread";client->Start();active = true;
client->SetEventHandle(receiveSignal)
用于 client 通知有音频数据,因为在 client 初始化的时候设置了 AUDCLNT_STREAMFLAGS_EVENTCALLBACK
4. IAudioClient::Start
Start 之后就开始使用采集对象来进行接受数据,设置一个接受数据的线程:
CreateThread(nullptr, 0,WASAPISource::CaptureThread, this,0, nullptr);
5. IAudioCaptureClient::GetNextPacketSize
官方解释
The GetNextPacketSize
method retrieves the number of frames in the next data packet in the capture endpoint buffer.
这里有两个注意的:
- 单位为 audio frame。
- 注意是采集 buffer(capture endpoint buffer)
仅在共享模式下生效,独占模式下无效。在调用 GetBuffer
之前,可调用 GetNextPacketSize
来获取下一个数据包的音频帧个数。
6. IAudioCaptureClient::GetBuffer
最重要的函数。用于获取 capture endpoint buffer 中下一个数据包的指针。
HRESULT GetBuffer([out] BYTE **ppData,[out] UINT32 *pNumFramesToRead,[out] DWORD *pdwFlags,[out] UINT64 *pu64DevicePosition,[out] UINT64 *pu64QPCPosition);
使用方法:
HRESULT res;LPBYTE buffer;UINT32 frames;DWORD flags;UINT64 pos, ts;UINT captureSize = 0;while (true) {res = capture->GetNextPacketSize(&captureSize);if (FAILED(res)) {if (res != AUDCLNT_E_DEVICE_INVALIDATED)blog(LOG_WARNING,"[WASAPISource::GetCaptureData]"" capture->GetNextPacketSize"" failed: %lX", res);return false;}if (!captureSize)break;res = capture->GetBuffer(&buffer, &frames, &flags, &pos, &ts);if (FAILED(res)) {if (res != AUDCLNT_E_DEVICE_INVALIDATED)blog(LOG_WARNING,"[WASAPISource::GetCaptureData]"" capture->GetBuffer"" failed: %lX", res);return false;}obs_source_audio data = {};data.data[0] = (const uint8_t*)buffer;data.frames = (uint32_t)frames;data.speakers = speakers;data.samples_per_sec = sampleRate;data.format = format;data.timestamp = useDeviceTiming ? ts*100 : os_gettime_ns();if (!useDeviceTiming)data.timestamp -= (uint64_t)frames * 1000000000ULL /(uint64_t)sampleRate;obs_source_output_audio(source, &data);capture->ReleaseBuffer(frames);}return true;
这个方法的最后一个参数可以作为音频数据的时间戳。
GetNextPacketSize
必须和 GetBuffer
及 IAudioCaptureClient::ReleaseBuffer
在同一线程中调用。
剩下就是音频数据的保存数据的处理。
写到最后
其中具体实现都是参考 OBS 的源码进行分析,obs 还包括采集到的声卡数据和麦克风的数据重采样、混音等操作动作。这篇文章也鉴介其他大佬们的博客,也有自己再开发中遇到的问题做了总结等。最后想说微软的开发手册才是最全的,当然都是英文资料。
Windows 音频环回录制
独钓寒江雪 发布于:2023-08-15 更新于:2024-04-14
所谓音频环回录制就是录制扬声器播放的声音。播放到扬声器的声音已经过混合,Windows 提供了 WASAPI 来获取这种混合的音频信号。本文将录制到原始音频样本数据写入到文件,可以使用 Audacity 导入原始数据试听。
在 Audacity 中导入原始数据时需要选择正确的音频参数,否则会导致无法播放。
初始化设备
声明相关变量:
IMMDeviceEnumerator* pDeviceEnum = NULL;
IMMDevice* pDevice = NULL;
IAudioClient* pAudioClient = NULL;
WAVEFORMATEX* pWaveFormat = NULL;
IAudioCaptureClient* pAudioCaptureClient = NULL;
获取默认音频输出设备并初始化环回录制服务:
HRESULT InitRecord() {HRESULT hr;hr = CoCreateInstance(CLSID_MMDeviceEnumerator, NULL, CLSCTX_ALL, IID_IMMDeviceEnumerator, (void**)&pDeviceEnum);if (FAILED(hr)) {printf("Create device enumerator failed, hr: 0x%x", hr);return hr;}hr = pDeviceEnum->GetDefaultAudioEndpoint(eRender, eConsole, &pDevice);if (FAILED(hr)) {printf("Get default audio device failed, hr: 0x%x", hr);return hr;}hr = pDevice->Activate(IID_IAudioClient, CLSCTX_ALL, NULL, (void**)&pAudioClient);if (FAILED(hr)) {printf("Create audio client failed, hr: 0x%x", hr);return hr;}hr = pAudioClient->GetMixFormat(&pWaveFormat);if (FAILED(hr)) {printf("Get mix format failed, hr: 0x%x", hr);return hr;}printf("Channel: %d, SamplesPerSec: %d, BitsPerSample: %d\n", pWaveFormat->nChannels, pWaveFormat->nSamplesPerSec, pWaveFormat->wBitsPerSample);hr = pAudioClient->Initialize(AUDCLNT_SHAREMODE_SHARED, AUDCLNT_STREAMFLAGS_LOOPBACK, BUFFER_TIME_100NS, 0, pWaveFormat, NULL);if (FAILED(hr)) {// 兼容 Nahimic 音频驱动// https://github.com/rainmeter/rainmeter/commit/0a3dfa35357270512ec4a3c722674b67bff541d6 // https://social.msdn.microsoft.com/Forums/windowsdesktop/en-US/bd8cd9f2-974f-4a9f-8e9c-e83001819942/iaudioclient-initialize-failure // 初始化失败,尝试使用立体声格式进行初始化pWaveFormat->nChannels = 2;pWaveFormat->nBlockAlign = (2 * pWaveFormat->wBitsPerSample) / 8;pWaveFormat->nAvgBytesPerSec = pWaveFormat->nSamplesPerSec * pWaveFormat->nBlockAlign;hr = pAudioClient->Initialize(AUDCLNT_SHAREMODE_SHARED, AUDCLNT_STREAMFLAGS_LOOPBACK, BUFFER_TIME_100NS, 0, pWaveFormat, NULL);if (FAILED(hr)) {printf("Initialize audio client failed, hr: 0x%x", hr);return hr;}}hr = pAudioClient->GetService(IID_IAudioCaptureClient, (void**)&pAudioCaptureClient);if (FAILED(hr)) {printf("Get audio capture client failed, hr: 0x%x", hr);return hr;}return S_OK;
}
采样
在初始化成功后,开启独立线程按固定间隔获取缓冲区中的音频样本。exitFlag
用于控制线程是否退出。
// 线程处理函数
void QueryAudioSampleThread() {UINT32 bufferFrameCount = 0;HRESULT hr = pAudioClient->GetBufferSize(&bufferFrameCount);if (FAILED(hr)) {printf("Get buffer frame count failed, hr: 0x%x", hr);return;}// 根据实际缓冲区中的样本数计算实际填满缓冲区需要的时间REFERENCE_TIME hnsActualDuration = (double)BUFFER_TIME_100NS *bufferFrameCount / pWaveFormat->nSamplesPerSec;UINT32 packetLength = 0;BYTE* buffer = NULL;UINT32 numFramesAvailable = 0;DWORD flags = 0;while (!exitFlag.load()){// 等待半个缓冲周期Sleep(hnsActualDuration / 10000 / 2);hr = pAudioCaptureClient->GetNextPacketSize(&packetLength);if (FAILED(hr)) {printf("Get next package size failed, hr: 0x%x", hr);break;}while (packetLength > 0){hr = pAudioCaptureClient->GetBuffer(&buffer, &numFramesAvailable, &flags, NULL, NULL);if (FAILED(hr)) {printf("Get capture buffer failed, hr: 0x%x", hr);break;}// 将捕获到的样本写入文件if (!WriteSample(buffer, numFramesAvailable * pWaveFormat->nChannels * pWaveFormat->wBitsPerSample / 8)) {printf("Write sample to file failed");}hr = pAudioCaptureClient->ReleaseBuffer(numFramesAvailable);if (FAILED(hr)) {printf("Release capture buffer failed, hr: 0x%x", hr);break;}hr = pAudioCaptureClient->GetNextPacketSize(&packetLength);if (FAILED(hr)) {printf("Get next package size failed, hr: 0x%x", hr);break;}}}
}
Sample 和 Frame 的含义
pWaveFormat->nSamplesPerSec
表示每秒采样的次数,如 48000 的采样率就是每秒采 48000 个 Sample,一个 Sample 是一个声道的一个采样。而 Frame 则是一个时间点的 Sample 集合,举例来说,一个线性的 PCM 双声道音频文件每个 Frame 有 2 个 Sample,一个左声道 Sample,和一个右声道 Sample。
释放设备和内存
在录制结束后释放设备和内存:
void UnInitRecord() {if (pWaveFormat) {CoTaskMemFree(pWaveFormat);pWaveFormat = NULL;}SAFE_RELEASE(pDeviceEnum);SAFE_RELEASE(pDevice);SAFE_RELEASE(pAudioClient);SAFE_RELEASE(pAudioCaptureClient);
}
完整示例代码见:AudioLoopbackRecord.cpp
WASAPI 实现环回录制
Matsuko 2024 - 07 - 07
背景——之前想做一个与音频处理相关的程序,就了解了一下 WASAPI,然后就发现了环回录制这个有趣的功能。环回录制是一种强大的音频捕获技术,允许开发者直接从系统音频输出中录制声音,而无需使用物理麦克风。
环回录制简介
环回录制(Loopback Recording)允许捕获计算机正在播放的任何音频,包括系统声音、应用程序音频等。这项技术可以用在屏幕录制软件、音频分析工具、游戏录制等功能。
WASAPI 简介
在深入探讨实现细节之前,我们还需要了解一下 WASAPI(Windows Audio Session API)。WASAPI 是 Windows Vista 及以后版本中引入的低延迟音频 API,它是 Windows 核心音频 API 的一部分。
WASAPI 相比之前的其他音频相关的 API 有以下优势:
- 低延迟:相比早期的音频 API,WASAPI 能够提供更低的音频延迟。
- 更好的音频质量:支持高采样率和位深度。
- 直接硬件访问:在独占模式下,WASAPI 可以直接访问音频硬件。
- 环回录制支持:允许捕获系统音频输出。
具体步骤
原理
WASAPI 环回录制主要是创建了一个虚拟的录音端点,这个端点连接到系统的音频渲染流程。当系统播放声音时,这些音频数据会被复制到这个虚拟录音端点,应用程序可以从这个虚拟端点读取音频数据,就像从普通麦克风录音一样。
1. 初始化 COM 库
#include <windows.h>
#include <mmdeviceapi.h>
#include <audioclient.h>
#include <audiopolicy.h>
#include <functiondiscoverykeys_devpkey.h>// …
CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);// …
CoUninitialize();
2. 获取音频端点设备
这里获取的是默认的音频端点设备(扬声器)。
IMMDeviceEnumerator* pEnumerator = NULL;
IMMDevice* pDevice = NULL;HRESULT hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL, CLSCTX_ALL,__uuidof(IMMDeviceEnumerator), (void**)&pEnumerator);if (SUCCEEDED(hr)) {hr = pEnumerator->GetDefaultAudioEndpoint(eRender, eConsole, &pDevice);
}
3. 激活音频客户端
IAudioClient* pAudioClient = NULL;if (SUCCEEDED(hr)) {// 激活 WASAPI 音频客户端hr = pDevice->Activate(__uuidof(IAudioClient), CLSCTX_ALL,NULL, (void**)&pAudioClient);
}
4. 配置音频流格式
// 配置 WASAPI 音频流,启用环回
hr = pAudioClient->Initialize(AUDCLNT_SHAREMODE_SHARED,AUDCLNT_STREAMFLAGS_LOOPBACK,0, 0, pwfx, NULL);
WASAPI 提供两种操作模式:独占模式(AUDCLNT_SHAREMODE_EXCLUSIVE
) 和 共享模式(AUDCLNT_SHAREMODE_SHARED
)。对于环回录制,我们必须使用共享模式。
5. 获取捕获客户端
创建一个捕获客户端来读取音频数据。
IAudioCaptureClient* pCaptureClient = NULL;if (SUCCEEDED(hr)) {hr = pAudioClient->GetService(__uuidof(IAudioCaptureClient),(void**)&pCaptureClient);
}
6. 开始录制
做好上面的准备工作就可以启动音频客户端并开始捕获循环啦。这里录制是在一个循环中进行的,所以我们可以单独创建一个线程用来处理捕获循环。
注意
- 错误处理:应该为每个
HRESULT
检查添加适当的错误处理(主要是清理资源并退出)。 - 清理资源:完成录制后,需要停止客户端并释放资源。
CoTaskMemFree(pwfx);
SAFE_RELEASE(pEnumerator)
SAFE_RELEASE(pDevice)
SAFE_RELEASE(pAudioClient)
SAFE_RELEASE(pCaptureClient)
结论
使用 Win32 API 和 WASAPI 实现环回录制为开发者提供了一种强大而灵活的方式来捕获系统音频。WASAPI 不仅允许我们实现环回录制,还提供了低延迟、高质量的音频处理能力,以及更细粒度的控制选项。在实际应用中,WASAPI 的使用需要考虑更多细节,如音频会话管理、格式协商和性能优化。
虽然已经学习到了这些内容,但是在实际使用中还是会遇到各种各样的问题。
Windows 设备音频录制
针对 Windows 平台扬声器音频录制需求,DirectSound 因生命周期与功能限制已不适用,第三方库和开源项目则存在功能局限或集成成本问题。
WASAPI 及其环回捕获功能是微软官方推荐的标准解决方案,能高效、稳定地实现高质量系统输出音频录制,符合微软技术规范与最佳实践。
一、DirectSound 的适用局限性
DirectSound 作为 DirectX API 集合的组件,曾为音频播放与录制提供低延迟接口,是早期开发者的常用选择,但在现代应用场景中存在显著局限。
(一)API 生命周期受限
随着 Windows 系统迭代,DirectSound 逐步被现代 API 替代。自 DirectX 10 起,DirectSound 的支持力度减弱,其功能被整合至 DirectShow 等上层 API 中,导致在新版本 DirectX SDK 中获取其开发库及示例的难度增加。
(二)录音功能定位偏差
DirectSound 的核心设计目标是捕获麦克风等输入设备的音频,对“扬声器录音”或“系统混音”无原生支持。实现此类功能需依赖操作系统混音器设置,不仅程序层面难以直接控制,在现代 Windows 版本中还可能出现功能失效或 capture buffer
初始化失败的问题。
二、第三方库的应用边界
当原生 API 无法满足需求时,第三方库可作为替代方案,但需关注其应用局限性。
(一)BASS.DLL 的功能定位
BASS.DLL 是主流音频库,提供简洁 API 支持音频播放、录制与处理,可高效实现输入设备录音。但其设计重心为直接访问音频设备,对操作系统级“扬声器回环录音”无原生支持,需依赖特定机制实现。
(二)开源项目的集成成本
Audacity 等开源音频软件已验证扬声器录音的技术可行性,但此类项目通常依赖多类第三方库,且需复杂编译环境。将其源代码集成至自有项目,面临高开发成本与陡峭学习曲线,不适用于快速开发或轻量级应用场景。
三、WASAPI 与循环回环捕获
Windows Vista 及更高版本引入的 Windows Audio Session API(WASAPI),是官方推荐的系统输出音频录制解决方案。
(一)WASAPI 的核心优势
- 低延迟与高质量:支持应用程序直接管理音频流,绕过传统音频处理层,实现低延迟、高质量的音频处理,提升性能与控制精度。
- 适配现代音频需求:原生支持多声道音频、高保真音频,可与 Windows 音频引擎深度集成,满足现代音频场景需求。
- 原生循环回环捕获:通过循环回环(Loopback)功能,可直接捕获渲染设备(扬声器、耳机)的输出音频流,无需物理回线或依赖系统混音器设置。
(二)核心实现步骤
- 设备枚举:调用
IMMDeviceEnumerator
接口,枚举系统中的音频渲染设备。 - 设备选择:从枚举结果中选定目标录音输出设备。
- 客户端激活:通过
IMMDevice::Activate
方法激活IAudioClient
接口,指定AUDCLNT_STREAMFLAGS_LOOPBACK
标志启用回环模式。 - 流初始化:配置音频格式(采样率、位深、通道数),完成音频流初始化。
- 捕获客户端获取:调用
IAudioClient::GetService
方法,获取IAudioCaptureClient
接口。 - 数据捕获:调用
IAudioClient::Start
启动音频流,循环使用IAudioCaptureClient::GetBuffer
与IAudioCaptureClient::ReleaseBuffer
读取音频数据。
via:
-
声卡数据采集 - 阿风小子 - 博客园
https://www.cnblogs.com/kn-zheng/p/17865367.html -
Windows 平台下使用 WASAPI 进行音频数据采集_window wasapi-CSDN 博客
https://blog.csdn.net/qq_29509035/article/details/106406994 -
Windows 音频环回录制 - 独钓寒江雪
https://jiangxueqiao.com/post/2236960039.html -
WASAPI 实现环回录制环回录制(Loopback Recording)允许捕获计算机正在播放的任何音频,包括系统声音、 - 掘金
https://juejin.cn/post/7388441764875583507 -
关于 WASAPI - Win32 apps | Microsoft Learn
https://learn.microsoft.com/zh-cn/windows/win32/coreaudio/wasapi?redirectedfrom=MSDN -
音频终结点设备 - Win32 apps | Microsoft Learn
https://learn.microsoft.com/zh-cn/windows/win32/coreaudio/audio-endpoint-devices?redirectedfrom=MSDN -
环回录制 - Win32 apps | Microsoft Learn
https://learn.microsoft.com/zh-cn/windows/win32/coreaudio/loopback-recording?redirectedfrom=MSDN