Michael Berry
Michael Berry

Reputation: 72334

Playing a sine wave of varying pitch

I have a (pretty simple) piece of code I've thrown together which plays a sine wave of a specific frequency and plays it - it works no problem:

public class Sine {

    private static final int SAMPLE_RATE = 16 * 1024;
    private static final int FREQ = 500;

    public static void main(String[] args) throws LineUnavailableException {
        final AudioFormat af = new AudioFormat(SAMPLE_RATE, 8, 1, true, true);
        try(SourceDataLine line = AudioSystem.getSourceDataLine(af)) {
            line.open(af, SAMPLE_RATE);
            line.start();
            play(line);
            line.drain();
        }
    }

    private static void play(SourceDataLine line) {
        byte[] arr = getData();
        line.write(arr, 0, arr.length);
    }

    private static byte[] getData() {
        final int LENGTH = SAMPLE_RATE * 100;
        final byte[] arr = new byte[LENGTH];
        for(int i = 0; i < arr.length; i++) {
            double angle = (2.0 * Math.PI * i) / (SAMPLE_RATE/FREQ);
            arr[i] = (byte) (Math.sin(angle) * 127);
        }
        return arr;
    }
}

I can also modify the getData() method to return a byte array that produces a gradual change in pitch as it plays, no problems there.

However, I'm struggling with a way to continuously play a sine wave which I can smoothly update the frequency and amplitude of "live" - i.e. having FREQ in the above example changed by another thread and have the sound update in real time. I've tried creating the byte array and then filling it later in a separate thread based on the required values, but either seem to get nothing or distortion. I've also tried writing to the SourceDataLine in chunks, but this provides "blocks" of discrete frequencies rather than the smooth transition I'm after. A search around doesn't seem to provide much other than what I've already tried.

It's for an emulation of a theramin, so ideally needs to be as smooth low-latency as possible.

I can do it ahead of time no problem - but live is proving tricky. Has anyone any ideas or examples they could share?

Upvotes: 3

Views: 2674

Answers (2)

Phil Freihofner
Phil Freihofner

Reputation: 7910

I wrote a Java theremin, and it can be played at this url:

http://www.hexara.com/VSL/JTheremin.htm

On that site, there are two links to the Java Gaming forum where there was some discussion on the various issues involved.

I use a wavetable, rather than a sin function, to generate the PCM data, but the method of changing the variable that is fed into the sin function can be set up in a similar manner.

The easiest thing to do is to have a volatile float or double in the base class that is consulted in the innermost while loop where the sound bytes are being created. Your GUI can update this variable, and the while loop can base the pitch calculation on this.

Consulting the pitch variable once per buffer load will not be satisfactory, so the next logical step is to have your while loop check this variable with every frame you process! Yes, that means referring to the pitch variable 44100 times per second, if that is your frame rate.

But even so, the problem remains that response is limited by the manner in which the JVM time slices threads. When the sound thread is not actively looping, it is also not reading the new values that have been placed into the "pitch" variable! Recall that while the sound thread is quite able to keep the frame rate constant, it is not doing so in "real time," but in bursts of activity. Thus the GUI may overwrite the pitch value several times during the period when the sound processing thread is sleeping, resulting in pitch discontinuities.

To get around this, I made a FIFO where I store and timestamp all the GUI-generated pitch changing events. In the innermost sound processing loop, this FIFO is consulted (instead of the volatile double mentioned earlier) to determine the pitch value to be used, on a per-sample basis. Since the pitch values from a GUI will be discrete values and come at varying times, you need a method of interpolating pitch values to fill the gaps. I use the time stamps and values to calculate a per-frame interpolation, and thus update a pitch variable in the innermost loop every sample.

I think there are a lot of issues, still, with the solution I wrote, and am looking forward to revisiting this!

Upvotes: 1

scleaver
scleaver

Reputation: 246

It looks like you are only reading from the data array once, so regardless of whether the data is modified, only one pitch will be produced. I would think you would need to be playing a shorter wave inside a loop that rereads the data array each iteration. I don't know how the SourceDataLine class functions though, so I don't know if this would produce the sound unsegmented.

Upvotes: 1

Related Questions