Monday, August 2, 2010

Controlling V4L2 radio with Perl on Linux.

Everyone buys their Hauppauge cards to use the TV aspect of the cards, but when my stereo gave up the ghost, I decided to look at using the computer in the kitchen as a replacement radio. This is my journey!

I first had a look at the apps bundled with Gnome and KDE that claimed to control the radio, but very few of them could control a V4L2 device, they were all aimed at V4L devices. I have a PVR350 in the box (from when it was a MythTV PVR back in the day), which is a V4L2 device. I wondered could I integrate the radio as a streaming source for my Logitech/SlimDevices Squeezeserver, and then get radio on any of the Squeezeboxes in the house, as well as anything else that could play a stream. I could have done this in C, but I like a challenge, and wanted to learn about device control! So I went with Perl.

The ivtv driver that controls the PVR350 comes with a sample radio command line program - ivtv-radio. It was an easy introduction to device control using ioctl, which I had no experience of before, but basically is a way of controlling I/O devices by sending a command and a variable. The command is sent as certain number, depending on the command you want, and the variable is a struct - a specific combination of ints, doubles etc to hold various information. The command is either a "set" or "get" - so either writing to the device using data in the struct, or reading from the device into the struct.

First, the commands. In C, there are header files (.h) that hold the correct commands and appropriate numbers, but not for Perl. There's a command line tool - h2ph - which converts C header files into Perl Header files. For V4L2 ioctl, the header file is <linux/videodev2.h>. So:

cd /usr/include
h2ph -d /example/perlradio/inc -a linux/videodev2.h

creates the perl version of this file and all necessary other .ph files in the /example/perlradio/inc directory.

It doesn't do the whole job unfortunately, and there's very little feedback when things go wrong. So it helps to redefine a function:


# redefine unhelpful _IOC_TYPECHECK macro
sub _IOC_TYPECHECK {
  $sizeof{$_[0]} or do {
  # complain that we don't know the size of this struct
  # (we can't die because we're being eval-ed by _IOR or _IOW)
  print STDERR "invalid size argument for IOC: ".
    "\%sizeof array contains no '$_[0]' key\n";
  exit 1;
  }
}

and we have to tell it how big each struct is using a hash array called %sizeof - C can figure this out by itself. So:

# define a sizeof hash
%sizeof = (
        'struct v4l2_tuner' => 84,
        'struct v4l2_frequency' => 44,
        'struct v4l2_control' => 8,
);

Those structs came from the videodev2.h file and the V4L2 documentation - the best of which I found here: http://v4l2spec.bytesex.org/spec/r7624.htm.

As an example, the first thing we do is query the tuner to get some information. This means sending a VIDIOC_G_TUNER command, with a v4l2_tuner struct to hold the response. The radio is /dev/radio0, so we open that and call ioctl on it.

require "sys/ioctl.ph";
require "linux/videodev2.ph";

open(RADIO_DEV, "+<", "/dev/radio0");
my $v4l2_tuner = pack("c84", 0);
my $retval = ioctl(RADIO_DEV, VIDIOC_G_TUNER(), $v4l2_tuner) || -1;
my @struct = unpack("LZ32iLLLLLLlLLLL", $v4l2_tuner);

@struct now contains the information. We need to create a place to hold the data, so we pack 84 null chars into $v4l2_tuner. Afterwards, this variable holds a packed set of variables, which we can extract using unpack. The "LZ32iLLLLLLlLLLL" comes from the documentation - __u32, __u8[32] (string),  __s32 etc. See pack from the perl documentation for more on this.

To set the frequency, we'll use the VIDIOC_S_FREQUENCY command, with a struct holding the frequency we want. Some tuner cards need the frequency in MHz (e.g 90.7) and some want it in KHz (e.g. 90700). To find out what we have, we can use the capability field in the struct we just retrieved. This is in @struct[3]. We logical-AND it with V4L2_TUNER_CAP_LOW - 1 means KHz, 0 means MHz.  There's also a constant mutiplier - 16.

my $freqMultiplier = $struct[3] & V4L2_TUNER_CAP_LOW() ? 1000 : 1;
my $frequency = sprintf("%u", 90.7 * $freqMultiplier * 16);
my $tuner = 0;
my $tuner_type = V4L2_TUNER_RADIO();

We pack up the v4l2_frequency struct, and provide it to ioctl:

$v4l2_frequency = pack("LiLLLLLLLLLL", 
    $tuner, $tuner_type, $frequency, 0,0,0,0,0,0,0,0);
$retval = ioctl(RADIO_DEV, VIDIOC_S_FREQUENCY(), $v4l2_frequency) || -1;

The radio is now set to 90.7MHz, all we need to do is play the audio, which comes through /dev/video24. A quick way to do this is to use aplay:

system("aplay -f dat < /dev/video24 &");