# Adaptive Identification and Inverse Filtering using Java

## Complete Program Listings

Complete listings of the classes discussed in this lesson are shown in the listings below.

/*File Adapt07.java.java Copyright 2005, R.G.Baldwin The purpose of this program is to illustrate adaptive identification and inverse identification. Further information on these two topics can be found at the following three web sites: www.eee.strath.ac.uk/r.w.stewart/adaptivejava/begin.htm www.mathworks.com/access/helpdesk/help/toolbox/ filterdesign/adaptiv5.html#5547 www.mathworks.com/access/helpdesk/help/toolbox/ filterdesign/adaptiv6.html#5557 This program requires the following classes: Adapt07.class AdaptEngine02.class (new to this lesson) AdaptiveResult.class ForwardRealToComplex01.class PlotALot01.class PlotALot03.class PlotALot05.class This program uses the adaptive engine named AdaptEngine02 to adaptively develop a filter. Depending on user input, the filter is either an identification filter or an inverse filter. The class named AdaptEngine02 is an upgrade to the class named AdaptEngine01 that was used in earlier programs. This upgrade makes it possible for the user to pass a boolean parameter to the adapt method of the engine to either enable or disable the adaptive update of the filter coefficients. When the filter is an identification filter, the adaptive process attempts to replicate the impulse response of a path through which wideband test data are flowing. When the filter is an inverse filter, the adaptive process attempts to develop a filter that is the inverse of the impulse response of a path through which wideband test data are flowing. User-selectable test cases are provided for six different path scenarios. The user may develop an identification filter or an inverse filter for any of the six cases. If the user opts for an identification filter, five separate graphs are produced. If the user opts for an inverse filter, six separate graphs are produced. The graphs appear on the screen in two rows with three graphs in each row. The following is a brief description of each of the six graphs, working from left to right across the top row and then working from left to right across the second row. 1. The impulse response of the path through which the wide band data are flowing. 2. The amplitude and phase response of the path through which the wideband data are flowing. 3. Four time series that illustrate the time behavior of the adaptive process. The adaptive behavior is disabled and impulses are appended onto the ends of the input time series to cause the impulse response of various filters to be displayed at the ends of the time series in this graph. 4. The amplitude and phase response of the adaptive filter at the end of every 400th iteration. 5. The impulse response of the adaptive filter at the end of every 400th adaptive iteration. 6. The amplitude and phase spectrum of the time series produced by convolving the final adaptive filter with the impulse response of the path. This graph is produced only when the filter being developed is an inverse filter. This amplitude and phase spectrum illustrates the extent to which the inverse filter is able to compensate for the path characteristics. Ideally the amplitude spectrum is flat and the phase spectrum is either flat or linear (indicating a time delay). Graphs 3, 4, and 5 consist of multiple pages stacked on top of one another. Move the pages on the top of the stack to view the pages further down. The pages on the top of the stack represent the results produced early in the adaptive process while those further down represent the results produced later in the adaptive process. The four time series that are plotted are, from top to bottom in the colors indicated: 1. Input to the adaptive filter (black). 2. Target for the adaptive process (red). 3. Output from the adaptive filter (blue). 4. Error computed within the adaptive process (green). Near the end of the run, the adaptive update process is disabled. The input data is set to zero for the remainder of the run except that on two subsequent occasions, an impulse is inserted into the data. By running the same path twice, once in identification mode and once in inverse filter mode, this makes it possible to see: 1. The impulse response of the path. 2. The impulse response of the final adaptive identification filter. 3. The extent to which the impulse response of the identification filter matches the impulse response of the path. 4. The impulse response of the final inverse filter. 5 The extent to which the convolution of the inverse filter with the impulse response of the path compensates for the characteristics of the path and produces the ideal output consisting of a single impulse. In operation, the program generates wideband test data produced by a random number generator and convolves it with a specified path impulse response to simulate the impact of the path on the wideband test data. The original wideband test data and the path output data are both presented to the adaptive engine. When the original wideband test data is presented as the data to be filtered within the adaptive engine and the path output is presented as the target, the adaptive process attempts to develop an adaptive filter that replicates the impulse response of the path. When the two are reversed, the adaptive process attempts to develop an adaptive filter that is the inverse of the impulse response of the path. User input is provided by five command-line parameters. If no command-line parameters are provided, default parameters are used. The command-line parameters are: feedbackGain: The gain factor that is used in the feedback loop to adjust the coefficient values in the adaptive filter. numberIterations: This is the number of iterations that the program executes before stopping and displaying all of the graphic results. filterLength: This is the number of coefficients in the adaptive filter. Must be at least 26. testCase: An integer from 1 to 9 that specifies the simulated path scenario. identification: Input is T or F. T specifies that the adaptive process is to develop an identification filter. F specifies that the adaptive process is to develop an inverse filter. The minimum filter length of 26 has to do with plotting alignment issues and has nothing to do with the adaptive process. See a description of the alignment issues in earlier lessons. The six path scenarios can be generally described as follows: 1. A low-pass filter of the sort that might be realized with a passive RC network. 2, Another low-pass filter that might be realized with a passive RC network but with a much longer time constant than scenario 1. 3. A high-pass filter of the sort that might be realized with a passive RLC network having the same long time constant as scenario 2. 4, A mid-band filter of the sort that might be realized with a passive RLC network having the same long time constant as scenarios 2 and 3. 5. A filter that might represent an acoustic signal in the presence of echoes. 6. A boxcar digital filter with a peak at half the Nyquist folding frequency. Tested using J2SE 5.0 and WinXP. J2SE 5.0 or later is required. **********************************************************/ import static java.lang.Math.*;//J2SE 5.0 req class Adapt07{ public static void main(String[] args){ //Default parameter values double feedbackGain = 0.0001; int numberIterations = 2001; int filterLength = 26;//Must be >= 26 for plotting. //Six test cases, numbered 1 through 6 are defined // later. int testCase = 1; //A value of true for the following variable causes the // adaptive process to attempt to develop an // identification filter. A value of false causes the // adaptive process to attempt to develop an inverse // filter for the path impulse response. boolean identification = true; //The following scale factor is applied to the // wideband test data. This is not an input // parameter. double wbTestDataScale = 10; //Process command-line arguments. Note that because of // plotting alignment issues discussed in earlier // lessons, the filter length must be at least 26. if(args.length != 5){ System.out.println( "Usage with all parameters following the " + "program name:n" + "java Adapt07n" + "feedbackGainn" + "numberIterationsn" + "filterLength >= 26n" + "testCase, 1 to 9n" + "identification, T or Fn"); System.out.println( "Input values were not provided.n"+ "Using following default values:n" + "feedbackGain: " + feedbackGain + "nnumberIterations: " + numberIterations + "nfilterLength: " + filterLength + "ntestCase: " + testCase + "nidentification: " + identification); }else{//Command line params were provided. feedbackGain = Double.parseDouble(args[0]); numberIterations = Integer.parseInt(args[1]); filterLength = Integer.parseInt(args[2]); //FilterLength must be 26 or greater to avoid // plotting alignment problems. if(filterLength < 26){ System.out.println( "nfilterLength must be 26 or greater"); System.out.println("Termnating program"); System.exit(0); }//end if testCase = Integer.parseInt(args[3]); if(args[4].toUpperCase().equals("T")){ identification = true; }else{ identification = false; }//end else System.out.println( "Input values were provided.n"+ "Using following values:n" + "feedbackGain: " + feedbackGain + "nnumberIterations: " + numberIterations + "nfilterLength: " + filterLength + "ntestCase: " + testCase + "nidentification: " + identification); }//end else //Instantiate a new object of the Adapt07 class // and invoke the method named process on that object. new Adapt07().process(feedbackGain, numberIterations, filterLength, wbTestDataScale, testCase, identification); }//end main //-----------------------------------------------------// //This is the primary adaptive processing and plotting // method for the program. void process(double feedbackGain, int numberIterations, int filterLength, double wbTestDataScale, int testCase, boolean identification){ //The following array will be populated with the // adaptive filter for display purposes. double[] filter = null; //Define several test cases for the path impulse // response. //Low-pass filter with short time constant. double[] pathA = {1.0, 0.5, 0.25, 0.125, 0.0625, 0.03125, 0.015625, 0.0}; //Low-pass filter with long time constant. double[] pathB = {1.0, 0.8, 0.64, 0.512, 0.4096, 0.32768, 0.262144, 0.2097152, 0.1677721, 0.1342176, 0.1073740, 0.0858992, 0.0687193, 0.0549754, 0.0439803, 0.0351842, 0.0}; //High-pass filter with long time constant. double[] pathC = {1.0, -0.8, 0.64, -0.512, 0.4096, -0.32768, 0.262144, -0.2097152, 0.1677721, -0.1342176, 0.1073740, -0.0858992, 0.0687193, -0.0549754, 0.0439803, -0.0351842, 0.0}; //Mid-pass filter with long time constant. double[] pathD = {1.0, 0.0, -0.64, 0.0, 0.4096, 0.0, -0.262144, 0.0, 0.1677721, 0.0, -0.1073740, 0.0, 0.0687193, 0.0, -0.0439803, 0.0}; //Simulation of an acoustic signal with echoes. double[] pathE = {1.0, 0.0, 0.64, 0.0, 0.0, 0.32768, 0.0, 0.0, 0.0, 0.1342176, 0.0, 0.0, 0.0, 0.0, 0.0439803, 0.0}; //Digital boxcar filter with peak at half the folding // frequency. double[] pathF = {1.0, 0.0, -1.0, 0.0, 1.0, 0.0, -1.0}; //A reference to the selected path operator will be // stored here. double[] pathOperator = null; if(testCase == 1){ pathOperator = pathA; }else if(testCase == 2){ pathOperator = pathB; }else if(testCase == 3){ pathOperator = pathC; }else if(testCase == 4){ pathOperator = pathD; }else if(testCase == 5){ pathOperator = pathE; }else if(testCase == 6){ pathOperator = pathF; }else{ System.out.println("Invalid testCase"); System.out.println("Terminating program"); System.exit(0); }//end else //Display the pathOperator //First instantiate a plotting object. PlotALot01 pathOperatorObj = new PlotALot01("Path", (pathOperator.length * 4) + 8,148,70,4,0,0); //Feed the data to the plotting object. for(int cnt = 0;cnt < pathOperator.length;cnt++){ pathOperatorObj.feedData(40*pathOperator[cnt]); }//end for loop //Cause the graph to be displayed on the computer // screen in the upper left corner. pathOperatorObj.plotData(0,0); //Now compute and plot the frequency response of the // selected path //Instantiate a plotting object for two channels of // frequency response data. One channel is for // the amplitude and the other channel is the phase. PlotALot03 pathFreqPlotObj = new PlotALot03("Path",264,148,35,2,0,0); //Compute the frequency response and feed the results // to the plotting object. displayFreqResponse(pathOperator,pathFreqPlotObj, 128,0); //Cause the frequency response data stored in the // plotting object to be displayed on the screen in // the top row of images. pathFreqPlotObj.plotData(112,0); //Instantiate an object to handle the adaptive behavior // of the program. The use of the class named // AdaptEngine02 is new to this lesson. AdaptEngine02 adapter = new AdaptEngine02( filterLength,feedbackGain); //Instantiate an array object that will be used as a // delay line for the wideband test data. double[] rawData = new double[pathOperator.length]; //Instantiate a plotting object for four channels of // time-serie data. PlotALot05 timePlotObj = new PlotALot05("Time",468,148,25,2,0,0); //Instantiate a plotting object for two channels of // filter frequency response data. One channel is for // the amplitude and the other channel is for the // phase. PlotALot03 freqPlotObj = new PlotALot03("Freq",264,487,35,2,0,0); //Instantiate a plotting object to display the filter // impulse response at specific time intervals during // the adaptive process. PlotALot01 filterPlotObj = new PlotALot01("Filter", (filterLength * 4) + 8,487,70,4,0,0); //Declare and initialize working variables. double output = 0; double err = 0; double target = 0; double input = 0; double wbTestData = 0; boolean adaptOn; //Perform the specified number of iterations. for(int cnt = 0;cnt < numberIterations;cnt++){ //The following variable is used to control whether // or not the adapt method of the adaptive engine // updates the filter coefficients when it is called. // The filters are updated when this variable is // true and are not updated when this variable is // false. adaptOn = true; //Get and scale the next sample of wideband test // data from a random number generator. Before // scaling by wbTestDataScale, the values are // uniformly distributed from -1.0 to 1.0. wbTestData = wbTestDataScale*(2*(Math.random() - 0.5)); //Set wideband input values near the end of the run // to zero and turn adaptation off. Insert an // impulse near the end that will produce several // interesting impulse responses. Among other // things, this will cause the impulse response of // the path to be visible in the graphs. //Set values to zero. if(cnt > (numberIterations - 5*filterLength)){ wbTestData = 0; adaptOn = false; }//end if //Now insert an impulse at one specific location in // the time series. if(cnt == numberIterations - 3*filterLength){ wbTestData = 2 * wbTestDataScale; }//end if //Insert the wideband test data into the raw data // delay line. flowLine(rawData,wbTestData); //Apply the path operator to the wideband test data. // Note the use of the method named // reverseDotProduct, which is new to this lesson. // This method provides the time reversal that is // necessary for true convolution. double pathData = reverseDotProduct(rawData,pathOperator); //Declare a variable that will be populated with the // results returned by the adapt method of the // adaptive engine. AdaptiveResult result = null; //Decide which type of filter to develop and do the // adaptive work. if(identification){//Develop an identification filter //Establish input values for the adaptive engine // that are appropriate for the development of // an identification filter. input = wbTestData; target = pathData; //Do the adaptive processing. result = adapter.adapt(input,target,adaptOn); }else{//Develop an inverse filter. //Establish input values for the adaptive engine // that are appropriate for the development of // an inverse filter. Note that the input values // are reversed relative to an identification // filter. input = pathData; target = wbTestData; //When the run is nearly over and the wideband // input data values are zero, insert an impulse // into the pathData. This happens later than the // point in time where the impulse was inserted // into the wideband test data. Note that // adaptation is still turned off at this point in // time. Among other things, this makes it // possible to view the impulse response of the // inverse filter in the time series that are // displayed. if(cnt == numberIterations - filterLength){ input = 2 * wbTestDataScale; }//end if //Do the adaptive processing result = adapter.adapt(input,target,adaptOn); }//end else //Get and save adaptive results for plotting and // spectral analysis output = result.output; err = result.err; filter = result.filterArray; //Feed the time series data to the plotting object. timePlotObj.feedData(input,target,output,-err); //Compute and plot summary results at the end of // every 400th iteration. if(cnt%400 == 0){ displayFreqResponse(filter,freqPlotObj, 128,filter.length - 1); //Display the filter impulse response coefficient // values. The adaptive engine returns the filter // with the time axis reversed relative to the // conventional display of an impulse response. // Therefore, it is necessary to display it in // reverse order. for(int ctr = 0;ctr < filter.length;ctr++){ filterPlotObj.feedData( 40*filter[filter.length - 1 - ctr]); }//end for loop }//End display of frequency response and filter }//End for loop, //Cause the data stored in the plotting objects to be // plotted. timePlotObj.plotData(376,0);//Top of screen freqPlotObj.plotData(0,148);//Left side of screen filterPlotObj.plotData(265,148); //For the case of an inverse filter, convolve the // filter with the impulse response of the path to // determine the extent to which the filter is able to // compensate for the characteristics of the path. if(!identification){ //Copy the filter into another array with zeros on // both ends to account for end effects in the // convolution process. double[] convInput = new double[ filter.length + 2*pathOperator.length]; for(int cnt = 0;cnt < filter.length;cnt++){ convInput[cnt + pathOperator.length] = filter[cnt]; }//end for loop //Create an array to receive the convolution output. double[] convOutput = new double[filter.length + pathOperator.length]; //Call the method named convolve01 to perform the // convolution. convolve01(convInput,pathOperator,convOutput); //Now compute and plot the frequency spectrum of the // time series that results from convolving the final // adaptive filter with the path impulse response. // Ideally, the amplitude response will be flat from // zero to the folding frequency and the phase // response will be flat, or possibly linear // (indicating a time delay). //Instantiate a plotting object for two channels of // frequency response data. One channel is for // the amplitude and the other channel is the phase. PlotALot03 finalFreqPlotObj = new PlotALot03("Final",264,137,35,2,0,0); //Compute the frequency response and feed the results // to the plotting object. displayFreqResponse(convOutput,finalFreqPlotObj, 128,convOutput.length - 1); //Cause the frequency response data stored in the // plotting object to be displayed on the screen. finalFreqPlotObj.plotData( 265 + filterLength * 4 + 8,148); }//end if }//end process method //-----------------------------------------------------// //This method simulates a tapped delay line. It receives // a reference to an array and a value. It discards the // value at index 0 of the array, moves all the other // values by one element toward 0, and inserts the new // value at the top of the array. void flowLine(double[] line,double val){ for(int cnt = 0;cnt < (line.length - 1);cnt++){ line[cnt] = line[cnt+1]; }//end for loop line[line.length - 1] = val; }//end flowLine //-----------------------------------------------------// void displayFreqResponse( double[] filter,PlotALot03 plot,int len,int zeroTime){ //Create the arrays required by the Fourier Transform. double[] timeDataIn = new double[len]; double[] realSpect = new double[len]; double[] imagSpect = new double[len]; double[] angle = new double[len]; double[] magnitude = new double[len]; //Copy the filter into the timeDataIn array System.arraycopy(filter,0,timeDataIn,0,filter.length); //Compute DFT of the filter from zero to the folding // frequency and save it in the output arrays. ForwardRealToComplex01.transform(timeDataIn, realSpect, imagSpect, angle, magnitude, zeroTime, 0.0, 0.5); //Note that the conversion to decibels has been // disabled. You can re-enable the conversion by // removing the comment indicators. /* //Display the magnitude data. Convert to normalized // decibels first. //Eliminate or change any values that are incompatible // with log10 method. for(int cnt = 0;cnt < magnitude.length;cnt++){ if((magnitude[cnt] == Double.NaN) || (magnitude[cnt] <= 0)){ //Replace the magnitude by a very small positive // value. magnitude[cnt] = 0.0000001; }else if(magnitude[cnt] == Double.POSITIVE_INFINITY){ //Replace the magnitude by a very large positive // value. magnitude[cnt] = 9999999999.0; }//end else if }//end for loop //Now convert magnitude data to log base 10 for(int cnt = 0;cnt < magnitude.length;cnt++){ magnitude[cnt] = log10(magnitude[cnt]); }//end for loop */ //Find the absolute peak value. Begin with a negative // peak value with a large magnitude and replace it // with the largest magnitude value. double peak = -9999999999.0; for(int cnt = 0;cnt < magnitude.length;cnt++){ if(peak < abs(magnitude[cnt])){ peak = abs(magnitude[cnt]); }//end if }//end for loop //Normalize to 20 times the peak value for(int cnt = 0;cnt < magnitude.length;cnt++){ magnitude[cnt] = 20*magnitude[cnt]/peak; }//end for loop //Now feed the normalized data to the plotting // object. for(int cnt = 0;cnt < magnitude.length;cnt++){ plot.feedData(magnitude[cnt],angle[cnt]/20); }//end for loop }//end displayFreqResponse //-----------------------------------------------------// //This method receives two arrays and treats each array // as a vector. The two arrays must have the same length. // The program reverses the order of one of the vectors // and returns the vector dot product of the two vectors. double reverseDotProduct(double[] v1,double[] v2){ if(v1.length != v2.length){ System.out.println("reverseDotProduct"); System.out.println("Vectors must be same length."); System.out.println("Terminating program"); System.exit(0); }//end if double result = 0; for(int cnt = 0;cnt < v1.length;cnt++){ result += v1[cnt] * v2[v1.length - cnt - 1]; }//end for loop return result; }//end reverseDotProduct //-----------------------------------------------------// //This method applies an incoming convolution operator to // an incoming set of data and deposits the filtered data // in an output array whose reference is received as an // incoming parameter. public void convolve01(double[] data,//input data array double[] operator,//operator array double[] output//output data array ){ //Apply the operator to the data, dealing with the // index reversal required by convolution. int dataLen = data.length; int operatorLen = operator.length; for(int i=0;i < dataLen-operatorLen;i++){ output[i] = 0; for(int j=operatorLen-1;j>=0;j--){ output[i] += data[i+j]*operator[j]; }//end inner loop }//end outer loop }//end convolve01 method //-----------------------------------------------------// }//end class Adapt07 |

/*File AdaptEngine02.java Copyright 2005, R.G.Baldwin General purpose LMS adaptive algorithm. This is an upgrade of the class named AdaptEngine01. This version allows the user to pass a boolean parameter to the adapt method to turn the adaptive process on or off. Otherwise, the behavior of the class is the same as the behavior of the class named AdaptEngine01. An object of this class is a general purpose adaptive engine that implements the classical LMS adaptive algorithm. The adaptive algorithm is implemented by the instance method belonging to the object named adapt. Each time the adapt method is called, it receives one sample from each of two different time series. One time series is considered to be the data that is to be filtered. The other time series is considered to be a target. The purpose of the adapt method is to adaptively create a convolution filter which, when applied to the data time series, will transform it into the target time series. Each time the method is called, it performs a dot product between the current version of the filter and the contents of a delay line in which historical data samples have been saved. The result of that dot product is compared with the target sample to produce an error value. The error value is produced by subtracting the value of the target sample from the result of the dot product. The error value is then used in a classical LMS adaptive algorithm to adjust the filter coefficients. The objective is to produce a set of filter coefficients that will drive the error to zero over time. This adaptive engine can be used as the solution to a variety of different signal processing problems, depending on the selection of time series that are provided as data and target. The constructor for the class receives two parameters: filterLength feedbackGain The filter length is used to construct two arrays. One array is used later to contain the filter coefficients. The other array is used later as a tapped delay line to contain the historical data samples and to precess them by one element each time the method is called. The feedback gain is used in the LMS adaptive algorithm to compute the new filter coefficients. Tested using J2SE 5.0 and WinXP. **********************************************************/ class AdaptEngine02{ double[] filterArray;//filter coefficients stored here double[] dataArray;//historical data is stored here double feedbackGain;//used in LMS adaptive algorithm //Constructor public AdaptEngine02(int filterLength, double feedbackGain){ //Construct the two arrays and save the feedback gain. filterArray = new double[filterLength]; dataArray = new double[filterLength]; this.feedbackGain = feedbackGain; }//end constructor //-----------------------------------------------------// //This method implements a classical LMS adaptive // algorithm to create and to apply a convolution filter. // The filter output, the error, and a reference to the // array containing the filter coefficients are // encapsulated in an object of type AdaptiveResult and // returned to the calling method. The adaptive update // process is disabled when the parameter named adaptOn // is false. AdaptiveResult adapt(double rawData, double target, boolean adaptOn){ //Insert the incoming data value into the data delay // line. flowLine(dataArray,rawData); //Apply the current filter coefficients to the data. double output = dotProduct(filterArray,dataArray); //Compute the error. double err = output - target; //Only update the coefficients when adaptOn is true. if(adaptOn){ //Use the error to update the filter coefficients. for(int ctr = 0;ctr < filterArray.length;ctr++){ filterArray[ctr] -= err*dataArray[ctr]*feedbackGain; }//end for loop. }//end if //Construct and return an object containing the filter // output, the error, and a reference to the array // object containing the current filter coefficients. return new AdaptiveResult(filterArray,output,err); }//end adapt //-----------------------------------------------------// //This method simulates a tapped delay line. It receives // a reference to an array and a value. It discards the // value at index 0 of the array, moves all the other // values by one element toward 0, and inserts the new // value at the top of the array. void flowLine(double[] line,double val){ for(int cnt = 0;cnt < (line.length - 1);cnt++){ line[cnt] = line[cnt+1]; }//end for loop line[line.length - 1] = val; }//end flowLine //-----------------------------------------------------// //This method receives two arrays and treats the first N // elements in each of the two arrays as a pair of // vectors. It computes and returns the vector dot // product of the two vectors. If the length of one // array is greater than the length of the other array, // it considers the number of dimensions of the vectors // to be equal to the length of the smaller array. double dotProduct(double[] v1,double[] v2){ double result = 0; if((v1.length) <= (v2.length)){ for(int cnt = 0;cnt < v1.length;cnt++){ result += v1[cnt]*v2[cnt]; }//emd for loop return result; }else{ for(int cnt = 0;cnt < v2.length;cnt++){ result += v1[cnt]*v2[cnt]; }//emd for loop return result; }//end else }//end dotProduct //-----------------------------------------------------// }//end class AdaptEngine02 //=======================================================// //This class is used to encapsulate the adaptive results // into an object for return to the calling method. class AdaptiveResult{ public double[] filterArray; public double output; public double err; //Constructor public AdaptiveResult(double[] filterArray, double output, double err){ this.filterArray = filterArray; this.output = output; this.err = err; }//end constructor }//end class AdaptiveResult //=======================================================// |

#### About the author

**Richard Baldwin**

*is a college professor (at Austin Community College in Austin, TX) and private consultant whose primary focus is a combination of Java, C#, and XML. In addition to the many platform and/or language independent benefits of Java and C# applications, he believes that a combination of Java, C#, and XML will become the primary driving force in the delivery of structured information on the Web.*

*Richard has participated in numerous consulting projects and he
frequently provides onsite training at the high-tech companies located in and
around Austin, Texas. He is the author of Baldwin's Programming
Tutorials, which have gained a
worldwide following among experienced and aspiring programmers. He has also
published articles in JavaPro magazine.*

*In addition to his programming expertise, Richard has many years of
practical experience in Digital Signal Processing (DSP). His first job after he
earned his Bachelor's degree was doing DSP in the Seismic Research Department of
Texas Instruments. (TI is still a world leader in DSP.) In the following
years, he applied his programming and DSP expertise to other interesting areas
including sonar and underwater acoustics.*

*Richard holds an MSEE degree from Southern Methodist University and has
many years of experience in the application of computer technology to real-world
problems.*

*Baldwin@DickBaldwin.com*

Page 3 of 3