LECTURE 8 マイク入力を別 PC で再生する(クライアント編)

○ はじめに

OSS の API の解説もある程度終わりました(僕が使う程度のものは。という意味です。。。)ので、今回からは少し遊べるものを作って行きましょう!

というわけで、今回および次回の 2 回で IP 電話もどきを作ってみたいと思います。

音声の圧縮やパケットの遅延などの考慮は全く行いませんので、せいぜい LAN 内で使用できる程度のものです。とはいっても、離れた場所で通話できるのは結構おもしろいのでぜひやってみて下さい。

ごく簡単なものとするために、著者の独断と偏見によって以下の仕様で作成します。

  • クライアント・サーバ構成とする
  • クライアントはマイクから入力された音声をサーバに UDP で送信する
  • サーバはクライアントから送られて来た音声をスピーカに出力する
  • 音声フォーマットは 16bit、44.1KHz、モノラルとする

仕様から分かる様に通信の方向はクライアント→サーバのみです。よって 2台の PC で双方向に通話したい場合は、自分の PC でサーバを実行した上で、クライアントを相手のサーバに接続する必要があります。

--- 図 1 ---
008iptel.jpg

今回はクライアントのプログラムについて解説します。UDP ソケット通信用システムコールについても簡単に解説していますが、詳細はソケットプログラミング関連の書籍などを参照して下さい。

○ -クライアント- サンプルプログラム

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

gcc -o client client.c

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

次回説明するサーバが実行されている PC の IP アドレス / あるいは名前解決が出来るホスト名を指定して実行すると、UDP ポートの 13000 番に対して入力された音声を送信し始めます。

--- client.c ---

    1	#include <fcntl.h>
    2	#include <stdio.h>
    3	#include <string.h>
    4	#include <sys/ioctl.h>
    5	#include <sys/types.h>
    6	#include <sys/socket.h>
    7	#include <unistd.h>
    8	#include <netdb.h>            /* gethostbyname() ... */
    9	#include <netinet/in.h>       /* htons() ... */
   10	#include <arpa/inet.h>        /* inet_aton() ... */
   11	#include <linux/soundcard.h>
   12	
   13	
   14	static int setup_dsp( int* dsp );
   15	
   16	static int setup_udp( 
   17		int* sock, struct sockaddr_in* server_addr, 
   18		char* host, unsigned short port );
   19	
   20	static int need_to_skip( unsigned char* buf, int len );
   21	
   22	
   23	/*
   24	 * サーバの UDP ポート
   25	 */
   26	#define PORT     13000
   27	
   28	
   29	/*
   30	 * 量子化ビット数 : 16 bits
   31	 * サンプリング周波数 : 44.1 KHz
   32	 * チャンネル数 : 1
   33	 *
   34	 * フラグメントサイズを 100 ms 分の領域
   35	 *
   36	 * 16 (bits) * 44100 (Hz) * 0.1 (sec) = 8820 (bytes)
   37	 *
   38	 * を越えない最大の 2 の巾乗数とする 
   39	 * ( SNDCTL_DSP_SETFRAGMENT の仕様による )
   40	 */
   41	#define FRAGSIZE 8192
   42	
   43	
   44	int 
   45	main( int argc, char** argv )
   46	{
   47		struct sockaddr_in server_addr;
   48		int sock;
   49		int dsp;
   50		int len;
   51	
   52		unsigned char buf[ FRAGSIZE ];
   53	
   54		if ( argc != 2 ) {
   55			fprintf( stderr, "usage : client host\n" );
   56			return 1;
   57		}
   58	
   59	
   60		/* /dev/dsp の設定 */
   61		if ( setup_dsp( &dsp ) != 0 ) {
   62			fprintf( stderr, "Setup /dev/dsp failed.\n" );
   63			return 1;
   64		}
   65	
   66		/* UDP ソケットの設定 */
   67		if ( setup_udp( &sock, &server_addr, argv[1], PORT ) != 0 ) {
   68			fprintf( stderr, "Setup UDP socket failed.\n" );
   69			close( dsp );
   70			return 1;
   71		}
   72	
   73	
   74		/*
   75		 * エラーが起こるか強制終了されるまでループします。
   76		 */
   77		while ( 1 ) {
   78			/*
   79			 * FRAGSIZE の入力を読み込むまでブロック
   80			 */
   81			if ( ( len = read( dsp, buf, sizeof( buf ) ) ) == -1 ) {
   82				perror( "read()" );
   83				close( dsp  );
   84				close( sock );
   85				return 1;
   86			}
   87	
   88			if ( ! need_to_skip( buf, len ) ) {
   89				/*
   90				 * 読み込んだ入力を UDP でサーバに送信
   91				 */
   92				if ( sendto( sock, buf, len, 0, 
   93					     ( struct sockaddr* )&server_addr, 
   94					     sizeof( struct sockaddr_in ) ) == -1 ) {
   95	
   96					perror( "sendto()" );
   97					close( dsp  );
   98					close( sock );
   99					return 1;
  100				}
  101			}
  102		}
  103	
  104		/*
  105		 * while() ループは終わらないのでここには来ません。。。
  106		 */
  107		close( dsp  );
  108		close( sock );
  109	
  110		return 0;
  111	}
  112	
  113	
  114	/*
  115	 * /dev/dsp を設定する
  116	 */
  117	static 
  118	int setup_dsp( int* dsp )
  119	{
  120		int fmt     = AFMT_S16_LE;
  121		int freq    = 44100;
  122		int channel = 1;
  123	
  124		int bit;
  125		int i;
  126		int fragarg;
  127	
  128	
  129		if ( ( *dsp = open( "/dev/dsp", O_RDONLY ) ) == -1 ) {
  130			perror( "open()" );
  131			return -1;
  132		}
  133	
  134		/* 
  135		 * フラグメントサイズの設定
  136		 *
  137		 * 0xMMMMSSSS ( MMMM はフラグメント数、SSSS は
  138		 * フラグメントサイズ = 2^{SSSS} となる数 )
  139		 *
  140		 * の形式で設定するため、まず SSSS を求める。
  141		 *
  142		 * ( 0 <= SSSS <= 65535, ただしフラグメントサイズ
  143		 *   は最大でも全バッファサイズ / 2 までしか
  144		 *   設定できない )
  145		 *
  146		 * また MMMM は特に制限を設けないため 0x7fff
  147		 * を設定する。
  148		 */
  149		for ( i = 0, bit = 1; i <= 65535; bit <<= 1, i ++ ) {
  150			if ( FRAGSIZE < bit ) {
  151				i = ( i == 0 ) ? 0 : i - 1;
  152				break;
  153			}
  154		}
  155	
  156		fragarg = ( 0x7fff << 16 ) | i;
  157	
  158		if ( ioctl( *dsp, SNDCTL_DSP_SETFRAGMENT, &fragarg )== -1 ) {
  159			perror( "ioctl( SNDCTL_DSP_SETFRAGMENT )" );
  160			close( *dsp );
  161			return -1;
  162		}
  163	
  164		if ( ioctl( *dsp, SOUND_PCM_SETFMT, &fmt ) == -1 ) {
  165			perror( "ioctl( SOUND_PCM_SETFMT )" );
  166			close( *dsp );
  167			return -1;
  168		}
  169	
  170		if ( ioctl( *dsp, SOUND_PCM_WRITE_CHANNELS, &channel ) == -1 ) {
  171			perror( "ioctl( SOUND_PCM_WRITE_CHANNELS )" );
  172			close( *dsp );
  173			return -1;
  174		}
  175	
  176		if ( ioctl( *dsp, SOUND_PCM_WRITE_RATE, &freq ) == -1 ) {
  177			perror( "ioctl( SOUND_PCM_WRITE_RATE )" );
  178			close( *dsp );
  179			return -1;
  180		}
  181	          
  182		return 0;
  183	}
  184	
  185	
  186	/*
  187	 * サーバとの通信用に UDP ソケットを設定する
  188	 */
  189	static int 
  190	setup_udp( 
  191		int* sock, struct sockaddr_in* server_addr, 
  192		char* host, unsigned short port )
  193	{
  194		struct in_addr     inaddr;
  195		struct hostent*    ent;
  196	
  197		if ( ( *sock = socket( AF_INET, SOCK_DGRAM, 0 ) ) == -1 ) {
  198			perror( "socket()" );
  199			return -1;
  200		}
  201	
  202		/*
  203		 * host が IP アドレスであると仮定して inet_aton() により
  204		 * 文字列表記の IP アドレスをバイナリ値に変換する。
  205		 *
  206		 * 失敗した場合はホスト名指定であると仮定し、
  207		 * gethostbyname() によって対応するホスト情報を取得する
  208		 * ( この場合、/etc/hosts ファイルか DNS によって名前解決
  209		 *   できる必要がある )。
  210		 */
  211		if ( ! inet_aton( host, &inaddr ) ) {
  212			ent = gethostbyname( host );
  213	
  214			if ( ent == NULL ) {
  215				fprintf( stderr, 
  216					 "Couldn't get host information specified by %s",
  217					 host );
  218	
  219				close( *sock );			
  220				return -1;
  221			}
  222	
  223			inaddr = *( struct in_addr* )( ent->h_addr_list[0] );
  224		}
  225	
  226		memset( server_addr, 0, sizeof( struct sockaddr_in ) );
  227	
  228		server_addr->sin_family = AF_INET;
  229		server_addr->sin_port   = htons( port );
  230	
  231		memcpy( &server_addr->sin_addr, &inaddr.s_addr, 
  232			sizeof( server_addr->sin_addr ) );
  233	
  234		return 0;
  235	}
  236	
  237	
  238	/*
  239	 * 入力バッファを調べて、無音状態に近い音声かどうかを判定する。
  240	 */
  241	static int 
  242	need_to_skip( unsigned char* buf, int len )
  243	{
  244		/*
  245		 * この値以上のデータが存在しない場合はサーバに送信しない。
  246		 * 自分の体感上、問題無い程度に適当に選んだ値です。。。
  247		 */
  248		int threshold = 0x0040;
  249	
  250		int i;
  251		unsigned short value;
  252	
  253		for ( i = 0; i < len; i += 2 ) {
  254			memcpy( &value, &buf[i], sizeof( value ) );
  255			if ( value >= threshold )
  256				return 0;
  257		}
  258	
  259		return 1;
  260	}
  261	
  262	
  263	/* End of client.c */


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

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

  • 14 〜 20 行目

    自作内部関数のプロトタイプ宣言です。setup_dsp() は /dev/dsp の設定を行います。setup_udp() は、 サーバとの通信を行うための UDP ソケットの設定を行います。 need_to_skip() は入力された音声をサーバに送るべきかどうかの判定を行います。

  • 23 〜 26 行目

    サーバがクライアントからの音声送信を受け付ける UDP ポート番号を定義しています。

  • 29 〜 41 行目

    LECTURE 5 リアルタイム性を考慮する で説明した fragment の大きさを定義しています。ここでは 16bits、44.1 KHz、100ms 分のバッファサイズとしました。 この大きさの単位で、サーバへの音声送信を行います。

  • 44 〜 111 行目 ( main() )

    main() 関数では、まず /dev/dsp の設定 ( setup_dsp() ) および UDP ソケットの設定 ( setup_udp() ) を行った後にマイク入力をサーバに送信する while() ループを実行します。

    while() ループでは、まずマイクからの入力を FRAGSIZE 分 read() で読み込み、読み込んだ音声を need_to_skip() によってサーバに送信するべきかどうか判定しています。 送信すべきと判定した場合は、sendto() ( ソケット関連のシステムコール ) でサーバに送信しています。

      #include <sys/types.h>
      #include <sys/socket.h>
    
      int sendto( int s,  const void *msg, size_t len, int flags, 
                  const struct sockaddr *to, socklen_t tolen );
     
      s     : socket() システムコールで作成したソケット
      msg   : 送信データバッファへのポインタ
      len   : 送信データ長
      flags : ソケット通信用フラグ。詳細はマニュアル参照。ここでは使用しないため 0 を設定
      to    : 送信先情報へのポインタ
      tolen : to の実データの大きさ
    
    

    エラーが発生した場合は -1 が返され、それ以外は送信されたデータ長が返されます。

  • 114 〜 183 行目 ( setup_dsp() )

    /dev/dsp の設定を行っています。134 〜 162 行目で fragment の大きさを設定しています。

    fragment の大きさは 0xMMMMSSSS ( MMMM は fragment 数、SSSS は fragment サイズ = 2^{SSSS} となる数 ) の形式で設定するため、SSSS を 149 〜 154 行目で求めています。MMMM には特に制限を設けないため 0x7fff とします。

    156 行目で SSSS および MMMM を fragarg に設定し、158 〜 162 行目の ioctl() で実際に fragment の大きさを設定しています。ここでは fragment がちゃんと設定されたかどうかを確認していませんが、実際には ioctl SNDCTL_DSP_GETBLKSIZE コマンドで確認した方が良いかもしれません (LECTURE 5 リアルタイム性を考慮する 参照)。

    それ以降の ioctl() はこれまでに説明した通りです。

  • 186 〜 235 行目 ( setup_udp() )

    サーバとの通信に使用する、UDP ソケットの設定を行っています。

    197 〜 200 行目では、socket() システムコールによって通信に使用するソケットの準備を行っています。

      #include <sys/types.h>
      #include <sys/socket.h>
    
      int socket( int domain, int type, int protocol );
    
      domain   : IP 通信の場合は PF_INET もしくは AF_INET を指定する
      type     : UDP の場合は SOCK_DGRAM を指定する
      protocol : ここでは 0 を指定して下さい
    
    

    成功すると、準備されたソケットを参照するディスクリプタが返されます。 以後はこの値を使用してソケットを指定します。エラーが発生した場合は -1 が返されます。

    202 〜 232 行目では、送信先の情報を struct sockaddr_in 型構造体に格納しています。 既に出て来た sendto() システムコールでは、この構造体を利用して送信先を決定します。

    まず引数に与えられた送信先名称が IP アドレスであると仮定して inet_aton() を実行します ( 211 行目 )。

      #include <sys/socket.h>
      #include <netinet/in.h>
      #include <arpa/inet.h>
    
      int inet_aton( const char *cp, struct in_addr *inp );
    
      cp  : ドット付き 10 進表示の IP アドレス ( 192.168.0.1 など )
      inp : 32ビットバイナリ値に変換された IP アドレスを格納する構造体
    
    

    返り値が 0 以外であれば変換が正常に終了しています。0 が返って来た場合は、 送信先名称がホスト名であったと判断して、gethostbyname() を使って名前解決を行い IP アドレスを取得します ( 212 行目 )。

      #include <netdb.h>
    
      struct hostent *gethostbyname( const char *name );
    
      name : ホスト名
    
    

    NULL が返された場合は名前解決が出来なかったことを意味します ( 214 〜 221 行目 )。 NULL 以外が返された場合は名前解決の結果が返されていますので、保存します ( 223 行目 )。

    228 行目でサーバとの通信に用いるプロトコルを ( IP の場合は PF_INET もしくは AF_INET )、 229 行目でポート番号を送信先情報として struct sockaddr_in 型の構造体に格納しています。 ポート番号については、ネットワークバイトオーダという形式で数値を格納する必要がありますので、 unsigned short 型の数値をネットワークバイトオーダに変換する htons() 関数を使っています。

    最後に 231 〜 232 行目で送信先 IP アドレスの 32 ビットバイナリ値を格納しています。

  • 238 〜 260 行目 ( need_to_skip() )

    入力された音声をサーバに送信すべきかどうかの判定処理を行っています。といっても、 入力されたデータがある一定の大きさ未満かどうかを判定しているだけです。

    この処理を行わないと、何も入力していない無音状態 ( 実際にはノイズなどの音が入り込んでいる ) でもサウンドカードからデータを取り込みサーバに送信してしまうため、 ネットワークの負荷が非常に高くなってしまいます。

    まあ、送信する音声の圧縮処理などを行っていないので、 話しっぱなしの場合はあまり意味が無いのですが気休め程度に。。。

    ちなみに 0x0040 という値は実験して適当に選んだ値です。ちゃんとやるならば、無音状態の 入力値を毎回設定できる様にする必要があるでしょう。


client.c の説明は以上です。IP 電話の実行は次回のサーバの説明までお待ち下さい m(__)m