Introduction
The MSDN documentation states that the Win32 Beep()
function is no longer supported on 64-bit versions of Windows. However, there may be a need to produce audible tones of different frequencies in a simple way without the overhead of creating and playing a sound file. In particular, I wrote this code to implement the QBasic PLAY
command to play a collection of old QBasic songs I had lying around.
Background
The original Beep function controlled a hardware timer directly, which in turn drove the speaker directly with a square wave, as described by Larry Osterman. We will seek to simulate this behavior, since a square wave is the simplest waveform to generate on the fly, although the code can be easily modified to generate a sine wave. I have elected not to do this since the code manages to work entirely without floating point math, and generating a sine wave will either involve floating point, or make the code much more complex with lookup tables.
Using the Code
The code is completely contained within the header file DSBeeper.h. Simply construct a DSBeeper
object, and invoke its Beep()
function:
DSBeeper beeper;
beeper.Beep(750, 300);
The constructor initializes DirectSound, and when the object goes out of scope, the destructor uninitializes. Note that the constructor also calls CoInitializeEx()
with a particular threading model. You will probably want to modify both the constructor and destructor to suit your particular application.
Implementation
The code to set up DirectSound is less than interesting, since it just follows the boilerplate suggested by Microsoft. However, synthesizing a square wave at the right frequency without using floating point math is the interesting part.
Given the frequency in Hertz, its reciprocal is the wave period in seconds. For a square wave, the waveform is constant but switches sign every half period. The audio device typically plays sounds sampled 44100 times a second, so the number of samples in one half period is 44100/(2 * frequency). For high frequencies (let's be extreme and use 32kHz), the half period will be less than one sample in length! Clearly, we cannot play such a wave.
An IDirectSoundBuffer
device can be created with the ability to control the playback frequency. Using that, we can generate waveform with an incorrect frequency, but play it at an adjusted rate so that it sounds like the right frequency. The half period computed above is rounded down, subject to integer arithmetic rules. This can result in errors of more than 30% at the high end of the frequency limits. To correctly adjust the playback frequency, we must multiply it by the correction factor, which is the rounded half period divided by the unrounded value. The code to compute the half period is:
INT32 half_period = SAMPLING_RATE * NUM_CHANNELS / (2*dwFreq);
Now, the adjustment to the playback frequency is:
DWORD play_freq;
m_lpDSBuffer->GetFrequency(&play_freq);
play_freq = MulDiv(play_freq, 2*dwFreq*half_period,
SAMPLING_RATE * NUM_CHANNELS);
m_lpDSBuffer->SetFrequency(play_freq);
We are using the MulDiv
function to multiply the first two arguments and divide by the last argument, with an intermediate precision of 64 bits (which is highly efficient in assembly on an x86 processor). This is not strictly necessary, since computation of the maximum value of the numerator does not exceed the limit of a 32-bit integer (for several cases I tried), it's better to be safe. Note that this does not do significantly worse than using a floating point scale factor, since the result is precise to 1 bit of an integer, which is all you can ask for when you have to set the frequency as an integer anyways.
QBPlayer - The Demo Project
This is really two projects combined into one, since the demo project does something not quite trivial, but makes the demo much more illustrative of the code's capabilities. The demo project is an implementation of the QBasic PLAY
command, which allowed you to generate music using the PC speaker. This was used to make games (for example, in the sample program GORILLA.BAS
), and many people might still have old song files lying around. The demo project parses a string of commands indicating which notes to play, or how to modify the player state, and generates the corresponding tones.
If you just want to hear something, then run the program QBPlayer_ds.exe, type "C" and then press Enter. You should hear a note play. Type CTRL-Z and press Enter to quit. For those of you running a 32-bit version of windows, you can compare the output with the PC speaker version (QBplayer.exe).
The code was compiled with Visual Studio 2005, using the DirectX SDK November 2009, and headers from Platform SDK for Windows Server 2003 R2. The code was linked with the Visual C++ 6.0 MSVCRT.LIB to avoid redistribution issues and code size.
History
2009-01-21 - Initial revision