音頻播放封裝(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;
}
 
        

封裝類下載地址https://download.csdn.net/download/qq_29939347/10746435

 
 
       


免責聲明!

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



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