音频播放封装(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

 

posted @ 2018-10-26 16:42  源之缘-OFD解决方案  阅读(2973)  评论(0编辑  收藏  举报
关注我