2016年11月4日金曜日

MP3ってどうなってるの? (2) フレームヘッダ情報表示

MP3の第2回目は、フレームヘッダを表示するプログラムを作ってみました.MP3を理解するにはフレームが大切な気がするからです.

プログラムはlibmadを使いますからちょちょいのちょいってなもんです.libmadのインストールについては第1回を読んで下さい.

フレーム長の半端さで悩むわたくし
前回でも触れましたとおり、MP3データはフレームという不定長のデータブロックでぶつ切りになっています.この不定長というのがひら的には気になって仕方ありません.

フレーム長はおおまかにはこんな計算になります.
・128kbpsのMP3ファイルの場合   (128kbpsは圧縮後のレート)
・PCMが毎秒44.1kサンプルなのに対し、MP3の1フレームは1152サンプルである
・ゆえに、1フレームのデータ量は、
(1152x2chx16bit) x 128kbps / (44.1kx2chx16bit)
= 3343.673bit/frame = 417.959BYTE/frame

417.959と割り切れない数字でありゃりゃっと思った貴方は鋭いです.放置したらデータロストかオーバーフローが生じてしまいかねません.
そこで辻褄あわせのカラクリがMP3フレームには存在します.それは、フレームヘッダにあるpaddingフラグです.paddingとは詰め物のことで、フォーマット関連でpaddingという言葉が出てくると、無駄データを隙間に詰め込んでやがるなーと推察されます.

(418BYTEとして、これにヘッダが+4BYTE、CRCがあれば+2BYTEとフレームトータルはもう少し大きいはずですが、さておきます)

わたしが128kbpsで生成したMP3ファイルを解析してみたところ、フレーム長が418BYTEと417BYTEが混在していてなんだこりゃ?と思ってしまいました.今回紹介するフレームヘッダ表示プログラムでその謎が解明されました.

同プログラムによるフレームヘッダ表示結果がこれです.
003407 MP3 128kbps 44.1kHz CRC=0(800d/0000) pad=0 2ch pcm#=1152 89.0sec
003408 MP3 128kbps 44.1kHz CRC=0(800d/0000) pad=1 2ch pcm#=1152 89.1sec
003409 MP3 128kbps 44.1kHz CRC=0(800d/0000) pad=1 2ch pcm#=1152 89.1sec
003410 MP3 128kbps 44.1kHz CRC=0(800d/0000) pad=1 2ch pcm#=1152 89.1sec
中略
003428 MP3 128kbps 44.1kHz CRC=0(800d/0000) pad=1 2ch pcm#=1152 89.6sec
003429 MP3 128kbps 44.1kHz CRC=0(800d/0000) pad=1 2ch pcm#=1152 89.6sec
003430 MP3 128kbps 44.1kHz CRC=0(800d/0000) pad=1 2ch pcm#=1152 89.6sec
003431 MP3 128kbps 44.1kHz CRC=0(800d/0000) pad=0 2ch pcm#=1152 89.7sec

paddingフラグを意味するpadというところに着目しますと、pad=0の出現個数とpad=1の出現個数が、pad0:pad1=1:23になっています.pad=0ならばフレーム長=417BYTE、pad=1ならばフレーム長=418BYTE なんです.これに1:23の出現割合を加味すると、
(417x1+418x23)/24=417.958
となって、上で割り切れなかった417.959にほぼ等しくなります.

つまりpaddingは、
・1BYTE追加されるか否かを示している.
・大局的なフレーム長を小数点以下まで設定する役目がある.

ここらへんの情報は、下記URLの「How to calculate frame length」にも書かれています.
http://www.datavoyage.com/mpgscript/mpeghdr.htm


可変レートMP3の実態
amazonで買ったMP3ファイルのヘッダを解析してみましたら、これは可変レートでエンコードされていました.さすがに売り物だけあって丁寧ですね.(ちな、ClarisのGravity)

まず曲の途中はこうなっています.256kbpsと320kbpsがフレーム毎にパコパコ切り替わっています.最初に解析したMP3ファイルが不幸なことにこれだったので少々ハマリました.
000574 MP3 256kbps 44.1kHz CRC=0(0e0a/2f00) pad=0 2ch pcm#=1152 15.0sec
000575 MP3 320kbps 44.1kHz CRC=0(0e0a/2f00) pad=0 2ch pcm#=1152 15.0sec
000576 MP3 320kbps 44.1kHz CRC=0(0e0a/2f00) pad=0 2ch pcm#=1152 15.1sec
000577 MP3 256kbps 44.1kHz CRC=0(0e0a/2f00) pad=0 2ch pcm#=1152 15.1sec
000578 MP3 256kbps 44.1kHz CRC=0(0e0a/2f00) pad=0 2ch pcm#=1152 15.1sec
000579 MP3 256kbps 44.1kHz CRC=0(0e0a/2f00) pad=0 2ch pcm#=1152 15.2sec

つぎに、曲の末尾のフェードアウトするところはこうなっています.音量が小さくなるに連れてbpsが低下して行き、最終的に無音パートでは32kbpsにまで低下してEOFに至ります.
009590 MP3 160kbps 44.1kHz CRC=0(0e0a/2f00) pad=0 2ch pcm#=1152 250.5sec
009591 MP3 128kbps 44.1kHz CRC=0(0e0a/2f00) pad=0 2ch pcm#=1152 250.6sec
009592 MP3 160kbps 44.1kHz CRC=0(0e0a/2f00) pad=0 2ch pcm#=1152 250.6sec
009593 MP3 128kbps 44.1kHz CRC=0(0e0a/2f00) pad=0 2ch pcm#=1152 250.6sec
009594 MP3 112kbps 44.1kHz CRC=0(0e0a/2f00) pad=0 2ch pcm#=1152 250.7sec
009595 MP3   96kbps 44.1kHz CRC=0(0e0a/2f00) pad=0 2ch pcm#=1152 250.7sec
009596 MP3 112kbps 44.1kHz CRC=0(0e0a/2f00) pad=0 2ch pcm#=1152 250.7sec
009597 MP3  64kbps 44.1kHz CRC=0(0e0a/2f00) pad=0 2ch pcm#=1152 250.7sec
009598 MP3  56kbps 44.1kHz CRC=0(0e0a/2f00) pad=0 2ch pcm#=1152 250.8sec
009599 MP3  56kbps 44.1kHz CRC=0(0e0a/2f00) pad=0 2ch pcm#=1152 250.8sec
009600 MP3  32kbps 44.1kHz CRC=0(0e0a/2f00) pad=0 2ch pcm#=1152 250.8sec
009601 MP3  32kbps 44.1kHz CRC=0(0e0a/2f00) pad=0 2ch pcm#=1152 250.8sec
009602 MP3  32kbps 44.1kHz CRC=0(0e0a/2f00) pad=0 2ch pcm#=1152 250.9sec
009603 MP3  32kbps 44.1kHz CRC=0(0e0a/2f00) pad=0 2ch pcm#=1152 250.9sec

なぜかpadding=0のままで突っ走ってますな...


フレームヘッダ表示例
上の一行を抜き出して説明します.
000577 MP3 256kbps 44.1kHz CRC=0(0e0a/2f00) pad=0 2ch pcm#=1152 15.1sec

000557:   MP3ファイルの先頭からのフレーム番号
MP3:       もちろんMP3の意味.もしかするとMP1とかMP2が表示されるかもしれない
256kbps:  MP3のビットレート
44.1kHz:   PCMサンプリングレート
CRC=0(0e0a/2f00):
    ・CRC=0ならCRCが埋め込まれていない、CRC=1ならCRCあり
    ・0e0a=libmadが計算したCRC、2f00=MP3エンコーダが計算して埋め込んだCRC
    ・このファイルはCRC=0なので2f00はCRCじゃない他の何かが見えてるだけと思われる
    ・フレーム同期の信頼性のためCRCは在るべきと思うが、CRC不在が通常なのかな?
pad:        0ならpaddingなし、1なら1BYTEのpaddingあり
2ch:        ステレオの意味
pcm#:     フレームが含むPCMサンプル数、フレームは1152x2x2=4608BYTEを持つ
15.1sec:  先頭からの演奏時間


フレームヘッダ表示プログラム
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <sys/soundcard.h>
#include <sys/ioctl.h>
#include "mad.h"

/*  使用例: mp3header < xxx.mp3
MP3ファイルを標準入力にすると、サウンドデバイス/dev/dspへ出力する.
ただし、/dev/dspの設定は、16bit 44.1kHz決めうち.
それと同時に、MP3フレームヘッダ情報を表示し続ける. */

int fp;     // /dev/dspのためのファイルハンドル

// メモリに展開されたMP3データの場所を指す
struct buffer {
  unsigned char const *start; // 同先頭アドレス
  unsigned long length; // 同長さ BYTE
};

/*
以下には mad_flow を返す関数がいくつか並んでいる.
これらの関数は、mainルーチンのmad_decoder_init()に引数渡しされる.
libmadは、自分でデザインした関数をlibmadに登録していいよ、という仕組みなのだ.
登録できる関数は、input(),header(),filter(),output(),error(),message() である.
libmadは最初に一度inputをcallし、その後フレーム毎に header→filter→output の順にcallしてくれる.errorとmessageは必要に応じてlibmadにcallされる.

なお、不要だと思うなら登録せんでもいい.たとえばoutput()が未登録ならば音声を出力しなくなる.登録されていない関数はスルーされる.例外としてinputが未登録だとlibmadは停止してしまう.
このプログラムではinputとoutputとerrorしか登録していない.

input()は、dataでポイントされたメモリ上のMP3データを、stream構造体へ嵌め込む役割である.
*/
static enum mad_flow input(void *data,
                           struct mad_stream *stream)
{
  struct buffer *buffer = data;
  if (!buffer->length)    return MAD_FLOW_STOP;
  mad_stream_buffer(stream, buffer->start, buffer->length);
  buffer->length = 0;
  return MAD_FLOW_CONTINUE;
}

// 16bit丸めサブ    (内部演算制度はもっと高いため)
static inline signed int scale(mad_fixed_t sample)
{
  /* round */
  sample += (1L << (MAD_F_FRACBITS - 16));
  /* clip */
  if (sample >= MAD_F_ONE)    sample = MAD_F_ONE - 1;
  else if (sample < -MAD_F_ONE)    sample = -MAD_F_ONE;
  /* quantize */
  return sample >> (MAD_F_FRACBITS + 1 - 16);
}

/*
フレーム毎にcallされるoutput関数.
フレームヘッダの表示と、リニアPCMデータを/dev/dspに出力する
*/
int framenum=0;
float timesec=0;
static enum mad_flow output(void *data,
                            struct mad_header const *header,
                            struct mad_pcm *pcm)
{
  unsigned int nchannels, nsamples, protected, padding;
  mad_fixed_t const *left_ch, *right_ch;

  // PCMの各種パラメータ
  nchannels = pcm->channels;
  nsamples  = pcm->length;
  left_ch   = pcm->samples[0];
  right_ch  = pcm->samples[1];

  // ヘッダ表示
  padding   = (header->flags & MAD_FLAG_PADDING)==0 ? 0 : 1;
  protected = (header->flags & MAD_FLAG_PROTECTION)==0 ? 0 : 1;
  timesec += (float)nsamples/(float)(header->samplerate);
  printf("%06d MP%d %dkbps %3.1fkHz CRC=%d(%04x/%04x) pad=%d %dch pcm#=%d %4.1fsec\n",framenum++,header->layer,header->bitrate/1000,header->samplerate/1000.0,protected,header->crc_check,header->crc_target,padding,nchannels,nsamples,timesec);

  // PCMオーディオ出力loop     1152回
  while (nsamples--) {
    signed int sample;
    char c;
    /* output sample in 16-bit signed little-endian PCM to /dev/dsp */
    sample = scale(*left_ch++);
    c=(sample >> 0) & 0xff;    write(fp,&c,1);
    c=(sample >> 8) & 0xff;    write(fp,&c,1);
    if (nchannels == 2) {
      sample = scale(*right_ch++);
      c=(sample >> 0) & 0xff;    write(fp,&c,1);
      c=(sample >> 8) & 0xff;    write(fp,&c,1);
    }
  }
  return MAD_FLOW_CONTINUE;
}

// エラー表示
static enum mad_flow error(void *data,
                           struct mad_stream *stream,
                           struct mad_frame *frame)
{
  struct buffer *buffer = data;
  fprintf(stderr, "decoding error 0x%04x (%s) at byte offset %u\n",
          stream->error, mad_stream_errorstr(stream),
          stream->this_frame - buffer->start);
  return MAD_FLOW_CONTINUE;
}

// メイン
int main(int argc, char *argv[])
{
  struct stat stat;
  struct buffer buffer;
  struct mad_decoder decoder;
  void *fdm;
  int format;

  if (argc != 1)    return 1;

  // 標準入力の情報を調べる.標準入力=MP3ファイルである
  if (fstat(STDIN_FILENO, &stat) == -1 ||
      stat.st_size == 0)    return 2;

  // サウンドデバイス/dev/dspをopenし、各種設定をする
  fp = open("/dev/dsp",O_WRONLY);
  format = AFMT_S16_LE;
  ioctl(fp, SNDCTL_DSP_SETFMT, &format);
  format = 1;
  ioctl(fp, SNDCTL_DSP_STEREO, &format);
  format = 44100;
  ioctl(fp, SNDCTL_DSP_SPEED, &format);

  // 標準入力を一気読みしてメモリに展開する
  fdm = mmap(0, stat.st_size, PROT_READ, MAP_SHARED, STDIN_FILENO, 0);
  if (fdm == MAP_FAILED)    return 3;

  // buffer構造体にメモリのアドレスと長さを設定する
  buffer.start  = fdm;
  buffer.length = stat.st_size;

  // libmadに自作関数をセットする
  mad_decoder_init(&decoder, &buffer,
                   input,
                   0, // header関数は登録せず
                   0, // filter関数は登録せず
                   output,
                   error,
                   0 // messageは登録せず
                   );

  // MP3デコードを自動でやらせる.ファイルが終わったら帰ってくる.
  mad_decoder_run(&decoder, MAD_DECODER_MODE_SYNC);

  // 後始末
  mad_decoder_finish(&decoder);
  close(fp);
  if (munmap(fdm, stat.st_size) == -1)    return 4;
  return 0;
}

=========ソースコードはここまで  mp3header.c =========

コンパイルは、
gcc mp3header.c -lmad -o mp3header
libmadを使うので、前回に書いたやり方でlibmadをインストールしておいてください.じゃないとコンパイルが通りません.
使い方は、
mp3header < xxx.mp3                 くの字をお忘れなく~

MP3フレームについて、ひら的にはこれで一段落しました.次は伸張アルゴリズム、と行きたいところですが、よくわかんないのでこれで本件は最終回にするかもしれません.

あと、上のプログラムの/dev/dspの使い方はちょっと面白いんでないかい? Linuxで好みの音を出せるね.

なお、上記の情報に誤りがあって貴方が鬼のような損害を蒙っても私は知らないので、その場合は素直に死んでね.

その1へ

かしこ


人気ブログランキングへ

0 件のコメント:

コメントを投稿