使用LibMad解碼MP3,Windows上播放MP3,MP3轉WAV例項程式碼
阿新 • • 發佈:2019-01-26
概述:
閒著也是閒著,就學習了下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左右,還算小的了。