LECTURE 13 左右の音量のバランスを変える

○ はじめに

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 の説明は終わりです。