|
○ はじめに
OSS の API の解説もある程度終わりました(僕が使う程度のものは。という意味です。。。)ので、今回からは少し遊べるものを作って行きましょう!
というわけで、今回および次回の 2 回で IP 電話もどきを作ってみたいと思います。
音声の圧縮やパケットの遅延などの考慮は全く行いませんので、せいぜい LAN 内で使用できる程度のものです。とはいっても、離れた場所で通話できるのは結構おもしろいのでぜひやってみて下さい。
ごく簡単なものとするために、著者の独断と偏見によって以下の仕様で作成します。
- クライアント・サーバ構成とする
- クライアントはマイクから入力された音声をサーバに UDP で送信する
- サーバはクライアントから送られて来た音声をスピーカに出力する
- 音声フォーマットは 16bit、44.1KHz、モノラルとする
仕様から分かる様に通信の方向はクライアント→サーバのみです。よって 2台の PC で双方向に通話したい場合は、自分の PC でサーバを実行した上で、クライアントを相手のサーバに接続する必要があります。
--- 図 1 ---
今回はクライアントのプログラムについて解説します。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