How to fix frequency drift when producing an arbitrary length signal by repeatedly looping over a single-period waveform?

I have an audio application that produces an arbitrary length signal by repeatedly looping over a single-period waveform. In pseudo-code:

// Build a single period of a waveform at the given frequency
samples_per_waveform = int(sampling_rate / frequency)
waveform = make_sin_waveform(samples_per_waveform)

...

// Repeat the single period waveform to produce a continuous signal
count = int(duration * sampling_frequency / samples_per_waveform)
repeat count times:
write(waveform)


Since the number of samples per waveform is obviously an integer, I think this is subject to rounding error:

As an example, when rounding to the nearest integer, at a 5kHz sampling rate, a single-period waveform for a 300Hz signal would require 17 samples--and thus leads to an actual frequency of 294Hz. I understand the higher will be the sampling rate, the lower will be the rounding error. However, it will still exist.

Is there a way to fix that, or is this approach doomed from the start?

EDIT: As a possible workaround, I envision to keep track of the rounding error in the output loop, and duplicating one sample when the error exceeds a sample duration. I don't know if this is clear enough, but it is somewhat similar in my mind to what the Bresenham line drawing algorithm is doing.

If you use a high quality interpolator, then you can interpolate a copy of the data at non-integer offsets. If you use a Sinc interpolator on bandlimited data, then the interpolated delayed samples will be near perfect (minus numerical and quantization noise) at any fractional delay (even as close as you can get to irrational fractions using finite width floating point).

I suggest interpolating more than one period of waveform to be repeated per period of result, and cross fading between these over-long sample repeats

• Thanks, Ronald: I think I understand the idea. Would you have some links or pointers to relevant documentation so I can sudy that more in depth? – Sylvain Leroux Dec 5 '19 at 17:13

My suggestion is to use a Numerically Controlled Oscillator (NCO). A typical NCO effectively has one cycle of a sine wave in memory and can play back that sine wave as a continuous waveform with very high frequency resolution. But this needn't be a sine wave: you could have any waveform in memory and adjust the rate at which it plays back in the same fashion. That said, I believe these posts will be helpful where I provide more details on how an NCO works:

Numerically Controlled Oscillator (NCO) for phasor implementation?

Problem in understanding DDFS (Direct Digital Frequency Synthesizer.)

As a simplest explanation the approach is to count through the the waveform at a fixed rate (the sampling rate) with an increment that is proportional to the output frequency desired. The higher the precision of your counter (accumulator), the higher precision in frequency can be achieved. If you count with an increment if 1, you will get the lowest non-zero frequency out, and it will take $$2^n$$ counts to get through the entire waveform in memory once where n is the precision of the accumulator. Only the MSB’s of the accumulator are needed to address the memory based on how big the memory is. If you count by 2 that frequency will twice as fast, etc. So ultimately the frequency resolution is

$$f_\Delta=\frac{f_{clk}}{2^n}$$

Where $$f_{clk}$$ is the sample rate and n is the number of bits in the accumulator.

And the output frequency assuming the memory contains exactly one cycle of your waveform will be

$$f_{out}= Nf_\Delta$$

Where N is the count increment.

So for example with a 10 MHz clock (update rate) and a 32 bit accumulator which are both typical and practical values, you can generate output frequencies in the 0 to 5MHz first Nyquist zone with 2.33 mHz resolution! (If combining with an D/A converter the practical upper limit would be approximately 40 MHz to allow for image filtering and an inverse Sinc filter would be considered to compensate for the Sinc reconstruction droop).

The links above go into full details of design parameters including signal quality (SNR and spur levels) and considerations on how many address bits should go to memory etc.