windows平台,實現錄音功能詳解


  音頻處理分為播放和錄音兩類。對這些處理,微軟提供了一些列函數,稱之為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平台為我們提供了錄音相關函數。平台提供的函數考慮到各種應用場景,使用起來非常靈活,但是容易出錯,需要關注細節。合適的才是最好的。根據自身的需求,選用一種合適的處理模式;這種模式既能滿足我們的功能需求,又要易於使用。本文不僅提供了對錄音函數的封裝,也提供一種處理復雜問題的思路。希望能拋磚引玉!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM