多媒体编程——声音播放(1)
第一部分使用waveOut进行声音播放。
要讲怎么用播放声音,首先我们要有声音数据才能进行播放嘛。所以在将播放之前,我们要先制作好供播放的数据。下面段是扫盲性讲解,已经了解的朋友可以跳过。
关于音频的格式很多,大家平时都有接触,比如什么mp3,wma,m4a格式的文件啊。
无论是声音还是视频,都存在两层格式,第一层是文件格式,第二层是编码格式。比如mp3文件,它的文件格式是mp3而恰巧声音的编码格式也是mp3。虽然我不会解析mp3,但是我知道,首先我们要分析mp3的文件格式,然后拿到里面音频部分的数据,然后再使用mp3的解码器才能解析mp3的音频数据。Mp4是文件格式,H264是编码格式。AVI是文件格式,MPEG是编码格式。注意区分。扯远了哈。主要是声音和视频本来就密不可分。
还是重点说声音吧,视频部分后面会讲。
声音数据可能存在于独立的文件中,例如mp3,wma。也可能存在于复合文件中,比如avi,mp4。不同的文件类型,也许用的不同的编码格式,比如声音的编码格式有mp3,AAC等等。必然存在着一种中间格式,硬件直接支持的格式,这样才能在不同的格式间转换,硬件可以直接播放,它就是PCM。可以理解为它就是最原始的声音采集信号,没有经过编码压缩。
对于视频图像来说,最原始的格式那当然是 RGB(位图),还有YUV(位图)。
声音是波形信号,波形信号数字化就是采样的过程。只要我们采样的频率够高,就可以大致还原出原始波形的波动曲线。所以PCM的数据其实就是模拟的波形信号的密集采样而已。它的格式参数包括这几部分:采样频率,采样深度,声道数。。所谓声道其实可以认为是两个采集器,同时采样的数据,合并写到一个文件里,这个文件的声音数据就是两声道。
PCM是一种格式类型,对应的文件名字,通常叫xxx.wav。可以用音乐播放器,将一首mp3转化成wav文件,一首4分钟,4M左右的MP3,转化成wav文件大小一下就会增加到10倍,可以想象下模拟波形信号是多么的占空间。准备好一个wav文件,用于后面的测试。下面我用千千静听,转化一首歌为wav文件。
生成了一首歌,wav文件在C盘根目录。
先要完成文件读取的部分,这样才能拿到PCM的格式数据。WAV文件的头的结构体如下:
/* WAV Riff文件头*/
typedef struct _RIFF_HEADER
{
char szRiffID[4]; // 'R','I','F','F'
DWORD dwRiffSize; //从下一字节,到文件结束的字节数。加上8就是整个文件的大小
char szRiffFormat[4]; // 'W','A','V','E'
}RIFF_HEADER,*LP_RIFF_HEADER;
typedef struct _WAV_FORMAT
{
WORD wFormatTag ; //格式种类 1为PCM
WORD wTracks ; //声道数
DWORD dwSamplesPerSec ; //采样频率
DWORD dwAvgBytesPerSec ; //每秒的字节数,是大B。
WORD wBlockAlign ; //数据的调整数,按B计算
WORD wBitsPerSample ; //样本采样位数
}WAV_FORMAT,*LP_WAV_FORMAT;
/* 数据块头*/
typedef struct _DATA_CHUNK
{
char szDataID[4]; // 'd','a','t','a'
DWORD dwDataSize; // 接下来数据的长度
}DATA_CHUNK,*LP_DATA_CHUNK;
typedef struct _FMT_CHUNK
{
char szFmtID[4] ; // 'f','m','t',' '
DWORD dwFmtSize ; // 一般等于16,表示WAVE_FORMAT的字节数
WAV_FORMAT wavFormat; //这个结构体大小刚好为16
}FMT_CHUNK,*LP_FMT_CHUNK;
WAV的声音数据,读出来的就直接是PCM格式的。而声卡播放时,需要输入的也是PCM格式的,所以中间不需要任何转码解码过程,只需要告诉声卡PCM格式的参数(从WAV文件读到的头就包含这些参数)
WaveOut 是Windows上比较低端的一组API,可以认为是直接操作硬件声卡的API,使用它进行声音播放非常的不方便,本文档主要是教大家使用方法,并不提供高级控制的技巧。
WaveOut 播放的的流程
1, 检测声卡个数。
2, 打开一个声卡。
3, 创建缓存。
4, 读数据到缓存。
5, 将缓存发送到声卡。
本程序中关于读文件部分使用MFC 的CFile。控制播放的流程也很粗略,使用的while循环检测的方式。WaveOut播放完一段缓存会改写一个标志,程序就知道了应该继续往里加数据了,而播放出来有噪音,也是可以理解的,注意那个sleep那里。系统已经播放完了,我们才能得到状态,还有执行两句代码才将数据继续发送到声卡,中间有卡断,所以有噪音,播放不流畅。
没关系,只是为了演示WaveOut的用法,至于说怎么样播放流畅,请期待下一节,使用Direct组件 DirectSound进行流畅的声音播放。
附全部代码: 控制台工程,共享方式使用MFC。
// WavePlayer.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include "afx.h"
#include "mmsystem.h"
#pragma comment(lib,"winmm.lib")
/* WAV Riff文件头*/
typedef struct _RIFF_HEADER
{
char szRiffID[4]; // 'R','I','F','F'
DWORD dwRiffSize; //从下一字节,到文件结束的字节数。加上就是整个文件的大小
char szRiffFormat[4]; // 'W','A','V','E'
}RIFF_HEADER,*LP_RIFF_HEADER;
typedef struct _WAV_FORMAT
{
WORD wFormatTag ; //格式种类1为PCM
WORD wTracks ; //声道数
DWORD dwSamplesPerSec ; //采样频率
DWORD dwAvgBytesPerSec; //每秒的字节数,是大B。
WORD wBlockAlign ; //数据的调整数,按B计算
WORD wBitsPerSample ; //样本采样位数
}WAV_FORMAT,*LP_WAV_FORMAT;
/* 数据块头*/
typedef struct _DATA_CHUNK
{
char szDataID[4]; // 'd','a','t','a'
DWORD dwDataSize; // 接下来数据的长度
}DATA_CHUNK,*LP_DATA_CHUNK;
typedef struct _FMT_CHUNK
{
char szFmtID[4] ; // 'f','m','t',' '
DWORD dwFmtSize ; // 一般等于,表示WAVE_FORMAT的字节数
WAV_FORMAT wavFormat; //这个结构体大小刚好为
}FMT_CHUNK,*LP_FMT_CHUNK;
int _tmain(int argc, _TCHAR* argv[])
{
LPCTSTR pWavFilePath = _T("C:/Lenka - Trouble Is A Friend.wav") ;
CFile file ;
file.Open(pWavFilePath,CFile::modeRead|CFile::shareDenyWrite);
RIFF_HEADER riffHeader ;
memset(&riffHeader,0,sizeof(RIFF_HEADER));
file.Read(&riffHeader,sizeof(RIFF_HEADER));
FMT_CHUNK fmtBlock ;
memset(&fmtBlock,0,sizeof(FMT_CHUNK));
file.Read(&fmtBlock,sizeof(FMT_CHUNK));
DATA_CHUNK dataBlock;
memset(&dataBlock,0,sizeof(DATA_CHUNK));
file.Read(&dataBlock,sizeof(DATA_CHUNK));
printf("文件大小应该为%u字节。\n",riffHeader.dwRiffSize + 8); //8是前面的四个字节
printf("所有的头信息包含%u字节。\n",sizeof(RIFF_HEADER) + sizeof(FMT_CHUNK) + sizeof(DATA_CHUNK));
UINT uiWaveOutDevNum = waveOutGetNumDevs();
if(uiWaveOutDevNum == 0)
{
MessageBox(NULL, _T("waveOutGetNumDevs"), _T("waveOut声音播放"),MB_ICONINFORMATION);
return 0 ;
}
WAVEFORMATEX winWaveFormatEx ;
winWaveFormatEx.wFormatTag = fmtBlock.wavFormat.wFormatTag ;
winWaveFormatEx.nChannels = fmtBlock.wavFormat.wTracks ;
winWaveFormatEx.nSamplesPerSec = fmtBlock.wavFormat.dwSamplesPerSec ;
winWaveFormatEx.nAvgBytesPerSec = fmtBlock.wavFormat.dwAvgBytesPerSec ;
winWaveFormatEx.nBlockAlign = fmtBlock.wavFormat.wBlockAlign ;
winWaveFormatEx.wBitsPerSample = fmtBlock.wavFormat.wBitsPerSample;
winWaveFormatEx.cbSize = sizeof(WAVEFORMATEX);
HWAVEOUT hWinWaveOut = NULL;
MMRESULT mmresult ;
//打开一个音频设备,设置回调函数和标志参数[0x10011001为标志,在回调中dwInstance就是这个值]
mmresult = waveOutOpen(&hWinWaveOut,/*WAVE_MAPPER*/0,&winWaveFormatEx,NULL,NULL,0);
if(MMSYSERR_NOERROR != mmresult)
{
MessageBox(NULL, _T("waveOutOpen"), _T("waveOut声音播放"),MB_ICONINFORMATION);
return 0 ;
}
//双缓冲区播放
const DWORD bufLen = 128 * 1024 ;//缓存大小,K。
LPBYTE pDataBuffer[2] ;
pDataBuffer [0]= new BYTE[bufLen] ;//数据缓存。
pDataBuffer [1]= new BYTE[bufLen] ;//数据缓存。
WAVEHDR winWaveHdr[2] ;
winWaveHdr[0].lpData = (LPSTR)pDataBuffer[0] ;
winWaveHdr[0].dwBufferLength = bufLen ; //存放缓存最大长度
winWaveHdr[0].dwBytesRecorded = 0 ; //存放当前缓存有多少数据
winWaveHdr[0].dwUser = 0 ;
winWaveHdr[0].dwFlags = WHDR_DONE ;
winWaveHdr[0].dwLoops = 1 ;
winWaveHdr[0].lpNext = NULL ;
winWaveHdr[0].reserved = 0 ;
winWaveHdr[1].lpData = (LPSTR)pDataBuffer[1] ;
winWaveHdr[1].dwBufferLength = bufLen ; //存放缓存最大长度
winWaveHdr[1].dwBytesRecorded = 0 ; //存放当前缓存有多少数据
winWaveHdr[1].dwUser = 0 ;
winWaveHdr[1].dwFlags = WHDR_DONE ;
winWaveHdr[1].dwLoops = 1 ;
winWaveHdr[1].lpNext = NULL ;
winWaveHdr[1].reserved = 0 ;
int iBufferIndex = 0 ;//由这个变量来判断是该使用哪个缓存
DWORD dwReadedBytes = 0 ;
while((dwReadedBytes = file.Read(pDataBuffer[iBufferIndex],bufLen)) > 0)
{//读取成功并得到有效数据
winWaveHdr[iBufferIndex].dwBytesRecorded = dwReadedBytes ;
//准备音频数据,这里判断的和正在播放的不是同一个缓冲区,可以更快的准备好数据。
while(!(winWaveHdr[iBufferIndex].dwFlags & WHDR_DONE))
{//waveOutWrite函数的对应的播放线程如果使用完了这个缓存,会给一个WHDR_DONE标志
Sleep(1);
}
mmresult = waveOutPrepareHeader(hWinWaveOut,&winWaveHdr[iBufferIndex],sizeof(WAVEHDR));
if(MMSYSERR_NOERROR != mmresult)
{
MessageBox(NULL, _T("waveOutPrepareHeader"), _T("waveOut声音播放"),MB_ICONINFORMATION);
return 0 ;
}
if(!(winWaveHdr[iBufferIndex].dwFlags & WHDR_PREPARED))
{
MessageBox(NULL, _T("waveOutPrepareHeader"), _T("waveOut声音播放"),MB_ICONINFORMATION);
return 0 ;
}
//写入音频数据到设备
mmresult = waveOutWrite(hWinWaveOut,&winWaveHdr[iBufferIndex],sizeof(WAVEHDR));
if(MMSYSERR_NOERROR != mmresult)
{
MessageBox(NULL, _T("waveOutWrite"), _T("waveOut声音播放"),MB_ICONINFORMATION);
return 0 ;
}
//控制切换缓冲区
iBufferIndex = iBufferIndex ? 0 : 1 ;
}
delete pDataBuffer[0] ;
pDataBuffer[0] = NULL ;
delete pDataBuffer[1] ;
pDataBuffer[1] = NULL ;
winWaveHdr[0].lpData = NULL ;
winWaveHdr[1].lpData = NULL ;
waveOutUnprepareHeader(hWinWaveOut,&winWaveHdr[0],sizeof(WAVEHDR));
waveOutUnprepareHeader(hWinWaveOut,&winWaveHdr[1],sizeof(WAVEHDR));
waveOutClose(hWinWaveOut);
hWinWaveOut = NULL ;
file.Close();
printf("播放完了。\n按任意键结束...");
getchar();
return 0;
}