Matthew van Eerde's web log
I am a Software Development Engineer in Test working for the Windows Sound team. You can contact me via email: mateer at microsoft dot com
Friend key: 28904932216450_59cd9d55374be03d8167d37c8ff4196b
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:
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
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 dataHRESULT 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 dataHRESULT 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 ϕ.)
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. :-)
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?
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.