audread, a simple OpenBSD sound demo

I wrote audread when I was trying to figure out how to use the native sound "audio" that comes with OpenBSD and NetBSD. Then I was working on porting chu to OpenBSD and I needed to see if my audio levels were right or if I was clipping. I tried getting Xoscope working, but this was a lot simpler in the short term. (like 5 minutes)

My interface is literally a few alligator clip leads from the factory pigtail on the back of my IC-7000 to a shielded piece of wire plugged into my motherboard's line in jack. That's it for hardware.

Because I wrote audread as a diagnotic tool to just dump some audio data into a text file as numbers, it was pretty simple to make some little files that drive gnuplot, so that's the only software dependancy as far as I know.

Some CW (Morse code) from 40 meters. You can actually see dits and dahs from different conversations on slightly different frequencies. Kinda works like a cheap oscilloscope huh? (scroll the frames left and right)   Not only a cheap scope but a cheap digital scope, which is sometimes the best kind. Web-friendly too.

CHU, with some static crashes. At the left side is part of one beep, then silence (with static crashes), then at the right is the start of the next beep. This is from 3.330 MHz at 12:21 pm local, which is better than the same thing on 7.85 MHz. The noise peaks are almost as high as the signal.

From 15 meters, somebody calling CQ on a very quiet band. Just one signal here. Sort of a classic good keying waveform with slightly rounded leading and trailing edges.

So this is 8000 samples per second, 16 bit samples, 1 channel, slinear_le encoding. The encoding: the s means signed numbers, the _le means little endian (not a Mac).

These are gifs made by gnuplot, 8050 pixels wide, 480 high. If you plot the default size everything squashes together into a blob and you can't resolve individual cycles. This is big enough to give at least 1 pixel per sample. The gifs have probably 4 colors, so the biggest is 133 K.

For showing these here, each image has its own little html file with the image linked into it. The main page uses iframes which have the little image html files in them. That's how you can scroll the images sideways without scrolling the page.

As a learning experience it's been a pretty good one. I went through the OpenBSD ports collection looking for native mode sound recording programs to study, but I didn't find a single one. Everything goes through PortAudio, sndio, or oss. So I had to figure it out on my own without examples. Maybe this can be of use to somebody else. Also look at man audio (4), man audio (9), /usr/include/sys/audioio.h, /usr/src/lib/libsndio/sio_sun.c. as well as man audioctl (1) and man mixerctl (1). At least you can start with this simple working example and modify from there. This audio setup supposedly originally came from Sun, but I don't know how much resemblance there is between this and anything they're still using. This was done under OpenBSD, but I couldn't help noticing NetBSD copyrights in headers and man pages, so they possibly still do things about the same way. Oh, and this system is called just "audio". Catchy name, huh?   Don't try Googling it.

Using audread

First, if you haven't noticed yet, show_gif and show_gifw make files with the same names each run. If you need to save them like to compare you need to rename them, otherwise they get overwritten.

My favorite image viewer is qiv and I have this alias defined in my .cshrc: "alias qg 'qiv -m *.gif'". I can browse to to the directory where the images are in mc, then type qg. It starts (-m) with maxaspect on, which means it scales the images to fit the screen. Hit "f" for fullscreen mode because you can only move the image in fullscreen mode. Go forward and backward in the image list with space/backspace. When you find the one you want toggle maxaspect off with "m" then you can scroll left and right with left and right arrow keys. "F1" brings up help on qiv. The file list wraps around so often backspace is the quickest way to the last image.

I later defined this alias, which requires qiv and gnuplot, and the paths will probably be different on your machine:

alias ss "cd /usr/programs/c/audread ; ./audread ; gnuplot show_gifw ; qiv -m audw.gif"
By having the intermediate files in /tmp they get deleted when you reboot. The name ss is for Sound Snap because snap is:
alias snap "sleep 5 ; xwd -root -out /tmp/snap.xwd"
which grabs a screenshot. But ss runs audread then gnuplot, then shows the image with qiv. I could add pushd and popd, but this leaves that terminal window in /usr/programs/c/audread where the new gif is. With a widescreen monitor at 1920x1080 resolution the image comes up at 24% zoom. Hit esc or q to close it.

The only ioctl used is AUDIO_SETINFO to load settings. This won't work with FreeBSD or Linux.

So anyway, using it as a scope to set levels: I was an electronics technician and a bit of an audiophile for 20 years, and I know that overdriving a piece of equipment causes clipping and distortion. When you've got multiple pieces of equipment each with their own level control connected together getting those level controls right can be tricky. Here there's a gain setting in the struct audio_info (info.record.gain) and 2 more in the mixer (inputs.line=135,135 and record.volume=255,255). Having the settings too low so that the signals are down in the noise can be almost as bad, but won't cause any permanent damage.

I was working with William Rossi's chu program and had it working fairly well, to the point that I ran the radio and computer for several days. I must have tweaked levels sometime in there. Then I shut it all down overnight to see what the clock drift was like. Since then I've had a really hard time getting a signal that works which could be a problem with distant thunderstorm activity (static crashes) or with levels. I can't easily hook a real scope into the computer to see what's getting through so I wrote this.

I didn't solve the problem directly with this but it did show me that mixerctl record.volume was controlling the level instead of mixerctl record.line like I expected. I set it to 192 instead of 255 and it works better. Your soundcard will probably be different. Absolute levels seem to be a weird thing, I couldn't see any clipping but apparently it was.

So here's the tarball, a whopping 2.6 K.

And a copy of the actual program source:

/*
  Very basic program to get an audio sample via NetBSD/OpenBSD Sun-type audio
  and dump it to a text file.  The show_gif and show_gifw files are for gnuplot
  and will display the audio.  There's also a raw binary dump.
  
  The audio is 1 channel 16 bit signed data with slinear_le encoding at an
  8000 samples per second sample rate (a 1 second recording).  show_gif will
  make a little 640x480 gif with the audio all bunched together.  show_gifw
  makes a gif 8050x480, so there's about 1 horizontal pixel per sample.  The
  result is quite oscilloscope-like.  View it at 100% scale for best results
  (scroll horizontally).
  
  Written under OpenBSD 5.0 5/27/2012 by AB1JX.  I don't know that it works
  with NetBSD but NetBSD copyrights are in the headers and man pages.
  
*/

#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/audioio.h>
#include <sys/ioctl.h>

typedef long ssize_t;  // pita

#define DATASIZE 8000  /* number of samples to take (1 second) */

int fd;  // file descriptor
char device[80];
short int buf[8192];  // 16 bits
ssize_t bytesread;
struct audio_info info;

void init_device() {  // set the sound card
  AUDIO_INITINFO(&info); // fill block with 0xff
  info.record.sample_rate = 8000;
  info.record.encoding = AUDIO_ENCODING_SLINEAR_LE;  // default anyway
  info.record.channels = 1;
  info.record.gain = 192;
  info.record.precision = 16;
  info.record.pause = 0;
  info.record.active = 1;
  info.mode = AUMODE_RECORD;
  if (ioctl(fd, AUDIO_SETINFO, &info) < 0) {  // write to sound card
    printf("Error doing AUDIO_SETINFO to initialize.\n");
    perror("init");
  }
}

void dump_asc(void) {  // dump as an ascii file
  FILE *opf;
  int i;
  opf = fopen("/tmp/audio.txt","w");
  if (opf != NULL) {
    for (i=0; i< DATASIZE; i++) 
      fprintf(opf,"%f\t%i\n",i/8000.0,buf[i]); // tab delimited
    fclose(opf);
  } else {
    printf("Ascii file didn't open.\n");
  }
}

void dump(void) { // write out the buffer, binary
  int fdw;
  fdw = open("/tmp/audio.dat", O_WRONLY | O_CREAT, 0777);
  if (fdw > 0) {
    (void) write(fdw,buf,DATASIZE*2);
    close(fdw);
  } else {
    printf("open outfile failed\n");
    perror("oops: ");
  }
}

void do_read(void) {  // read 1 second of sound
  bytesread =  read(fd, buf, DATASIZE * 2);  // 16 bit samples
  if (bytesread > 0) {
    printf("Read %lu bytes.\n",bytesread); 
  } else {
    printf("read failed\n");
    perror("oops: ");
  }
  close(fd);
}

void emptybuf(void) {  // get rid of turnon click
  short int tmpbuf[(DATASIZE/10)];
  bytesread =  read(fd, tmpbuf, DATASIZE / 10);  // 16 bit samples
  if (bytesread > 0) {
    printf("Read %lu bytes to clear buffer.\n",bytesread); 
  } else {
    printf("read failed\n");
    perror("oops: ");
  }
}
                      

void do_open(void) {  // open the sound card
  fd = open(device, O_RDONLY | O_NOCTTY, 0777);
  if (fd < 0) {
    printf("Error opening %s\n", device);
    perror("oops ");
    exit(1);
  }
}

int main(void) {
  strncpy(device,"/dev/audio",80);
  do_open();
  init_device();
  emptybuf();
  do_read();
  dump_asc();
  dump();
  printf("ascii data written to /tmp/audio.txt, binary to /tmp/audio.dat\n");
  return 0;
}

And show_gifw:


# file to use gnuplot for plotting audio captured by audread.  Makes a gif 
# that's 8050x480 pixels to accomodate audread's 8000 sample batch.
# Run audread first with sound input, then "gnuplot show_gifw"

reset

set terminal gif giant size 8050,480
set output "audw.gif"

# by default running 8000 samples/sec and this shows 1 second

set style data linespoints
set xtics auto
plot '/tmp/audio.txt' using 1:2 with lines title "raw audio data"


I later made a modified version of this to try to show PEP audio power based on something I saw in QST. I tried it on a CHU signal here

AB1JX / toys/