ACMを用いたMP3のストリーミング再生

過去に私が書いたこちらの記事を少し整理しました。

今回はACMを用いたMP3ストリーミング再生についての説明です。

ACMとはAudio Compression Managerの略で、いわばWindowsで管理されたコーデックをアプリケーションから使用できるようにしたものです。
このACMを用いることで、たとえばアプリケーションでデコーダを用いてMP3圧縮データをデコードしたりエンコードしたり出来ます。
DirectShowよりもっと低レベルな操作が出来るため、小回りが利く処理を実装できます。

プログラムはDirectShowのときよりかなり長くなります。
大まかな流れとしては、

  MP3のデータ構造を解析してMP3データを取得する
   ↓
  オーディオフォーマットを取得する
   ↓
  ACMストリームを開いてMP3からWAVE形式へ
  データをデコードする準備を行う
   ↓
  変換開始
   ↓
  サウンド再生用のAPIを用いてMP3から
  デコードしたWAVE形式のデータを再生する
   ↓
  後始末

となります。

順を追って説明していきます。

1.プログラミングの準備
ACMのAPIを使えるようにするために、以下のファイルをインクルードします。

  #include <windows.h>
  #include <mmreg.h>
  #include <msacm.h>

また、マルチメディア操作用としてwinmm.lib、ACMを使うためにmsacm32.libをインポートします。

  #pragma comment(lib, "winmm.lib")
  #pragma comment(lib, "msacm32.lib")

2.MP3ファイルからオーディオフォーマットを取得する
まずは、MP3ファイル構造について知る必要があります。
ファイル構造は、主にオーディオの形式を格納するヘッダのような部分と、MP3のオーディオデータが格納される部分に大別されます。
MP3ファイルにはファイルの先頭または末尾にタグ情報が存在しているものがあります。
タグが無いもの、またはタグが末尾にあるものはid3v1、タグが先頭にあるものはid3v2と、2種類のファイル構造があります。
まずは、この2つのうちのどちらであるかを導き出します。

  ID3v1
   [MP3データ]
    or
   [MP3データ] [“TAG”(3byte)] [タグ情報(125byte)]

  ID3v2
   [“ID3″(3byte)] [バージョン(2byte)] [フラグ(1byte)]
   [タグサイズ(4byte)] [拡張ヘッダ] [MP3データ]

第一の目的は、ファイル構造を解析してMP3データ場所とサイズを導き出すことです。
まず、ID3v1かID3v2の判断ですが、先頭3バイトが”ID3″という文字列であればID3v2、そうでなければID3v1と判断できます。

ID3v1の場合は、ファイルの末尾に128バイトのタグがついている場合とついていない場合があり、タグがついているときは必ずタグの先頭3バイトが”TAG”という文字列になっています。
ID3v2の場合は、上図の[タグサイズ]に拡張タグを含めたタグ全体のサイズが格納されています。
しかし、このサイズの格納の仕方は以下のようになります。

  データ構造:[0AAAAAAA] [0BBBBBBB] [0CCCCCCC] [0DDDDDDD]
  実際のタグサイズ:AAAAAAABBBBBBBCCCCCCCDDDDDDD
   (A,B,C,D はビットデータ)

要するに、タグデータの最上位ビットは必ず0で、これを省いてつめたものが実際のサイズとなります。
以上を踏まえてコードに落とし込むと、ファイルを開いてからMP3データ場所とサイズを取得する処理は、以下のような感じになります。

  // ファイルを開く
  HANDLE hFile = CreateFile(
    pFileName,
    GENERIC_READ,
    0,
    NULL,
    OPEN_EXISTING,
    FILE_ATTRIBUTE_NORMAL,
    NULL
  );

  if ( hFile == INVALID_HANDLE_VALUE )
    return FALSE; // エラー

  // ファイルサイズ取得
  DWORD fileSize = GetFileSize(hFile, NULL);

  BYTE header[10];
  DWORD readSize;

  // ヘッダの読み込み
  ReadFile(hFile, header, 10, &readSize, NULL);

  DWORD offset; // MP3データの位置
  DWORD size; // MP3データのサイズ

  // 先頭3バイトのチェック
  if ( memcmp(header, "ID3", 3) == 0 ) {
    // タグサイズを取得
    DWORD tagSize =
      ((header[6] << 21) |
      (header[7] << 14) |
      (header[8] << 7) |
      (header[9])) + 10;

    // データの位置、サイズを計算
    offset = tagSize;
    size = fileSize - offset;
  } else {
    // 末尾のタグに移動
    BYTE tag[3];
    SetFilePointer(hFile, fileSize - 128, FILE_BEGIN);
    ReadFile(hFile, tag, 3, &readSize, NULL);

    // データの位置、サイズを計算
    offset = 0;
    if ( memcmp(tag, "TAG", 3) == 0 )
      size = fileSize - 128; // 末尾のタグを省く
    else
      size = fileSize; // ファイル全体がMP3データ
  }

  // ファイルポインタをMP3データの開始位置に移動
  SetFilePointer(hFile, offset, FILE_BEGIN);

  return TRUE;

上のプログラムはあくまでも一例です。ID3v1、ID3v2といえどもMP3データの開始位置とサイズを取得してしまえば、後の処理は共通になります。

3.MP3データからオーディオフォーマットを取得する
MP3データを取得できたなら、次はオーディオフォーマットの取得です。MP3データの構造大まかには以下のようになっています。

  [フレームヘッダ(4byte)] [データ]
  [フレームヘッダ(4byte)] [データ]
  [フレームヘッダ(4byte)] [データ]
    ・
    ・
    ・

オーディオフォーマットは上のフレームヘッダから取得できます。フレームデータの構造は以下の通り。

  [IIJJKLMM] [EEEEFFGH] [AAABBCCD] [AAAAAAAA]

 A – 同期ビット(必ず1)
 B – MP3のバージョン
 C – レイヤー数
 D – CRC保護の有無
 E – ビットレート
 F – サンプリング周波数
 G – パディング
 H – 拡張
 I – チャンネルモード
 J – 拡張
 K – 著作権の有無
 L – オリジナル
 M – 強調

これらのうち、フォーマット取得に必要なビットは、B,C,E,F,G,Iです。
まず、フレームヘッダかどうかを調べるためにファイルを読み込んで、

  BYTE header[4];
  DWORD readSize;

  ReadFile(hFile, header, 4, &readSize, NULL);

ビットAがすべて1であるかどうかを調べます。

  if ( !(header[0] == 0xFF && (header[1] & 0xE0) == 0xE0) )
    return FALSE;

次に、ビットBからMPEGのバージョンを取得します。

  BYTE version = (header[1] >> 3) & 0x03;

ビットとバージョンの対応は以下の通り。

  00 – MPEG2.5
  01 – 予約
  10 – MPEG2
  11 – MPEG1

バージョンの取得と同じように、レイヤー数をビットCから取得します。

  BYTE layer = (header[1] >> 1 ) & 0x03;

ビットとレイヤーの対応は以下の通り。

  00 – 予約
  01 – Layer3
  10 – Layer2
  11 – Layer1

バージョンとレイヤーを取得できたら、これらとビットEからビットレートを導き出します。これには対応表がありますが、書くのが面倒なので、コード上で示します。

  // ビットレートのテーブル
  const WORD bitRateTable[][16] = {
    // MPEG1, Layer1
    {0,32,64,96,128,160,192,224,256,288,320,352,384,416,448,-1},
    // MPEG1, Layer2
    {0,32,48,56,64,80,96,112,128,160,192,224,256,320,384,-1},
    // MPEG1, Layer3
    {0,32,40,48,56,64,80,96,112,128,160,192,224,256,320,-1},
    // MPEG2/2.5, Layer1,2
    {0,32,48,56,64,80,96,112,128,144,160,176,192,224,256,-1},
    // MPEG2/2.5, Layer3
    {0,8,16,24,32,40,48,56,64,80,96,112,128,144,160,-1}
  };

  INT index;
  if ( version == 3 ) {
    index = 3 - layer;
  } else {
    if ( layer == 3 )
      index = 3;
    else
      index = 4;
  }

  WORD bitRate = bitRateTable[index][header[2] >> 4];

同様にサンプリング周波数をバージョンとビットFから取得します。

  // サンプリングレートのテーブル
  const WORD sampleRateTable[][4] = {
    { 44100, 48000, 32000, -1 }, // MPEG1
    { 22050, 24000, 16000, -1 }, // MPEG2
    { 11025, 12000, 8000, -1} // MPEG2.5
  };

  switch ( version ) {
  case 0:
    index = 2;
    break;
  case 2:
    index = 1;
    break;
  case 3:
    index = 0;
    break;
  }
  WORD sampleRate =
    sampleRateTable[index][(header[2] >> 2) & 0x03];

ここでようやく大きな山を越えられました。
後は、パディング、チャンネルモードを取得して読み込むものはこれですべてです。

  BYTE padding = header[2] >> 1 & 0x01;
  BYTE channel = header[3] >> 6;

チャンネルは、ビットが二進数で11のときはモノラルで、それ以外の時はステレオと考えてほぼ問題ないかと思います。

最後に、1フレームのサイズとフォーマットを取得して完了です。
フォーマットは、MPEGLAYER3WAVEFORMAT構造体の中に格納します。

  // サイズ取得
  WORD blockSize = ((144 * bitRate * 1000) / sampleRate) + padding;

  // フォーマット取得
  MPEGLAYER3WAVEFORMAT mf;
  mf.wfx.wFormatTag = WAVE_FORMAT_MPEGLAYER3;
  mf.wfx.nChannels = channel == 3 ? 1 : 2;
  mf.wfx.nSamplesPerSec = sampleRate;
  mf.wfx.nAvgBytesPerSec = (bitRate * 1000) / 8;
  mf.wfx.nBlockAlign = 1;
  mf.wfx.wBitsPerSample = 0;
  mf.wfx.cbSize = MPEGLAYER3_WFX_EXTRA_BYTES;

  mf.wID = MPEGLAYER3_ID_MPEG;
  mf.fdwFlags =
    padding ?
    MPEGLAYER3_FLAG_PADDING_ON :
    MPEGLAYER3_FLAG_PADDING_OFF;
  mf.nBlockSize = blockSize;
  mf.nFramesPerBlock = 1;
  mf.nCodecDelay = 1393;

4.MP3からWAVEにデコードする
WAVEへデコードするために、ACMを用います。まず、MP3からWAVEに変換した後のフォーマットを取得します。

  MPEGLAYER3WAVEFORMAT mf = ファイルから取得したフォーマット;
  WAVEFORMATEX wf;

  wf.wFormatTag = WAVE_FORMAT_PCM;
  acmFormatSuggest(
    NULL,
    &mf.wfx,
    &wf,
    sizeof(WAVEFORMATEX),
    ACM_FORMATSUGGESTF_WFORMATTAG
  );

これが終わったら、いよいよACMストリームを開きます。

  HACMSTREAM has;
  acmStreamOpen(&has, NULL, &mf.wfx, &wf, NULL, 0, 0, 0);

MP3のデータを解析して得られたブロックサイズからWAVE形式へデコード後のブロックサイズを取得します。

  DWORD mp3BlockSize = MP3のブロックサイズ;
  DWORD waveBlockSize;
  acmStreamSize(
    has,
    mp3BlockSize,
    &waveBlockSize,
    ACM_STREAMSIZEF_SOURCE
  );

このWAVE形式へ変換後のブロックサイズを取得することで、ストリーミング再生でMP3データをブロックごとにデコードした後にどれくらいの大きさのバッファが必要かを見当できます。
サイズを取得できたなら、変換情報を指定するために、以下の構造体に値をセットします。

  ACMSTREAMHEADER ash = {0};
  ash.cbStruct = sizeof(ACMSTREAMHEADER);
  ash.pbSrc = new BYTE[mp3BlockSize];
  ash.cbSrcLength = mp3BlockSize;
  ash.pbDst = new BYTE[waveBlockSize];
  ash.cbDstLength = waveBlockSize;

後は、以下の関数呼び出しでデコードの準備をします。

  acmStreamPrepareHeader(has, &ash, 0);

これでいつでもデコードできるようになりました。
以下の処理で、MP3データのうちの一部分をWAVEにデコードします。

  DWORD readSize;
  ReadFile(hFile, ash.pbSrc, ash.cbSrcLength, &readSize, NULL);
  acmStreamConvert(has, &ash, ACM_STREAMCONVERTF_BLOCKALIGN);

acmStreamConvertを呼び出すことでash.pbDstにデコードされたWAVEデータが格納され、ash.cbDstLengthUsedにデコードされたWAVEデータのサイズが渡されます。
デコード後のWAVEデータのサイズは常に一定ではなく、ash.cbDstLength以下になります。
後は、MP3データを毎回ash.cbSrcLengthずつ読み込んで、acmStreamConvertでWAVEに変換していきます。

プログラムの雛形は大体以下のような感じです。

  LPBYTE pBuffer = デコード後のWAVEデータを格納するバッファ;
  DWORD bufSize = バッファのサイズ;
  DWORD bufRead = バッファを読み込んだサイズ;
  DWORD size = MP3のデータサイズ;
  DWORD pos = 0;
  DWORD readSize;

  while ( size - pos >= mp3BlockSize ) { // MP3データの終端?
    // 1ブロック分だけMP3データを読み込む
    ReadFile(hFile, ash.pbSrc, ash.cbSrcLength, &readSize, NULL);
    pos += readSize;

    if ( bufSize - bufRead > ash.cbDstLengthUsed ) {
      // WAVEデータを格納するバッファに余裕があれば、
      // デコードしたWAVEデータをそのまま格納
      CopyMemory(
        pBuffer + bufRead,
        ash.pbDst,
        ash.cbDstLengthUsed
      );
      bufRead += ash.cbDstLengthUsed;
    } else {
      // WAVEデータを格納するバッファに余裕がなければ、
      // バッファの残り分だけデータを書き込む
      CopyMemory(
        pBuffer + bufRead,
        ash.pbDst,
        bufSize - bufRead
      );
      bufRead += bufSize - bufRead;
      break;
    }
  }

後は、デコードしたWAVEデータを再生用のバッファに逐次コピーするだけです。
再生に関しては、DirectSoundやwaveOut系のAPIを用いる方法がありますが、いずれにせよ再生用のバッファにデータをコピーして再生することになります。

再生用の処理も説明したほうがよいかと思いましたが、これを説明してしまうと、内容がさらに膨大化してしまうので、今回はMP3再生の中でも最大の山場であるMP3データを逐次WAVEデータにデコードする処理を記述する部分に絞りました(泣)

5.後始末
最後に、以下の処理で後始末します。

  // ACMの後始末
  acmStreamUnprepareHeader(has, &ash, 0);
  acmStreamClose(has, 0);

  // ファイルを閉じる
  CloseHandle(hFile);

  // 動的確保したデータを開放
  delete[] ash.pbSrc;
  delete[] ash.pbDst;

・・・処理に関しては以上です。
DirectShowでのMP3の再生と比べると、とても大変ですね・・・(泣)

過去のブログのアクセス履歴を見るとMP3再生で検索に来られる方が多かったので、改めてこちらにまとめさせていただきました。

■参考サイト
EternalWindows MP3
CodecをつかったMP3のデコード・再生編-2(MP3のフレームヘッダ解析)
最初から説明するInside MP3