windows平臺,實現錄音功能詳解
阿新 • • 發佈:2018-11-09
音訊處理分為播放和錄音兩類。對這些處理,微軟提供了一些列函式,稱之為Waveform Functions。這篇文章討論錄音功能。會對微軟提供的函式做簡單說明,並對這些函式封裝成c++類,再進一步封裝成c#類。
1 Waveform Functions函式簡介
根據錄音處理步驟,對這些函式做簡單介紹。
1.1 waveInOpen
MMRESULT waveInOpen( LPHWAVEIN phwi, UINT uDeviceID, LPCWAVEFORMATEX pwfx, DWORD_PTR dwCallback, DWORD_PTR dwCallbackInstance, DWORD fdwOpen );
pwfx為錄音格式。普通對講錄音一般取樣頻率為8000HZ,位長為16bit,單聲道。fdwOpen為回撥型別,一般採用CALLBACK_FUNCTION,就是函式回撥方式。當有錄音裝置開啟、關閉、錄音完成等事件發生時,系統會呼叫我們提供的回撥函式。
1.2 waveInPrepareHeader,waveInAddBuffer
當錄音裝置開啟後,需要你提供記憶體區域來存放錄音資料。這兩個函式就是完成這項功能。waveInPrepareHeader是準備記憶體,waveInAddBuffer是將記憶體加入到錄音佇列。
當錄音完畢,會有回撥通知,這時我們提供的記憶體中就存放著錄音資料。回撥函式是通過waveInOpen函式的dwCallbackInstance指定的。為了保持錄音的連續性,錄音佇列要時時刻刻不能為空。錄音佇列的記憶體塊個數一般要超過3個。就是第一次準備3個記憶體塊。當有錄音完畢,記憶體塊個數會減1,這時我們立即補充一個記憶體塊。
1.3 waveInStart,waveInStop
這兩個函式分別是啟動和停止錄音。一切準備完畢後,呼叫waveInStart,才會開始錄音。
1.4 waveInClose
關閉錄音。這個函式看起來非常簡單,其實不然。這個函式會引發一些列事件,需要把這些事件處理好,否則會導致記憶體洩漏。當該函式被呼叫時,尚未存放錄音的記憶體塊會通過回撥通知我們,這時需要將這些記憶體釋放掉。
2 音訊函式的c++封裝
封裝目的就是隱藏細節,提供一種易於使用的呼叫模式。通過上文可以看到有幾個細節難於處理:函式回撥、記憶體塊準備、記憶體釋放。本類將這些細節隱藏,對外提供的模式為:
開啟錄音裝置--》啟動錄音--》不停輪詢,讀取已錄音成功的資料--》關閉錄音
上述處理過程完全是線性化的。隱藏了資料準備、函式回撥、記憶體釋放等細節。封裝類如下:
標頭檔案
#pragma once #include "Mmsystem.h" #include <list> #include <queue> #include "osType.h" class PcmRecord { public: PcmRecord(); ~PcmRecord(); BOOL IsOpen(); void SetRecordDataLen(int len); //每個錄音塊長度 BOOL Open(int nSamplesPerSec, int wBitsPerSample, int nChannels); void Close(); BOOL WaitRecordedData(int waitMillisecond); int GetRecordData(char* buffer, int bufferLen, int& bufferReadLen, int waitMillisecond = 0); BOOL StartRecord(); BOOL StopRecord(); private: BOOL AddRecordBuffer(); BOOL HaveRecordingBuffer(); void AddToRecording(WAVEHDR *header); void RemoveFromRecording(WAVEHDR *header); void OnRcvRecordData(WAVEHDR * header); void AddToRecorded(WAVEHDR *header); void DelAllRecordData(); void PrepareRecordData(int count); void OnClose(); private: BOOL m_bInClosing; HWAVEIN m_hWaveIn; WAVEFORMATEX m_waveForm; int m_recordBufferLen; std::list<WAVEHDR*> m_listWaveInRecording; CCritical m_recordingLock; std::queue<WAVEHDR*> m_listWaveInRecorded; CCritical m_recordedLock; HANDLE m_recordedDataEvent; static void WaveInProc(HWAVEOUT hwo, UINT uMsg, DWORD dwInstance, DWORD dwParam1, DWORD dwParam2); };
實現檔案
#include "stdafx.h" #include "PcmRecord.h" const int MaxDataCountInRecording = 10; //同時準備多少個 正在錄音的buffer void FreeWaveHeader(WAVEHDR *header); PcmRecord::PcmRecord() { m_hWaveIn = NULL; m_recordBufferLen = 1600; m_recordedDataEvent = CreateEvent(NULL, FALSE, FALSE, L""); } PcmRecord::~PcmRecord() { Close(); } void PcmRecord::WaveInProc(HWAVEOUT hwo, UINT uMsg, DWORD dwInstance, DWORD dwParam1, DWORD dwParam2) { PcmRecord *record = (PcmRecord*)dwInstance; if (uMsg == WOM_OPEN) //音訊開啟 { return; } if (uMsg == WOM_CLOSE) //音訊控制代碼關閉 { record->OnClose(); return; } if (uMsg == WIM_DATA)//獲取了錄製資料 { WAVEHDR *header = (WAVEHDR*)dwParam1; record->OnRcvRecordData(header); } } void PcmRecord::OnClose() { if (!m_bInClosing) Close(); } void PcmRecord::OnRcvRecordData(WAVEHDR *header) { //MMRESULT mmres = waveInUnprepareHeader(m_hWaveIn, header, sizeof(WAVEHDR)); RemoveFromRecording(header); if (header->dwBytesRecorded > 0) { AddToRecorded(header); } else { FreeWaveHeader(header); } if (!m_bInClosing) { PrepareRecordData(MaxDataCountInRecording); } } void PcmRecord::AddToRecorded(WAVEHDR * header) { { CCriticalLock lock(m_recordedLock); m_listWaveInRecorded.push(header); } SetEvent(m_recordedDataEvent); } void PcmRecord::DelAllRecordData() { CCriticalLock lock(m_recordedLock); while (m_listWaveInRecorded.size() > 0) { WAVEHDR *header = m_listWaveInRecorded.front(); m_listWaveInRecorded.pop(); FreeWaveHeader(header); } } BOOL PcmRecord::WaitRecordedData(int waitMillisecond) { { CCriticalLock lock(m_recordedLock); if (m_listWaveInRecorded.size() > 0) return TRUE; } WaitForSingleObject(m_recordedDataEvent, waitMillisecond); { CCriticalLock lock(m_recordedLock); return (m_listWaveInRecorded.size() > 0); } } int PcmRecord::GetRecordData(char* buffer, int bufferLen, int& bufferReadLen, int waitMillisecond) { bufferReadLen = 0; BOOL haveData; { // 因為有WaitForSingleObject呼叫,等待時間可能很長,所以要快速解鎖 CCriticalLock lock(m_recordedLock); haveData = m_listWaveInRecorded.size() > 0; } if (!haveData && waitMillisecond == 0) { ResetEvent(m_recordedDataEvent); return 0; } //等待資料到來 if (!haveData) { ResetEvent(m_recordedDataEvent); WaitForSingleObject(m_recordedDataEvent, waitMillisecond); } CCriticalLock lock2(m_recordedLock); int copyIndex = 0; while ((bufferLen - copyIndex) >= m_recordBufferLen && m_listWaveInRecorded.size() > 0) { WAVEHDR *header = m_listWaveInRecorded.front(); m_listWaveInRecorded.pop(); memcpy(buffer, header->lpData, header->dwBytesRecorded); copyIndex += header->dwBytesRecorded; FreeWaveHeader(header); } bufferReadLen = copyIndex; return bufferReadLen; } BOOL PcmRecord::IsOpen() { return m_hWaveIn != NULL; } BOOL PcmRecord::Open(int nSamplesPerSec, int wBitsPerSample, int nChannels) { m_waveForm.nSamplesPerSec = nSamplesPerSec; /* sample rate */ m_waveForm.wBitsPerSample = wBitsPerSample; /* sample size */ m_waveForm.nChannels = nChannels; /* channels*/ m_waveForm.cbSize = 0; /* size of _extra_ info */ m_waveForm.wFormatTag = WAVE_FORMAT_PCM; m_waveForm.nBlockAlign = (m_waveForm.wBitsPerSample * m_waveForm.nChannels) >> 3; m_waveForm.nAvgBytesPerSec = m_waveForm.nBlockAlign * m_waveForm.nSamplesPerSec; MMRESULT mmres = waveInOpen(&m_hWaveIn, WAVE_MAPPER, &m_waveForm, (DWORD_PTR)WaveInProc, (DWORD_PTR)this, CALLBACK_FUNCTION); if (mmres != MMSYSERR_NOERROR) { return FALSE; } m_bInClosing = FALSE; PrepareRecordData(MaxDataCountInRecording); return TRUE; } void PcmRecord::SetRecordDataLen(int len) { m_recordBufferLen = len; } BOOL PcmRecord::StartRecord() { MMRESULT mmres = waveInStart(m_hWaveIn); return (mmres == MMSYSERR_NOERROR); } BOOL PcmRecord::StopRecord() { MMRESULT mmres = waveInStop(m_hWaveIn); return (mmres == MMSYSERR_NOERROR); } void PcmRecord::Close() { m_bInClosing = TRUE; MMRESULT mmres = waveInReset(m_hWaveIn); int n = 0; while (HaveRecordingBuffer() && n < 500) { Sleep(1); n++; } mmres = waveInClose(m_hWaveIn); m_hWaveIn = NULL; DelAllRecordData(); } BOOL PcmRecord::HaveRecordingBuffer() { CCriticalLock lock(m_recordingLock); return m_listWaveInRecording.size() > 0; } void PcmRecord::AddToRecording(WAVEHDR *header) { CCriticalLock lock(m_recordingLock); m_listWaveInRecording.push_back(header); } void PcmRecord::RemoveFromRecording(WAVEHDR *header) { CCriticalLock lock(m_recordingLock); m_listWaveInRecording.remove(header); } void PcmRecord::PrepareRecordData(int count) { CCriticalLock lock(m_recordingLock); while (m_listWaveInRecording.size() < count) { if (!AddRecordBuffer()) return; } } BOOL PcmRecord::AddRecordBuffer() { WAVEHDR *header = new WAVEHDR(); ZeroMemory(header, sizeof(WAVEHDR)); //對應回撥函式 DWORD_PTR dwParam1, header->dwUser = (DWORD_PTR)header; header->dwBufferLength = m_recordBufferLen; header->lpData = new char[m_recordBufferLen]; MMRESULT result = waveInPrepareHeader(m_hWaveIn, header, sizeof(WAVEHDR)); if (result != MMSYSERR_NOERROR) { FreeWaveHeader(header); return FALSE; } AddToRecording(header); result = waveInAddBuffer(m_hWaveIn, header, sizeof(WAVEHDR)); if (result != MMSYSERR_NOERROR) { RemoveFromRecording(header); FreeWaveHeader(header); return FALSE; } return TRUE; }
對於讀取錄音函式的使用特別說明一下。該函式定義如下:
int PcmRecord::GetRecordData(char* buffer, int bufferLen,int& bufferReadLen, int waitMillisecond)
bufferLen的長度要大於一個記憶體塊。waitMillisecond為等待的毫秒數;當沒有錄音資料時,該函式會最多等待waitMillisecond毫秒。當waitMillisecond為0時,就是非阻塞呼叫。對於阻塞呼叫可以,採用獨立執行緒讀取;對於非阻塞,可以採用定時器方式輪詢。
3 音訊函式的c#封裝
在對c++類實現的基礎上的進一步封裝為c函式,可以供c#呼叫。這裡的關鍵是c++函式封裝為c函式。
LIBPCMPLAY_API int64_t PcmRecord_CreateHandle(); LIBPCMPLAY_API void PcmRecord_SetRecordDataLen(int64_t handle, int len); LIBPCMPLAY_API BOOL PcmRecord_Open(int64_t handle, int nSamplesPerSec, int wBitsPerSample, int nChannels); LIBPCMPLAY_API BOOL PcmRecord_IsOpen(int64_t handle); LIBPCMPLAY_API BOOL PcmRecord_Start(int64_t handle); LIBPCMPLAY_API BOOL PcmRecord_Stop(int64_t handle); LIBPCMPLAY_API int PcmRecord_GetRecordData(int64_t handle, char* buffer, int bufferLen, int& bufferReadLen, int waitMillisecond = 0); LIBPCMPLAY_API void PcmRecord_Close(int64_t handle);
PcmRecord_CreateHandle 就是生成一個PcmRecord的例項,將該例項的指標返回。將handle定義為64位,這樣32、64平臺下處理方式就完全一樣。handle就是類指標,這樣就將複雜的類函式隱藏了。
實現函式
int64_t PcmPlay_CreateHandle() { CPcmPlay *play = new CPcmPlay(); return (int64_t)play; } BOOL PcmPlay_IsOpen(int64_t handle) { CPcmPlay *play = (CPcmPlay*)handle; return play->IsOpen(); } BOOL PcmPlay_Open(int64_t handle, int nSamplesPerSec, int wBitsPerSample, int nChannels) { CPcmPlay *play = (CPcmPlay*)handle; return play->Open(nSamplesPerSec, wBitsPerSample, nChannels); } BOOL PcmPlay_SetVolume(int64_t handle, int volume) { CPcmPlay *play = (CPcmPlay*)handle; return play->SetVolume(volume); } int PcmPlay_Play(int64_t handle, char* block, int size) { CPcmPlay *play = (CPcmPlay*)handle; return play->Play(block, size); } void PcmPlay_StopPlay(int64_t handle) { CPcmPlay *play = (CPcmPlay*)handle; play->StopPlay(); } BOOL PcmPlay_IsOnPlay(int64_t handle) { CPcmPlay *play = (CPcmPlay*)handle; return play->IsOnPlay(); } int64_t PcmPlay_GetLeftPlaySpan(int64_t handle) { CPcmPlay *play = (CPcmPlay*)handle; return play->GetLeftPlaySpan(); } int64_t PcmPlay_GetCurPlaySpan(int64_t handle) { CPcmPlay *play = (CPcmPlay*)handle; return play->GetCurPlaySpan(); } void PcmPlay_Close(int64_t handle) { CPcmPlay *play = (CPcmPlay*)handle; play->Close(); } void PcmPlay_CloseHandle(int64_t handle) { CPcmPlay *play = (CPcmPlay*)handle; play->Close(); delete play; } //錄音 int64_t PcmRecord_CreateHandle() { PcmRecord *record = new PcmRecord(); return (int64_t)record; } void PcmRecord_SetRecordDataLen(int64_t handle, int len) { PcmRecord *record = (PcmRecord*)handle; record->SetRecordDataLen(len); } BOOL PcmRecord_Open(int64_t handle, int nSamplesPerSec, int wBitsPerSample, int nChannels) { PcmRecord *record = (PcmRecord*)handle; return record->Open(nSamplesPerSec,wBitsPerSample,nChannels); } BOOL PcmRecord_IsOpen(int64_t handle) { PcmRecord *record = (PcmRecord*)handle; return record->IsOpen(); } BOOL PcmRecord_Start(int64_t handle) { PcmRecord *record = (PcmRecord*)handle; return record->StartRecord(); } BOOL PcmRecord_Stop(int64_t handle) { PcmRecord *record = (PcmRecord*)handle; return record->StopRecord(); } int PcmRecord_GetRecordData(int64_t handle, char* buffer, int bufferLen, int& bufferReadLen, int waitMillisecond) { PcmRecord *record = (PcmRecord*)handle; return record->GetRecordData(buffer, bufferLen, bufferReadLen, waitMillisecond); } void PcmRecord_Close(int64_t handle) { PcmRecord *record = (PcmRecord*)handle; record->Close(); }
實現了對c語言的封裝,下一步就是在c語言的基礎上,封裝成c#類。
class PcmRecord { long _handle = 0; int recordTimespan = 320;//每次錄音長度 毫秒 int bufferLenPerSample; public PcmRecord() { bufferLenPerSample = 16* recordTimespan; } ~PcmRecord() { if(_handle != 0) { PcmRecordWrapper.PcmRecord_Close(_handle); } } bool _isOpen = false; public bool IsOpen => _isOpen; public bool Open() { if (_isOpen) throw new Exception("先關閉,再開啟!"); _handle = PcmRecordWrapper.PcmRecord_CreateHandle(); PcmRecordWrapper.PcmRecord_SetRecordDataLen(_handle, bufferLenPerSample); _isOpen = PcmRecordWrapper.PcmRecord_Open(_handle, 8000, 16, 1)==1; return true; } public bool Start() { if (!_isOpen) throw new Exception("錄音裝置還沒開啟!"); return PcmRecordWrapper.PcmRecord_Start(_handle) == 1; } public bool Stop() { if (!_isOpen) throw new Exception("錄音裝置還沒開啟!"); return PcmRecordWrapper.PcmRecord_Stop(_handle) == 1; } public byte[] GetPcmData(int waitMillisecond) { if (!_isOpen) throw new Exception("錄音裝置還沒開啟!"); byte[] bufferRecord = new byte[bufferLenPerSample]; GCHandle hinBuffer = GCHandle.Alloc(bufferRecord, GCHandleType.Pinned); byte[] readLen = new byte[4]; GCHandle hinReadLen = GCHandle.Alloc(readLen, GCHandleType.Pinned); PcmRecordWrapper.PcmRecord_GetRecordData(_handle, hinBuffer.AddrOfPinnedObject(), bufferRecord.Length, hinReadLen.AddrOfPinnedObject(), waitMillisecond); hinBuffer.Free(); hinReadLen.Free(); int returnLen = BitConverter.ToInt32(readLen, 0); if (returnLen == 0) return null; if (returnLen == bufferRecord.Length) return bufferRecord; Array.Resize(ref bufferRecord, returnLen); return bufferRecord; } public void Close() { if (!_isOpen) return; PcmRecordWrapper.PcmRecord_Close(_handle); _handle = 0; _isOpen = false; } } public class PcmRecordWrapper { private const string DLLName = "LibPcmPlay.dll"; [DllImport(DLLName, EntryPoint = "PcmRecord_CreateHandle", CallingConvention = CallingConvention.Cdecl)] public static extern long PcmRecord_CreateHandle(); [DllImport(DLLName, EntryPoint = "PcmRecord_SetRecordDataLen", CallingConvention = CallingConvention.Cdecl)] public static extern void PcmRecord_SetRecordDataLen(long handle, int len); [DllImport(DLLName, EntryPoint = "PcmRecord_Open", CallingConvention = CallingConvention.Cdecl)] public static extern int PcmRecord_Open(long handle, int nSamplesPerSec, int wBitsPerSample, int nChannels); [DllImport(DLLName, EntryPoint = "PcmRecord_IsOpen", CallingConvention = CallingConvention.Cdecl)] public static extern int PcmRecord_IsOpen(long handle); [DllImport(DLLName, EntryPoint = "PcmRecord_Start", CallingConvention = CallingConvention.Cdecl)] public static extern int PcmRecord_Start(long handl); [DllImport(DLLName, EntryPoint = "PcmRecord_Stop", CallingConvention = CallingConvention.Cdecl)] public static extern int PcmRecord_Stop(long handl); [DllImport(DLLName, EntryPoint = "PcmRecord_GetRecordData", CallingConvention = CallingConvention.Cdecl)] public static extern int PcmRecord_GetRecordData(long handle, IntPtr buffer, Int32 bufferLen, IntPtr bufferReadLen, int waitMillisecond); [DllImport(DLLName, EntryPoint = "PcmRecord_Close", CallingConvention = CallingConvention.Cdecl)] public static extern void PcmRecord_Close(long handl); } }
總結:windows平臺為我們提供了錄音相關函式。平臺提供的函式考慮到各種應用場景,使用起來非常靈活,但是容易出錯,需要關注細節。合適的才是最好的。根據自身的需求,選用一種合適的處理模式;這種模式既能滿足我們的功能需求,又要易於使用。本文不僅提供了對錄音函式的封裝,也提供一種處理複雜問題的思路。希望能拋磚引玉!