|
○ はじめに
LECTURE 12 複数の音を足す単純ミキサ では、「2 つの音源を単純に合成して 1 つの音を作り出す」低性能ミキサを書いてみました。今回は、音の定位(音源の位置)を変化させる機能のみを持ったプログラムを書いてみたいと思います。前回の低性能ミキサと今回の定位調整の知識を使って GUI でも用意すれば、音質には拘らない(?)最低限の機能を備えたミキサを作れる…ああ、せめて音量調整機能を付けなければ…
ちなみにサンプルプログラムを実行していると、昔どこかの遊園地に行った時に入ったホラー系のアトラクションで、幽霊の声が左右に動く度にぞくぞくしたのを思い出しました。幽霊の声があれば再生してみたいんですが。
○ 音の定位(パン)について
本ページで扱う音の定位(パンとも言う様です)とは、聞き手に対して音源がどこに配置されるかを表します。例えば聞き手の左方向にあれば大部分の音は左から聞こえ、右方向にあれば右から聞こえるわけです。
正確には立体的な位置関係を考慮したり、遠い近いといった距離間による音量の変化も発生しますが、本ページのサンプルプログラムでは、左右を直線上に移動し距離による音量の変化は発生しないとして扱います(低性能)。
○ サンプルプログラム
指定した WAVE ファイルを、左右の音量のバランスを変化させながら再生するサンプルプログラム pan.c です。はじめに のページから、全てのサンプルプログラムを固めた tar ボールをダウンロードすることもできます。
pthread を使っていますので
gcc -o pan pan.c -lpthread
の様に -lpthread オプションを付けてコンパイルします。
引数に WAVE ファイルの絶対パスを指定して実行すると、以下の様なメニューが表示されます。番号を入力して Enter を押すと、指定した内容に従って WAVE ファイルを再生します。再生中は左右の音量のバランスが自動的に変化します。
Select menu ... 1. Play specified wave file. 2. Stop playing. 99. Exit. Command ?
指定できる WAVE ファイルは
- PCM フォーマット
- 量子化ビット数 16 ビット
- 1 チャンネル
です。
--- pan.c ---
1 #include <fcntl.h> 2 #include <limits.h> 3 #include <stdio.h> 4 #include <stdlib.h> 5 #include <string.h> 6 #include <linux/soundcard.h> 7 #include <pthread.h> 8 #include <sys/ioctl.h> 9 #include <unistd.h> 10 11 12 #define COMMAND_STOP 0 13 #define COMMAND_PLAY 1 14 #define COMMAND_EXIT 2 15 16 #define PAN_MIN 0 17 #define PAN_MAX 256 18 #define PAN_STEP 4 19 20 #define BUFSIZE 4096 21 22 23 /* WAVE ファイルの情報を格納する構造体 */ 24 typedef struct { 25 FILE* fp; /* ファイル構造体 */ 26 short is_pcm; /* PCM フォーマットの場合は 1、それ以外は 0 */ 27 short channel; /* モノラルの場合は 1、ステレオの場合は 2 */ 28 int rate; /* サンプリング周波数 */ 29 short bits; /* 量子化ビット数 */ 30 long offset; /* ファイル先頭から PCM データまでのオフセット */ 31 int len; /* PCM データ部の長さ */ 32 int current; /* 現在の再生位置 */ 33 } 34 WAVE; 35 36 37 /* 再生に必要なデータをまとめた構造体 */ 38 typedef struct { 39 pthread_t thread_id; 40 int command; 41 int dsp; 42 WAVE wave; 43 short buffer[ BUFSIZE / sizeof( short ) ]; 44 /* 45 * PAN_MIN(0) <= pan <= PAN_MAX(256) 46 * pan == PAN_MIN が L のみで pan == PAN_MAX が R のみ 47 */ 48 int pan; 49 } 50 Context; 51 52 static Context context; 53 54 55 static int read_file( char* fname, WAVE* wave ); 56 static int setup_dsp( int* fd, WAVE* wave ); 57 static void show_menu( void ); 58 static void* play_thread( void* param ); 59 static void fill_buffer( short* dst, WAVE* wave, int pan ); 60 static void shutdown(); 61 62 63 int 64 main( int argc, char** argv ) 65 { 66 pthread_attr_t attr; 67 int ret; 68 69 if ( argc != 2 ) { 70 fprintf( stderr, "usage : ./wave filename\n" ); 71 return 1; 72 } 73 74 if ( read_file( argv[1], &( context.wave ) ) != 0 ) { 75 fprintf( stderr, "Failed to read wave files.\n" ); 76 return 1; 77 } 78 79 if ( setup_dsp( &( context.dsp ), &( context.wave ) ) != 0 ) { 80 fprintf( stderr, "Failed to setup /dev/dsp.\n" ); 81 return 1; 82 } 83 84 pthread_attr_init( &attr ); 85 pthread_attr_setdetachstate( &attr, PTHREAD_CREATE_JOINABLE ); 86 87 ret = pthread_create( &( context.thread_id ), &attr, 88 play_thread, ( void* )( &context ) ); 89 90 pthread_attr_destroy( &attr ); 91 92 if ( ret ) { 93 fprintf( stderr, "Failed to create thread.\n" ); 94 shutdown(); 95 return 1; 96 } 97 98 show_menu(); 99 pthread_join( context.thread_id, NULL ); 100 shutdown(); 101 102 return 0; 103 } 104 105 106 static int 107 read_file( char* fname, WAVE* wave ) 108 { 109 char buf[32]; 110 int len; 111 112 if ( ( wave->fp = fopen( fname, "r" ) ) == NULL ) { 113 fprintf( stderr, "Failed to open %s\n", fname ); 114 goto err; 115 } 116 117 /* 118 * 先頭 4 バイトが "RIFF" であることを確認 119 * 更に 4 バイトスキップしておく 120 */ 121 fread( buf, 8, 1, wave->fp ); 122 123 if ( strncmp( buf, "RIFF", 4 ) != 0 ) { 124 fprintf( stderr, "Specified file is not RIFF file.\n" ); 125 goto err; 126 } 127 128 129 /* 次の 4 バイトが "WAVE" であることを確認 */ 130 fread( buf, 4, 1, wave->fp ); 131 132 if ( strncmp( buf, "WAVE", 4 ) != 0 ) { 133 fprintf( stderr, "Specified file is not WAVE file.\n" ); 134 goto err; 135 } 136 137 /* fmt チャンクを探す */ 138 while ( 1 ) { 139 fread( buf, 8, 1, wave->fp ); 140 len = *( int* )( &buf[4] ); 141 142 if ( strncmp( buf, "fmt ", 4 ) != 0 ) { 143 if ( fseek( wave->fp, len, SEEK_CUR ) == -1 ) { 144 fprintf( stderr, "Failed to find fmt chunk.\n" ); 145 goto err; 146 } 147 } 148 else { 149 break; 150 } 151 } 152 153 fread( buf, len, 1, wave->fp ); 154 155 wave->is_pcm = *( ( short* )( &buf[0] ) ); 156 wave->channel = *( ( short* )( &buf[2] ) ); 157 wave->rate = *( ( int* )( &buf[4] ) ); 158 wave->bits = *( ( short* )( &buf[14] ) ); 159 160 /* 161 * PCM / 16 bits / Monoral 以外の WAVE データについては 162 * 本プログラムでは対象外 163 */ 164 if ( wave->is_pcm != 1 ) { 165 fprintf( stderr, "WAVE file must be PCM format.\n" ); 166 goto err; 167 } 168 169 if ( wave->bits != 16 ) { 170 fprintf( stderr, "WAVE file must be 16 bits .\n" ); 171 goto err; 172 } 173 174 if ( wave->channel != 1 ) { 175 fprintf( stderr, "WAVE file must be monoral.\n" ); 176 goto err; 177 } 178 179 /* data チャンクを探す */ 180 while ( 1 ) { 181 fread( buf, 8, 1, wave->fp ); 182 len = *( int* )( &buf[4] ); 183 184 if ( strncmp( buf, "data", 4 ) != 0 ) { 185 if ( fseek( wave->fp, len, SEEK_CUR ) == -1 ) { 186 fprintf( stderr, "Failed to find data chunk.\n" ); 187 goto err; 188 } 189 } 190 else { 191 break; 192 } 193 } 194 195 wave->len = len; 196 197 if ( ( wave->offset = ftell( wave->fp ) ) == -1 ) { 198 fprintf( stderr, "Failed to find offset of PCM data.\n" ); 199 goto err; 200 } 201 202 printf( "\n" ); 203 printf( "WAVE file format of %s:\n", fname ); 204 printf( " rate = %d\n", wave->rate ); 205 printf( " channel = %d\n", wave->channel ); 206 printf( " bits = %d\n", wave->bits ); 207 printf( " offsets = %ld\n", wave->offset ); 208 printf( " length = %d\n", wave->len ); 209 printf( "\n" ); 210 211 return 0; 212 213 err: 214 if ( wave->fp != NULL ) { 215 fclose( wave->fp ); 216 } 217 218 return 1; 219 } 220 221 222 static int 223 setup_dsp( int* dsp, WAVE* wave ) 224 { 225 int fragment = 0x0004000c; /* size = 4096 bytes, num of fragments = 4. */ 226 int format = AFMT_S16_LE; 227 int rate; 228 int channel = 2; 229 230 rate = ( int )wave->rate; 231 232 if ( ( *dsp = open( "/dev/dsp", O_WRONLY ) ) == -1 ) { 233 perror( "open()" ); 234 return 1; 235 } 236 237 if ( ioctl( *dsp, SNDCTL_DSP_SETFRAGMENT, &fragment ) == -1 ) { 238 perror( "ioctl( SNDCTL_DSP_SETFRAGMENT )" ); 239 close( *dsp ); 240 return 1; 241 } 242 243 if ( ioctl( *dsp, SNDCTL_DSP_SETFMT, &format ) == -1 ) { 244 perror( "ioctl( SNDCTL_DSP_SETFMT )" ); 245 close( *dsp ); 246 return 1; 247 } 248 249 if ( ioctl( *dsp, SNDCTL_DSP_SPEED, &rate ) == -1 ) { 250 perror( "ioctl( SNDCTL_DSP_SPEED )" ); 251 close( *dsp ); 252 return 1; 253 } 254 255 if ( ioctl( *dsp, SNDCTL_DSP_CHANNELS, &channel ) == -1 ) { 256 perror( "ioctl( SNDCTL_DSP_CHANNELS )" ); 257 close( *dsp ); 258 return 1; 259 } 260 261 return 0; 262 } 263 264 265 static void 266 show_menu( void ) 267 { 268 char line[4]; 269 int command; 270 271 while ( 1 ) { 272 printf( "Select menu ...\n" ); 273 printf( " 1. Play specified wave file.\n" ); 274 printf( " 2. Stop playing.\n" ); 275 printf( " 99. Exit.\n" ); 276 printf( "\n" ); 277 printf( "Command ? " ); 278 279 fgets( line, sizeof( line ), stdin ); 280 sscanf( line, "%d\n", &command ); 281 282 switch ( command ) { 283 case 1: 284 context.command = COMMAND_PLAY; 285 break; 286 case 2: 287 context.command = COMMAND_STOP; 288 break; 289 case 99: 290 context.command = COMMAND_EXIT; 291 return; 292 } 293 } 294 } 295 296 297 static void* 298 play_thread( void* param ) 299 { 300 static int sign = 1; 301 Context* context = ( Context* )param; 302 303 while ( context->command != COMMAND_EXIT ) { 304 memset( ( void* )( context->buffer ), 0, BUFSIZE ); 305 306 switch ( context->command ) { 307 case COMMAND_PLAY: 308 context->pan += PAN_STEP * sign; 309 310 if ( context->pan > PAN_MAX ) { 311 context->pan = PAN_MAX; 312 sign *= -1; 313 } 314 else if ( context->pan < 0 ) { 315 context->pan = 0; 316 sign *= -1; 317 } 318 319 fill_buffer( ( void* )( context->buffer ), 320 &( context->wave ), context->pan ); 321 break; 322 323 case COMMAND_STOP: 324 default: 325 break; 326 } 327 328 write( context->dsp, ( void* )( context->buffer ), BUFSIZE ); 329 } 330 331 pthread_exit( NULL ); 332 } 333 334 335 static void 336 fill_buffer( short* dst, WAVE* wave, int pan ) 337 { 338 /* 339 * buf は入力音源と同じ内容を転送するためのバッファ。 340 * BUFSIZE は出力用バッファサイズであり、出力はステレオ/入力はモノラル 341 * であるため、BUFSIZE / 2 としている。 342 */ 343 short buf[ ( BUFSIZE / 2 ) / sizeof( short ) ]; 344 int copied = 0; 345 int i; 346 347 while ( 1 ) { 348 int src_left = wave->len - wave->current; 349 int dst_left = BUFSIZE / 2 - copied; 350 int copy_len = src_left > dst_left ? dst_left : src_left; 351 352 if ( src_left == 0 ) { 353 fseek( wave->fp, wave->offset, SEEK_SET ); 354 wave->current = 0; 355 continue; 356 } 357 358 if ( dst_left == 0 ) { 359 break; 360 } 361 362 fread( ( void* )( buf + copied / sizeof( short ) ), 363 copy_len, 1, wave->fp ); 364 365 for ( i = 0; i < copy_len / sizeof( short ); i ++ ) { 366 dst[ ( copied / sizeof( short ) + i ) * 2 + 0 ] = 367 buf[i] * ( PAN_MAX - pan ) / PAN_MAX; /* L */ 368 dst[ ( copied / sizeof( short ) + i ) * 2 + 1 ] = 369 buf[i] * pan / PAN_MAX; /* R */ 370 } 371 372 wave->current += copy_len; 373 copied += copy_len; 374 } 375 } 376 377 378 static void 379 shutdown( void ) 380 { 381 fclose( context.wave.fp ); 382 close( context.dsp ); 383 } 384 385 386 /* End of pan.c */
○ サンプルプログラムの説明
では、サンプルプログラムについて説明していきます。pthread を用いて WAVE ファイル再生用スレッドを生成する等、基本的な構造は LECTURE 12 複数の音を足す単純ミキサ とほぼ同じです。よって、説明が不足している部分等についてはそちらを参照してください。
- 37 〜 52 行目
WAVE ファイル再生スレッドで使用する変数を一つの構造体にまとめて定義した上で、ファイルスコープを持つ context 変数を宣言しています。LECTURE 12 複数の音を足す単純ミキサ のサンプルプログラムの内容に、pan というメンバを追加しています。コメントの通り、pan は PAN_MIN (0) から PAN_MAX (256) の値が設定されます。PAN_MIN の場合は L チャンネルのみ、PAN_MAX の場合は R チャンネルのみの音を意味します。
- 63 〜 103 行目 ( main() )
pan.c の main 関数です。各処理について以下で説明していきます。
- 74 〜 82 行目
74 〜 77 行目で、指定された WAVE ファイルを読み込む自作関数 read_file() を実行しています。LECTURE 12 複数の音を足す単純ミキサ のサンプルプログラムの read_file() とは、一つのファイルのみを読み込む以外は同じです。
その後 79 〜 82 行目で、自作関数 setup_dsp() を実行し、/dev/dsp の初期化を行っています。前回のサンプルプログラムの setup_dsp() とは、デバイスの設定を 2 チャンネルに固定している点のみ異なります。これは、1 チャンネルの WAVE ファイルデータから LR の 2 チャンネルに音量を振り分けて再生するためです。
- 84 〜 103 行目
前回のサンプルプログラムと全く同じです。
- 265 〜 294 行目 ( show_menu() )
具体的なコマンドが異なる以外は、前回のサンプルプログラムと同じです。
- 297 〜 332 行目 ( play_thread() )
WAVE ファイル再生スレッドの実行関数です。前回のサンプルプログラムとは、音の加算を行わないことと、fill_buffer() によって再生用バッファに WAVE ファイルデータを転送する際に、左右の音量を調整するための context->pan 変数を操作している点です。
307 〜 321 行目において、まず context->pan の値を更新した上で fill_buffer() により再生用バッファに WAVE ファイルデータを転送しています。context->pan は最初は 0 から始まり、PAN_MAX に到達するまでは PAN_STEP ずつ増加していきます。PAN_MAX に到達した以降は、PAN_MIN (0) になるまで PAN_STEP ずつ減少していきます。これはつまり、最初は左チャンネルのみから音が出力され、次第に右チャンネルのみに移動していくことを表しています。
- 335 〜 375 行目 ( fill_buffer() )
指定バッファ ( dst ) に WAVE ファイルデータを転送します。
前回のサンプルプログラムとは、362 〜 370 行目にて一度関数内部のバッファに WAVE ファイルデータを転送し、そこから dst の左右のチャンネルに振り分けている点が異なります。
2 チャンネルの場合、音声データは LRLRLR... の順に並びます。よって 366 〜 367 行目で左チャンネル用データを作成し、368 〜 369 行目で右チャンネル用データを作成しています。それぞれのデータは以下の様にして計算します。
左 : 入力データ * ( PAN_MAX - pan ) / PAN_MAX; 右 : 入力データ * pan / PAN_MAX;
これにより、pan == PAN_MIN (0) の場合には
左 : 入力データ 右 : 0
となり、pan == PAN_MAX の場合には
左 : 0 右 : 入力データ
となります。
- 378 〜 383 行目 ( shutdown() )
リソース解放処理です。WAVE ファイルのファイルポインタを解放し、/dev/dsp も解放しています。
以上で pan.c の説明は終わりです。