多媒体编程——声音播放(1)

多媒体编程——声音播放(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;
}


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值