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