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 float
s almost exclusively,
because they are good enough, and often we can operate on them twice as quickly as double
s.
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.