FC2ブログ

ホーム > MP3ファイルフォーマット > MP3ファイルの再生 第2部

MP3ファイルの再生 第2部



MP3ファイルの読み込み手順は以下の通りです。

  1. MP3ファイルを開いて、ID3v2ヘッダー/ID3v1フッターの有無を調べます。
  2. フレームヘッダーを探し出し、そのデータからMPEGLAYER3WAVEFORMAT構造体を作成します。同時に一番最初のフレームヘッダーの先頭位置をMP3データの先頭位置として保存し、ファイルサイズからこの先頭位置とフッターがあればフッターサイズを差し引いて、MP3データサイズを算出します。
  3. 作成したMPEGLAYER3WAVEFORMAT構造体から、デコード後のWAVEFORMATEX構造体を取得します。
  4. ACMストリームをオープンします。
  5. MP3データのブロックサイズから変換後のWAVE音源データのブロックサイズを取得します。
  6. MP3の総データサイズから変換後のWAVE音源の総データサイズを取得し、WAVE音源データのブロックサイズの平均値を算出します。(再生中における再生位置変更時に、ファイル上での相対位置を算出するために必要になります。)
  7. 作業用バッファを確保して、ACMストリームヘッダーを作成します。
  8. 先に取得したWAVEFORMATEX構造体を元にWAVE出力デバイスを開きます。
  9. WAVE出力デバイスからのコールバックイベントから呼び出されるオーバーライド関数(StreamIn関数)において、MP3ファイルからMP3データを読み込み、WAVE変換して得られた音源データをダブルバッファに書き込みながら演奏していきます。
  10. ファイル末尾まで読み込むか、演奏停止イベントがあればWAVE出力デバイスを停止してから閉じます。
  11. 次のファイルが読み込まれるか、デストラクタが呼び出されれば、ACMストリームヘッダーを削除してから、ACMストリームをクローズします。





MP3ファイルをオープンして、ReadHeader関数を呼び出します。

//Mp3ファイルをオープンします。
// lpszFileName:オープンするファイル名。

BOOL CMP3File::OpenFile(LPCTSTR lpszFileName)
{
Delete();

if (!lpszFileName) return FALSE;
//拡張子名の取得
LPTSTR pExt=::PathFindExtension(lpszFileName);
//拡張子名が「.mp3」若しくは「.mpeg」で無い場合は、
//CDDAFile::OpenFile関数を呼び出します。

if ((StrCmpNI(pExt,_T(".mp3"),4)!=0)&&
(StrCmpNI(pExt,_T(".mpeg"),5)!=0))
{
return CDDAFile::OpenFile(lpszFileName);
}

//ファイルのオープンに失敗した場合は、FALSEを戻して終了します。
CFile* infile=new CFile;
if (!infile->Open(lpszFileName,CFile::modeRead)){
delete infile;
return FALSE;
}

//ヘッダー読み込みに失敗した場合は、CDDAFile::OpenFile関数を呼び出します。
if (!ReadHeader(infile)){
infile->Close();
delete infile;
return CDDAFile::OpenFile(lpszFileName);
}
m_infile=infile;
m_strPathName=lpszFileName;
return TRUE;
}


MP3ファイルフレームヘッダからMPEGLAYER3WAVEFORMAT構造体を作成し、この構造体を元にACMストリームをオープンし、デコードと同時に演奏するための準備としてACMストリームヘッダを作成しておきます。

//MP3ヘッダーの読み込み
BOOL CMP3File::ReadHeader(CFile* infile)
{
ASSERT(AfxIsValidAddress(infile,sizeof(CFile),0));

//読み飛ばし位置は、現在のファイル位置とします。
m_dwSkipOffset=(DWORD)infile->GetPosition();
//ファイルサイズを取得します。
DWORD fileSize=(DWORD)(DWORD_PTR)infile->GetLength();

//フレームヘッダーを探します。
BYTE header[128];
FRAMEHEADER frameHeader={0};
while(TRUE){
//読み飛ばし位置に移動してから10バイト読みます。
infile->Seek(m_dwSkipOffset,CFile::begin);
if (infile->Read(header,10)!=10) return FALSE;
//フレームヘッダーがあった場合。
if (_ParseFrameHeader((PBYTE)&header,&frameHeader)){
//ファイルポインタ
DWORD offset=m_dwSkipOffset+frameHeader.frameSize;
//フレームサイズが正しいかどうか調べます。
FRAMEHEADER fh={0};
//読み飛ばし位置に移動してから4バイト読みます。
infile->Seek(offset,CFile::begin);
if (infile->Read(header,4)!=4) break;
//フレームヘッダーかどうか調べます。
if (_ParseFrameHeader((PBYTE)&header,&fh)) break;
m_dwSkipOffset++;
}
//"ID3"タグがある場合。
else if (::memcmp(header,"ID3",3)==0){
//ID3V2タグサイズを算出します。
DWORD sizeTag=((header[6]&0x7F)<<21)|((header[7]&0x7F)<<14)|
((header[8]&0x7F)<< 7)|(header[9]&0x7F);
//読み飛ばし位置を算出します。
m_dwSkipOffset+=(sizeTag+10);
//バッファに生データを保存します。
BYTE* pBuffer=NULL;
switch(header[3]){
case 2: //ID3v2.2タグの場合
if (!m_byteID3v22.GetSize()){
m_byteID3v22.SetSize(sizeTag+10);
pBuffer=m_byteID3v22.GetData();
m_bID3v22=TRUE;
}
break;
case 3: //ID3v2.3タグの場合
if (!m_byteID3v23.GetSize()){
m_byteID3v23.SetSize(sizeTag+10);
pBuffer=m_byteID3v23.GetData();
m_bID3v23=TRUE;
}
break;
case 4: //ID3v2.4タグの場合
if (!m_byteID3v24.GetSize()){
m_byteID3v24.SetSize(sizeTag+10);
pBuffer=m_byteID3v24.GetData();
m_bID3v24=TRUE;
}
break;
default:
pBuffer=NULL;
}
//ID3v2タグバッファが確保されている場合は、
//ID3v2タグの内容をバッファに読み込みます。

if (pBuffer){
::CopyMemory(pBuffer,&header,10);
UINT size=infile->Read(pBuffer+10,sizeTag);
if (size<sizeTag) return FALSE;
}
}
else
//フレームヘッダーではなく、ID3v2タグでもない場合は、
//読み飛ばし位置を1バイト進めてから再び読み直します。

m_dwSkipOffset++;
}

//フレームヘッダーから得られた値を、それぞれメンバー変数に保存します。
m_iVersion =frameHeader.version; //MPEGバージョン番号
m_iLayer =frameHeader.layer; //レイヤー番号
m_iBitRate =frameHeader.bitRate; //ビットレート
m_mp3BlockSize=frameHeader.frameSize; //Mp3ブロックサイズ

//MPEGLAYER3WAVEFORMAT構造体を作成します。
MPEGLAYER3WAVEFORMAT mp3wf={0};

mp3wf.wfx.wFormatTag =WAVE_FORMAT_MPEGLAYER3;
mp3wf.wfx.nChannels =frameHeader.channel;
mp3wf.wfx.nSamplesPerSec =frameHeader.sampleRate;
mp3wf.wfx.nAvgBytesPerSec =(frameHeader.bitRate*1000)/8;
mp3wf.wfx.nBlockAlign =1;
mp3wf.wfx.wBitsPerSample =0;
mp3wf.wfx.cbSize =MPEGLAYER3_WFX_EXTRA_BYTES;

mp3wf.wID =MPEGLAYER3_ID_MPEG;
mp3wf.fdwFlags =(frameHeader.padding)?MPEGLAYER3_FLAG_PADDING_ON:MPEGLAYER3_FLAG_PADDING_OFF;
mp3wf.nBlockSize =frameHeader.frameSize;
mp3wf.nFramesPerBlock =1;
mp3wf.nCodecDelay =1393;

//Xingヘッダーがあるか調べます。
m_uFlagBitRate=CBR;
DWORD offset=(frameHeader.version==1)?
((frameHeader.channel==1)?17:32):((frameHeader.channel==1)?9:17);
infile->Seek(m_dwSkipOffset+offset+4,CFile::begin);
if (infile->Read(header,4)!=4) return FALSE;

//Xingヘッダーがある場合
if ((::memcmp(header,"Xing",4)==0)||
(::memcmp(header,"Info",4)==0))
{
m_uFlagBitRate=VBR;
if (infile->Read(header,4)!=4) return FALSE;
DWORD dwflags=(((((header[0]<<8)|header[1])<<8)|header[2])<<8)|header[3];
if (dwflags&1){//フレーム数の取得
if (infile->Read(header,4)!=4) return FALSE;
m_nFrames=(((((header[0]<<8)|header[1])<<8)|header[2])<<8)|header[3];
}
if (dwflags&2){//データ総数
if (infile->Read(header,4)!=4) return FALSE;
m_dwStreamSize=(((((header[0]<<8)|header[1])<<8)|header[2])<<8)|header[3];
}
if (dwflags&4){//TOCエントリー
if (infile->Read(m_vbrTOC,100)!=100) return FALSE;
}
if (dwflags&8){//VBR品質((高)0~100(低))
if (infile->Read(header,4)!=4) return FALSE;
m_vbrQuality=(((((header[0]<<8)|header[1])<<8)|header[2])<<8)|header[3];
}
}

//すべてのフレームを調べて、フレーム総数と平均ビットレートを算出します。
int nFrames=1;
//ビットレートの合計
double ttlBitRate=0.0;
//データ総数
DWORD dwStreamSize=m_mp3BlockSize;
//ファイルポインタ
offset=m_dwSkipOffset+m_mp3BlockSize;

//すべてのフレームについて調べます。
while(TRUE){
//次のフレームの先頭位置を探します。
BOOL bFind=FALSE;
while(TRUE){
//読み飛ばし位置に移動してから4バイト読みます。
infile->Seek(offset,CFile::begin);
if (infile->Read(header,4)!=4) break;
//フレームヘッダーかどうか調べます。
if (_ParseFrameHeader((PBYTE)&header,&frameHeader)){
bFind=TRUE;
break;
}
//見つからない場合は、
//ファイルポインタを1つ進めます。

offset++;
}
if (!bFind) break;
//一番目のフレームのビットレートと違えば、ABRにします。
if ((m_uFlagBitRate==CBR)&&(m_iBitRate!=frameHeader.bitRate))
m_uFlagBitRate=ABR;

ttlBitRate+=(double)frameHeader.bitRate;
offset+=frameHeader.frameSize;
dwStreamSize+=frameHeader.frameSize;
nFrames++;
}
//平均ビットレートを算出します。
m_iAveBitRate=(int)(ttlBitRate/(double)nFrames);
m_nFrames=nFrames;
if (!m_dwStreamSize) m_dwStreamSize=dwStreamSize;

//ID3v1タグがあるか調べます。
//ファイル末尾から128バイト戻った位置にシークします。

infile->Seek(fileSize-128,CFile::begin);

UINT size=infile->Read(header,128);
if (size<128) return FALSE;

//"TAG"がある場合。(ID3v1タグ)
if (::memcmp(header,"TAG",3)==0){
//ID3v1タグがある場合は、ID3v1タグサイズを無視すべきファイル末尾のバイト数として保存します。
m_dwSkipBottom=128;
//バージョン番号を保存します。
m_id3v1Version=0x100|((header[125]=='\0')?1:0);
//バッファに生データを保存します。
m_byteId3v1.SetSize(128);
::CopyMemory(m_byteId3v1.GetData(),&header,128);
}

//Mp3総データサイズは、ファイル長さから読み飛ばし位置と、
//フッターサイズを差し引きます。

m_mp3StreamSize=fileSize-m_dwSkipOffset-m_dwSkipBottom;

//ファイルサイズから算出した総データサイズがすべてのMP3ストリームサイズより大きい場合は、
//MP3ストリームサイズをMp3総データサイズとします。

if (m_mp3StreamSize>m_dwStreamSize)
m_mp3StreamSize=m_dwStreamSize;

//現在のファイル位置を、読み飛ばし位置に移動します。
infile->Seek(m_dwSkipOffset,CFile::begin);

//ACMドライバーを開きます。
MMRESULT mmr=::acmDriverOpen(&m_hDriver,m_hDriverIdDec,0);
if (mmr!=MMSYSERR_NOERROR) m_hDriver=NULL;

//MPEGLAYER3WAVEFORMAT構造体からWAVEFORMATEX構造体を取得します。
m_wfx.wFormatTag=WAVE_FORMAT_PCM;
m_wfx.cbSize=sizeof(WAVEFORMATEX);
mmr=::acmFormatSuggest(m_hDriver,&mp3wf.wfx,&m_wfx,sizeof(WAVEFORMATEX),ACM_FORMATSUGGESTF_WFORMATTAG);
if (mmr!=MMSYSERR_NOERROR) return FALSE;

//ACMストリームをオープンします。
mmr=::acmStreamOpen(&m_hAcmStream,m_hDriver,&mp3wf.wfx,&m_wfx,NULL,0,0,ACM_STREAMOPENF_NONREALTIME);
if (mmr!=MMSYSERR_NOERROR) return FALSE;

//Mp3ブロックサイズから変換後のWaveブロックサイズを取得します。
::acmStreamSize(m_hAcmStream,m_mp3BlockSize,&m_wavBlockSize,ACM_STREAMSIZEF_SOURCE);

//Mp3総データサイズから変換後のWave総データサイズを取得します。
::acmStreamSize(m_hAcmStream,m_mp3StreamSize,(LPDWORD)&m_nDataSize,ACM_STREAMSIZEF_SOURCE);
//Waveブロックサイズの平均値を算出します。
m_nAveWaveBlockSize=ROUND((double)m_mp3BlockSize*(double)m_nDataSize/(double)m_mp3StreamSize);

//作業用バッファを確保します。
DWORD dwSrcSize=m_mp3BlockSize; //転送元バッファサイズ
DWORD dwDstSize=m_wavBlockSize; //転送先バッファサイズ
//転送先バッファサイズは4608バイト以上になるようにします。(libmpg123対応)
if (dwDstSize<4608) dwDstSize=4608*2;

m_byteSrcBuf.SetSize(dwSrcSize); //転送元バッファ
m_byteDstBuf.SetSize(dwDstSize); //転送先バッファ
m_byteRemain.SetSize(dwDstSize); //音源データの端数を保持する残りバッファ
m_nRemain=0; //音源データの端数

//ACMストリームヘッダーを作成します。
::ZeroMemory(&m_AcmStreamHeader,sizeof(ACMSTREAMHEADER));
m_AcmStreamHeader.cbStruct =sizeof(ACMSTREAMHEADER);
m_AcmStreamHeader.pbSrc =m_byteSrcBuf.GetData();
m_AcmStreamHeader.cbSrcLength=(DWORD)(DWORD_PTR)m_byteSrcBuf.GetSize();
m_AcmStreamHeader.pbDst =m_byteDstBuf.GetData();
m_AcmStreamHeader.cbDstLength=(DWORD)(DWORD_PTR)m_byteDstBuf.GetSize();

::acmStreamPrepareHeader(m_hAcmStream,&m_AcmStreamHeader,0);

//音源データサイズと実際のデータバイト数を設定します。
m_nDataSaved=m_nDataSize;
//オーナーウィンドウがあれば、プログレスバーの位置を通知します。
if (m_pWndOwner) m_pWndOwner->PostMessage(PBM_SETPOS,m_nDataSaved,0);

//MPEGLAYER3WAVEFORMAT構造体を保存しておきます。
SetMpegLayer3WaveFormat(0,&mp3wf);

return TRUE;
}


MP3ファイル再生時に音源データ読み込みのために、基底クラスから呼び出されるオーバーライド関数です。

//MP3からWave音源に変換してストリーム読み込み
UINT CMP3File::StreamIn(LPVOID pBuffer,UINT nBufSize,UINT* pIndexSample)
{
ASSERT(AfxIsValidAddress(pBuffer,nBufSize));
ASSERT(AfxIsValidAddress(pIndexSample,sizeof(UINT)));

//ACMストリームハンドルが無効な場合は、CDDAFile::StreamIn関数を呼び出します。
if (!m_hAcmStream) return CDDAFile::StreamIn(pBuffer,nBufSize,pIndexSample);

//現在位置と指定位置を算出します。
DWORD dwPosRecent=(DWORD)m_infile->GetPosition();
UINT indexBlock=ROUND((double)(*pIndexSample*m_wfx.nBlockAlign)/(double)m_nAveWaveBlockSize);
DWORD dwPosRead=m_dwSkipOffset+(m_mp3BlockSize*indexBlock);

//現在演奏中で現在位置と指定位置が100㍉秒以上違えば、指定位置にシークします。
if ((IsActivate())&&
(abs((int)(dwPosRead-dwPosRecent))>(m_wfx.nAvgBytesPerSec/10))){
//前回の残りの音源データ数を0にします。
m_nRemain=0;

//先頭フレームに移動して、ファイルを読み込みます。
m_infile->Seek(m_dwSkipOffset,CFile::begin);
::ZeroMemory(m_AcmStreamHeader.pbSrc,m_AcmStreamHeader.cbSrcLength);
UINT size=m_infile->Read(m_AcmStreamHeader.pbSrc,m_AcmStreamHeader.cbSrcLength);
if (!size) return FALSE;
//実際に読み込んだサイズを有効なサイズとします。
m_AcmStreamHeader.cbSrcLengthUsed=size;
//Mp3データからWave音源データに変換します。
MMRESULT mmr=::acmStreamConvert(m_hAcmStream,&m_AcmStreamHeader,ACM_STREAMCONVERTF_BLOCKALIGN);

//フレームヘッダーを探します。
BYTE header[4];
FRAMEHEADER frameHeader;
while(TRUE){
//読み飛ばし位置に移動してから4バイト読みます。
m_infile->Seek(dwPosRead,CFile::begin);
if (m_infile->Read(header,4)!=4) return FALSE;
//フレームヘッダーかどうか調べます。
if (_ParseFrameHeader((PBYTE)&header,&frameHeader)) break;
//フレームヘッダーではない場合は、
//読み飛ばし位置を1バイト進めてから再び読み直します。

dwPosRead++;
}
//探し出したフレームヘッダーの先頭位置に移動します。
m_infile->Seek(dwPosRead,CFile::begin);
}

UINT readBytes=0;
PBYTE dst=(PBYTE)pBuffer;

//前回の残りの音源データをバッファに取り込みます。
UINT writeBytes=min(nBufSize,m_nRemain);
if (m_nRemain){
PBYTE pRemain=m_byteRemain.GetData();
::CopyMemory(dst,pRemain,writeBytes);
dst+=writeBytes;
readBytes+=writeBytes;
if (writeBytes<m_nRemain) ::CopyMemory(pRemain,pRemain+writeBytes,m_nRemain-writeBytes);
m_nRemain-=writeBytes;
}
//前回の残りがなく、まだバッファに空きがある場合。
if ((!m_nRemain)&&(readBytes<nBufSize))
{
DWORD fdwConvert=ACM_STREAMCONVERTF_BLOCKALIGN;
if (indexBlock==0) fdwConvert|=ACM_STREAMCONVERTF_START;
//ファイルからMP3データを読み込みデコードします。
while(readBytes<nBufSize){
::ZeroMemory(m_AcmStreamHeader.pbSrc,m_AcmStreamHeader.cbSrcLength);
UINT size=m_infile->Read(m_AcmStreamHeader.pbSrc,m_AcmStreamHeader.cbSrcLength);
if (!size) break;
if (size!=m_AcmStreamHeader.cbSrcLength)
fdwConvert|=ACM_STREAMCONVERTF_END;

//実際に読み込んだサイズを有効なサイズとします。
m_AcmStreamHeader.cbSrcLengthUsed=size;
//ID3v1タグがある場合。
if(m_dwSkipBottom){
DWORD dwPos=(DWORD)m_infile->GetPosition();
DWORD dwEnd=m_mp3StreamSize+m_dwSkipOffset;
//ID3v1タグを読み込んだ場合はその分を差し引きます。
if (dwPos>dwEnd){
DWORD dwDelta=dwPos-dwEnd;
if (m_AcmStreamHeader.cbSrcLengthUsed<dwDelta) break;
else m_AcmStreamHeader.cbSrcLengthUsed-=dwDelta;
::ZeroMemory(m_AcmStreamHeader.pbSrc+m_AcmStreamHeader.cbSrcLengthUsed,
m_AcmStreamHeader.cbSrcLength-m_AcmStreamHeader.cbSrcLengthUsed);
fdwConvert|=ACM_STREAMCONVERTF_END;
}
}
//Mp3データからWave音源データに変換します。
MMRESULT mmr=::acmStreamConvert(m_hAcmStream,&m_AcmStreamHeader,fdwConvert);

//lameACM対応
#define MP3_NEED_MORE 0x200

//MP3データ追加入力要求ではなく、エラーもなく
//デコード済みデータがある場合は、バッファに取り込みます。

if ((mmr!=MP3_NEED_MORE)&&
(mmr==MMSYSERR_NOERROR)&&
((m_AcmStreamHeader.cbDstLengthUsed>0))
{
writeBytes=min(m_AcmStreamHeader.cbDstLengthUsed,nBufSize-readBytes);
::CopyMemory(dst,m_AcmStreamHeader.pbDst,writeBytes);
dst+=writeBytes;
readBytes+=m_AcmStreamHeader.cbDstLengthUsed;
}
fdwConvert=ACM_STREAMCONVERTF_BLOCKALIGN;
}
//バッファに書ききれなかった端数の音源データがある場合。
if (readBytes>nBufSize){
//端数の音源データを残りバッファに格納します。
m_nRemain=readBytes-nBufSize;
ASSERT(m_nRemain==m_AcmStreamHeader.cbDstLengthUsed-writeBytes);
ASSERT(m_nRemain<=(UINT)m_byteRemain.GetSize());
PBYTE pRemain=m_byteRemain.GetData();
::CopyMemory(pRemain,m_AcmStreamHeader.pbDst+writeBytes,m_nRemain);
readBytes-=m_nRemain;
}
//指定の演奏位置を、読み込んだMp3データに相当する音源データ分進めます。
dwPosRecent=(DWORD)m_infile->GetPosition();
indexBlock=(dwPosRecent-m_dwSkipOffset)/m_mp3BlockSize;
*pIndexSample=(m_nAveWaveBlockSize*indexBlock)/m_wfx.nBlockAlign;
}

return readBytes;
}


CMP3Fileクラスのインスタンスを作成した後、CMP3File::OpenFile関数MP3ファイルを開きます。
エラーがなければCWaveFile::Play関数で演奏を開始します。演奏を中断したり演奏終了後、WAVE出力デバイスを閉じる場合は、CWaveFile::Stop関数を呼び出して下さい。
入力したMP3ファイルが再生されれば成功です。

サンプルプログラム(VisualC++net2003ソリューション)MP3FileTest01.zip

上記のzipファイルをダウンロードしてからWinRAR等で解凍し、「MP3FileTest01」フォルダー内にある「MP3FileTest.sln」を開きます。
「F5」キーを押すと、ビルド確認のダイヤログが表示されるので「Yes」を選択してソリューションをビルドします。



ビルドが終わると直ちにプログラムが自動起動して下の図にあるダイヤログが表示されます。



ダイヤログにある「ファイルを開く」ボタンを押すと、「ファイルを開く」ダイヤログが表示されます。



[ctrl+A]キーで、resフォルダー内にあるすべてのMP3ファイルを選択し、「開く」ボタンを押すと、コンボボックスにファイル名が表示され、スライドバーの動きに合わせて、MP3ファイルが再生されるか確認します。



「MP3のプロパティ」ダイヤログに表示されたMP3ファイルの内容についても、下記の通りに表示されているか確認して下さい。

「Mpeg1Layer3-double-ID3v2header.mp3」
Mpegバージョン1、Layer3でID3v2タグが二重書きされています。

「Mpeg2Layer3.mp3」
Mpegバージョン2、Layer3です。

「Mpeg25Layer3.mp3」
Mpegバージョン2.5、Layer3です。



<<MP3の再生 第1部ページの先頭libid3tagのインストール>>



スポンサーサイト



コメント: 0

この記事へのコメント
ブログ著者にのみ知らせます。

Trackback+Pingback: 0

TrackBack URL for this entry
http://hiroshi0945.blog75.fc2.com/tb.php/53-a32871ab
Listed below are links to weblogs that reference
MP3ファイルの再生 第2部 from マルチメディアファイルフォーマット

Home > MP3ファイルフォーマット > MP3ファイルの再生 第2部

タグクラウド
ブロとも申請フォーム

この人とブロともになる

ブロとも一覧

このページの先頭へ戻る