Generating a sine wave

Generating a sine wave

  • Comments 1

One common task on the Windows Sound test team is taking a known-good sine wave, passing it through some chunk of code, and looking at what comes out.  Is it a sine wave?  Is it the same frequency that it should be?  Etc., etc.

So the quality of the sine wave generator is important.

There's a commonly available sine wave generator: double sin(double).   It's fairly simple to use this to generate a buffer, if you keep a few things in mind.  But there are pitfalls too.

In full generality, a sine wave can be completely described by four measurements:

  1. Amplitude: half the difference between the maximum and minimum samples.
    There's a mathematical reason to use half the difference instead of the whole difference.  We'll go into this later.
    Yes, there were some bugs due to this "use half the difference" convention.
    For our purposes, we'll consider "full scale" to be from -1.0 to 1.0; the amplitude of a full-scale sine wave is 1.0.  the amplitude of a -3 dB FS sine wave is 10-3/20, or about 0.707945...
  2. Frequency or Period.  Frequency is the number of complete cycles in a fixed unit of time (usually one second.)  Period is the time length of a single complete cycle.  Frequency * Period = 1, unless one of them is zero (in which case the other is infinite.)
  3. dc.  dc is the average value over a complete cycle.
  4. Phase.  This tells where in the cycle the first sample is.  If you measure the angle in radians... which is usual... then the phase is a number between 0 and 2π.

These can all be linked together by a single formula which tells you the mathematical value of a sine wave with these features at any given time.  Let:

a = amplitude (0.0 to 1.0)
f = frequency (in cycles/sec)
c = dc (0.0 to 1.0; to avoid clipping, 0.0 to (1.0 - a))
ϕ = starting phase (0.0 to 2π)
t = the time of interest in seconds

Then

w(t) = a sin(2πft + ϕ) + c

Note in passing that w(0) = a sin((2πf)0 + ϕ) + c = a sin(ϕ) + c is independent of f.

So much for mathematics.  When you want to realize a sine wave in a PCM wave format, you have to discretize the time dimension and the sample dimension.  To discretize the time dimension you need a sample rate (usually 44100 samples per second) and to discretize the sample dimension you need to specify a sample format (usually 16-bit int).

Let's talk about discretizing the time dimension first.  Let s = the sample rate, and i be the sample of interest.  Now:

w(i) = a sin(2πfi / s + ϕ) + c

As a design simplification, we collect constants, and eliminate ϕ and c...

w(i) = a sin((2πf / s) i)

And we have an implementable function

// given a buffer, fills it with sine wave data
HRESULT FillBufferWithSine(
    LPCWAVEFORMATEX pWfx, // includes sample rate
    double lfAmplitude,
    double lfFrequency,
    PBYTE pBuffer,
    UINT32 cbBytes
);

This would start at i = 0, loop through i = cbBytes / pWfx->nBlockAlign, and set sample values for each i.

It is often necessary to get continuous sine wave data in chunks... say, 1 KB at a time.  It is important that the phase of the sine wave match at chunk boundaries.  This suggests another parameter:

// given a buffer, fills it with sine wave data
HRESULT FillBufferWithSine(
    LPCWAVEFORMATEX pWfx, // includes sample rate
    double lfAmplitude,
    double lfFrequency,
    UINT32 ifFrameOffset,
    PBYTE pBuffer,
    UINT32 cbBytes
);

... where ifFrameOffset is the count of frames to skip.  Now a client can fill a 1 KB buffer with 256 frames of 16-bit int stereo sine wave, pass it off to someone else, fill another 1 KB buffer starting at frame offset 256 with 16-bit int stereo sine wave, etc., and the effect is an almost infinite continuous sine wave.  (Oh, and as a side effect, we get a kinda-sorta implementation of ϕ.)

Almost.

Why almost?

Well, because I used UINT32 as the frame offset.  Suppose you, as a client, use a WAVEFORMATEX with a nice high sample rate like 384 kHz.  A UINT32 wraps at 232.   At 384,000 samples per second, you'll burn through 232 samples in (232 / 384000) seconds... that's, um, 1184.8106666... seconds... three hours, six minutes, twenty-five seconds.  This is uncomfortably close to reasonable.

At that point, one of two things happen.  If you were clever in your chosen frequency, you will wrap back to a point in the sine wave where the phase is the same as what you want anyway... and there will be no observable artifact in the generated sine wave.  Or, if you were unlucky, you will wrap back to a point in the sine wave where the phase is different - and there will be a discontinuity.  This will probably result in an audible "pop" artifact in the signal tone.

Our test tone generator had this problem.  We knew about it - we didn't care.

But sometimes little things nag at me and I have to fix them or I can't sleep at night.  It's a fixation... a terrible disease... but there it is.

So I fixed it - almost.  Now we use a UINT64 for the sample offset.  That won't wrap until 264 samples.  Even at 384 kHz, that won't wrap until (264/384000) seconds - that's, um,  48038396025285.29 seconds... 1,521,800-odd years.  I'll let a tester from that era file a bug then. :-)

Almost.

Why almost?

Well, it turns out that fixing that bug uncovered another bug.  We know about it - we don't care.  (But sometimes little things nag at me...)

Exercise... what is the bug?

Leave a Comment
  • Please add 7 and 3 and type the answer here:
  • Post
  • I think a bug will show up because of the double representation of large numbers.

    A better implementation would be one that calculates additional phase given ifFrameOffset, and starts the counter (i) at 0.  If I'm understanding the post correctly, this should be superior.

Page 1 of 1 (1 items)