音频采集 via Waveform API
Waveform API 采集音频
Waveform API 从 Windows 3.0 时代就登上历史舞台了,至今依然可以运行在最新的 OS 上,不得不佩服 Windows 的兼容性。不过新的 waveform API 是基于 Core Audio 架构之上的,其实就是一个 wrapper。
Waveform API 的优势就是非常简单,兼容性好,不依赖于任何框架,虽然通常我们都使用它录制成 Wav 格式的文件(缺点是非常占用空间),但实际上也可以和其他框架(如 FFmpeg,DShow,MF 等)配合使用编码成压缩的音频格式(如 mp3,aac 等)。
采集流程
采集代码
以下是整个 Waveform API 采集过程的概要代码,略去各个函数的具体实现和错误处理:
m_wavInHelper.enumDevices(this, _fillDevices);
m_wavInHelper.selectDevice(nSel);
hr = m_wavInHelper.getSupportedFormats(supportedFormats);
if (SUCCEEDED(hr)) { // add supported formats to a combobox
if (WAVE_FORMAT_1M08 == (supportedFormats & WAVE_FORMAT_1M08))
pFormats->SetItemData(pFormats->AddString(_T("11.025 kHz, mono, 8-bit")), WAVE_FORMAT_1M08);
… …
}
m_wavInHelper.selectDevice(nSel);
m_wavInHelper.openDevice(samplesPerSec, channels, bitsPerSample);
m_wavInHelper.startRecording(szRecFile);
… … // capturing
m_wavInHelper.stopRecording();
m_wavInHelper.closeDevice();
CWaveInHelper::enumDevices 函数
枚举到的每一个设备经由回调函数返回设备名称,然后填充到界面上的列表控件中(下图中的 Input ComboBox),每个设备支持的格式也可以通过 API waveInGetDevCaps 枚举出来。
HRESULT CWaveInHelper::enumDevices(void* pContext, PFENUM_DEV_CALLBACK pCallback)
{
RETURN_IF_NULL(pCallback);
UINT nDevices = ::waveInGetNumDevs();
for (UINT i = 0; i < nDevices; ++i) {
WAVEINCAPS devCaps = { 0 };
MMRESULT mRes = waveInGetDevCaps(i, &devCaps, sizeof(WAVEINCAPS));
if (mRes == MMSYSERR_NOERROR)
pCallback(pContext, devCaps.szPname);
}
return S_OK;
}
HRESULT CWaveInHelper::getSupportedFormats(DWORD& supportFormats)
{
WAVEINCAPS devCaps = { 0 };
ZeroMemory(&devCaps, sizeof(WAVEINCAPS));
MMRESULT mRes = ::waveInGetDevCaps(m_deviceID, &devCaps, sizeof(WAVEINCAPS));
RETURN_IF_FALSE_EX(mRes == MMSYSERR_NOERROR, HRESULT_FROM_WIN32(mRes));
supportFormats = devCaps.dwFormats;
return S_OK;
}
CWaveInHelper::openDevice 函数
waveInOpen 指定了回调方式,即在某个时间片的音频数据被录完后,Windows 将通过这个回调来激活对这些数据的处理过程,支持的类型包括 EVENT, FUNCTION, THREAD 和 WINDOW,下面使用的是 FUNCTION 回调,waveInOpen 的第四个参数 _waveInProc 即回调函数。
HRESULT CWaveInHelper::openDevice(DWORD samplesPerSec, DWORD channels, DWORD bitsPerSample)
{
if (NULL != m_hWaveIn)
closeDevice();
m_wavFormat.cbSize = sizeof(WAVEFORMATEX);
m_wavFormat.nSamplesPerSec = samplesPerSec;
m_wavFormat.nChannels = (WORD)channels;
m_wavFormat.wBitsPerSample = (WORD)bitsPerSample;
m_wavFormat.wFormatTag = WAVE_FORMAT_PCM;
m_wavFormat.nBlockAlign = m_wavFormat.nChannels * m_wavFormat.wBitsPerSample / 8;
m_wavFormat.nAvgBytesPerSec = m_wavFormat.nSamplesPerSec * m_wavFormat.nBlockAlign;
MMRESULT mRes = ::waveInOpen(&m_hWaveIn, m_deviceID, &m_wavFormat,
(DWORD_PTR)_waveInProc, (DWORD_PTR)this, CALLBACK_FUNCTION);
RETURN_IF_FALSE_EX(mRes == MMSYSERR_NOERROR, HRESULT_FROM_WIN32(mRes));
return S_OK;
}
CWaveInHelper::startRecording 函数
waveInPrepareHeader 和 waveInAddBuffer 用来添加接收音频数据的缓存,一般至少需要准备两块缓存。因为录音不能间断,当一块填满时没有时间等待你去送入下一块缓存,所以必须提前就准备好。
HRESULT CWaveInHelper::startRecording(LPCTSTR recFilePath)
{
HRESULT hr = E_FAIL;
MMRESULT mRes = 0;
hr = m_wavFile.create(recFilePath, m_wavFormat);
RETURN_IF_FAILED(hr);
hr = _prepareBuffers();
GOTO_LABEL_IF_FAILED(hr, OnErr);
mRes = ::waveInStart(m_hWaveIn);
GOTO_LABEL_IF_FALSE_EX(mRes == MMSYSERR_NOERROR, HRESULT_FROM_WIN32(mRes), OnErr);
m_recording = true;
return S_OK;
OnErr:
_unPrepareBuffers();
return hr;
}
CWaveInHelper::_waveInProc 回调函数
当送入的缓存被录满后,Windows 就会通过指定的方式进行回调,在回调中我们把语音数据写到文件中,然后将下一个缓存添加进去。考虑到这个处理要一定时间,所以要预先加入若干个缓存。
注意:不要在回调函数中调用各种系统 API,容易造成死锁,具体请参考 msdn。
Applications should not call any system-defined functions from inside a callback function, except for EnterCriticalSection, LeaveCriticalSection, midiOutLongMsg, midiOutShortMsg, OutputDebugString, PostMessage, PostThreadMessage, SetEvent, timeGetSystemTime, timeGetTime, timeKillEvent, and timeSetEvent. Calling other wave functions will cause deadlock.
void CALLBACK CWaveInHelper::_waveInProc(HWAVEIN hwi, UINT uMsg, DWORD dwInstance, DWORD dwParam1, DWORD dwParam2)
{
switch(uMsg) {
case WIM_CLOSE:
break;
case WIM_DATA:
CWaveInHelper*pThis = (CWaveInHelper*)dwInstance;
pThis->_processInput((WAVEHDR*)dwParam1);
break;
case WIM_OPEN:
break;
default:
break;
}
}
HRESULT CWaveInHelper::_processInput( WAVEHDR* pHdr )
{
if (WHDR_DONE == (WHDR_DONE & pHdr->dwFlags)) {
m_wavFile.append(pHdr->lpData, pHdr->dwBytesRecorded);
if (m_recording) {
MMRESULT mRes = waveInAddBuffer(m_hWaveIn, pHdr, sizeof(WAVEHDR));
RETURN_IF_FALSE_EX(mRes == MMSYSERR_NOERROR, HRESULT_FROM_WIN32(mRes));
}
}
}
CWaveInHelper::stopRecording 函数
waveInReset 可以清掉尚在等待录音的缓冲区,并通过回调函数返回给程序,注意在停止录音后不要在回调函数中再添加缓存,否则会造成死锁。
HRESULT CWaveInHelper::stopRecording()
{
HRESULT hr = E_FAIL;
MMRESULT mRes = 0;
m_recording = false;
mRes = waveInStop(m_hWaveIn);
RETURN_IF_FALSE_EX(mRes == MMSYSERR_NOERROR, HRESULT_FROM_WIN32(mRes));
mRes = waveInReset(m_hWaveIn);
PRINT_ERROR_LOG_IF_FALSE(mRes == MMSYSERR_NOERROR, mRes);
hr = _unPrepareBuffers();
RETURN_IF_FAILED(hr);
m_wavFile.close();
return S_OK;
}
其他框架下的采集
请参考对应的文章。
– EOF –