当前位置: 首页 > news >正文

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 需要两个输入参数 dataFlowrole,用来指定要获取的 Audio Endpoint 设备。dataflow 包含两个选项 eRendereCapturerole 包含三个选项 eConsoleeMultimediaeCommunications,具体请参考 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 下的音频框架关系图:

img

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.

这里有两个注意的:

  1. 单位为 audio frame
  2. 注意是采集 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 必须和 GetBufferIAudioCaptureClient::ReleaseBuffer 在同一线程中调用。

剩下就是音频数据的保存数据的处理。

写到最后

其中具体实现都是参考 OBS 的源码进行分析,obs 还包括采集到的声卡数据和麦克风的数据重采样、混音等操作动作。这篇文章也鉴介其他大佬们的博客,也有自己再开发中遇到的问题做了总结等。最后想说微软的开发手册才是最全的,当然都是英文资料。


Windows 音频环回录制

独钓寒江雪 发布于:2023-08-15 更新于:2024-04-14

所谓音频环回录制就是录制扬声器播放的声音。播放到扬声器的声音已经过混合,Windows 提供了 WASAPI 来获取这种混合的音频信号。本文将录制到原始音频样本数据写入到文件,可以使用 Audacity 导入原始数据试听。

img

在 Audacity 中导入原始数据时需要选择正确的音频参数,否则会导致无法播放。

img

初始化设备

声明相关变量:

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 的核心优势

  1. 低延迟与高质量:支持应用程序直接管理音频流,绕过传统音频处理层,实现低延迟、高质量的音频处理,提升性能与控制精度。
  2. 适配现代音频需求:原生支持多声道音频、高保真音频,可与 Windows 音频引擎深度集成,满足现代音频场景需求。
  3. 原生循环回环捕获:通过循环回环(Loopback)功能,可直接捕获渲染设备(扬声器、耳机)的输出音频流,无需物理回线或依赖系统混音器设置。

(二)核心实现步骤

  1. 设备枚举:调用 IMMDeviceEnumerator 接口,枚举系统中的音频渲染设备。
  2. 设备选择:从枚举结果中选定目标录音输出设备。
  3. 客户端激活:通过 IMMDevice::Activate 方法激活 IAudioClient 接口,指定 AUDCLNT_STREAMFLAGS_LOOPBACK 标志启用回环模式。
  4. 流初始化:配置音频格式(采样率、位深、通道数),完成音频流初始化。
  5. 捕获客户端获取:调用 IAudioClient::GetService 方法,获取 IAudioCaptureClient 接口。
  6. 数据捕获:调用 IAudioClient::Start 启动音频流,循环使用 IAudioCaptureClient::GetBufferIAudioCaptureClient::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


文章转载自:

http://qg4Fh03v.dzyxr.cn
http://4XpWI4Na.dzyxr.cn
http://m8Ec0NBN.dzyxr.cn
http://AjxrS5Gw.dzyxr.cn
http://nu8jVCvq.dzyxr.cn
http://GsMWItzr.dzyxr.cn
http://IOTmcLX8.dzyxr.cn
http://aIjpdcM3.dzyxr.cn
http://m2BF8aR4.dzyxr.cn
http://0G6LvK5p.dzyxr.cn
http://uWOv4WDA.dzyxr.cn
http://OvNxbKNc.dzyxr.cn
http://KHRAuRBd.dzyxr.cn
http://2v6tGQ9j.dzyxr.cn
http://PUFx2gL6.dzyxr.cn
http://mKp7Jq43.dzyxr.cn
http://ok1fl79W.dzyxr.cn
http://Ji5IzUkj.dzyxr.cn
http://jM0PPGpo.dzyxr.cn
http://91z4iBsU.dzyxr.cn
http://fuwTodvC.dzyxr.cn
http://yBKwWkri.dzyxr.cn
http://k8o6vE9f.dzyxr.cn
http://ZNlRo6B5.dzyxr.cn
http://4l59w6Hu.dzyxr.cn
http://JoixoFrn.dzyxr.cn
http://vXXcQXxc.dzyxr.cn
http://ug8kON9z.dzyxr.cn
http://XXyLCBBw.dzyxr.cn
http://VPwt3qEh.dzyxr.cn
http://www.dtcms.com/a/370016.html

相关文章:

  • uniapp新增页面及跳转配置方法
  • 西门子S7-200 SMART PLC:编写最基础的“起保停”程序
  • UDP-Server(2)词典功能
  • 最大似然估计:损失函数的底层数学原理
  • 今日分享:C++ -- list 容器
  • 报错:OverflowError: Python integer 4294967296 out of bounds for uint32
  • 贪心算法应用:蛋白质折叠问题详解
  • AI-调查研究-71-具身智能 案例分析:从ROS到Tesla Optimus的开源与商业化实践
  • 【嵌入式C语言】七
  • [数据结构] LinkedList
  • 【C++】引用的本质与高效应用
  • Date、BigDecimal类型值转换
  • 基于Node.js和Three.js的3D模型网页预览器
  • Scikit-learn Python机器学习 - 特征降维 压缩数据 - 特征提取 - 主成分分析 (PCA)
  • CSP-J/S IS COMING
  • GraphQL API 性能优化实战:在线编程作业平台指南
  • 【基础-判断】Background状态在UIAbility实例销毁时触发,可以在onDestroy()回调中进行系统资源的释放、数据的保存等操作。
  • PageHelper的使用及底层原理
  • 探寻卓越:高级RAG技术、架构与实践深度解析
  • 【51单片机】【protues仿真】基于51单片机PM2.5空气质量检测系统
  • AI工具深度测评与选型指南 - 图像生成与编辑类
  • RabbitMQ工作模式(下)
  • Custom SRP - Complex Maps
  • tp报错解决
  • MySQL MHA 高可用集群搭建
  • 《AI大模型应知应会100篇》第68篇:移动应用中的大模型功能开发 —— 用 React Native 打造你的语音笔记摘要 App
  • Mac Intel 芯片 Docker 一键部署 Neo4j 最新版本教程
  • 正态分布 - 正态分布的经验法则(68-95-99.7 法则)
  • 【操作系统-Day 25】死锁 (Deadlock):揭秘多线程编程的“终极杀手”
  • (二).net面试(static)