多媒體編程——聲音播放(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;
}


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章