[ Top page ]

« Purpose of this blog | Main | Receiving and playing linear voice (VoIP) using Perl »

MultiMedia

Recording and sending linear voice using Perl

A Perl program that sends a voice stream using RTP (Real-time Transport Protocol) is show below. The default codec for input voice is 16-bit linear, but u-Law (G.711) and some other codecs can be used. The port numbers and the IP address of the receiver are specified in this program ($REMOTE_PORT_RTP, $LOCAL_PORT_RTP, $ip, etc.). RTCP (Real-Time Control Protocol) can also be handled, but no specific control is intended. A detailed description will be given in future, but it will be explained when it becomes necessary.

#!/usr/bin/perl
##############################################################################
#
#	Wave File Streamer
#
##############################################################################

use strict;
use Socket;
use Audio::Wav;

Audio::Wav is a package that handles wave files.

use Time::HiRes qw(time sleep);	# for exact time measurements and sleep

Here, package Time::HiRes is to measure the time accurately.

my $CODEC = 'linear16';		# 'ulaw', 'ulaw16', 'linear16' or 'linear32'

my $CHANNELS = $CODEC eq 'ulaw16' || $CODEC eq 'linear32' ? 2 : 1;

This program handles 1- or 2-channel (i.e., stereo) signal. If the value of $CODEC is 'ulaw' or 'linear16', the voice is 1-channel, and it is 'ulaw16' or 'linear32', the voice is 2-channel.

my $SAMPLING_RATE = 8000;

The sampling rate is 8000 Hz. This program must run when replacing the sampling rate by a different value.

my $volume = 0.45;

The sound volume can be changed by this value.

my $wav_file = "WaveFile.wav";

The file name is given here.

my $FRAME_LENGTH = $SAMPLING_RATE / 50;	# number of samples in a packet

my $PACKET_SIZE = 1500;	# Assumed max UDP packet size

my $REMOTE_PORT_RTP = 10000;
my $REMOTE_PORT_RTCP = 10001;

my $LOCAL_PORT_RTP = 8000;
my $LOCAL_PORT_RTCP = 8001;	# local ports for output (UDP)
        # input and output local ports must be different.
        # (implementation restriction)

my $ip = '192.168.1.33';

IP address and port number IP アドレス s とポート番号 d はここできめている.

my $rtp_addr = pack_sockaddr_in($REMOTE_PORT_RTP, inet_aton($ip));
my $rtcp_addr = pack_sockaddr_in($REMOTE_PORT_RTCP, inet_aton($ip));

my ($fd_out_rtp, $fd_out_rtcp, $fd_in_rtcp);

my (@out_ip_rtp, @out_buf_rtp);
my (@out_ip_rtcp, @out_buf_rtcp);

my $debug_switch = 0;
my $inspection_switch = 0;

$inspection_switch |= $debug_switch;


#=============================================================================
# Utility function
#=============================================================================

my $power32 = 4294967296;
my $power16 = 65536;

### current_npt_time()
#   return exact current time
#
sub current_npt_time() {
    my $time = time;
    my $lower = ($time - int($time)) * 4294967296.0;
    my $upper = int($time) + 2208988800;
    return ($upper, $lower);
}

### decode_sockaddr($sockaddr)
#   decode sockaddr_in structure to "$IP:$port" format.
#
sub decode_sockaddr($) {
    my ($sockaddr) = @_;
    if (length($sockaddr) != 16) {
        return '';
    };
    my ($port, $ip) = unpack_sockaddr_in($sockaddr);
    return inet_ntoa($ip) . ":$port";
}

### check_codec($codec)
#   check whether the specified CODEC is appropriate
#
sub check_codec($) {
    my ($codec) = @_;
    if ($codec ne 'ulaw' && $codec ne 'ulaw16' &&
        $codec ne 'linear16' && $codec ne 'linear32') {
	print STDERR "Unknown CODEC: $codec\n";
    };
}

### check_wav($details)
#   check whether the wave file format is appropriate
#
sub check_wav($) {
    my ($details) = @_;
    my $error = 0;
    # my $channels = $$details{channels};
    my $bits_sample = $$details{bits_sample};
    if ($bits_sample != 16) {
	$error = 1;
	print STDERR "#bits in sample ($bits_sample) must be 16\n";
    };
    # my $sample_rate = $$details{sample_rate};
    # if ($sample_rate != 8000) {
    #	$error = 1;
    #	print STDERR "Sample rate ($sample_rate) must be 8000\n";
    # };
    if ($error) {
	exit 1;
    };
}


#=============================================================================
# Network Output 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, pack_sockaddr_in($port, INADDR_ANY)) ||
        die "bind($fd)$!\n";
    return $fd;
}

my %packet_count;
my %octet_count;

### send_rtp($destination_addr, $buf)
#   send an RTP packet which is stored in the ring buffer.
#
sub send_rtp($$) {
    my ($destination_addr, $buf) = @_;
    send($fd_out_rtp, $buf, 0, $destination_addr) || die "send()$!";
    $packet_count{$destination_addr}++;
    $octet_count{$destination_addr} += length($buf);
    if ($inspection_switch) {
        my ($d1, $d2, $seq_no, $timestamp, $ssrc, $a1, $a2, $a3, $a4) =
            unpack('CCnNNCCCC', $buf);
        print decode_sockaddr($destination_addr),
	" out: $a1 $a2 $a3 $a4 seq=$seq_no ssrc=$ssrc\n";
	if ($debug_switch) {
	    print "RTP timestamp=$timestamp length=", length($buf), "\n";
	};
    };
}

### send_rtcp($destination_addr, $buf)
#   send an RTCP packet.
#
sub send_rtcp($$) {
    my ($destination_addr, $buf) = @_;
    send($fd_out_rtcp, $buf, 0, $destination_addr) || die "send()$!";
    if ($inspection_switch) {
	my ($d1, $d2, $seq_no, $timestamp, $ssrc, $a1, $a2, $a3, $a4) =
	    unpack('CCnNNCCCC', $buf);
        print decode_sockaddr($destination_addr),
	" out: $a1 $a2 $a3 $a4 seq=$seq_no ssrc=$ssrc\n";
	if ($debug_switch) {
	    print "RTCP timestamp=$timestamp length=", length($buf), "\n";
	};
    };
}

### receive_rtcp()
#   receive an RTCP packet and reply to the sender.
#
sub receive_rtcp() {
    my $buf;
    my $source_addr = recv($fd_in_rtcp, $buf, $PACKET_SIZE, 0);
    defined($source_addr) || die "recv()$!";
    my ($dummy, $ip) = unpack_sockaddr_in($source_addr);
    my ($first_byte, $payload_type, $length, $ssrc) = unpack('CCnN', $buf);
    my $version = $first_byte >> 6;		# must be 2
    unless ($version == 2) {
        print STDERR "Invalid RTCP packet header: version=$version\n";
    };

    # if ($inspection_switch) {
        my $report_count = $first_byte & 31;
        print "RTCP payload_type=$payload_type rerport_count=$report_count\n";
        print "Control data received from ", inet_ntoa($ip),
        ", length=", length($buf), "\n";
    # };

    ### to analyze the packet here ###

    if ($payload_type == 201) {	# if Receiver Report
        analyze_receiver_report($buf);
    };
}

### analyze_receiver_report()
#
sub analyze_receiver_report($) {
    my ($message) = @_;
    my $jitter = 0;
    my ($time_upper, $time_lower) = current_npt_time();
    my $curr_time = ($time_upper << 16) + ($time_lower >> 16);
    my $delay_since_last_SR = 0;
    my ($ssrc, $frac_lost, $highest_seq_no, $jitter);
    my ($last_SR, $delay_since_last_SR);
    ($_, $_, $ssrc, $frac_lost, $highest_seq_no, $jitter,
     $last_SR, $delay_since_last_SR) =
        unpack('NNNNNNNN', $message);	# 2B rtp header + 6B report block 1
    my $fraction_lost = $frac_lost >> 24;
    my $cumulative_packets_lost = $frac_lost & 0xFFFFFF;
    printf "Round trip time: %5.6f S\n", ($curr_time - $last_SR) / 65536.0;
}

ここから RTCP メッセージを生成するための部分である.


#=============================================================================
# RTCP Message Generator
#=============================================================================

my $CNAME = 1;

my $RTCP_INTERVAL = 5; # sec

my %rtcp_next_time;

### generate_outgoing_rtcp($out_addr, $ssrc, $cname, $timestamp)
#   generate Source Description and Sender Report messages of RTCP
#
sub generate_outgoing_rtcp($$$$) {
    my ($out_addr, $ssrc, $cname, $timestamp) = @_;
    my $current_time = time;
    if ($current_time >= $rtcp_next_time{$out_addr}) {
	if ($rtcp_next_time{$out_addr} == 0) {	# first time
	    $rtcp_next_time{$out_addr} = rand ($RTCP_INTERVAL / 2);
	    # Compute initial sending time
        # } elsif ($rtcp_next_time{$out_addr} == 0) {
	# 			# first time (SDES only)
        #     send_rtcp($out_addr, source_description($ssrc, $cname));
        #         # send a Source Description message of RTCP
        } else {		# not first time (SR and SDES)
            send_rtcp($out_addr,
                      sender_report($out_addr, $ssrc, $timestamp) .
                      source_description($ssrc, $cname));
                # send SR and SDES messages of RTCP
            print "Sending SR and SDES to ", decode_sockaddr($out_addr), "\n";

	    $rtcp_next_time{$out_addr} = $current_time +
		$RTCP_INTERVAL / 2 + rand ($RTCP_INTERVAL);
	};
    };
}

### source_description($ssrc, $my_cname)
#   Return a Source Description of RTCP
#
sub source_description($$) {
    my ($ssrc, $my_cname) = @_;
    my $cname_length = length($my_cname);
    return pack("nnNCCC*",
                0x81ca,	# V=2, PT=202 (SDES), SourceCount=1 (single chunk)
                1 + ($cname_length + 2 + 3) / 4, # length in words
		$ssrc, $CNAME, $cname_length) .
          $my_cname;
}

### sender_report($ssrc, $timestamp)
#   return a Sender Report of RTCP
#
sub sender_report($$$) {
    my ($out_addr, $ssrc, $timestamp) = @_;
    my ($curr_time_upper, $curr_time_lower) = current_npt_time();
    my $packet_count = $packet_count{$out_addr};
    my $octet_count = $octet_count{$out_addr};
    $packet_count{$out_addr} = 0;
    $octet_count{$out_addr} = 0;
    my $sender_info = pack("NNNNN", $curr_time_upper, $curr_time_lower,
                           $timestamp, $packet_count, $octet_count);
    my $header = pack("nnN", 
                      0x81c8, # V=2, PT=200(SR), ReportCount=1 (single chunk)
                      1 + length($sender_info) / 4,	# length in words
                      $ssrc);
    return $header . $sender_info;
}

ここから G.711 のデコーダである. G.711 のあつかいについては 「Perl による G.711 の処理」 において,よりくわしくあつかっている.


#=============================================================================
# Linear to ulaw conversion table generator
#=============================================================================

my $QUANT_MASK = 0xf;
my $BIAS = 0x84;
my $SEG_MASK = 0x70;
my $SEG_SHIFT = 4;
my $SIGN_BIT = 0x80;

my (@u2l, @l2u);

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);
    };
}

### gen_l2u()
#   generate linear-to-ulaw conversion table (@l2u)
#   (This method might not generate an optimum converter.)
#
sub gen_l2u() {
    for (my $i = 0; $i < 256; $i++) {
        my $j = $u2l[$i];
        if ($j < 0) {
            $j += 65536;
        };
        $l2u[$j] = $i;
    };
    for (my $i = 1; $i < 65536; $i++) {
        if ($l2u[$i] == 0) {
            $l2u[$i] = $l2u[$i-1];
        };
    };
}

### gen_ul_conv()
#   generate ulaw <-> linear conversion tables
#
sub gen_ul_conv() {
    gen_u2l();
    gen_l2u();
}

ここから主要部分である.


#=============================================================================
# main
#=============================================================================

if ($ARGV[0] ne '') {
    $wav_file = "$SAMPLING_RATE/$ARGV[0]";
    print "Playing file $wav_file\n";
};
if ($ARGV[1] ne '') {
    $volume = $ARGV[1];
    print "Volume=$volume\n";
};
my $loop = 1;
if ($ARGV[2] eq 'noloop') {
    $loop = 0;
}

$fd_out_rtp =  open_socket($LOCAL_PORT_RTP);
$fd_out_rtcp = open_socket($LOCAL_PORT_RTCP);
$fd_in_rtcp = $fd_out_rtcp;

gen_ul_conv();

check_codec($CODEC);

my $wav = new Audio::Wav;
my $wav_reader = $wav->read($wav_file);

check_wav($wav_reader->details());

my $ssrc = $ARGV[3] | int(rand 0x80000000);
my $seq_no = 0;

my $time_interval = 1000 * $FRAME_LENGTH / $SAMPLING_RATE;
my $timestamp = 0;
my $real_time = time;
my $playout_time = 1000 * $real_time;

for (;;) {
    my @buf;
    for (my $i = 0; $i < $FRAME_LENGTH; $i++) {
        my @data = $wav_reader->read();
        if (!defined($data[0])) {
            if (!$loop) {
                exit 0;
            };
            $wav_reader = $wav->read($wav_file);	# read the file again
            @data = $wav_reader->read();
            print "Repeating...\n";
        };
        $data[0] = $volume * $data[0];
        if ($CODEC eq 'ulaw') {
            $buf[$i] = $l2u[$data[0]];	# only the first channel data is used
        } elsif ($CODEC eq 'linear16') {
            $buf[$i] = $data[0];	# only the first channel data is used
        } elsif ($CODEC eq 'linear32') {
            if (@data >= 2) {		# use two-channel data
                $buf[2*$i] = $data[0];
                $data[1] = $volume * $data[1];
                $buf[2*$i+1] = $data[1];
            } else {			# duplicate single-channel data
                $buf[2*$i] = $data[0];
                $buf[2*$i+1] = $data[0];
            };
        } elsif ($CODEC eq 'ulaw16') {
            if (@data >= 2) {		# use two-channel data
                $buf[2*$i] = $l2u[$data[0]];
                $data[1] = $volume * $data[1];
                $buf[2*$i+1] = $l2u[$data[1]];
            } else {			# duplicate single-channel data
        	my $data = $l2u[$data[0]];
                $buf[2*$i] = $data;
                $buf[2*$i+1] = $data;
            };
        };
    };
    my $buf;
    if ($CODEC eq 'ulaw') {
        $buf = pack('NNNC*', 0x80000000 + $seq_no, $timestamp, $ssrc, @buf);
    } elsif ($CODEC eq 'ulaw16') {
        $buf = pack('NNNC*', 0x80600000 + $seq_no, $timestamp, $ssrc, @buf);
    } else { # linear16 or linear32
        $buf = pack('NNNn*',
        	    ($CODEC eq 'linear16' ? 0x80610000 : 0x80620000) + $seq_no,
        	    $timestamp, $ssrc, @buf);
    };

    send_rtp($rtp_addr, $buf);
    my $cname = getlogin() . ":$LOCAL_PORT_RTP";
    generate_outgoing_rtcp($rtcp_addr, $ssrc, $cname, $timestamp);

    my $rin = '';
    vec($rin, fileno($fd_in_rtcp), 1) = 1;
    my $rout;
    my $nfound = select($rout = $rin, undef, undef, 0);
    if ($nfound > 0) {			# Non-blocking I/O possible
        if (vec($rout, fileno($fd_in_rtcp), 1)) {	# RTCP data readable
            receive_rtcp();
        };
    };

    if ($debug_switch) {
        print "$real_time $playout_time ", decode_sockaddr($rtp_addr), "\n";
    };

    $seq_no++;
    if ($seq_no >= 65536) {
        $seq_no = 0;
    };
    $timestamp += @buf / $CHANNELS;
    $real_time = time;
    $playout_time += $time_interval;
    my $sleep_time = $playout_time / 1000 - $real_time;
    if ($sleep_time > 0) {
        sleep($sleep_time);
    };
    # printf "%f %f\n", $real_time, $sleep_time;
}
Keywords:

TrackBack

TrackBack URL for this entry:
https://www.kanadas.com/mt/mt-tb.cgi/1654

Post a comment

About

This page contains a single entry from the blog posted on November 16, 2007 12:40 AM.

Many more can be found on the main index page or by looking through the archives.

Creative Commons License
This weblog is licensed under a Creative Commons License.
Powered by Movable Type