Monitoring a Real-Time Digital Signal in C and C++ on Linux

Monitoring a real-time signal is a requirement that comes with many Digital Signal Processing applicaitons. There are different definitions for real-time. Here when I say real-time processing I mean "reliable processing of a digital signal within a predictable amount of time". When I say real-time signal, I mean "an unpredictable digital signal that's continually generated by a device" A real-time signal needs reliable processing strategies to be useful. Most of the time when dealing with complex systems you'll need a multithreaded software design to achieve the level of throughput needed to process the signal.

In order to do things right you'll have to create some abstractions in C++. Object-oriented programming provides easy code organization features. Our first abstraction we'll create is the Device abstraction. This abstraction will create an interface for the digital signal device. If this were a production system we would use exception handling and more error checking. The C++ source code is shown next and resembles the interface for Linux device drivers the only difference is that Linux drivers are written in C not C++, the source code form this article will be available on my site at http://www.engineerjames.com.

class Device {
public:
  Device(char* device = "");
  virtual ~Device();
protected:
  virtual int open()=0;
  virtual int close()=0;
  virtual int read()=0;
  virtual int write()=0;
  virtual int init()=0;
  int fd;
  const string device_path;
};

This interface is a pure virtual base class. It allows us to keep a common interface to all devices used by our application. Now, we can use the base class created above to build our specific class that will be used to manipulate our signal device.

class SignalDevice : public Device {
public:
  SignalDevice(char* device);
  virtual ~SignalDevice();
  virtual int open();
  virtual int close();
  virtual int read();
  virtual int write();
  virtual int init();
protected:
  void get_signal();
  static void *run_statistics(void*);
  static void *acquire(void*);
private: 
  Buffers& buffer;
};

Other types of signal device C++ classes can be derived from SignalDevice, but we'll implement of SignalDevice for our application. This will not be a complex signal device since we'll use the audio device mapped to /dev/dsp on Linux. The functions will be implemented inline to keep the amount of code to a minimum. The C++ source code is shown below.

class SignalDevice : public Device {
public:
  SignalDevice(char* device) : Device(device) { }
  ~SignalDevice(){ }
protected:
  int open()
{
   unsigned int arg;
   int status,res;

   fd = ::open("/dev/dsp", O_RDWR);
   if (fd lessthan 0)
    {
       perror("open of /dev/dsp failed");
       return 1;
     } 
  arg = SIZE;
  status = ioctl(fd, SOUND_PCM_WRITE_BITS, &arg);
  if (status == -1)
      perror("SOUND_PCM_WRITE_BITS ioctl failed");
  if (arg != SIZE)
      perror("unable to set sample size");
  arg = CHANNELS; 
  status = ioctl(fd, SOUND_PCM_WRITE_CHANNELS, &arg); 
  if (status == -1) 
      perror("SOUND_PCM_WRITE_CHANNELS ioctl failed");
  if (arg != CHANNELS)
      perror("unable to set number of channels");
  arg = RATE;
  status = ioctl(fd, SOUND_PCM_WRITE_RATE, &arg);
  if (status == -1)
      perror("SOUND_PCM_WRITE_WRITE ioctl failed");
  return res;
}

int close() {   return ::close(fd);   }

int read(vector float ::iterator &it, long c)
{
    int res=0;
    unsigned char ch;
    for(long i=0;i less-than c;i++)
        {
             res+=::read(fd,&ch,1);
            *it=(ch*0.01);
             it++;
        }
    return res;
}

 

Notice, the 'less-than' used in the expressions. This is because I could not use the actual signs because, the article would not save.

I initialized the device at /dev/dsp to use 8 bit sample sizes so, we'll use a unsigned char to read each sample, 1 channel, the quality of the sound is not an issue here, the sample rate is how many samples will be read in a second. I set this to 8000. The constants used for these values are listed below. The number of seconds worth of digital data to read is set to 1. SIGSZ is used to hold the calculation of the buffer length we'll need to use when creating the arrays that will hold the digital data.

A DSP application that's monitoring a real-time signal will have to be fast enough to handle the signal frequency. For instance, tracking signals for guiding a fast moving object for some abnormal behavior requires more processing speed than processing an audio signal from a PC. The thing to remember is that we are not requiring anything to be really fast, just fast enough to process the signal and give useful results. So, in our case the setting below are fast enough for the real-time signal.

const unsigned long LENGTH = 1; /* how many seconds of speech to store */
const unsigned long RATE = 8000; /* the sampling rate */
const unsigned long SIZE = 8; /* sample size: 8 or 16 bits */
const unsigned long CHANNELS = 1;
const unsigned long SIGSZ = ((LENGTH*RATE*SIZE*CHANNELS/8));

const unsigned long NUM_BUFFERS=64;

 

 

NUM_BUFFERS is the number of buffers used to in the list. The read() function will iterate through the list of buffers using each one and moving to the next. This allows our statistics function to run statistics on each buffer of data while the read() function continuously reads data from the DSP device. The application will work on a one second timing resolution. If a signal change is detected we could begin to analyze the problem using the buffers that were previously process by our statistics function. The statistics function could store the value of the average deviation of the sample from the Mean value of the signal in a variable associated with the buffer. This way we'll know how many seconds to go back to catch the complete signal change. Each buffer holds one second of data. While scanning the buffer list if we recognize an abnormal change in the standard deviation of a buffer we know that is were the signal change began. The structures we'll use to hold the signal data is shown next. Multithreaded programming techniques are use to synchronize the access to the structures used to hold the digital data. The mutex used to protect the critical sections of code is used in the Buffers::operator[]() member function of the Buffers C++ class. When a thread attempts to access this container of vectors it must lock the mutex first or sleep until it is released.

struct buff {
buff() : arr(SIGSZ) { pthread_mutex_init(&mutex,NULL); }
pthread_mutex_t mutex;
vector<float>arr;
};

class Buffers {
public:
  Buffers() : sig_buff(NUM_BUFFERS) { }
  ~Buffers(){ }
  vector buff ::iterator operator[](int dex) 
  {
      /*
          Lock the mutex and return the buffer at dex position. If the mutex is locked wait for it.
      */ 
     vector buf ::iterator it = (pthread_mutex_lock(&sig_buff[dex].mutex)?it:sig_buff.begin()+dex); 
     return it;
}
void release_buffer(int dex) /* Release the mutex */

    pthread_mutex_unlock(&sig_buff[dex].mutex);
}
private:
  vector buff sig_buff;
};

 

 

The buffers themselves hold float values, but the data coming from the DSP device is 8 bits. The reason for this is that DSP processing is intensive and the unsigned chars are sufficient to read the samples but not for computing the attributes of the signal. We handle this problem by scaling the values returned from the device. By multiplying each sample by 0.01 before assigning putting it into the buffer of floats we create a manageable set of data for our signal processing routines. The read function is shown below.

int read(vector float ::iterator &it, long c)
{
    int res=0;
    unsigned char ch;
    for(long i=0;i lessthan c;i++)
       {
           res+=::read(fd,&ch,1);
          *it=(ch*0.01); //scale the value before assigning it to the vector element
           it++;
       } 
    return res;
}

 

 

The read function should be started first because we want the statistics function to begin processing the digital data immediately after its thread starts. The function run_statistics() does the following calculations:

1. Calculate the Mean value of the signal
2. Get each samples deviation from the mean
3. Calculate the average deviation

The functions implementation is shown below.


void* SignalDevice::run_statistics(void*arg)
{
    float avg_dev= 0,sample_sum=0,mean=0;
   floatstd_dev[SIGSZ],upper_bound=0,lower_bound=0;
    int dex=0;
    SignalDevice* s=(SignalDevice*)arg;
    vector<float> arr;
    extra_task();
    while(true)
          { 
             vector<buff>::iterator it=s->buffer[dex];
             arr=it->arr;
             s->buffer.release_buffer(dex);
             cout << "Run_statistics() dex: " << dex << '\n';
             if(dex greater-than-or-equal NUM_BUFFERS-1)
                 dex=0;
             else
                 dex++;
            for(unsigned long i=0;i less-than (SIGSZ-1);i++)
                   sample_sum+=arr[i];
            mean=(sample_sum/(SIGSZ-1));
            for(unsigned long i=0;i less-than (SIGSZ-1);i++)
                {
                    std_dev[i]=(arr[i]-mean);
                    avg_dev+=std_dev[i];
                }
           avg_dev/=(SIGSZ-1);
           upper_bound=mean+(avg_dev+3);
           lower_bound=mean-(avg_dev-3);
           cout << " Mean: " << mean << '\n';
           cout << "Avg Dev: " << avg_dev << '\n'; 
           cout << "Upper Bound " << upper_bound << '\n';
           cout << "Lower Bound " << -lower_bound << '\n';
           cout << "Sample Sum " << sample_sum << '\n';
           sample_sum=0;
           avg_dev=0;
         }
}

 

 

Now we need the functions that will start the programs thread functions and call our read() member function. The functions are shown below. Notice the static members functions used to interface with the POSIX API pthread_create() function. static member functions don't have a pointer to their object passed to them so, they are enough like C style functions for the POSIX APIs to use them as function pointers. The C++ source code for these functions is shown below.

void SignalDevice::get_signal()
{
   open();
   pthread_t tid;
   pthread_create(&tid,NULL,SignalDevice::acquire,this);
   pthread_create(&tid,NULL,Signal::run_statistics,this);
   pthread_join(tid,NULL);
   close();
}


void *SignalDevice::acquire(void*arg)
{
    int dex=0;
    SignalDevice* s=(SignalDevice*)arg; 
    vector<buff>::iterator it;
    vector<float>::iterator float_it;
    while(true)
           {
               it=s->buffer[dex];
              float_it=it->arr.begin();
              cout << "Acquire(): dex is at " << dex << '\n'; 
              s->read(float_it,SIGSZ);
              s->buffer.release_buffer(dex);
             if(dex greater-than-or-equal NUM_BUFFERS-1) 
                  dex=0;
             else 
                  dex++;
          }
}

The functions used to analyze the data buffers when a signal change occurs are not implemented here. This application was moderately easy because, the Standard Template Library ( STL ) was used to implement the data structures. The original application did not use the STL and was a much more complex program.


You can add the other DSP processing for the stored signal or use filters in addition to what is done here. The scaling is not what I desired to do because, of the data type size difference between the unsigned char and float. More information on this topic is explained in my article on Digital Signal Detection on Linux using C and C++ . There are many things to consider when writing software for signal processing. This program demonstrates only a small portion of the task.


 

Home Page

 

 

word to html converter html help workshop This Web Page Created with PageBreeze Free Website Builder  chm editor perl editor ide