CS 211, 004/006
Project 3
due Thursday, October 25th at noon


The objective of this project is to practice producing class hierarchies, by developing a set of data filters which could be used for an application such as audio processing. This project will involve using abstract classes and inheritance to create several related classes which share functionality.

Overview:

  1. Create the abstract class DataSource, which allows data to be read one floating point number at a time.
  2. Create three subclasses of DataSource, namely ArraySource which draws its input from an array; SineSource which produces a sine-wave output; and DataFilter, which draws its input from another DataSource.
  3. Create subclass of DataFilter called ScaleFilter, which scales (multiplies) the input by a constant; and SineScaleFilter, which scales the input by a sine wave.
  4. Create an abstract subclass of DataFilter called BufferedFilter, which uses a circular buffer to remember a range of previous input.
  5. Create a subclass of BufferedFilter called EchoFilter which echoes the input with a certain delay.
Audio processing typically involves taking streams of digital data - streams of discrete, oscillating numbers - and performing various mathematical operations on them. However, there are a number of different operations you can use to process an audio stream - from the simple, like amplitude scaling (increasing and decreasing the volume), to more complicated, like filters which isolate certain audio frequencies, to specialized ones which produce different audio effects.

In this project, we will set up a framework for audio filtering, and produce a few basic filters.

Rules

  1. This project is an individual effort; the Honor Code applies
  2. You may not import any extra functionality besides the default (i.e. System, Math)
  3. The main method will not be tested; you may use it any way you want.
  4. All fields must be declared private or protected. This will be verified manually.
  5. You may write your own helper methods, but any methods which are specifically asked for must match exactly (i.e. capitalization of the method name; number and order of the parameters).
DataSource: (10p)
public abstract class DataSource implements java.util.Iterator<Double>

We can begin with a class that represents a generic source of data. The data we will be dealing with is a stream of Double data (remember that Double is just the reference version of the double primitive type). The data from the stream is to be read one floating point number at a time, for as long as there is data left on the stream. This class is abstract - our data-reading methods are left there as a placeholder for future classes to override. The class should implement the following:

Notice how the class declares that it implements some kind of iterator? That's something which we gained for free, just by ensuring that we gave our methods the right names.

ArraySource: (10p)
public class ArraySource extends DataSource

We already have data sources which have no data! Let's do something about that. This basic data source will allow you to enter an array of Double values to use as input. Calls to next() will begin with the first value in the array, and continue in order until there are no more values in the array, at which point hasNext() will begin to return false. The only new method in this class will be the constructor, although next() and hasNext() must be overriden so that the class is no longer abstract:

SineSource: (10p)
public class SineSource extends DataSource

The SineSource is another source of data, but this time it generates its own data. As you may be able to guess from the name, it generates a sine wave. A sine wave has several parameters: the amplitude a represents how large it is; the frequency f represents how quickly it oscillates; and the offset d represents the moment it begins. Together, they form an equation like the following:

a×sin(f×t + d)

The variable t above represents the time step: the first time you call next() corresponds to zero, the next time to 1, etc. Assume that the angle is in radians, to be consistent with Java's built in Math functionality. We want the input to be finite so our constructor will include a maximum number of time steps before the sine wave runs out. You will implement:

Once you've created this class and you think it works, you can try the following as a simple test to see what happens:

new SineSource(3.0, Math.PI/20, 0.0, 100).display();

DataFilter: (10p)
public class DataFilter extends DataSource

This class takes another DataSource as input, and sends it directly through to the next() method. This will not be useful in and of itself, but we will derive classes later which modify this functionality in order to be able to do something interesting with the data. Like with the other sources, you have the following to implement:

ScaleFilter: (10p)
public class ScaleFilter extends DataFilter

This filter doesn't just pass the input to the output: it also multiplies it by a scaling constant before doing so. So if it has a scaling constant of c=5.0 and it recieves an input of 0.2, then the output it produces with next() will be 10.0. This filter will implement the following:

SineScaleFilter: (10p)
public class SineScaleFilter extends DataFilter

This filter is similar to the ScaleFilter, except that instead of multiplying by a constant at every step, it multiplies by a sine wave (see SineSource) at every step. This filter will implement the following:

Implementation note: although you must derive this class from DataFilter, you are not required to derive directly from DataFilter. You can either leave the declaration as-is, or use a variant (for example, derive SineScaleFilter from ScaleFilter, or derive both of them from some intermediate class).

BufferedFilter: (20p)
public class BufferedFilter extends DataFilter

While you're reading data through the filter, it's possible to use a buffer (in this case an array of Double values) to remember input which you've already seen. For example, if you have an array of 20 Double values, then you can remember the past 20 input values which have passed through your filter. If you want to remember which data you've seen 5 time units ago, all you need to do is look back 5 places in your array.

There's a catch, though: when you begin, you only have a fixed-size buffer, but your input can go on for a long time. The input may be so long, that you don't even want to keep a buffer large enough to capture all of it. Maybe your input is 100,000 time units long, but you only care about remembering the past 20 time steps. A solution is to use a circular buffer. The buffer fills up, but when it gets to the end, it starts back at the beginning and starts overwriting what's already there. Basically, the buffer has a moving head and a moving tail.

So if our buffer is length 20, at the beginning, our head is at position 0, our tail is at position 1, and the data for the previous time step is at position 19, and five time steps ago is at position 15. We read in a bit of data, and it gets stored at position 0. Now, the head is at position 1, the tail is at position 2, the previous time step (the one you've just read in) is at position 0, and five time steps ago is at position 16. Etc...

For this class you implement a circular buffer which will store input values as you read them from the source, after which you forward them out as-is through the filter. You will implement the following:

EchoFilter: (10p)
public class EchoFilter extends BufferedFilter

This filter uses the memory of previous inputs to produce an echo effect. At every step, it will output a value which is the sum of the current input and an input from some n steps ago. To do this, the following needs to be implemented:

Testing:

Download and use the following to test your code (in addition to the tester jar which you already have):

Also note that you can instantiate your own filters, and use the display() method to produce a visual output to see what they look like.

Grading:

The implementation of each class carries the weight mentioned in each part above (10% each except for BufferedFilter, which is 20%). Half of that score comes from automatic testing, and half comes from manual inspection. 10% will be granted for style (i.e. commenting code, not writing unreadable code), including properly distinguishing which fields and methods should be private.

Partial credit is possible, but hard-coding to avoid test cases will receive an automatic zero on the manual inspection points. Programs which do not compile without modification will receive a zero in most cases.

Submission:

Submission instructions are as follows.

  1. Let xxx be your lab section number (one of 213-220), and let yyyyyyyy be your GMU userid. Create the directory xxx_yyyyyyyy_P3/
  2. Place the eight files corresponding to the 8 classes you've written in the directory you've just created.
  3. Create the file ID.txt in the format shown below, containing your name, userid, G#, lecture section and lab section, and add it to the directory. Your directory should contain nine files total.

    Full Name: Donald Knuth
    userID: dknuth
    G#: 00123456
    Lecture section: 004
    Lab section: 213

  4. compress the folder and its contents into a .zip file, and upload the file to Blackboard.