Alternative call waiting service with Perl

The Listener


If you can't get through to your home number, a script can check the line status and send a signal to you via the Web to tell you when the line is free.

By Michael Schilli

You're trying to call home, but somebody is hogging the line. Of course, you could always ask your telco to enable call waiting [2]. The service lets marathon phone users know that a call is waiting by beeping every couple of seconds. On the other hand, the service costs money, and the beep can get on your nerves. This is what prompted me to buy a small phone amplifier (Figure 1 and [3]), and put together a handy application.

Figure 1: Smart Phone Recorder Control by Radio Shack feeds the signal from the phone line to the sound card in a Linux computer.

The Smart Phone Recorder Control grabs the signal off the phone line and feeds it to the sound card in my Linux computer. Linux can access the signal via the /dev/dsp device, and the Perl Audio::DSP module from CPAN will read it. The script uses a couple of heuristic tricks to determine whether the line is busy, and if so, it keeps on trying until the line is free, before notifying the user via a hidden CGI script on a website.

Figure 2: A website enables the script on the Linux machine and outputs the call status.

Remote Sleeper

In its idle state, the phonewatch script (Listing 1) on the Linux computer in your home checks the website every 60 seconds to see whether someone has clicked to run the CGI script and enabled the "Check" status (Figure 3). If so, the script wakes up and starts to collect data off the phone line. It displays a "busy" message on the website while the line is busy, refreshing the browser page every 60 seconds.

Figure 3: The CGI script is in idle mode; the listener is waiting for action.

Figure 4: The listener verifies that the line is busy and displays a message.

If you hang up, phonewatch detects the status change and switches the CGI script status back to "idle".

The infinite loop that starts in Line 24 detects the status on the website, and sleeps for $IDLE_POLL seconds, while state is set to idle. The state() function simply serves to query the current status. However, it can set a new status if you pass a parameter in to it. Both of these tasks are handled by a get call from the LWP::Simple module, which references a website by its URL. It filters the script status out of the < b > tag in the page content returned in the response.

Listing 1: phonewatch
01 #!/usr/bin/perl -w
02
03 use strict;
04 use Audio::DSP;
05 use Log::Log4perl qw(:easy);
06 use SoundActivity;
07 use LWP::Simple;
08
09 Log::Log4perl->easy_init(
10  {
11   file =>
12     "/tmp/phonewatch.log",
13   level => $INFO,
14  }
15 );
16
17 my $IN_USE_POLL = 10;
18 my $IDLE_POLL   = 60;
19 my $STATUS_URL  =
20 'https://u:p@_foo.com/phonewatch.cgi';
21 my $SAMPLE_RATE = 1024;
22
23 INFO "Starting up";
24 while (1) {
25  my $state = state();
26
27  if (!defined $state) {
28   DEBUG "Fetch failed";
29   sleep $IDLE_POLL;
30   next;
31  }
32
33  DEBUG
34    "web site state: $state";
35
36  if ($state eq "idle") {
37   DEBUG "Staying idle";
38   sleep $IDLE_POLL;
39   next;
40  }
41
42  INFO "Monitor requested";
43  state("busy");
44  poll_busy();
45  state("idle");
46 }
47 #############################
48 sub poll_busy {
49 #############################
50  my $dsp = new Audio::DSP(
51   buffer   => 1024,
52   channels => 1,
53   format   => 8,
54   rate     => $SAMPLE_RATE,
55  );
56
57  $dsp->init()
58    or die $dsp->errstr();
59
60  my $act =
61    SoundActivity->new();
62
63  while (1) {
64   DEBUG "Reading DSP";
65   $dsp->read()
66     or die $dsp->errstr();
67
68   $act->sample_add(
69    $dsp->data());
70   $dsp->clear();
71
72   if (!$act->is_active()) {
73    INFO "Hangup detected";
74    $dsp->close();
75    return 1;
76   }
77   sleep $IN_USE_POLL;
78  }
79 }
80 #############################
81 sub state {
82 #############################
83  my ($value) = @_;
84
85  my $url = $STATUS_URL;
86  $url .= "?state=$value"
87    if $value;
88  DEBUG "Fetching $url";
89  my $content = get $url;
90  if ($content =~
91   m#<b>(.*?)</b>#)
92  {
93   return $1;
94  }
95 }

Audio Fishing

The Audio::DSP class constructor expects four parameters: the length of the data buffer to fill (1024), the number of channels (1 here, for mono), the sample format (unsigned 8bit), and the sampling rate (1024 samples per second).

Digital audio data is written in a numeric format that an ADC samples n times per second from the analog sound signal. The sampling rate n must be twice the size of the highest audio frequency you want to sample; that is about 40,000 samples per second for HiFi quality (slightly above 20 kHz). As we do not actually need to eavesdrop the conversation, 1024 samples per second are perfectly ok. For more information on "Digital Audio" refer to [2].

Within a second, phonewatch fills up the data buffer, and feeds it to the sample_add() method of the module shown in the SoundActivity.pm script (Listing 2). The method unpacks the 8-bit values (unpack("C")) from the data block passed to it, and calculates the standard deviation for the values. The sound card will not be able to pick up a signal from a line that is down; there is only some noise at the sound card's microphone input.

In idle state, the sampling values, which have a range of between 0 and 255, will all be approximately 127, and will deviate by a maximum of 1 to either side. The standard deviation was typically around 0.5 in my lab.

The sdev method that starts in Line 84 of SoundActivity.pm calculates the standard deviation for the elements in an array passed in as a pointer to an accuracy of two decimal places. sdev() uses the CPAN Statistics::Basic::StdDev module for the simple mathematics required here.

Listing 2: SoundActivity.pm
01 #############################
02 # Mike Schilli, 2006
03 # m@perlmeister.com
04 #############################
05 package SoundActivity;
06 #############################
07
08 use strict;
09 use warnings;
10 use
11   Statistics::Basic::StdDev;
12 use Log::Log4perl qw(:easy);
13
14 #############################
15 sub new {
16 #############################
17
18  my ($class, %options) = @_;
19
20  my $self = {
21   min_hist       => 5,
22   max_hist       => 5,
23   history        => [],
24   sdev_threshold => 0.01,
25   %options,
26  };
27
28  bless $self, $class;
29 }
30
31 #############################
32 sub sample_add {
33 #############################
34
35  my ($self, $data) = @_;
36
37  my $len     = length($data);
38  my @samples =
39    unpack("C$len", $data);
40
41  my $sdev =
42    $self->sdev(\@samples);
43
44  my $h = $self->{history};
45  push @$h, $sdev;
46  shift @$h
47    if @$h >
48    $self->{max_hist};
49  DEBUG "History: [",
50    join(', ', @$h), "]";
51 }
52
53 #############################
54 sub is_active {
55 #############################
56
57  my ($self) = @_;
58
59  if (@{ $self->{history} } <
60   $self->{min_hist})
61  {
62   DEBUG
63     "Not enough samples yet";
64   return 1;
65  }
66
67  my $sdev =
68    $self->sdev(
69   $self->{history});
70  DEBUG "sdev=$sdev";
71
72  if ($sdev <
73   $self->{sdev_threshold})
74  {
75   DEBUG
76     "sdev too low ($sdev)";
77   return 0;
78  }
79
80  return 1;
81 }
82
83 #############################
84 sub sdev {
85 #############################
86
87  my ($self, $aref) = @_;
88
89  return sprintf "%.2f",
90    Statistics::Basic::StdDev
91    ->new($aref)->query;
92 }
93
94 1;

Wake Up and Sample!

In active mode, phonewatch hangs around in the poll_busy() function. It samples the sound card for a second, waits for 10 seconds, and then ascertains the next sample point. The five recorded values for the standard deviation would look something like this for a phone call: [0.64, 0.78, 0.73, 0.89, 0.86]

The maximum number of entries in the standard deviation history is defined by the parameter max_hist in SoundActivity.pm. min_hist defines the minimum number of historic values required by the module to safely guess the line status. When you hang up, the line goes quiet again, and the standard deviation history levels out at a constant value: [0.51, 0.51, 0.51, 0.51, 0.51]

To distinguish between these two states, SoundActivity.pm simply reevaluates the standard deviation for these five historic values. If it is less than 0.01, the line is assumed to be down, and is_active() will return a value of false.

The CGI script on the web server stores its state across multiple calls in phonewatch.dat, where it stores a persistent hash called %store. New status values are written by the CGI state parameter.

All the script has to do then is to retrieve the appropriate state color from the %states hash, and call the process method from the template toolkit with the HTML template defined in the DATA attachment. This generates an HTML rendering of the page by interpreting the simple but effective template language. At the same time, a reload meta-tag is injected into the page to tell the browser to refresh the page every 30 seconds with a call to the CGI script. In turn, this ensures that the state() change, detected by the Linux machine when the caller hangs up, is passed on to the browser to end the user's wait.

When called without any parameters, the CGI script simply returns the current state as formatted HTML: However, if a new state is passed to the script in the CGI status parameter (idle, busy, or check), the script modifies its internal status, and stores it permanently.

It is not a good idea to expose the script on the untrusted Internet; a simple form of protection would be to use a .htaccess file that prompts the user for a username and password. When phonewatch calls the CGI script, it simply bundles the credentials with the URL, https://user:pass@URL....

Same Old Song

Getting a sound card to run on Linux is not always simple, however, Audacity worked perfectly with ALSA 1.0.13, which I used for my experiments. It is important to switch the microphone input from "Line" to "Mic" in the alsamixer (Figure 5) to enhance the gain.

Figure 5: To make sure the microphone input on your sound card works properly, the control on the far right in alsamixer should be set to "Mic" and not to "Line".

The CPAN modules used by the scripts are easily installed using a CPAN shell, as always; you will want to install the additional SoundActivity module in the same directory as phonewatch, or in a directory in which phonewatch will be able to find it.

The logging level for phonewatch is set to $INFO by default, and this default level only appends a minimal amount of critical status information to the /tmp/phonewatch.log logfile. You might prefer to set the level to $DEBUG for more detail.

Set up an entry in your inittab to automatically launch the script. This sends the program into an infinite loop and gives you a friendly helper that you can enable from wherever you are with a web browser.

Listing 3: phonewatch.cgi
01 #!/usr/bin/perl -w
02 use strict;
03 use CGI qw(:all);
04 use DB_File;
05 use Template;
06
07 my %states = (
08  idle  => 'green',
09  check => 'yellow',
10  busy  => 'red',
11 );
12 tie my %store, "DB_File",
13   "data/phonewatch.dat"
14   or die $!;
15 $store{state} = "idle"
16   unless
17   defined $store{state};
18 print header();
19 my $new = param('state');
20 if ($new
21  and exists $states{$new})
22 {
23  $store{state} = $new;
24 }
25 my $tpl = Template->new();
26 $tpl->process(
27  \join('', <DATA>),
28  {
29   bgcolor =>
30     $states{ $store{state} },
31   state => $store{state},
32   self  => url(),
33  }
34   )
35   or die $tpl->error;
36 #############################
37 __DATA__
38 <HEAD>
39   <META HTTP-EQUIV="Refresh"
40         CONTENT="30;
41             URL=[% self %]">
42 </HEAD>
43 <BODY>
44   <H1>Phone Monitor</H1>
45   <TABLE CELLPADDING=5>
46     <TR>
47       <TD BGCOLOR="[% bgcolor %]">
48         Status: <b>[% state %]</b>
49       </TD>
50       [% IF state == "idle" %]
51       <TD>
52         <A HREF="[% self %]?state=check">
53         check</A>
54       </TD>
55       [% END %]
56     </TR>
57   </TABLE>
58 </BODY>
INFO
[1] Call Waiting: http://en.wikipedia.org/wiki/Call_waiting
[2] Ken C. Pohlmann: Principles of Digital Audio, McGraw Hill, 2005
[3] Smart Phone Recorder Control, Radio Shack catalog, http://www.radioshack.com/search/index.jsp?kw=43-2208
[4] Listings for this article: http://www.linux-magazine.com/Magazine/Downloads/74/Perl