介绍 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 = ;
m_waveForm.nSamplesPerSec = nSamplesPerSec; /* sample rate */
m_waveForm.wBitsPerSample = wBitsPerSample; /* sample size */
m_waveForm.nChannels = nChannels; /* channels*/
m_waveForm.cbSize = ; /* size of _extra_ info */
m_waveForm.wFormatTag = WAVE_FORMAT_PCM;
m_waveForm.nBlockAlign = (m_waveForm.wBitsPerSample * m_waveForm.nChannels) >> ;
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 = ;
while (IsOnPlay() && n < )
{
n++;
::Sleep();
}
}
关闭播放时,有一点需要注意,有可能播放还没完毕。调用waveOutClose后,回调函数给通知,即uMsg == WOM_DONE,在回调函数中将缓冲数据释放。
当所有的数据释放完毕,才能安全退出。
这就是播放的基本流程,其实不难。但是,因为播放是异步的,所以处理缓冲释放方面有点小技巧。

当然本类对其他一些函数也做了封装,方便调用,代码如下:
//根据数据长度,计算播放长度 单位秒
double CPcmPlay::GetPlayTimeSpan(int bufferLen)
{
if (m_waveForm.nSamplesPerSec ==
|| m_waveForm.nSamplesPerSec == )
return ; double n = m_waveForm.nSamplesPerSec*m_waveForm.wBitsPerSample /;
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 <= )
n = ;
if (volume >= )
n = ; n = n * 0xFFFF / ;
DWORD dwVolume = n;
dwVolume = (dwVolume << );
dwVolume += n; MMRESULT result = waveOutSetVolume(m_hWaveOut, dwVolume);
return (result == MMSYSERR_NOERROR);
} //获取已播放时长 单位秒
double CPcmPlay::GetCurPlaySpan()
{
if (m_hWaveOut == NULL)
return ; MMTIME mm = { };
mm.wType = TIME_BYTES;
MMRESULT result = waveOutGetPosition(m_hWaveOut, &mm, sizeof(mm));
if (mm.wType != TIME_BYTES
|| result != MMSYSERR_NOERROR)
return ; double span = GetPlayTimeSpan(mm.u.cb);
return span;
} //获取剩余播放时长 单位秒
double CPcmPlay::GetLeftPlaySpan()
{
if (m_hWaveOut == NULL)
return ; MMTIME mm = { };
mm.wType = TIME_BYTES;
MMRESULT result = waveOutGetPosition(m_hWaveOut, &mm, sizeof(mm));
if (mm.wType != TIME_BYTES
|| result != MMSYSERR_NOERROR)
return ; double span = GetPlayTimeSpan(m_totalPlayBuffer - mm.u.cb);
return span;
}

封装类下载地址https://download.csdn.net/download/qq_29939347/10746435

 
												

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

  1. 如何实现Windows平台RTMP播放器/RTSP播放器播放窗口添加OSD文字叠加

    好多开发者在做Windows平台特别是单屏多画面显示时,希望像监控摄像机一样,可以在播放画面添加OSD台标,以实现字符叠加效果,大多开发者可很轻松的实现以上效果,针对此,本文以大牛直播SDK (Git ...

  2. Windows平台真实时毫秒级4K H264/H265直播技术方案

    背景 在刚提出4K视频的时候,大多数人都觉得没有必要,4K的出现,意味着更高的硬件规格和传输要求,1080P看的很爽.很清晰,完全满足了日常的需求.随着电视的尺寸越来越大,原本1080P成像已经无法满 ...

  3. Android 音视频开发(一):PCM 格式音频的播放与采集

    什么是 PCM 格式 声音从模拟信号转化为数字信号的技术,经过采样.量化.编码三个过程将模拟信号数字化. 采样 顾名思义,对模拟信号采集样本,该过程是从时间上对信号进行数字化,例如每秒采集 44100 ...

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

    本文记录SDL播放音频的技术.在这里使用的版本是SDL2.实际上SDL本身并不提供视音频播放的功能,它只是封装了视音频播放的底层API.在Windows平台下,SDL封装了Direct3D这类的API ...

  5. 视频和音频播放的演示最简单的例子9:SDL2广播PCM

    ===================================================== 最简单的视频和音频播放的演示样品系列列表: 最简单的视音频播放演示样例1:总述 最简单的视音 ...

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

    本文记录DirectSound播放音频的技术.DirectSound是Windows下最常见的音频播放技术.目前大部分的音频播放应用都是通过DirectSound来播放的.本文记录一个使用Direct ...

  7. 最简单的视音频播放演示样例8:DirectSound播放PCM

    ===================================================== 最简单的视音频播放演示样例系列文章列表: 最简单的视音频播放演示样例1:总述 最简单的视音频 ...

  8. Android音频: 怎样使用AudioTrack播放一个WAV格式文件?

    翻译 By Long Luo 原文链接:Android Audio: Play a WAV file on an AudioTrack 译者注: 1. 因为这是技术文章,所以有些词句使用原文,表达更准 ...

  9. Windows平台RTMP/RTSP播放器实现实时音量调节

    为什么要做实时音量调节 RTMP或RTSP直播播放音量调节,主要用于多实例(多窗口)播放场景下,比如同时播放4路RTMP或RTSP流,如果音频全部打开,几路audio同时打开,可能会影响用户体验,我们 ...

随机推荐

  1. Oracle中B-TREE索引的深入理解(转载)

    索引概述 索引与表一样,也属于段(segment)的一种.里面存放了用户的数据,跟表一样需要占用磁盘空间.只不过,在索引里的数据存放形式与表里的数据存放形式非常的不一样.在理解索引时,可以想象一本书, ...

  2. mysql高级

    视图: 视图是一条select语句执行后返回的结果集 试图是对若干张基础表的引用 定义视图: 建议以v_开头 create view 试图名称 as select 语句 查看视图 show table ...

  3. maven之web工程的搭建

    参考之前jave application的工程创建的步骤,我们只需要修改最后一步 这样就创建了个web maven工程 与java application应用程序的区别,还有别的区别这里不做多的阐述. ...

  4. Jmeter-安装与配置

    前言 越长大越无脑,很多东西还是很容易忘记,哈哈,虽然网上也有很多关于Jmeter的安装配置教程,但还是想在自己的博客上记录下,便于以后查阅. JMeter的安装配置过程 我的环境信息如下: 操作系统 ...

  5. vc6中向vs2010迁移的几个问题

    vc6版本支持的库编译:CJ60lib 1. 用vs2010打开CJ60库的源码的dsw,强制打开 (1)设置项目属性的语言 因为,如果代码字符的编码集不一样,则会出现函数冲定义,参数冲突等问题,这可 ...

  6. nutch相关目录说明

    Nutch数据包含3个目录结构,分别是: 1.Crawldb:用于存储Nutch将要检索的url信息,以及检索状态(是否检索.何时检索) 2.Linkdb:用于存储每一个url所包含的超链接信息(包括 ...

  7. toast js

    参考别人的,自己改写了下,很好用. <!DOCTYPE html> <html lang="zh-CN"> <head> <meta ch ...

  8. SRM474

    250pt 题意:在一个N维的空间里,有一个人开始在原点,现在给出N<=50个指令序列,每个指令序列为某一维+1或者减一,问是否经过某个点至少2次. 思路:操作很小,直接模拟判断即可 code: ...

  9. C# 子线程调用主线程窗体的解决方法

    摘自其他人博客,自己试过确实解决问题.(如在自己定义的线程里面给textbox赋值) 由于Windows窗体控件本质上不是线程安全的.因此如果有两个或多个线程适度操作某一控件的状态(set value ...

  10. Android sharedUserId 和系统权限

    sharedUserId 给不同的应用使用同一个 sharedUserId 可以运行在这几个应用间互相访问数据(数据库,SharedPreferences,文件). sharedUserId 一旦使用 ...