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: 
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:


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:

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