Improving the sine wave oscillator by accounting for frequency

In my previous example, I showed how to create a basic sine wave oscillator. Now, I want to make it a little bit better.

In my next step, I want to factor out the oscillator function that produces a signal. Now, this function should not rely on any outside processing to produce the correct signal: the goal is to pass in a variable that represents the next sample to insert into the source data line, and get a Short value that we will actually put into the source data line.

So, we'll need to pass in more than just the next sample index: we will also need to pass in the frequency and the sample rate. And, instead of just passing in the max value of Short to produce our volume, let's also add a way of controlling for volume. Our function will end up having a signature like this:

getSignal(position, amplitude, frequency, sampleRate)

Translated, position is our time unit, amplitude is our volume, frequency is the actual note we are playing, and sampleRate is how many time units per second we will have.

Now, we want to get the same output, but our inputs are a little bit different. We are still going to do a sine on 2*pi*r just like we were doing last time - but now we need to calculate the r.

We can calculate r by dividing frequency by sample rate, and multiplying that by position. Then, we do a sin(2*pi*r) and multiply by amplitude to produce our volume.

Oh, one other thing - this is all based on a radius of 1, so we need to keep our r below 1. The code ends up looking like this:

fun getSignal(position: Int, amplitude: Short, frequency: Double, sampleRate: Double): Short =
    (amplitude * sin( 2 * PI * getRadius(position, frequency, sampleRate))).toInt().toShort()

 

private fun getRadius(position: Int, frequency: Double, sampleRate: Double): Double {
    val radius = position * (frequency / sampleRate)
    return if (radius > 1) radius - 1 else radius
}

Yes, we still have to do the annoying conversions because doubles can't be converted directly to shorts in Kotlin - yet.

We'll need another change in the class that calls the oscillator: previously, we were doing a series of for loops from 0 to n. That will no longer work, since when we start a new for loop, starting over at zero will produce an incorrect value. We need to be continuously counting up, without resetting the number. Obviously, this won't work for long-running oscillators. But it will work for our demo. Here's what I arrived at:

Now, you'll see that I factored out the opening and closing of the source data line, and moved some constants into a companion object. The for loop no longer begins at zero.

For volume, I actually changed the volume scale from 0-128. I think smaller numbers are more useful for volume than going all the way up to 37,000 with everything in between. The getAmplitude function translates the volume parameter into the amplitude that we will use in our oscillator.

The effect is the same: it plays the same note for the same length, except you can change the volume now, too.

Next up, I'm going to try to introduce more oscillator wave forms, and introduce stereo panning. Eventually, to truly make a useful oscillator, it will need to be more self-contained, maintaining its own internal counter, with us simply getting the next value continuously. But that will be a task for another day.

Comments