Make a fluent transition using sine waves

Viewed 106

I have following code:

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;

public class Test {
    private static final int MAX_LENGTH = 1000;
    private Random r = new Random();
    protected static final int SAMPLE_RATE = 32 * 1024;

    public static byte[] createSinWaveBuffer(double freq, int ms) {
        int samples = ((ms * SAMPLE_RATE) / 1000);
        byte[] output = new byte[samples];
        double period = (double) SAMPLE_RATE / freq;
        for (int i = 0; i < output.length; i++) {
            double angle = 2.0 * Math.PI * i / period;
            output[i] = (byte) (Math.sin(angle) * 0x7f);
        }
        return output;
    }

    public static void main(String[] args) throws LineUnavailableException {
        List<Double> freqs = new Test().generate();
        System.out.println(freqs);
        final AudioFormat af = new AudioFormat(SAMPLE_RATE, 8, 1, true, true);
        SourceDataLine line = AudioSystem.getSourceDataLine(af);
        line.open(af, SAMPLE_RATE);
        line.start();
        freqs.forEach(a -> {
            byte[] toneBuffer = createSinWaveBuffer(a, 75);
            line.write(toneBuffer, 0, toneBuffer.length);
        });
        line.drain();
        line.close();
    }

    private List<Double> generate() {
        List<Double> frequencies = new ArrayList<>();
        double[] values = new double[] { 4.0/3,1.5,1,2 };
        double current = 440.00;
        frequencies.add(current);
        while (frequencies.size() < MAX_LENGTH) {
            //Generate a frequency in Hz based on harmonics and a bit math.
            boolean goUp = Math.random() > 0.5;
            if (current < 300)
                goUp = true;
            else if (current > 1000)
                goUp = false;
            if (goUp) {
                current *= values[Math.abs(r.nextInt(values.length))];
            } else {
                current *= Math.pow(values[Math.abs(r.nextInt(values.length))], -1);
            }
            frequencies.add(current);
        }
        return frequencies;
    }

}

I want to generate a random "melody", beginning from A(hz=440). I do this using random numbers to determine, whether the tone goes up or down.

My problem: I can generate the melody, but if I play it, there is always a "knocking" sound between each tone. What could I do to remove it, so it sounds better?

1 Answers

At the beginning and ending of each tone, the signal going to your SourceDataLine jumps from volume 0 to the full out sine wave instantaneously. Or, it jumps from some arbitrary value in one sine wave to the beginning value in the next. Large jumps can create many overtones which are often heard as clicks.

To remedy this, in your method createSineWaveBuffer, it would be helpful to smooth out the start and end of the buffer by multiplying the values by a factor that ranges from 0 to 1 for the start of the tone, and 1 to 0 for the end of the tone. The number of frames over which you do this depends mostly on esthetics and the sample rate. I think 1 millisecond transitions might work as a ballpark minimum. A commercial digital synth that I have uses that as the smallest value. For 44100 fps, that comes to dividing the transition into 44 steps, e.g., 0/44, 1/44, 2/44, etc. that you multiply to the data values at the start of the buffer, and the reverse that you multiple against the end of the buffer.

I'd be tempted to prefer 64 or 128 steps. 128 steps at 44100 comes to a note onset that only takes about 0.003 seconds, and it should make the transition smooth enough to eliminate the "discontinuity" in the signal. Of course you can choose longer transitions if it sounds more pleasing.

If you do this (if the transition is long enough) there shouldn't be any need to apply low-pass filtering.

Related