RTP (Real-time Transport Protocol) による 2 チャンネル (ステレオ) の音声 (VoIP, Voice over IP) を受信してオーディオ再生するプログラムをしめす. 音声は 16 bit 線形を基本とするが,u-Law (G.711) などもあつかえるようにしてある. 受信のためのポート番号はこのプログラムのなかで指定されている ($IN_PORT_RTP). とりあえず,こまかい説明ははぶくが,あとで必要に応じて説明をくわえることにしたい.
#!/usr/bin/perl ############################################################################## # # 2-channel RTP Stream Player # ############################################################################## use strict; use Socket; use Time::HiRes qw(time); require 'sys/ioctl.ph'; require 'sys/soundcard.ph'; my $PACKET_SIZE = 1500; # Assumed max UDP packet size my $IN_PORT_RTP = 8000; my $IN_PORT_RTCP = 8001; # local ports for input my ($source_ip, $source_port_rtp, $source_port_rtcp); my ($fd_in_rtp, $fd_in_rtcp); my %buf_rtp; my $buf_displ = -1; my %repeat_count; my $curr_time = 0; my $delta_time; my $inspection_switch = 0; #============================================================================= # Utility function #============================================================================= my $power32 = 4294967296; my $power16 = 65536; ### current_time() # return current time in the timestamp format. # sub current_time() { return int(time * 8000); # return int(((time + 2208988800) * $power16) % $power32); } #============================================================================= # Network Input functions #============================================================================= ### open_socket($proto) # open a UDP port of the local host (both for input and output), # and return the file descriptor. # sub open_socket($) { my ($port) = @_; my $fd; socket($fd, AF_INET, SOCK_DGRAM, getprotobyname('udp')) || die "socket($fd)$!\n"; setsockopt($fd, SOL_SOCKET, SO_REUSEADDR, pack("l", 1)) || die "setsockopt()$!\n"; bind($fd, sockaddr_in($port, INADDR_ANY)) || die "bind($fd)$!\n"; return $fd; } my %delay_estimate; ### receive_rtp_packet($curr_time) # receive an RTP packet and put it into the ring buffer. # sub receive_rtp_packet($) { my ($curr_time) = @_; my $buf; my $source_addr = recv($fd_in_rtp, $buf, $PACKET_SIZE, 0); defined($source_addr) || die "recv()$!"; my ($port, $ip) = sockaddr_in($source_addr); if ($source_ip ne '' && $ip ne $source_ip) { printf stderr "Packets from two IPs received: $source_ip and $ip\n"; }; $source_ip = $ip; if ($source_port_rtp ne '' && $port ne $source_port_rtp) { printf stderr "RTP Packets from two ports received: $source_port_rtp and $port\n"; }; $source_port_rtp = $port; my ($first_byte, $second_byte, $seq_no, $timestamp, $ssrc) = unpack('CCnNN', $buf); if ($inspection_switch) { my ($d1, $d2, $d3, $a1, $a2, $a3, $a4) = unpack('LNNCCCC', $buf); print inet_ntoa($ip), " in: $a1 $a2 $a3 $a4 seq=$seq_no ssrc=$ssrc\n"; }; my $payload_type = $second_byte & 7; test_rtp_packet($first_byte, $second_byte, $payload_type, $ip, $buf); my $delta = length($buf) - 12; if ($delta_time == 0) { $delta_time = $delta; } elsif ($delta != $delta_time) { print stderr "Sample size not equal!\n"; }; my $time_diff = $curr_time - $timestamp; if ($delay_estimate{$ssrc} == 0) { $delay_estimate{$ssrc} = $time_diff; } else { $delay_estimate{$ssrc} = (31 * $delay_estimate{$ssrc} + $time_diff) / 32; }; if ($buf_displ < 0) { # $buf_displ not yet defined $buf_displ = $seq_no; }; if ($seq_no >= $buf_displ) { ${$buf_rtp{$ssrc}}[$seq_no - $buf_displ] = $buf; } elsif ($seq_no == $buf_displ) { ${$buf_rtp{$ssrc}}[0] = $buf; $repeat_count{$ssrc} = 0; } else { # Discard the data if it is too old (i.e., nothing to do). }; } ### test_rtp_packet ($first_byte, $second_byte, $payload_type) # test validity of RTP contents. # sub test_rtp_packet ($$$) { my ($first_byte, $second_byte, $payload_type) = @_; my $version = $first_byte >> 6; # must be 2 my $extension = ($first_byte >> 4) & 1; # must be 0 my $n_csrc = $first_byte & 15; my $marker = $second_byte >> 7; ($version == 2 && $extension == 0 && $n_csrc == 0 && $marker == 0) || die "Invalid RTP packet header: version=$version " . "extension=$extension #CSRC(CC)=$n_csrc marker=$marker\n"; $payload_type == 0 || die "Payload type ($payload_type) must be 0 (G.711 ulaw)\n"; }; ### receive_rtcp_packet() # receive an RTCP packet.and put it into the ring buffer. # sub receive_rtcp_packet() { my $buf; my $source_addr = recv($fd_in_rtcp, $buf, $PACKET_SIZE, 0); defined($source_addr) || die "recv()$!"; my ($port, $ip) = sockaddr_in($source_addr); if ($source_ip ne '' && $ip ne $source_ip) { printf stderr "Packets from two IPs received: $source_ip and $ip\n"; }; $source_ip = $ip; if ($source_port_rtcp ne '' && $port ne $source_port_rtcp) { printf stderr "RTCP Packets from two ports received: $source_port_rtcp and $port\n"; }; $source_port_rtcp = $port; my ($first_byte, $packet_type, $length, $ssrc, $timestamp1, $timestamp2) = unpack('CCnN', $buf); my $version = $first_byte >> 6; # must be 2 ($version == 2) || die "Invalid RTCP packet header: version=$version\n"; }
ここから G.711 のエンコーダである. G.711 のあつかいについては 「Perl による G.711 の処理」 において,よりくわしくあつかっている.
#============================================================================= # ulaw -> linear conversion table generator #============================================================================= my $QUANT_MASK = 0xf; my $BIAS = 0x84; my $SEG_MASK = 0x70; my $SEG_SHIFT = 4; my $SIGN_BIT = 0x80; my @u2l; sub u2l($) { my ($uval) = @_; $uval = ~$uval; my $t = (($uval & $QUANT_MASK) << 3) + $BIAS; $t <<= ($uval & $SEG_MASK) >> $SEG_SHIFT; return ($uval & $SIGN_BIT) ? ($BIAS - $t) : ($t - $BIAS); } ### gen_u2l() # generate ulaw-to-linear conversion table (@u2l) # sub gen_u2l() { for (my $i = 0; $i < 256; $i++) { $u2l[$i] = u2l($i); }; }
ここから音声出力のための初期化をおこなう部分である.
#============================================================================= # Open sound output #============================================================================= my $BUF_SIZE = 2000; sub open_sound_output() { my $status; my $parameter; open(SOUND_OUT, ">/dev/audio") || die "Sound output open failed!\n"; # 16 bit sampling for output my $bits = pack("L", 16); $status = ioctl(SOUND_OUT, SOUND_PCM_WRITE_BITS(), $bits); if ($status < 0) { print stderr "8 bit sampling setting failed!\n"; }; # 2-channel (binaural) # $parameter = pack("L", 1); # $status = ioctl(SOUND_OUT, SNDCTL_DSP_STEREO(), $parameter); # if ($status < 0) { # print stderr "Stereo setting failed!\n"; # }; # Number of channels = 2 $parameter = pack("L", 2); $status = ioctl(SOUND_OUT, SNDCTL_DSP_CHANNELS(), $parameter); # $status = ioctl(SOUND_OUT, SOUND_PCM_WRITE_CHANNELS(), $parameter); if ($status < 0) { print stderr "#Channels setting failed!\n"; }; # 8000 Hz sampling rate for output my $sampling = pack("L", 8000); $status = ioctl(SOUND_OUT, SNDCTL_DSP_SPEED(), $sampling); # $status = ioctl(SOUND_OUT, SOUND_PCM_WRITE_RATE(), $sampling); if ($status < 0) { print stderr "8000 Hz sampling rate setting failed!\n"; }; # Buffer underrun may be cause by network $parameter = APF_NETWORK(); $status = ioctl(SOUND_OUT, SNDCTL_DSP_PROFILE(), $parameter); if ($status < 0) { print stderr "ioctl(SNDCTL_DSP_PROFILE) failed!\n"; }; # Block size $parameter = pack("L", $BUF_SIZE); $status = ioctl(SOUND_OUT, SNDCTL_DSP_GETBLKSIZE(), $parameter); if ($status < 0) { print stderr "ioctl(SNDCTL_DSP_GETBLKSIZE) failed!\n"; }; } #============================================================================= # Playout (deciding timing for output) #============================================================================= my ($left_ssrc, $right_ssrc); my %repeat_count; ### playout() # make the first element of each buffer the playout data. # sub playout() { if ($left_ssrc eq '') { my @ssrcs = keys %buf_rtp; if (@ssrcs >= 2) { # already received two streams (SSRCs) if ($ssrcs[0] > $ssrcs[1]) { $left_ssrc = $ssrcs[1]; $right_ssrc = $ssrcs[0]; } else { $left_ssrc = $ssrcs[0]; $right_ssrc = $ssrcs[1]; }; } } else { my $left_buf = ${$buf_rtp{$left_ssrc}}[0]; my $right_buf = ${$buf_rtp{$right_ssrc}}[0]; if ($repeat_count{$left_ssrc} == 0 && $repeat_count{$right_ssrc} == 0 || @{$buf_rtp{$left_ssrc}} > 2 && @{$buf_rtp{$right_ssrc}} > 2) { if ($inspection_switch) { my ($left_seq, $right_seq, $left_ts, $right_ts); ($_, $left_seq, $left_ts) = unpack('nnNN', $left_buf); ($_, $right_seq, $right_ts) = unpack('nnNN', $right_buf); if ($left_seq != $right_seq) { print stderr "Seq num mismatched! $left_seq $right_seq\n"; }; if ($left_ts != $right_ts) { print stderr "Timestamp mismatch! $left_ts $right_ts\n"; }; }; shift @{$buf_rtp{$left_ssrc}}; # discard an old packet shift @{$buf_rtp{$right_ssrc}}; # discard an old packet if (${$buf_rtp{$left_ssrc}}[0] eq '') { # no data in the L-ch buffer head ${$buf_rtp{$left_ssrc}}[0] = $left_buf; # repeat the data $repeat_count{$left_ssrc}++; } else { $repeat_count{$left_ssrc} = 0; }; if (${$buf_rtp{$right_ssrc}}[0] eq '') { # no data in the R-ch buffer head ${$buf_rtp{$right_ssrc}}[0] = $right_buf; # repeat the data $repeat_count{$right_ssrc}++; } else { $repeat_count{$right_ssrc} = 0; }; $buf_displ++; my (@left_data, @right_data); ($_, $_, $_, @left_data) = unpack('LLLC*', $left_buf); ($_, $_, $_, @right_data) = unpack('LLLC*', $right_buf); if ($#left_data != $#right_data) { print stderr "Numbers of left/right data different!\n"; }; # decode @left_data and @right_data my $outbuf; for (my $i = 0; $i <= $#left_data; $i++) { $outbuf .= pack("SS", $u2l[$left_data[$i]], $u2l[$right_data[$i]]); }; syswrite(SOUND_OUT, $outbuf); }; }; }
このプログラムの主要部分である.
#============================================================================= # main #============================================================================= open_sound_output(); $fd_in_rtp = open_socket($IN_PORT_RTP); $fd_in_rtcp = open_socket($IN_PORT_RTCP); gen_u2l(); for (;;) { ## Test non-blocking I/O possibilities ## # $rin = file descriptor set [$fd_in_rtp, $fd_in_rtcp] my $rin = ''; vec($rin, fileno($fd_in_rtcp), 1) = 1; vec($rin, fileno($fd_in_rtp), 1) = 1; my $rout; my $nfound = select($rout = $rin, undef, undef, 0); if ($nfound > 0) { # Non-blocking I/O possible ## Accept packets ## if (vec($rout, fileno($fd_in_rtcp), 1)) { # RTCP data readable # (Control data prioritized) receive_rtcp_packet(); }; if (vec($rout, fileno($fd_in_rtp), 1)) { # RTP data readable receive_rtp_packet($curr_time); }; }; playout(); }