|
|
○ はじめに
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 秒間に使用するバイト数 |
| 12 | 2 | 1 つのサンプルデータのバイト数 |
| 14 | 2 | 量子化ビット数 ( 8 か 16 ) |
| 16 | 2 | ヘッダ部構造体のサイズ |
ただし、最後の "ヘッダ部構造体のサイズ" は存在しない場合があるらしいので、ヘッダ部の直前の "ヘッダ部のデータ長" を信用して下さい。
データ部の形式は、量子化ビット数およびチャンネル数によって変化します。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 の説明は終わりです。みなさん、好きな音楽でも再生して楽しんで下さい!