Vison Center Lab


This is a very simple tutorial for an extremely inexpensive device to capture low frequency signals (<200 Hz or so) using a sound card.

The problem with sound cards is that they bandpass their input signals. So, if you are wanting to record any signals below 20Hz or so, you are out of luck, ordinarily...

There are two solutions to this problem that I have found. The first, is that if you are lucky, you can modify your sound card to remove the high-pass filter, which is removing the signals below 20Hz. The first option has a very nice tutorial written by Scott Molloy (http://www.mandanet.net/adc/adc.shtml). Unfortunately, the sound cards that I had access to could not be modified to remove the high-pass filtering (they are actually done in an IC now). The second option is to make a Frequency Modulated circuit, this idea was first published by David Prutchi and Michael Norris in August 19, 2002's edition of Electronic Design (http://www.elecdesign.com/Articles/ArticleID/2641/2641.html). This tutorial is based upon their design. The difference is that I have simpified the design and made it dual channel.  The total cost should be $15 to $20.

This is the circuit schematic: schematic

The circuit expects a +/-10 volt input signal. The circuit should generate a carrier frequency of approximately 4.4Khz and a change of frequency of up to 3Khz. A greater range could be used to reduce the signal to noise and increase the upper frequency of the input signal (approximately 200Hz with this circuit).

The carrier frequency is mainly determined by resisters R2 and R5 and capacitors C2 and C4.  The range of frequency modulation is mainly determined by resisters R3 and R6.  With a 10 volt input range having R2 = R3 and R5 = R6 seems to work well.  Due to variability in the XR-2206 and the capacitors used for C2 and C4, the actual carrior frequency will probably be off.  If the actual carrior frequency is substantially different between the two, you can try different resistors.  In my circuit I ended up using 50K resistors for the lower circuit (R5, R6) because the carrior frequency was higher than I would like using 40Ks.

If you use a lower voltage input range you will need to use smaller resistors for R3 and R5, unfortunately changing these will also change the carrior frequency some, so you will need to play around with different resistor combinations until you find something that works well for you.  The optimal range for the carrior frequency should be between 4 and 5Khz.

To measure the output frequencies of the device I captured a second of sound and then used a spectrum analizer to measure the frequency which had the strongest power; I used Matlab to do this, but there are probably other tools as well.  Make sure to measure the carrier frequency when 0 volts is being fed into the circuit, other wise it will default to an input of 3 volts (due to the design of the XR-2206).  Another "trick" that can be used to measure the carrier frequency is that when the CarrierFreq value in the code below is right, it should return approximately 0 when 0 volts is being inputed.  So, changing CarrierFreq higher or lower should allow you to find the value that gets you the closest to 0.

Parts:
1 prototyping board - I used the Radio Shack Universal Component PC Board with 780 Holes for $3.29
1 stereo jack - Radio Shack's
1/8" Stereo Panel-Mount Phone Jack (274-246) would do, it costs $2.99; just make sure that it has a ring bolt because this is how we attach the face plat to the card.
1 HD drive power cable - I scavanged one off of an old CPU fan
1 CD audio cable - make sure you know what type of connector you need for your system before getting this cable
2 XR-2206 - The best source for the XR-2206 that I could find was Jameco for $3.59 each.
2 0.01uF capacitors (C2,C4)
4 1uF capacitors (C1,C3,C5,C6)
1 10uF capacitor (C7) - to filter the supply voltage, in theory the bigger the better
2 200ohm resistors (R1,R4)
2 1Kohm resistors (R9,R10)
2 10Kohm resistors (R7,R8)
4 40Kohm resistors (R2,R3,R5,R6)
(in reality you will probably want several pairs of resistors ranging from 35K up to 50K in case you need to tweek the carrior frequency)
5 short peices of wire, ideally 1 black, 2 red and 2 white
1 Face plate - you can probably just take the one in the slot that you are planning on putting this device from your computer
1 1/16inch drill bit - to drill holes into the prototyping board to attach the stereo jack.
1 probably 3/16inch drill bit - to drill the hole in the face plate -- or take the face plate off an old sound card like I did.

Here are the pictures of the device when assembled:
device from the front
device from the back

As you can see the design fits quite nicely on this size proto board and the ground and power supply runners made the layout pretty easy.

The installation into the computer is pretty simple too:
device inside of computer.
Just attach the CD audio cable to the CD audio port (or AUX port if you have one) and then attach the HD power cable to a spare line.

The only complication is that the card wasn't stable enough to insert the stereo cable without any additional support, so I added a loop of tape at the top of the card to hold it in place (you can see the blue tape on the upper right hand side).

Now for the software side of things...

All of the code is written in C and I used the FFTW libaries to do FFTs.

#include <fftw3.h>
...
#define FILTSIZE 2  // how far back does the butterworth filter go...

// define our variables...
fftwf_complex* e[2];
fftwf_complex* out1;
fftwf_complex* out2;
fftwf_plan forwardplan;
fftwf_plan inverseplan;
int block;
float downsamplescale;
float downsampleIndex[2];
int SamplingFreq;
float CarrierFreq[2];
float scale[2];
float previousPhase[2];
float* blockbuffer;
float pblockbuffers[2][FILTSIZE];
float acc[2][2];
short* buffer[2];
int bufIndex[2];

// 2nd order butterworth filter parameters for 200Hz
float a[] = {-1.95970703381558f, 0.960502919439762f};
float b[] = {0.000198971406045079f, 0.000397942812090157f, 0.000198971406045079f};

...
void setup(float rate, int* portNrs, int nrPorts, int nrBits, int nrSamples)
{
    SamplingFreq = 44100;
    block = (int)(SamplingFreq*10/1000.0); //10ms blocks

    for (int p=0;p<2;p++)
    {
        CarrierFreq[p] = 4400;
        scale[p] = (1<<15)/PI; // maximize the dynamic range, for my circuit (1<<15)/0.4 is better
    }

    blockbuffer = (float*)malloc(sizeof(float)*block);
    out1 = (fftwf_complex*) fftwf_malloc(sizeof(fftwf_complex) * block);
    out2 = (fftwf_complex*) fftwf_malloc(sizeof(fftwf_complex) * block);

    forwardplan = fftwf_plan_dft_r2c_1d(block, blockbuffer, out1, FFTW_MEASURE);
    inverseplan = fftwf_plan_dft_1d(block, out1, out2, FFTW_BACKWARD, FFTW_MEASURE);

    this->rate = rate;
    currentIndex = 0;
    sampleSize = (int)ceil(nrBits/8);
    wordSize = sampleSize*nrPorts;
    this->nrPorts = nrPorts;
    this->portNrs = (int*)malloc(sizeof(int)*nrPorts);
    memcpy(this->portNrs,portNrs,sizeof(int)*nrPorts);
    downsamplescale = SamplingFreq/rate;

    if (sampleSize != 2) return -111; // only 16 bit is supported!

    for (int i=0;i<nrPorts;i++)
    {
        if (portNrs[i] < 0 || portNrs[i] >= 2) return -1;
        buffer[i] = (short*) malloc(nrSamples*wordSize);
    }

    // initialize the "e" vector
    for (int p=0;p<nrPorts;p++)
    {
        e[p] = (fftwf_complex*) fftwf_malloc(sizeof(fftwf_complex) * block);
        for (int i=0;i<block;i++)
        {
            e[p][i][0] = cosf(-2*PI*CarrierFreq[p]/SamplingFreq*i);
            e[p][i][1] = sinf(-2*PI*CarrierFreq[p]/SamplingFreq*i);
        }
    }
}
...
// pass the data captured from the sound card
void processBlock(short* capdata)
{
    for (int p=0;p<nrPorts;p++)
    {
        int port = portNrs[p];
        //copy the data into blockbuffer
        for (int i=block-1, j=block*nrPorts-2+port;i>=0;i--,j-=nrPorts) blockbuffer[i] = capdata[j];

        // do the FM demodulation
        fftwf_execute(forwardplan); // this does the fft of "blockbuffer" and puts it in "out1"

        // "cheat" with the hilbert transform by just dividing by 2 for the DC (and Nyquist frequency, if even)
        // instead of multipling by 2 for all but DC and ...
        // also the fftwf_plan_dft_r2c_1d transform makes the negative frequencies 0, which is part of the hilbert
        out1[0][0] /= 2;
        if ((block & 0x1) == 0) out1[block/2][0] /= 2;

        fftwf_execute(inverseplan); // this does the ifft of "out1" and puts it in "out2"

        for (int i=0;i<block;i++)
        {
            // "complex" multiply by "e" and calculate the resulting phase
            float tmpPhase1 = atan2f(out2[i][0] * e[port][i][1] + out2[i][1] * e[port][i][0],out2[i][0] * e[port][i][0] - out2[i][1] * e[port][i][1]);
            // calculate the difference in phase
            float dPhase = tmpPhase - previousPhase[port];
            previousPhase[port] = tmpPhase;
            // if the difference is too great shift it back into range
            if (dPhase < -PI) dPhase += 2*PI;
            else if (dPhase > PI) dPhase -= 2*PI;
            blockbuffer[i] = dPhase;
        }

        // implement a 2nd order butterworth filter and downsample at the same time.
        int startIndex = (int)(bufIndex[port]*downsamplescale);
        int nextAccumInd = (int)(downsampleIndex[port] + downsamplescale - startIndex);
        float x0,x1,x2,acctmp;

        x1 = pblockbuffers[port][FILTSIZE-1];
        x2 = pblockbuffers[port][FILTSIZE-2];

        for (int i=0;i<block;i++)
        {
            x0 = blockbuffer[i];
            acctmp = b[0]*x0 + b[1]*x1 + b[2]*x2 - a[0]*acc[port][0] - a[1]*acc[port][1];
            acc[port][1] = acc[port][0];
            acc[port][0] = acctmp;

            x2 = x1;
            x1 = x0;

            if (i == nextAccumInd)
            {
                // copy the down sampled data into our storage buffer, we have to scale it because the output atan2's is in radians (i.e. -Pi to Pi)
                // but we generally want it to make out the dynamic range of a short
                buffer[port][bufIndex[port]++] = (short)(acctmp*scale[port]);

                downsampleIndex[port]+= downsamplescale;
                nextAccumInd = (int)(downsampleIndex[port] + downsamplescale - startIndex);
            }
        }

        // store the current blockbuffer so that we can continue our filtering for the next iteration
        memcpy(pblockbuffers[port],blockbuffer+block-FILTSIZE,FILTSIZE*sizeof(float));
    }
}

I have incorporated this with sound capturing so that it happens in real time.  On Windows systems this is pretty easy, I haven't tried it for other OSs.

To make the code faster you can change the atan2f to the following:

// Fast approximate arctan2 code posted by Jim Shima
float arctan2(float y, float x)
{
    float angle;
    float abs_y = y>=0?y:-y;
    if (x>=0)
    {
        float r = (x - abs_y) / (x + abs_y + 1e-40f); // add 1e-40 to prevent 0/0 condition
        angle = 0.1963f * r * r * r - 0.9817f * r + PI/4;
    } else {
        float r = (x + abs_y) / (abs_y - x);
        angle = 0.1963f * r * r * r - 0.9817f * r + 3*PI/4;
    }
    if (y < 0) return(-angle);     // negate if in quad III or IV
    else return(angle);
}

Using more professional sound cards, ones that can record at 96Khz or even 192Khz would allow for greater bandwidth.  It would allow for either a higher frequency range for the input signal or allow for multiple channels to be recorded on the same input.  Currently you in theory can have 2 channels on the same input, with carrior frequencies of 3Khz and 8Khz with a 2Khz range which would probably be able to handle about 150Hz input signals.

Implementing a 4th order butterworth filter wouldn't be hard, it would require setting FILTSIZE to 4, declaring new variables x3, x4 and determining what vectors a and b should be and adding the new terms to acctmp.

micah@salk.edu