UNIT
1:
PROGRAMMING
ENVIRONMENT
1.1 Introduction
This unit introduces the programming environment for the Digital Signal Processing course. It gives a brief description of the C++ classes available, and reviews support routines for signal access, complex arithmetic and graphics.
When you have worked through this unit you should:
· understand the three basic C++ classes used in the course
· know how to use this document for reference information
· know how to manipulate complex numbers in your programs
· know how to create and manipulate signals, waveforms and complex waveforms
· know how to plot graphs
· know how to play and record signals
· have tried out a simple programming example
1.2 DSP Classes
The DSP classes used for the course were developed to provide a simple and clean programming environment for the definition and demonstration of signal processing algorithms. To a great extent the complexity of C++ is hidden by the classes provided, so that the implementation details of DSP concepts are not obscured by features of the language. Through support for file I/O, replay, acquisition and graphics, the classes also make demonstration programs succinct and easy to understand.
There are three main base classes:
· Complex support for complex arithmetic
· Graph support for simple graphics
· Wave support for waveform vectors
The Complex and Graph classes are straightforward and described in more detail below. The Wave class is an abstract base class from which a small hierarchy of classes has been developed:
The implementation classes WaveShort, WaveDouble and WaveComplex provide an implementation of basic construction, assignment, element access, concatenation, partitioning and graph plotting of vectors of integers, floating-point numbers and complex numbers respectovely. The lowest level of classes provide convenient objects for manipulation in DSP algorithms:
· Signal Supports recording, replay, file I/O
· Waveform Supports amplitude-time graphs
· Spectrum Supports complex amplitude-frequency graphs
·
ComplexWaveform Supports
complex amplitude-time graphs
1.3 Complex Class
Complex numbers consist of two double-precision floating-point numbers, representing the real part and the imaginary part of the complex number. Complex numbers may be declared as:
Complex
c(1.0,2.0);
Complex d =
Complex(1.0,2.0);
Complex e(1.0);
Complex f =
Complex(1.0);
Complex g = 1.0;
Here complex numbers c and d have real parts equal to 1.0, and imaginary parts set to 2.0. Complex numbers e, f and g have real parts equal to 1.0 and imaginary parts set to 0.0.
Arithmetic support is available for addition, subtraction, multiplication and division of complex numbers through overloading of the standard operators:
Complex sum = c +
d;
There is also support for the +=, -=, *= and /= operators for updating complex numbers.
The following global functions are also available:
double
mag(Complex c) Magnitude of c
double arg(Complex
c) Argument of c
Complex
sqrt(Complex c) Square root of c
Complex
exp(Complex c) Exponential of c
Direct access to the real and imaginary parts of a complex number may be performed through the use of the member functions real() and imag(). Complex numbers may also be input and output using the iostream operators >> and <<.
1.4 Graph Class
The Graph class supports the graphical display of simple X-Y graphs. The class is implemented using a device-independent graphics library that supports PC Super-VGA screens, X-windows, postscript printers and GIF image files.
Graph objects are constructed with a certain number of sub-graphs in the vertical and horizontal dimensions, and with an overall title. Individual graphs are then positioned on this page by their index number (starting as graph number 1 for the top left graph, and incrementing left to right, to to bottom). Each sub graph may have individual titles.
The Graph constructor may be called as:
Graph
gr(2,3,”Example Graphs”);
which would create a page with 2 rows and 3 columns of graphs with the overall title ‘Example Graphs’.
The member functions wait() and close() may be used to pause for a keypress before continuing and to return the screen to its normal mode. These operations are also performed automatically by the Graph class deconstructor,
Although the Graph class has member functions for plotting arrays of numbers, these are normally called internally by objects of the various Wave classes. The Wave objects are instructed through member functions to plot themselves as described below. These member functions take the name of the Graph object and the number of the sub-graph as arguments.
1.5 Wave Class Hierarchy
Conceptually the Wave classes support arrays of numerical values that also have an associated temporal or frequency parameter. Thus a waveform is an array of sample values at a particular sample rate. The classes allow the indexing of the elements of the array, concatenation of compatible arrays, and partitioning of arrays. The arrays are created dynamically to a particular size, but they may also be ‘grown’ in size in use.
The Wave abstract base class provides the following interface through member functions:
int count() number of samples in array
double rate() sampling rate
double period() sampling period
The implementation classes WaveShort, WaveDouble and WaveComplex provide typed arrays with appropriate indexing operators:
short
WaveShort::operator[](int idx);
double
WaveDouble::operator[](int idx);
Complex
WaveComplex::operator[](int idx);
These indexing operators are protected against range errors. Attempts to index elements outside the current size of the arrays is guaranteed to return the value 0. Attempts to set values of elements outside the current size of the array are safely ignored. This characteristic of the Wave classes is used to simplify many of the signal processing algorithm implementations.
Compatible wave objects may be concatenated using the + operator, for example:
WaveShort owv =
iwv1 + iwv2;
Creates an output object owv that is the concatenation of objects iwv1 and iwv2 which must match in type and sampling rate. Wave objects may also be grown one sample at a time by concatenation with a single numerical value:
WaveShort owv =
iwv1 + 2;
The += operator may also be used for concatenation with arrays or single values.
Every wave object also supports the selection of a sub-array through the member function
cut(start_sample,number_of_samples)
Signal Class
The Signal class supports quantised (short integer) time series, a format that is used to import and export waveforms from/to disk or from/to analogue signals.
The constructor looks like
Signal
sig(1000,20000);
which constructs a signal of 1000 samples at a sampling rate of 20,000 samples/second. The samples are indexed from 0 to 999.
Signals know how to play themselves, record themselves and load/save themselves from/to disk files through these member functions:
record() record signal (PC
only)
replay() replay signal
load(filename,itemstring) load signal from file
save(filename,history) save signal to file
The library uses the UCL signal format called SFS for disk files. This format supports multiple signals in a single file. The itemstring parameter is a character string that identifies which input signal should be loaded from the given file. The history parameter is a character string that identifies the output signal in the given file. SFS files need to be created with the hed program, before signals can be saved into them.
Waveform Class
The Waveform class is used a generic container for discrete time series used in DSP applications. Unlike the Signal class, Waveform objects are indexed from 1 since this is the convention found in many DSP texts. Thus the constructor
Waveform
wav(1000,20000);
creates an array of 1000 floating point numbers from wav[1] to wav[1000] with an associated sample rate of 20,000 samples per second. Knowledge of the sample rate is used to label graph axes correctly.
ComplexWaveform Class
The ComplexWaveform class is used to contain complex time series, such as that produced by the inverse Fourier transform. Like Waveform objects, these are also indexed from 1.
Interconversion between the types Signal, Waveform and ComplexWaveform is achieved by the following support functions
Waveform SignalToWaveform(Signal s);
Signal WaveformToSignal(Waveform w);
ComplexWaveform
WaveformToComplexWaveform(Waveform w);
Waveform
ComplexWaveformMagToWaveform(ComplexWaveform
c);
Waveform
ComplexWaveformRealToWaveform(ComplexWaveform
c);
Note there are two ways of converting a ComplexWaveform to a normal Waveform.
Objects of the Signal, Waveform and ComplexWaveform classes know how to plot themselves through member functions with the calling parameters:
void
plot(Graph gr,int graphno,char *title,char *xlabel,char *ylabel);
The graphno value identifies into which sub-graph on the screen the waveform should be displayed (see 1.4). The optional title parameter specifies an overall title. The optional xlabel and ylabel parameters specify axis labels.
ComplexWaveform objects may be plotted in a number of ways with the following member functions:
plotMag() plot magnitude of complex waveform
plotLogMag() plot log magnitude (in dB)
plotPhase() plot phase (in radians)
plotReal() plot real part
plotImag() plot imaginary part
Spectrum Class
The Spectrum class is a container for complex spectral information. Like the ComplexWaveform class, it contains an array of complex numbers, but the associated time parameter is in samples per Hertz rather than samples per second. Spectrum objects are also indexed from 0. Thus the definition:
Spectrum
resp(1000,0.05);
Represents a spectrum of 1000 samples extending from resp[0] at 0Hz, to resp[999] at 19,980Hz (each Hz is 0.05 of a sample). To generate a suitable Spectrum object to plot the frequency characteristics of a linear system operating at an associated sample rate of SRATE samples/sec. choose enough samples to make a clear graph (say 1000) and for a frequency axis extending to SRATE/2, use a definition:
Spectrum
resp(1000,1000/(SRATE/2));
Spectrum objects support the same varieties of the plot() member functions as ComplexWaveform objects.
1.6 Using DSP Classes
Look at the Departmental Computing handout for general details about editing and compiling C++ programs.
The definitions for the standard classes may be included in user programs through the use of the C++ 'include' file mechanism. The line
#include “tools.h”
provides access to the Complex and Graph classes and the Wave hierarchy. It also provides a set of useful constant definitions, including:
const double
PI=3.1459265359; definition of p
const int MAXSHORT=32767; largest quantised sample
As an example, here is a simple C++ program that calculates, replays and displays a sinusoid. The output is displayed over the page.
// sinedemo.cpp - calculate, display and replay a sinusoid waveform #include "tools.h" const int NUMSAMPLE=20000; // number of samples const double SAMPRATE=20000.0; // sampling rate const double SINEFREQ=500.0; // sine at 500Hz const double SINEAMP=10000.0; // sine amplitude int main() { Signal sinewave(NUMSAMPLE,SAMPRATE); // create signal for (int i=0;i<sinewave.count();i++) sinewave[i] = (int)(SINEAMP*sin(2*PI*SINEFREQ*i/SAMPRATE)); sinewave.replay(); // replay signal Graph gr(1,1,"DSP Course"); // open graphics Signal partwave=sinewave.cut(0,200); // chop off first 200 samples partwave.plot(gr,1,"Sinewave"); // display graph } |
The
algorithms presented in the DSP Units as pieces of C++ code need to be declared
in your program before you may use them in your own programs. To do this, include the corresponding ‘.h’
file in your source. For example, to
use the function for generating noise samples described in Unit 3 (noise.cpp),
include at the top of your program source the line: #include
“noise.h”
Exercise
1.1 Type in, compile and run the example program above.