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

 


原文地址:https://www.cnblogs.com/yuanchenhui/p/pcm_play.html

时间: 2024-10-27 06:17:16

音频播放封装(pcm格式,Windows平台 c++)的相关文章

最简单的视音频播放示例9:SDL2播放PCM

本文记录SDL播放音频的技术.在这里使用的版本是SDL2.实际上SDL本身并不提供视音频播放的功能,它只是封装了视音频播放的底层API.在Windows平台下,SDL封装了Direct3D这类的API用于播放视频:封装了DirectSound这类的API用于播放音频.因为SDL的编写目的就是简化视音频播放的开发难度,所以使用SDL播放视频(YUV/RGB)和音频(PCM)数据非常的容易. SDL简介 SDL(Simple DirectMedia Layer)是一套开放源代码的跨平台多媒体开发库,

Android平台中关于音频播放

Android平台中关于音频播放有以下三种方式: 1.SoundPool ---- 适合短促且对反应速度比较高的情况(游戏音效或按键声等) 2.MediaPlayer ---- 适合比较长且时间要求不高的情况 3.AudioTrack ---- 播放解码后的PCM码流 方法一:SoundPool 1)SoundPool简介 SoundPool类是Android用于管理和播放应用程序的音频资源的类.一个SoundPool对象可以看作是一个可以从APK中导入资源或者从文件系统中载入文件的样本集合.它

Android音频: 如何使用AudioTrack播放一个WAV格式文件?

翻译 By Long Luo 原文链接:Android Audio: Play a WAV file on an AudioTrack 译者注: 1. 由于这是技术文章,所以有些词句使用原文,表达更准确. 2. 由于水平有效,有些地方可能翻译的不够准确,如有不当之处,敬请批评指正. 3. 针对某些语句,适当补充了上下文及更适合中文阅读,尽量做到信达雅. 如果你已经成功地了解了关于AudioTrack的一些话题,那么你可能享受它带来的好处,例如低延迟(在STATIC(静态)模式),能够生成流式音频

最简单的视音频播放示例8:DirectSound播放PCM

本文记录DirectSound播放音频的技术.DirectSound是Windows下最常见的音频播放技术.目前大部分的音频播放应用都是通过DirectSound来播放的.本文记录一个使用DirectSound播放PCM的例子.注:一位仁兄已经提醒我DirectSound已经计划被XAudio2取代了.后来考证了一下发现确有此事.因此在下次更新中考虑加入XAudio2播放PCM的例子.本文仍然记录一下DirectSound这位“元老”. DirectSound简介 DirectSound是微软所

Android 音频播放——AudioTrack直接播PCM、MediaPlayer播媒体文件可以是audio

http://blog.csdn.net/java_android_c/article/details/52678265 Android平台播放音频的方式一般有3种.1.利用系统内置的应用程序播放音频    2.利用AudioTrack播放原始音频   3.使用MediaPlayer播放.此3种音频播放方式,以第三种MediaPlayer播放这种方式使用的最多,必须掌握! 一.使用系统内置的程序. Google想的"周到",一般都给我们提供了一些内置程序,然而这些内置程序的UI效果,那

Android 音频播放之SoundPool的使用和封装

一般大家使用的是MediaPlayer来播放音频,它的创建和销毁都是非常消耗资源的,如果我们的需求是播放一些短促而且频繁播放的音频的话MediaPlayer就有些不合适了,我们来讲讲SoundPool来播放短促的音频: SoundPool结构如下 初始化SoundPool 初始化SoundPool 我们直接new SoundPool (int maxStreams, int streamType, int srcQuality)即可 参数解释: 参数 解释 maxStreams 最大的流的数量

自己编写的Windows字符串类 封装字符串格式转换和常用操作 方便使用

最近开发的语音识别的项目经常碰到转码的问题,各种宽窄字节转换,ASNI, UTF8, 宽字节 --  代码写得冗长,繁琐,维护性太差了.决定自己写一个能直接使用各种编码的字符串类,于是实现了一个,功能不多,但是也够用.由于string 和 wstring 在多线程下共享数据会出问题,这里只用最基础的char 和 wchar_t. 基于Windows平台,代码如下 /* ************************************************** Title: 自定义字符串

第一个Windows Phone 8 程序开发之后台音频播放

对于播客的音频应该是连续多个的列表,作为在后台连续播放.在网上搜了一下,通过wp8后台音频代理播放,而且例子都是静态的播放列表,不满足动态生成列表播放. 尝试着将播放列表对象声明为公有静态的,在外部对列表进行操作,发现这个静态的播放列表在agent里和我的操作类不是同一个引用,此方法行不通. 最后在 http://www.devdiv.com/forum.php?mod=redirect&goto=findpost&ptid=199381&pid=960706 找到了思路: 在wp

如何将Pcm格式的音频文件转换成Wave格式的文件

最近在做一款变声App,其中就用到了将pcm格式转wave格式,下面贴出源代码,希望带有需求的童鞋有帮助!!!这里是c++语言写的,也可以用java实现.当然java调用native函数要用到jni技术.具体jni技术自己到网路上找找资料. Cpp文件Pcm2Wave.cpp #include <stdlib.h> #include <string.h> #include <stdio.h> #include "wave.h" #include &q