数据漫谈 2017-10-15
从Vista开始,windows底层的音频架构发生了改变:原本是底层API的waveXXX、mixerXXX等都在Core Audio APIs的基础上进行了重构,上升为了高层API;底层API变为Core Audio API。 由于这个原因,在利用遗留音频技术(waveXXX、mixerXXX等)进行开发的时候,在WinXp和其他系统上的表现会不太一致。
但是如果要在Xp上进行开发的话,就必须要使用这些老旧的技术,没得选。
在Xp下进行开发,大概只有DirectX、waveXXX和mixerXXX可选了。 这里我们简单描述它们的优缺点:
优点:
缺点:
我们选择waveXXX api来实现这个开发实例,因为waveXXX相对来说比较好用,这样我们不用花费过多的时间去了解其他概念上的细节。
先调用waveInGetNumDevs()获取设备总数,然后传入设备序号(0 ~ 总数-1),并选择设备支持的PCM数据格式中的一种打开设备,获取到设备句柄:
auto inputAudioDeviceNum = waveInGetNumDevs(); for (int i = 0; i < inputAudioDeviceNum; ++i) { WAVEINCAPS waveInCaps; auto returnValue = waveInGetDevCaps(i, &waveInCaps, sizeof(waveInCaps)) ; ...... WAVEFORMATEX waveFormatEx = chooseAppropriateFormat(); auto returnValue = waveInOpen((LPHWAVEIN)&deviceInfo.handle, index, &waveFormatEx, (DWORD_PTR)CoreAudioHelper::waveInProc, (DWORD_PTR)this, CALLBACK_FUNCTION); ...... }
为了获取音频数据,我们需要准备一个Buffer,并将这个Buffer添加到你想要获取数据的音频设备上,然后开始这个设备的音频捕获:
bool CoreAudioHelper::startPeakGetter() { Q_ASSERT(m_currentDeviceIndex >= 0 && m_currentDeviceIndex < m_infos.size()); auto& deviceInfo = m_infos[m_currentDeviceIndex]; ZeroMemory(m_buffer, sizeof(m_buffer)); m_waveHdr.dwFlags = 0; m_waveHdr.lpData = (LPSTR)m_buffer; m_waveHdr.dwBufferLength = sizeof(m_buffer); auto returnValue = waveInPrepareHeader(deviceInfo.handle, &m_waveHdr, sizeof(m_waveHdr)); CHECK_RETURN(returnValue); returnValue = waveInAddBuffer(deviceInfo.handle, &m_waveHdr, sizeof(m_waveHdr)); CHECK_RETURN(returnValue); returnValue = waveInStart(deviceInfo.handle); CHECK_RETURN(returnValue); deviceInfo.started = true; return true; }
当这个Buffer被数据填满的时候,系统就会通知你,这时候我们需要先调用waveInUnprepareHeader()来取消先前准备的Buffer,然后就可以对数据进行操作了(这里我们计算了音频的音量大小)。在之前打开设备的时候,你可以选择多种通知方式:回调、窗口消息、事件或者线程,这里我选择使用回调方法。如果要连续的获取捕获到的数据,我们就要在Buffer被填满的时候不断添加新的Buffer。注意因为在回调中基本上不可以调用任何系统api,所以我们需要另一个线程来添加新Buffer,并利用信号量来进行同步:
void CoreAudioHelper::waveInProc(HWAVEIN hwi, UINT uMsg, DWORD_PTR dwInstance, DWORD_PTR dwParam1, DWORD_PTR dwParam2) { switch (uMsg) { case WIM_OPEN: break; case WIM_CLOSE: { ...... } case WIM_DATA: { ...... break; } default: Q_ASSERT(false && "never receive other msg!"); } } // non-qt thread have no qt event loop which causing signal/slot not working, // we use a queue to keep the value and a semaphore to notify the internal thread // to emit the signal. void CoreAudioHelper::appendPeakValue(qint16 value) { m_peakValueQueue.push(value); // cannot call Win32 api inside a callback, so we notify the buffer waiter thread m_bufferFilled.release(1); } void CoreAudioHelper::BufferWaiterThread::run() { while (true) { m_helper->m_bufferFilled.acquire(1); m_helper->unprepareBuffer(); if (m_helper->m_stopThread) break; if (m_helper->m_emitUnplugged) { emit m_helper->currentDeviceUnplugged(); m_helper->m_emitUnplugged = false; break; } m_helper->emitPeakLevelAndContinue(); } } bool CoreAudioHelper::unprepareBuffer() { auto deviceInfo = m_infos.at(m_currentDeviceIndex); auto returnValue = waveInUnprepareHeader(deviceInfo.handle, &m_waveHdr, sizeof(m_waveHdr)); CHECK_RETURN(returnValue); return true; }
根据PCM数据是8位还是16位,我们把Buffer中的比特数据转换成合适的变量并计算保存最小值和最大值。因为实际音频波形是以0点为水平上下波动的,
我们只需要把最大波动值除以上限值就可以获得音量大小了(具体见下一小节)。
// buffer already filled with input audio data CoreAudioHelper* helper = reinterpret_cast<CoreAudioHelper*>(dwInstance); Q_ASSERT(helper->m_waveHdr.dwFlags & WHDR_DONE); qint32 peakMin = 255; qint32 peakMax = 0; for (char* ptr = helper->m_buffer; ptr < &helper->m_buffer[16]; ) { qint32 dataValue; if (helper->m_is8BitsSample) { dataValue = *(unsigned char*)ptr; ptr++; } else { dataValue = *(qint16*)ptr; ptr += 2; } if (dataValue < peakMin) peakMin = dataValue; if (dataValue > peakMax) peakMax = dataValue; } helper->appendPeakValue(max(-peakMin, peakMax));
waveXXX API只提供了音频数据捕获,因此我们需要自己来模拟音量和静音的控制,这里我们把这些控制应用在获取到的音量大小上:
void CoreAudioHelper::emitPeakLevelAndContinue() { if (!m_peakValueQueue.empty()) { qint32 peakValue = m_peakValueQueue.front(); m_peakValueQueue.pop(); if (!m_infos.at(m_currentDeviceIndex).muted) { if (m_is8BitsSample) { // when 8-bit sample, the range is 0--255, the silence data value is 127 emit peakChanged(qint32(abs(peakValue - 127) / 1.27) * m_infos.at(m_currentDeviceIndex).volumeFilterPercent); } else { // when 16-bit sample, the range is -32767--32767, the silence data value is 0 emit peakChanged(qint32(abs(peakValue) / 327.67) * m_infos.at(m_currentDeviceIndex).volumeFilterPercent); } startPeakGetter(); } } }
结果就是这样啦,完整代码见此处。