1. 程式人生 > >使用LibMad解碼MP3,Windows上播放MP3,MP3轉WAV例項程式碼

使用LibMad解碼MP3,Windows上播放MP3,MP3轉WAV例項程式碼

概述:

閒著也是閒著,就學習了下LibMad解碼MP3(解碼成PCM資料流),順便把Windows上播放PCM,以及PCM檔案轉換成WAV檔案學習了下。以前做過PCM轉WAV,原理很簡單,就是在PCM流前面加上一個WAV資料頭。借鑑了網上的一些文章,感謝大家。

LIbMad解碼部分:

在初始化LibMad前,我們需要定義回撥函式

//讀取mp3資源的函式
mad_flow input_fun(void *data, struct mad_stream *stream)
{
	MP3File* pMp3 = (MP3File*)data;
	if ( stream->buffer == NULL )
	{
		mad_stream_buffer(stream, pMp3->szBuffer, pMp3->nBufferUse);
		return MAD_FLOW_CONTINUE;
	}

	pMp3->nBufferPos = stream->next_frame - pMp3->szBuffer;
	int nRemain = pMp3->nBufferUse - pMp3->nBufferPos;
	memcpy(pMp3->szBuffer, pMp3->szBuffer+pMp3->nBufferPos, nRemain);
	size_t nRead = fread(pMp3->szBuffer+nRemain, 1, pMp3->nBufferPos, pMp3->fp);
	if ( nRead <= 0 )
	{
		return MAD_FLOW_STOP;
	}
	pMp3->nBufferPos = 0;
	pMp3->nBufferUse = nRemain + nRead;
	mad_stream_buffer(stream, pMp3->szBuffer, pMp3->nBufferUse);
	return MAD_FLOW_CONTINUE;
}

//處理mp3頭部資訊的函式
mad_flow header_fun(void *data, struct mad_header const *header)
{
	return MAD_FLOW_CONTINUE;
}

////輸出函式
mad_flow output_fun(void *data, struct mad_header const *header, struct mad_pcm *pcm)
{
	int *left_ch = pcm->samples[0], *right_ch = pcm->samples[1];
	int nLength = pcm->length;
	int nMalloc = nLength*2;
	if (pcm->channels == 2)//雙聲道,儲存空間擴大4倍
		nMalloc = nLength * 4;
	if ( g_pBuffer == NULL )
		g_pBuffer = (unsigned char*)malloc(nMalloc);
	else
	{
		if ( g_nSize<nMalloc )
		{
			free(g_pBuffer);
			g_pBuffer = (unsigned char*)malloc(nMalloc);
		}
	}
	g_nSize = nMalloc;
	unsigned char* pBase = g_pBuffer;
	for (int i = 0; i < nLength; ++i)
	{
		signed int sample = scale(*left_ch++);
		*(pBase++) = sample >> 0;
		*(pBase++) = sample >> 8;
		if (pcm->channels == 2)
		{
			signed int sample = scale(*right_ch++);
			*(pBase++) = sample >> 0;
			*(pBase++) = sample >> 8;
		}
	}
	//播放
	g_pcmPlayer.Play(g_pBuffer, nMalloc);

// 	FILE* fp = fopen("mp3.dat", "ab+");
// 	if( fp )
// 	{
// 		fwrite(pBase, 1, nMalloc, fp);
// 		fclose(fp);
// 	}
	g_nFrames++;
	//g_nTime += header->duration;
	return MAD_FLOW_CONTINUE;
}

mad_flow error_fun(void *data, struct mad_stream *stream, struct mad_frame *frame)
{
	return MAD_FLOW_CONTINUE;
}

/*這一段是處理取樣後的pcm音訊 */
static inline signed int scale(mad_fixed_t sample)
{
	sample += (1L << (MAD_F_FRACBITS - 16));
	if (sample >= MAD_F_ONE)
		sample = MAD_F_ONE - 1;
	else if (sample < -MAD_F_ONE)
		sample = -MAD_F_ONE;
	return sample >> (MAD_F_FRACBITS + 1 - 16);
}
初始化
MP3File m;
	memset(&m, 0, sizeof(MP3File)); 
	m.fp = fopen(pFile, "rb");
	if ( m.fp == NULL )
	{
		return 0;
	}
	fseek(m.fp, 0, SEEK_END);
	m.nFileSize = ftell(m.fp);
	fseek(m.fp, 0, SEEK_SET);
	size_t nRead = fread(m.szBuffer, 1, BUFFER_LEN, m.fp);
	m.nBufferUse = nRead;
	struct mad_decoder decoder;
	mad_decoder_init(&decoder,
		&m,
		input_fun,
		header_fun,
		NULL,//濾波(未設定,置0)
		output_fun,
		error_fun,//錯誤函式
		NULL//資訊函式
		);
	/* start decoding */
	int result = mad_decoder_run(&decoder, MAD_DECODER_MODE_SYNC);

	/* release the decoder */
	mad_decoder_finish(&decoder);
	g_pcmPlayer.SetEncodeEnd(true);
	while( g_pcmPlayer.IsPlaying() )
		Sleep(1000);
	if ( g_pBuffer )
		free(g_pBuffer);

這裡定義了下一MP3結構體,主要是為了在讀取檔案過程中不斷往後移動指標,記錄讀取位置。簡單的講,就是要把MP3檔案,一段一段得讀取到記憶體中,然後送給LibMad去解碼。檔案讀取最基礎的東西了,不在這裡贅述。
#define BUFFER_LEN	1024*10
struct MP3File
{
	FILE* fp;
	size_t nFileSize;
	int nFilePos;
	unsigned char szBuffer[BUFFER_LEN];
	int nBufferPos;
	int nBufferUse;
};

重點處理在解碼後的回撥函式output_fun,在這裡返回給我們的就是解碼後的PCM資料流了,我們可以將其儲存到檔案中,也可以將其輸送到Windows音訊裝置進行播放了。不過在做這些之前,需要進行一些處理,見程式碼。

在Windows上播放:

播放的原理:Windows提供了一系列WAVE API,操作相當簡單,我們只需要把PCM資料填入到一個一個的WAVEHDR資料頭中,然後把這個資料頭送到裝置中就可以播放了。寫了一個簡單的PCM播放類,程式碼如下

#pragma once
#include "Lock.h"
#include <vector>
using std::vector;


typedef bool (*PfnWaveOutProcDone)(LPWAVEHDR lpHdr, void* lpParam);

class CPcmPlayer
{
public:
	CPcmPlayer(PfnWaveOutProcDone pDataCb=NULL, void* lpParam=NULL);
	~CPcmPlayer(void);
	void SetDataCallback(PfnWaveOutProcDone pDataCb, void* lpParam);
	bool Init();
	void Play(void* lpData, int nSize);
	bool IsPlaying()const			{ return m_nCount>0; }
	void SetEncodeEnd(bool bEnd)	{ m_bDecodeEnd = true; }
	void Stop();

protected:
	static void CALLBACK waveOutProc(HWAVEOUT hwo, UINT uMsg, DWORD_PTR dwInstance, DWORD_PTR dwParam1, DWORD_PTR dwParam2);
	LPWAVEHDR	GetWaveHdr();				//申請一個數據頭
	void		Reuse(LPWAVEHDR lpHdr);		//使用完一個數據頭後,回收再利用
	void		Release();					//釋放申請的空間
	void		FreeWaveHdr(LPWAVEHDR lpHdr);

private:
int		m_nHdrCount;
	bool	m_bDecodeEnd;					//解碼完成後設定,釋放不用記憶體,減少記憶體佔用提高系統效率
	LONG	m_nCount;						//記錄新增播放資料頭的數目,已確定是否播放完畢
	HWAVEOUT	m_hDev;	
	WAVEFORMATEX m_wavFmt;
	PfnWaveOutProcDone	m_pDataCallback;
	void*	m_lpParam;
	CLock	m_lockList;
	vector<LPWAVEHDR> m_freeList;
	vector<LPWAVEHDR> m_mallocList;

};
#include "StdAfx.h"
#include "PcmPlayer.h"

CPcmPlayer::CPcmPlayer(PfnWaveOutProcDone pDataCb/*=NULL*/, void* lpParam/*=NULL*/)
: m_hDev(NULL)
, m_pDataCallback(pDataCb)
, m_lpParam(lpParam)
, m_nCount(0)
, m_bDecodeEnd(false)
, m_nHdrCount(40)//MP3解碼速度遠快於播放速度,這個快取數目還是應該大一點
{
	m_wavFmt.wFormatTag = WAVE_FORMAT_PCM;
	m_wavFmt.nChannels = 2;
	m_wavFmt.nSamplesPerSec = 44100;
	m_wavFmt.nAvgBytesPerSec = 44100 * 16 * 2 / 8;
	m_wavFmt.nBlockAlign = 4;
	m_wavFmt.wBitsPerSample = 16;
	m_wavFmt.cbSize = 0;
}

CPcmPlayer::~CPcmPlayer(void)
{
	Release();
}

bool CPcmPlayer::Init()
{
	MMRESULT mr = waveOutOpen(&m_hDev, WAVE_MAPPER, &m_wavFmt, (DWORD_PTR)waveOutProc, (DWORD_PTR)this, CALLBACK_FUNCTION);
	if ( mr != MMSYSERR_NOERROR )
		return false;
	WAVEOUTCAPS woc;  
	waveOutGetDevCaps((UINT_PTR)m_hDev, &woc, sizeof(WAVEOUTCAPS));
	m_freeList.reserve(m_nHdrCount);
	m_mallocList.reserve(m_nHdrCount);
	for ( int i=0; i<m_nHdrCount; ++i )
	{
		LPWAVEHDR lpHdr = (LPWAVEHDR)malloc(sizeof(WAVEHDR));
		memset(lpHdr, 0, sizeof(WAVEHDR));
		m_freeList.push_back(lpHdr);
		m_mallocList.push_back(lpHdr);
	}
	return true;
}

void CALLBACK CPcmPlayer::waveOutProc(HWAVEOUT hwo, UINT uMsg, DWORD_PTR dwInstance, DWORD_PTR dwParam1, DWORD_PTR dwParam2)
{
	CPcmPlayer* pThis = (CPcmPlayer*)dwInstance;
	switch (uMsg)
	{
	case WOM_CLOSE:
		break;
	case WOM_DONE:
		{//一個數據頭裡的資料播放完畢,回收利用
 			LPWAVEHDR lpHdr = (LPWAVEHDR)dwParam1;
			if ( pThis->m_pDataCallback )
			{
				bool bRet = pThis->m_pDataCallback(lpHdr, pThis->m_lpParam);
				if ( bRet )
					waveOutWrite(hwo, lpHdr, sizeof(WAVEHDR));
			}
			if ( pThis->m_bDecodeEnd )
			{//解碼完成了,直接釋放記憶體
				pThis->FreeWaveHdr(lpHdr);
			}
			else
				pThis->Reuse(lpHdr);
			break;
		}
	case WOM_OPEN:
		break;
	}
}

void CPcmPlayer::SetDataCallback(PfnWaveOutProcDone pDataCb, void* lpParam)
{
	m_pDataCallback = pDataCb;
	m_lpParam = lpParam;
}

void CPcmPlayer::Play(void* lpData, int nSize)
{
	if ( nSize<=0 )
		return ;
	LPWAVEHDR hdr = GetWaveHdr();
	if ( hdr->dwBufferLength == 0 )
		hdr->lpData = (LPSTR)malloc(nSize);
	else if ( hdr->dwBufferLength<nSize )
	{
		free(hdr->lpData);
		hdr->lpData = (LPSTR)malloc(nSize);
	}
	memcpy(hdr->lpData, lpData, nSize);
	hdr->dwBufferLength = nSize;
	waveOutPrepareHeader(m_hDev, hdr, sizeof(WAVEHDR));
	waveOutWrite(m_hDev, hdr, sizeof(WAVEHDR));
	InterlockedExchangeAdd(&m_nCount, 1);
	/**************************************************************
	*解碼速度太快,在這裡通過判斷還有多少個未處理的頭進行等待
	*/
	static int nCount = 0;
	nCount++;
	if ( nCount>=m_nHdrCount )
	{
		while( m_nCount>1 )
			Sleep(100);
		nCount = 0;
	}
}

LPWAVEHDR CPcmPlayer::GetWaveHdr()
{
	CScopeLock sl(m_lockList);
	if ( m_freeList.empty() )
	{
		for( int i=0; i<5; ++i )
		{//申請資料頭後,一定要把記憶體清零
			LPWAVEHDR lpHdr = (LPWAVEHDR)malloc(sizeof(WAVEHDR));
			memset(lpHdr, 0, sizeof(WAVEHDR));
			m_freeList.push_back(lpHdr);
			m_mallocList.push_back(lpHdr);
		}
	}
	LPWAVEHDR lpRet = m_freeList[0];
	m_freeList.erase(m_freeList.begin());
	return lpRet;
}

void CPcmPlayer::Reuse(LPWAVEHDR lpHdr)
{
	CScopeLock sl(m_lockList);
	m_freeList.push_back(lpHdr);
	InterlockedExchangeAdd(&m_nCount, -1);
}

void CPcmPlayer::Release()
{
	for ( size_t i=0; i<m_mallocList.size(); ++i )
	{
		LPWAVEHDR lpHdr = m_mallocList[i];
		if ( lpHdr->lpData )
			free(lpHdr->lpData);
		free(lpHdr);
	}
	m_mallocList.clear();
}

void CPcmPlayer::FreeWaveHdr(LPWAVEHDR lpHdr)
{
	CScopeLock sl(m_lockList);
	vector<LPWAVEHDR>::iterator it = m_mallocList.begin();
	for ( ; it!=m_mallocList.end(); ++it )
	{
		if ( (*it) == lpHdr )
		{
			m_mallocList.erase(it);
			break;
		}
	}
	free(lpHdr->lpData);
	free(lpHdr);
}

void CPcmPlayer::Stop()
{
	if ( m_hDev )
	{
		waveOutReset(m_hDev);
		waveOutClose(m_hDev);
		m_hDev = NULL;
	}
}

這裡主要涉及到記憶體的迴圈利用,提高系統執行效率,後面會說說。

MP3轉WAV:

在解碼函式中,我會把PCM流儲存到檔案中,最後整個解碼流程完成後,計算檔案大小,填充WAV資料頭,將其寫入WAV檔案就可以了,這裡我專門寫了一個函式進行轉換

// TestPcm.cpp : 定義控制檯應用程式的入口點。
//
/**********************************************
*Jelin
*MailTo://jelinyao163.com
**********************************************/

#include "stdafx.h"
#include <windows.h>
#include <MMSystem.h>
#pragma comment(lib, "Winmm.lib")


//檔案頭結構體
typedef struct _WAV_HEADER  
{  
	char	fccID[4];//識別符號RIFF        
	ULONG	dwSize;//從下個地址開始到檔案尾的總位元組數            
	char	fccType[4];//WAV檔案標誌(WAVE)    
}WAV_HEADER,*LPWAV_HEADER;  

typedef struct _WAV_FMT
{  
	char   fccID[4];//波形格式標誌(FMT)       
	ULONG   dwSize;//過濾位元組(一般為00000010H)            
	USHORT  wFormatTag;//格式種類(值為1時,表示資料為線性PCM編碼)    
	USHORT  wChannels;//通道數,單聲道為1,雙聲音為2  
	ULONG   dwSamplesPerSec;//取樣頻率 
	ULONG   dwAvgBytesPerSec;//波形資料傳輸速率(每秒平均位元組數)  
	USHORT  wBlockAlign;//資料的調整數(按位元組計算)  
	USHORT  uiBitsPerSample;//樣本資料位數 
}WAV_FMT,*LPWAV_FMT;  

typedef struct _WAV_DATA
{ 
	char   fccID[4];//資料標誌符(data)         
	ULONG   dwSize;//取樣資料總數             
}WAV_DATA,*LPWAV_DATA;  

inline bool PcmToWav(const char* pData, int nSize, const TCHAR* pWavFile)
{
	//初始化檔案頭結構體
	WAV_HEADER head;
	memset(&head,0,sizeof(head));
	memcpy(head.fccID, "RIFF", 4);
	memcpy(head.fccType, "WAVE", 4);
	head.dwSize=nSize+sizeof(WAV_FMT)+sizeof(WAV_DATA)+4;
	WAV_FMT fmt;
	memset(&fmt,0,sizeof(fmt));
	memcpy(fmt.fccID, "fmt ", 4);
	fmt.dwSamplesPerSec = 44100;//44100;  
	fmt.dwAvgBytesPerSec=fmt.dwSamplesPerSec*sizeof(unsigned short)*2;  
	fmt.uiBitsPerSample=16;  
	fmt.dwSize=16;  
	fmt.wBlockAlign=4;  
	fmt.wChannels = 2;  
	fmt.wFormatTag = WAVE_FORMAT_PCM;  
	WAV_DATA data;
	memset(&data,0,sizeof(data));
	memcpy(data.fccID, "data", 4);
	data.dwSize = nSize;
	//fpRead=NULL;
	FILE *fpWrite=NULL;
	fpWrite=_tfopen(pWavFile,_T("wb+"));
	if(!fpWrite)
	return false;
	//寫入資料頭
	int nRet = fwrite(&head, 1, sizeof(head), fpWrite);
	nRet = fwrite(&fmt, 1, sizeof(fmt),fpWrite);
	nRet = fwrite(&data, 1, sizeof(data), fpWrite);
	nRet = fwrite(pData, 1, nSize, fpWrite);
	//關閉檔案流
	fclose(fpWrite);
	return true;
}
WAV頭包括一個檔案資訊頭和一個WAVE格式頭,寫入頭資料後在後面追加上PCM流,OK,WAV檔案建立完畢,就這麼簡單。

後記:

還有以下細節沒處理,比如使用可等待事件來替代Sleep函式,這個快取資料頭數量可以提供介面設定等。這樣處理後播放一個4M左右的MP3佔用記憶體也才3M左右,還算小的了。