LECTURE 10 WAVE ファイルを再生する

○ はじめに

WAVE ファイルは Windows で標準的に扱えるサウンドフォーマットとして登場し、現在では非常に有名なファイル形式です。CD から楽曲を吸い出す場合もまずは WAVE 形式ですよね。

ここでは PCM 形式で音声データが格納されている WAVE ファイルを読み込んで再生するプログラムを作成します。

○ WAVE ファイルの形式

サンプルプログラムの前に、WAVE ファイルの形式 ( フォーマット ) について簡単に説明します。

WAVE ファイルは以下の様に構成されています。

オフセット(byte)サイズ(byte)要素の説明
0 4 文字列 "RIFF"
4 4 これより後のデータ長
8 4 文字列 "WAVE"
12 4 文字列 "fmt "。末尾の空白に注意
16 4 ヘッダ部のデータ長(以後 x とする)
20 x ヘッダ部
--- --- 場合によっては次の "data" との間に別データが挿入される場合がある様です。 挿入される場合は 4 バイトの文字列、4 バイトのデータ長、データ のセット ( このセットをチャンクと呼ぶ ) で挿入されるので、先頭 4 バイトが "data" で無い場合は "次の 4 バイトの値だけ飛ばす" という処理を繰り返す必要があります。
20+x 4 文字列 "data"
24+x 4 データ部のデータ長(以後 y とする)
28+x y データ部

ヘッダ部は以下の様に構成されています。

オフセット(byte)サイズ(byte)要素の説明
0 2 フォーマットの種類。PCM の場合は 1 で、それ以外は調査していません m(__)m
2 2 チャンネル数
4 4 サンプリング周波数
8 4 1 秒間に使用するバイト数
122 1 つのサンプルデータのバイト数
142 量子化ビット数 ( 8 か 16 )
162 ヘッダ部構造体のサイズ

ただし、最後の "ヘッダ部構造体のサイズ" は存在しない場合があるらしいので、ヘッダ部の直前の "ヘッダ部のデータ長" を信用して下さい。

データ部の形式は、量子化ビット数およびチャンネル数によって変化します。8 ビットの場合は符号無し、16 ビットの場合は符号有りの数値です。

  • 8 ビット 1 チャンネル

    1 バイトの PCM データが順次格納されます

  • 8 ビット 2 チャンネル

    1 バイトの PCM データが "左右左右..." の順に格納されます

  • 16 ビット 1 チャンネル

    2 バイトの PCM データが順次格納されます

  • 16 ビット 2 チャンネル

    2 バイトの PCM データが "左右左右..." の順に格納されます

3 チャンネル以上の場合にどの様に PCM データが並ぶのかは未調査です m(__)m

○ サンプルプログラム

WAVE ファイルを再生するサンプルプログラム wave.c です。はじめに のページから、全てのサンプルプログラムを固めた tar ボールをダウンロードすることもできます。

gcc -o wave wave.c

としてコンパイルします。コンパイルオプションは各自の環境などに応じて修正して下さい。

引数に WAVE ファイルの絶対パスを指定して実行します。エラーが無ければ以下の例の様に表示され、WAVE ファイルの再生が開始されます。何 % 再生したかの表示は随時更新されます。なお、juice_pm_mix.wav は手元にある B'z のアルバム Green から個人利用のために吸い出したものです。みなさん、自分で持っている CD から個人利用の用途に限って吸い出すか、著作権フリーの素材を探すか、自分で作成するかした WAVE ファイルを使用して下さい。著作権法には注意しましょう!

表示例)

ryo@phoenix:~$ ./wave juice_pm_mix.wav 

WAVE file format :
 rate    = 44100
 channel = 2
 bits    = 16
 offsets = 44
 length  = 42724080

Now playing specified sound file juice_pm_mix.wav ...
 64% played.

--- wave.c ---

    1	#include <fcntl.h> 
    2	#include <stdio.h>
    3	#include <stdlib.h>
    4	#include <string.h>
    5	#include <sys/ioctl.h>
    6	#include <unistd.h>
    7	#include <linux/soundcard.h>
    8	
    9	
   10	/* WAVE ファイルの情報を格納する構造体 */
   11	typedef struct {
   12		FILE* fp;        /* ファイル構造体 */
   13		short is_pcm;    /* PCM フォーマットの場合は 1、それ以外は 0 */
   14		short channel;   /* モノラルの場合は 1、ステレオの場合は 2 */
   15		int   rate;      /* サンプリング周波数 */
   16		short bits;      /* 量子化ビット数 */
   17		long  offset;    /* ファイル先頭から PCM データまでのオフセット */
   18		int   len;       /* PCM データ部の長さ */
   19	}
   20	WAVE;
   21	
   22	
   23	/* 関数プロトタイプ */
   24	static int  wave_read_file( char* fname, WAVE* wave );
   25	static int  wave_setup_dsp( int* fd, WAVE* wave );
   26	static void wave_progress( int* processed, int current, WAVE* wave );
   27	
   28	
   29	#define BUFSIZE 64000
   30	
   31	int 
   32	main( int argc, char** argv )
   33	{
   34		WAVE wave;
   35		char buf[ BUFSIZE ];
   36		int  dsp;
   37		int  len;
   38		int  processed = 0;
   39	
   40		if ( argc != 2 ) {
   41			fprintf( stderr, "usage : wave filename\n" );
   42			return 1;
   43		}
   44	
   45		/* WAVE ファイルヘッダ読み込み */
   46		if ( wave_read_file( argv[1], &wave ) != 0 ) {
   47			fprintf( stderr, "Failed to read specified WAVE file"
   48				 ": %s\n", argv[1] );
   49			return 1;
   50		}
   51	
   52		printf( "\n" );
   53		printf( "WAVE file format :\n" );
   54		printf( " rate    = %d\n",  wave.rate );
   55		printf( " channel = %d\n",  wave.channel );
   56		printf( " bits    = %d\n",  wave.bits );
   57		printf( " offsets = %ld\n", wave.offset );
   58		printf( " length  = %d\n",  wave.len );
   59		printf( "\n" );
   60	
   61		/* /dev/dsp の設定 */
   62		if ( wave_setup_dsp( &dsp, &wave ) != 0 ) {
   63			fprintf( stderr, "Setup /dev/dsp failed.\n" );
   64			fclose( wave.fp );
   65			return 1;
   66		}
   67	
   68		printf( "Now playing specified wave file %s ...\n", argv[1] );
   69		fflush( stdout );
   70	
   71		fseek( wave.fp, wave.offset, SEEK_SET );
   72		wave_progress( &processed, 0, &wave );
   73	
   74		while ( 1 ) {
   75			len = fread( buf, 1, BUFSIZE, wave.fp );
   76	
   77			if ( len < BUFSIZE ) {
   78				if ( feof( wave.fp ) ) {
   79					if ( write( dsp, buf, len ) == -1 ) {
   80						perror( "write()" );
   81					}
   82					else {
   83						wave_progress( &processed, len, &wave );
   84					}
   85				}
   86				else {
   87					perror( "fread()" );
   88				}
   89	
   90				break;
   91			}
   92	
   93			if ( write( dsp, buf, len ) == -1 ) {
   94				perror( "write()" );
   95				break;
   96			}
   97	
   98			wave_progress( &processed, len, &wave );
   99		}
  100	
  101		fclose( wave.fp );
  102		close( dsp );
  103	
  104		printf( "\ndone.\n" );
  105		return 0;
  106	}
  107	
  108	
  109	/*
  110	 * WAVE ファイル情報を読み込む
  111	 */
  112	static int 
  113	wave_read_file( char* fname, WAVE* wave )
  114	{
  115		char   buf[32];
  116		int    len;
  117	
  118		if ( ( wave->fp = fopen( fname, "r" ) ) == NULL ) {
  119			fprintf( stderr, "Failed to open %s\n", fname );
  120			return -1;
  121		}
  122	
  123		/* 
  124		 * 先頭 4 バイトが "RIFF" であることを確認
  125		 * 更に 4 バイトスキップしておく
  126		 */
  127		fread( buf, 8, 1, wave->fp );
  128	
  129		if ( strncmp( buf, "RIFF", 4 ) != 0 ) {
  130			fprintf( stderr, "Specified file is not RIFF file.\n" );
  131			fclose( wave->fp );
  132			return -1;
  133		}
  134	
  135	
  136		/* 次の 4 バイトが "WAVE" であることを確認 */
  137		fread( buf, 4, 1, wave->fp );
  138	
  139		if ( strncmp( buf, "WAVE", 4 ) != 0 ) {
  140			fprintf( stderr, "Specified file is not WAVE file.\n" );
  141			fclose( wave->fp );
  142			return -1;
  143		}
  144	
  145		/* fmt チャンクを探す */
  146		while ( 1 ) {
  147			fread( buf, 8, 1, wave->fp );
  148			len = *( int* )( &buf[4] );
  149	
  150			if ( strncmp( buf, "fmt ", 4 ) != 0 ) {
  151				if ( fseek( wave->fp, len, SEEK_CUR ) == -1 ) {
  152					fprintf( stderr, "Failed to find fmt chunk.\n" );
  153					fclose( wave->fp );
  154					return -1;
  155				}
  156			}
  157			else {
  158				break;
  159			}
  160		}
  161	
  162	
  163		/* WAVE フォーマットを読み込む */
  164		fread( buf, len, 1, wave->fp );
  165	
  166		wave->is_pcm  = *( ( short* )( &buf[0]  ) );
  167		wave->channel = *( ( short* )( &buf[2]  ) );
  168		wave->rate    = *( ( int*   )( &buf[4]  ) );
  169		wave->bits    = *( ( short* )( &buf[14] ) );
  170	
  171		if ( wave->is_pcm != 1 ) {
  172			wave->is_pcm = 0;
  173		}
  174	
  175		/* data チャンクを探す */
  176		while ( 1 ) {
  177			fread( buf, 8, 1, wave->fp );
  178			len = *( int* )( &buf[4] );
  179	
  180			if ( strncmp( buf, "data", 4 ) != 0 ) {
  181				if ( fseek( wave->fp, len, SEEK_CUR ) == -1 ) {
  182					fprintf( stderr, "Failed to find data chunk.\n" );
  183					fclose( wave->fp );
  184					return -1;
  185				}
  186			}
  187			else {
  188				break;
  189			}
  190		}
  191	
  192		wave->len = len;
  193	
  194		if ( ( wave->offset = ftell( wave->fp ) ) == -1 ) {
  195			fprintf( stderr, "Failed to find offset of PCM data.\n" );
  196			fclose( wave->fp );
  197			return -1;
  198		}
  199	
  200		return 0;
  201	}
  202	
  203	
  204	/*
  205	 * /dev/dsp を設定する
  206	 */
  207	static int 
  208	wave_setup_dsp( int* dsp, WAVE* wave )
  209	{
  210		int format;
  211		int rate;
  212		int channel;
  213	
  214		/*
  215		 * PCM フォーマットであるかどうか確認する
  216		 */
  217		if ( ! wave->is_pcm ) {
  218			fprintf( stderr, "Specified file's sound data is not PCM format.\n" );
  219			fclose( wave->fp );
  220			return -1;
  221		}
  222	
  223		/*
  224		 * 8 bit の場合は AFMT_U8、16 bit の場合は AFMT_S16_LE をフォーマット
  225		 * として選択し、それ以外の数の場合はエラーとする。
  226		 */
  227		if ( wave->bits == 8 ) {
  228			format = AFMT_U8;
  229		}
  230		else if ( wave->bits == 16 ) {
  231			format = AFMT_S16_LE;
  232		}
  233		else {
  234			fprintf( stderr, "Specified file's sound data is %d bits,"
  235				 "not 8 nor 16 bits.\n", wave->bits );
  236			fclose( wave->fp );
  237			return -1;
  238		}
  239	
  240		rate    = ( int )wave->rate;
  241		channel = ( int )wave->channel;
  242	
  243	
  244		if ( ( *dsp = open( "/dev/dsp", O_WRONLY ) ) == -1 ) {
  245			perror( "open()" );
  246			return -1;
  247		}
  248	
  249		if ( ioctl( *dsp, SNDCTL_DSP_SETFMT, &format ) == -1 ) {
  250			perror( "ioctl( SOUND_PCM_SETFMT )" );
  251			close( *dsp );
  252			return -1;
  253		}
  254	
  255		if ( ioctl( *dsp, SOUND_PCM_WRITE_RATE, &rate ) == -1 ) {
  256			perror( "ioctl( SOUND_PCM_WRITE_RATE )" );
  257			close( *dsp );
  258			return -1;
  259		}
  260	
  261		if ( ioctl( *dsp, SOUND_PCM_WRITE_CHANNELS, &channel ) == -1 ) {
  262			perror( "ioctl( SOUND_PCM_WRITE_CHANNELS )" );
  263			close( *dsp );
  264			return -1;
  265		}
  266	
  267		return 0;
  268	}
  269	
  270	
  271	/*
  272	 * 進捗率の表示
  273	 */
  274	static void 
  275	wave_progress( int* processed, int current, WAVE* wave )
  276	{
  277		int progress;
  278	
  279		*processed += current;
  280		progress = (int)( ( ( double )*processed / ( double )wave->len ) * 100 );
  281		printf( "\r%3d%% played.", progress );
  282		fflush( stdout );
  283	}
  284	
  285	
  286	/* End of wave.c */

○ サンプルプログラムの説明

では、サンプルプログラムについて説明していきます。

  • 10 行目 〜 20 行目

    WAVE ファイルから読み込んだ情報を格納しておく構造体を定義しています。 WAVE ファイルの再生に必要な箇所だけ定義しています。各メンバの内容はコメントの通りです。 PCM データ部は WAVE ファイルによって大きさが変わるし とてつも無く大きくなる可能性があるため、メモリ上に一括して読み込むことは行わずに、 ファイル先頭からのオフセットとデータ長のみを保存しておくことにしました。

  • 23 〜 26 行目

    内部関数のプロトタイプ宣言です。wave_read_file() で WAVE ファイルの読み込みを行います。 wave_setup_dsp() は読み込んだ WAVE ファイルのパラメータを使用して /dev/dsp の設定を行います。wave_progress() で WAVE ファイル再生中に再生の進捗率を表示します。

  • 29 行目

    一度に読み込んで /dev/dsp に書き込む PCM データのバッファ長を定義しています。

  • 31 〜 106 行目 ( main() )

    main() 関数では、まず wave_read_file() で指定された WAVE ファイルの情報を読み込み、 WAVE 型変数に保存しておきます。その後、52 〜 59 行目で読み込んだ情報を表示します。

    次に wave_setup_dsp() で、読み込んだ WAVE ファイルの情報を使用して /dev/dsp の設定を行います。

    /dev/dsp の設定が終わったら、68 〜 72 行目で再生用の while() ループ直前の用意をしています。 68 行目で再生開始メッセージを表示した後 fflush( stdout ) を実行していますが、 これを行わないとバッファリングされて画面に表示されない場合があります ( というか著者の環境では確実に表示されません )。71 行目で、既に読み込んだ PCM データの先頭位置まで WAVE ファイルのファイルポインタを fseek() で移動しておき、 wave_progress() で最初の再生進捗率を表示しておきます。

    while() ループでは、まず fread() でバッファのサイズ分 PCM データを読み込みます。 バッファのサイズ分データを読み込んだ場合は、93 〜 96 行目の通り write() で PCM データを書き込み、wave_progress() で進捗率を更新します。

    読み込んだデータ長がバッファのサイズ未満であった場合は、まず feof() で EOF かどうかを判定します。EOF であった場合は write() で PCM データの書き込みを行い、 wave_progress() で進捗率を更新した上で while() ループから抜けます。 EOF でない場合はエラーであるため、perror() でエラー表示をして while() ループから抜けます。

    while() ループから抜けたら、WAVE ファイルポインタおよび /dev/dsp のファイルディスクリプタを閉じて終了します。

  • 109 〜 201 行目 ( wave_read_file() )

    WAVE ファイルを読み込んで、WAVE 型変数に情報を格納する関数です。

    • 123 〜 133 行目

      先頭 4 バイトが文字列 "RIFF" であるかどうかを確認しています。また、続く 4 バイトは不要であるためスキップしています。

    • 136 〜 143 行目

      文字列 "WAVE" を確認しています。

    • 145 〜 160 行目

      fmt チャンクを検索しています。fmt チャンクは "WAVE" の直後に続くはずなので、 この様に検索処理を行う必要は無いと思いますが、data チャンクで同じ検索処理を実装したのでついでに行ってます。

      まず続く 8 バイトを読み込み、後半 4 バイトを int 型変数 len に格納しています。 これは、現在参照しているチャンクのデータ部の長さとなります。前半 4 バイトが "fmt " であるかどうかを調べ、"fmt " でない場合は len の長さだけスキップします。 "fmt " である場合は while() ループによる検索処理を抜けます。

    • 163 〜 173 行目

      fmt チャンクを検出したので、フォーマット情報を読み込みます。必要となるのは フォーマット種別、チャンネル数、サンプリング周波数、量子化ビット数です。 これらを読み込んだ後、フォーマット種別が 1 ( PCM ) でない場合は強制的に 0 としておきます。

    • 175 〜 190 行目

      fmt チャンクと同様のアルゴリズムで data チャンクを検索しています。"fmt " の代わりに "data" という文字列を比較に使っています。fmt チャンクと data チャンクの間には別のチャンクが挿入されることがあるので、これは必須の処理です。

    • 192 〜 198 行目

      まず PCM データ長を WAVE 構造体に保存します。この時点でファイルポインタは PCM データの先頭にあるので、ファイルポインタのファイル先頭からのオフセット位置を ftell() で取得し保存しています。

  • 204 〜 268 行目 ( wave_setup_dsp() )

    214 〜 221 行目で PCM フォーマットであるかどうかを確認しています。PCM フォーマットでない場合は、その時点で終了します。

    223 〜 238 行目では、量子化ビット数によって /dev/dsp に設定するフォーマットを決定しています。ビット数が 8 の場合は AFMT_U8、ビット数が 16 の場合は AFMT_S16_LE です。それ以外の場合は正しい WAVE ファイルのフォーマットではないため、その時点で終了します。

    244 行目以降では、これまでのサンプルプログラムと同じ様に ioctl() で /dev/dsp の設定を行っています。

  • 271 〜 283 行目 ( wave_progress() )

    WAVE ファイルの再生中に、コンソール画面に現在の再生率を表示します。/dev/dsp に書き込むデータ長 ( current ) をこれまでに書き込んだデータ長 ( processed ) に加算し、WAVE ファイルの PCM データ長で割って再生率を求めています。後は printf() で表示しているだけです。\r によって表示位置を変えない様にしています。

    また、printf() の直後で fflush() によって強制的に画面表示を更新しています。 前述の通り、これを行わないとバッファリングによって画面に表示が出ない場合があります ( 著者の環境では確実に表示されないです )。


以上で wave.c の説明は終わりです。みなさん、好きな音楽でも再生して楽しんで下さい!