You need to avoid discontinuities in the output waveform (these are the clicks you are hearing). The easiest way to do this is with a LUT-based waveform generator - this works for any periodic waveform (i.e. not just pure sine waves). Typically you use a fixed point phase accumulator, which is incremented for each new sample by a delta value which corresponds to the current output frequency. You can safely modify delta however you like and the waveform will still be continuous.
Pseudo code (for one output sample):
const int LUT_SIZE;
const int LUT[LUT_SIZE]; // waveform lookup table (typically 2^N, N >= 8)
Fixed index; // current index into LUT (integer + fraction)
Fixed delta; // delta controls output frequency
output_value = LUT[(int)index];
// NB: for higher quality use the fractional part of index to interpolate
// between LUT[(int)index] and LUT[(int)index + 1], rather than just
// truncating the index and using LUT[(int)index] only. Depends on both
// LUT_SIZE and required output quality.
index = (index + delta) % LUT_SIZE;
Note: to calculate
delta
for a given output frequency
f
and a sample rate
Fs
:
delta = FloatToFixed(LUT_SIZE * f / Fs);