Variable-Pitch Sine Wave Output

The parable of the Game Jam

Don't let this happen to you, kids. You need good audio hardware to debug audio code.

Debugging the audio code

Square vs Sine Waves

Because square waves are already pretty harsh, they prevent our ability to diagnose some audio bugs. A Sine wave is a "purer" tone, and will enhance our ear's ability to pick up on weirdness. The sin function, however, is defined to return a value between -1 and 1, so we need to talk how to represent fractional numbers on a computer.

Fixed-point arithmetic

Fixed point is just integer math. We define some number of bits at the low end of our integer to represent the fractional part of the number, and the remaining bits represent the whole part. Normal addition, subtraction, multiplication, and division work fine, although we need to be aware of the rounding characteristics of fixed-point when doing any numeric computation.

Fixed-point math was used more widely before computers commonly had floating point hardware. Today every computer, GPU, and phone has very strong floating point capabilities, and so it is the defacto way to do numerics on a modern computer.

Floating-point representation

Floating-point is a more complicated (although very rigorously defined) way to represent fractional values. It approaches the problem by dividing the available bits into:

  • Sign bit
  • Exponent
  • Mantissa

Such that the value represented is given by (sign)(mantissa * 2^exponent). This allows us to preserve a consistent number of bits of precision (like "significant figures" from your physics class), given by the size of the mantissa, regardless of the scale of our numbers, given by the exponent. This means that values representable by floating point will be more densely packed near zero, and more sparse near the limits.

Floating Point values come in a few different precisions: float (single-precision, 32-bit), double (double-precision, 64-bit), and long double (128-bit). We will rely on single-precision floats almost exclusively, because they are good enough, and often we can operate on them twice as quickly as doubles.

Generating a sine-wave test tone

For the test code, we use the c standard sinf function. It's defined in math.h. Its defined to accept a float "angle" and return a float in the range [-1.0f, 1.0f]. The angle is a function of:

  • How many samples we have written in total. Call it RunningSampleIndex.
  • The sampling frequency
  • The frequency of the tone we want to play (200-500 Hz is a good range for testing).

When we set the tone frequency, we calculate its period in samples, and call it WavePeriod.

The "angle" is then given by 2.0f*PI*((float)RunningSampleIndex / (float)WavePeriod).

The SampleValue is given by the sinf(angle) * Volume.

Smoothing the waveform on frequency change

When you change the frequency with the current code, you'll end up with an artifact. To combat this, you need to track an additional value in your synth, basically your progress through the period of the wave, here called tSine. Incrementally accumulate it per sample written:

tSine += 2.0f*Pi32*1.0f/(float)WavePeriod // tSine = 2*Pi*how many "WavePeriods" we've played since we started

Then just use it as the angle for the SampleValue calculation.

SampleValue = sinf(tSine) * ToneVolume;

Additional Fixes

  • There is yet another XInput version that may be the only one available on some Windows 8 installs. xinput9_1_0.dll. Add it to the chain when loading libs.
  • Bitshifting to divide will give you unexpected results for negative numbers. Use actual divide instead.