Telecom programs 2a and 2b

Due: Dec 7 or later

You are to write the following two programs, again involving manipulation of .wav files. Both involve loading the sound file into a big array, eg short[] samples, and then applying the "convolution" operation of setting result[i] to be a suitable "moving average" of values in samples[].

Specifically, convolution of an array samples[] by a "kernel" K, a shorter array of double of length len, means to set result[n] as follows, for 0≤n<samples.length-len

	double sum = 0;
for (int i = 0; i<len; i++) sum += samples[i+n]*K[i];
result[n] = (short) sum;
Purists do the reversed indexing of K, replacing K[i], above, with K[len-i-1], but in general  all you have to do is make sure that K itself is created in the reversed order. If K is symmetrical, you won't even have to do that.

I am giving you a starter file, convolution.java, that contains the basic file-setup operations and the following operation:
    convolve(short[] samples, double[] kernel);
See the comments for minor implementation issues.

Program 2a: echo

Add back to the original a delayed and slightly reduced copy of itself. The delay and attenuation should be specified by constants declared at the beginning of your program, or else read in or provided on the command line.

You can do this directly: result[i] = samples[i]+A*samples[i-delay]. Or you can convolve by a kernel of the following form:

A 0 0 0 0 0 ... (length depends on delay) ... 0 0 1
(That kernel is of the "samples[i+n]*K[i]" orientation).

Program 2b: bandpass

Create a kernel to pass only frequencies in the range between f1 and f2 (in Hertz), where f1 and f2 are specified at the beginning of your program. The length of the kernel will also be specified. The program will take an input file on the command line (or embedded in the program), and generate an output file (I just had my version create "output.wav" each time, which I then renamed as necessary). The output.wav file is the result of the convolution, and should consist of the sounds in the input file that are in the frequency range from f1 to f2.

The simplest approach is to create two separate kernels: a lowpass filter to reject all frequencies > f2, and a highpass filter to then reject all frequencies < f1. However, it is a little more efficient to combine these into a single kernel, by convolving them together. If you do this, you must handle the "tails" properly. Consider each kernel as extended by 0's in each direction, and allow for any possible degree of overlap. If n is the length of each separate kernel, then their convolution will have length 2*n-1.

The lowpass filter uses the sinc function, y = sin(x)/x. Specifically, let f_low = f2/samplerate (do the division as a double; note f_low is the higher of f1 and f2), and let n be the number of components to be used on either side of the center. The entire kernel will thus have length 2*n+1. Ideally n will be close to a multiple of the wavelength 1.0/f_low; typically, n is in the range of 5 to 20 wavelengths. Set

	L[i] = A*Math.sin(2*pi*f_low*(i-n)) / (i-n);

as i goes from 0 to 2*n, except for i=n. At i=n (where the denominator is 0), set L[i] = A*2*pi*f_low.

For the time being, you may take A=1.0; that is, omit it entirely. But see below for normalization.

For filling in the L[i], you will have a loop like this:
    for (int i = 0; i<= 2*n; i++) {
if (i!=n) L[i] = Math.sin(2*pi*freq*(i-n)) / (i-n); // make sure you have the right units for freq
}
L[n] = 2*pi*freq;

The above kernel ends abruptly at the ends; to taper it more gradually, the following tweak (the "Blackman window") is recommended. It would be applied after you have built L as above.

    for (int i = 0; i<=n; i++) {
double factor = 0.42 - 0.5*Math.cos(pi*i/n) + 0.08*Math.cos(2*pi*i/n);
L[i] *= factor;
L[2*n-i] *= factor;
}
Note that the kernel L does not involve any of your sound data. You convolve it later with the sound data.

This "cosine" wave rises and falls just once in the entire range, versus many wavelengths for the sin part above.

After you create your basic filter kernel L, without A, normalize it by adding up the sum of all the components L[i] and then multiplying every component by A = 1.0/sum. This will ensure that the sum of the coefficients is now 1.0.

To create a highpass filter H, just subtract the lowpass filter from the identity. In other words,

	H[i] = -L[i], i ≠ n
H[n] = 1.0 - L[n]; // the 1.0 appears at i=n only!
I implemented this as a method invert() in my SincFilter class; I would create the two filters as follows:
    SincFilter Lo = new SincFilter(f2/samplerate, n);
SincFilter Hi = new SincFilter(f1/samplerate, n);
Hi.invert();
Note that the high- and low-pass filters will have different fundamental frequencies.

A reasonably safe value for n is 100, which is 5 wavelengths of a 400 Hz tone. Note that what I did, though, was to specify a number of wavelengths, and have the SincFilter constructor compute n as numWaves*sampleRate/frequency. The bigger n is, the more "accurate" your filter is, but also the slower.

You will be graded to a small degree on whether you combine the two filters into one (harder) and whether you implement the Blackman window (easier). Consider these optional.

Testing

(This section is required for graduate students only.)

Here is bobhalmixed.wav, a sound mix of Sideshow Bob (Kelsey Grammer), highpass-filtered to allow only frequencies at or above 1400 Hz, and Hal the computer from 2001 (Douglas Rain), lowpass-filtered to allow only frequencies below 1200. You can separate the two with a high or low-pass filter (even a "weak" one) at 1300 Hz.

I have provided "chord" files, consisting of 3-second blends of the following five musical notes:

note
frequency
f1
f2
C
261.6 Hz
230
290
E
329.6 Hz
310
350
G
392.0 Hz
370
415
A
440.0 Hz
420
460
C2
523.2 Hz
490
550

Create five bandpass filters, each centered around one of the notes and narrow enough to rule out the neighbors (the f1 and f2 values above are suggested), and then use your bandpass filter to pick out the middle tone only. Identify what tones each chord is composed of; you should also be prepared to get roughly the correct intensity. (I will measure that, and you can measure that with my stats.java program, but you don't need to turn that in.)

The chord files:
chord1.wav
chord2.wav
chord3.wav
chord4.wav
chord5.wav
chord6.wav

You can use the SoX program to confirm your measurements.