1. 程式人生 > >音訊播放封裝(pcm格式,Windows平臺 c++)

音訊播放封裝(pcm格式,Windows平臺 c++)

介紹 pcm格式是音訊非壓縮格式。如果要對音訊檔案播放,需要先轉換為pcm格式。

windows提供了多套函式用於播放,本文介紹Waveform Audio Functions系列函式。

原始的播放函式比較難用,因工作需要,我寫了一個播放器,將播放相關函式封裝了;非常好用,還不易出錯。

 播放流程

 程式標頭檔案 可以根據標頭檔案窺探函式功能,下面再做簡單介紹。

class CPcmPlay
{
public:
    CPcmPlay();
    ~CPcmPlay();

    //是否打開了 播放裝置
    BOOL IsOpen();

    //nSamplesPerSec 取樣頻率 8000
    
//取樣位數 :8,16 //聲道個數: 1 BOOL Open(int nSamplesPerSec, int wBitsPerSample, int nChannels); //設定聲音大小 0到100 BOOL SetVolume(int volume); //播放記憶體資料 //非同步播放,block指標資料可以立即刪除 MMRESULT Play(LPSTR block, DWORD size); void StopPlay(); //停止播放 BOOL IsOnPlay(); //是否有資料在播放 void Close();//
關閉播放裝置 double GetCurPlaySpan(); //獲取當前塊已播放的時長 double GetLeftPlaySpan(); //獲取剩餘播放播放的時長 BOOL IsNoPlayBuffer();//開啟音訊還沒播放過 private: void OnOpen(); void OnClose(); void OnDone(WAVEHDR *header); void AddHeader(WAVEHDR *header); void DelHeader(WAVEHDR *header); //根據資料長度,計算播放長度 單位秒
double GetPlayTimeSpan(int bufferLen); void static CALLBACK MyWaveOutProc(HWAVEOUT hwo, UINT uMsg, DWORD_PTR dwInstance, DWORD_PTR dwParam1, DWORD_PTR dwParam2); private: UINT64 m_totalPlayBuffer; WAVEFORMATEX m_waveForm; HWAVEOUT m_hWaveOut; std::list<WAVEHDR*> m_listWaveOutHead; CCritical m_listLock; };

1)開啟音訊裝置

BOOL CPcmPlay::Open(int nSamplesPerSec,int wBitsPerSample,int nChannels)
{
    if (IsOpen())
        return FALSE;

    {
        CCriticalLock  lock(m_listLock);
        m_listWaveOutHead.clear();
    }
    m_totalPlayBuffer = 0;
    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;

    if (waveOutOpen(&m_hWaveOut, WAVE_MAPPER, &m_waveForm, (DWORD_PTR)MyWaveOutProc, (DWORD_PTR)this, CALLBACK_FUNCTION) != MMSYSERR_NOERROR)
    {
        return FALSE;
    }
    return TRUE;
}

需要先設定pcm格式,pcm相關介紹請參考別的文章。

開啟音訊傳入的有個引數值為CALLBACK_FUNCTION,表示播放事件,通過函式回撥方式通知。

由於音訊播放是非同步的,當音訊播放完畢、音訊裝置關閉等訊息,需要一個通知機制。回撥函式如下:

void  CALLBACK   CPcmPlay::MyWaveOutProc(
    HWAVEOUT  hwo,
    UINT      uMsg,
    DWORD_PTR dwInstance,
    DWORD_PTR dwParam1,
    DWORD_PTR dwParam2
)
{
    CPcmPlay *play = (CPcmPlay*)dwInstance;
    if (uMsg == WOM_OPEN) //音訊開啟
    {
        play->OnOpen();
        return;
    }
    if (uMsg == WOM_CLOSE) //音訊控制代碼關閉
    {
        play->OnClose();
        return;
    }

    if (uMsg == WOM_DONE)//音訊緩衝播放完畢
    {
        WAVEHDR *header = (WAVEHDR*)dwParam1;
        play->OnDone(header);
    }
}
waveOutOpen 傳入引數與回撥函式的引數有一定關聯。waveOutOpen傳入引數(DWORD_PTR)this,就是回撥函式的DWORD_PTR dwInstance;通過這種關聯,就可以找到類變數(CPcmPlay *play = (CPcmPlay*)dwInstance;)。2)播放資料
MMRESULT CPcmPlay::Play(LPSTR block, DWORD size)
{
    if (m_hWaveOut == NULL)
        return MMSYSERR_INVALHANDLE;

    WAVEHDR *header = new WAVEHDR();
    ZeroMemory(header, sizeof(WAVEHDR));

    //對應回撥函式 DWORD_PTR dwParam1,
    header->dwUser = (DWORD_PTR)header;

    //new新的資料,並將block資料複製。
    //這樣函式返回,block的資料可以立即釋放
    LPSTR blockNew = new char[size];
    memcpy(blockNew, block, size);
    header->dwBufferLength = size;
    header->lpData = blockNew;

    //準備資料
    MMRESULT result = waveOutPrepareHeader(m_hWaveOut, header, sizeof(WAVEHDR));
    if (result != MMSYSERR_NOERROR)
    {
        FreeWaveHeader(header);
        return result;
    }

    //播放資料加入緩衝佇列
    //播放時非同步的,播放完畢之前,緩衝的資料不能釋放
    AddHeader(header);
    result = waveOutWrite(m_hWaveOut, header, sizeof(WAVEHDR));
    if (result != MMSYSERR_NOERROR)
    {
        DelHeader(header);
        return result;
    }
    m_totalPlayBuffer += size;

    return MMSYSERR_NOERROR;
}

有一點特別注意,播放函式是非同步的,就是播放完畢之前,播放緩衝資料不能釋放。為了方便呼叫,重新將輸入引數block的資料又new一塊記憶體存放,呼叫方不必關心記憶體塊啥時釋放。

我們將播放緩衝加入一個list列表中,當播放完畢,我們需要釋放該緩衝。怎麼知道緩衝資料是否播放完畢?是通過回撥機制。參加前文回撥函式。

if (uMsg == WOM_DONE)//音訊緩衝播放完畢
    {
       //對應回撥函式 DWORD_PTR dwParam1,
    //header->dwUser = (DWORD_PTR)header;

        WAVEHDR *header = (WAVEHDR*)dwParam1;
        play->OnDone(header);
    }
回撥引數dwParam1對應header->dwUser,我們將dwUser設定為緩衝指標,這樣,通過回撥函式的引數就找到了對應播放緩衝。播放完畢的緩衝,需要釋放。
void CPcmPlay::DelHeader(WAVEHDR *header)
{
    {
        CCriticalLock  lock(m_listLock);
        m_listWaveOutHead.remove(header);
    }
    FreeWaveHeader(header);
}

void FreeWaveHeader(WAVEHDR *header)
{
    delete[]header->lpData;
    delete header;
}

由於回撥函式和播放函式屬於不同的執行緒,所以對列表操作加了鎖。

 3 關閉音訊播放

void CPcmPlay::Close()
{
    if (m_hWaveOut == NULL)
        return;
    
    StopPlay();
    MMRESULT result = waveOutClose(m_hWaveOut);
    m_hWaveOut = NULL;

    //等待釋放所有的播放緩衝
    int n = 0;
    while (IsOnPlay() && n < 5000)
    {
        n++;
        ::Sleep(1);
    }
}
關閉播放時,有一點需要注意,有可能播放還沒完畢。呼叫waveOutClose後,回撥函式給通知,即uMsg == WOM_DONE,在回撥函式中將緩衝資料釋放。當所有的資料釋放完畢,才能安全退出。這就是播放的基本流程,其實不難。但是,因為播放是非同步的,所以處理緩衝釋放方面有點小技巧。當然本類對其他一些函式也做了封裝,方便呼叫,程式碼如下:
//根據資料長度,計算播放長度 單位秒
double CPcmPlay::GetPlayTimeSpan(int bufferLen)
{
    if (m_waveForm.nSamplesPerSec == 0
        || m_waveForm.nSamplesPerSec == 0)
        return 0;

    double n = m_waveForm.nSamplesPerSec*m_waveForm.wBitsPerSample /8;
    double result = ((double)bufferLen)/n;
    return result;
}


//設定音量大小 volume取值範圍0--100
BOOL CPcmPlay::SetVolume(int volume)
{
    if (m_hWaveOut == NULL)
        return FALSE;

    UINT16 n = volume;
    if (volume <= 0)
        n = 0;
    if (volume >= 100)
        n = 100;

    n = n * 0xFFFF / 100;
    DWORD dwVolume = n;
    dwVolume = (dwVolume << 16);
    dwVolume += n;

    MMRESULT result = waveOutSetVolume(m_hWaveOut, dwVolume);
    return (result == MMSYSERR_NOERROR);
}

//獲取已播放時長 單位秒
double CPcmPlay::GetCurPlaySpan()
{
    if (m_hWaveOut == NULL)
        return 0;

    MMTIME mm = { 0 };
    mm.wType = TIME_BYTES;
    MMRESULT result = waveOutGetPosition(m_hWaveOut, &mm, sizeof(mm));
    if (mm.wType != TIME_BYTES
        || result != MMSYSERR_NOERROR)
        return 0;

    double span = GetPlayTimeSpan(mm.u.cb);
    return span;
}

//獲取剩餘播放時長 單位秒
double CPcmPlay::GetLeftPlaySpan()
{
    if (m_hWaveOut == NULL)
        return 0;

    MMTIME mm = { 0 };
    mm.wType = TIME_BYTES;
    MMRESULT result = waveOutGetPosition(m_hWaveOut, &mm, sizeof(mm));
    if (mm.wType != TIME_BYTES
        || result != MMSYSERR_NOERROR)
        return 0;

    double span = GetPlayTimeSpan(m_totalPlayBuffer - mm.u.cb);
    return span;
}